mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-20 22:26:23 +00:00
refactor(agent-core-v2): register built-in tools via DI
Make built-in tool registration DI-native: - each tool is now a DI class whose dependencies are injected via @IX instead of hand-wired through a parent service constructor - each domain registers its tools from an Eager Agent-scope *ToolsService that builds them with IInstantiationService.createInstance - split tool registration out of domain services that the tools call back into (cron/background/goal/plan/swarm/skill) to avoid constructor cycles - drop the manual force-pulls (rpc marker injections, the initializeBuiltinTools callback, runChildAgent ensureAgentTool) now that Eager registration runs at Agent scope creation - defer IHostEnvironment reads in GlobTool/BashTool to getters so Eager construction does not require environment readiness Media tools (ReadMediaFile) are left as-is pending a capability-gated registration design.
This commit is contained in:
parent
37e5dacd52
commit
b293d6f030
72 changed files with 908 additions and 588 deletions
|
|
@ -12,9 +12,9 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { BuiltinTool } from '#/agent/tool';
|
||||
import type { ILogger } from '#/app/log';
|
||||
import { ILogService } from '#/app/log';
|
||||
import { collectGitContext } from '#/session/agentFs';
|
||||
import type { ISessionProcessRunner } from '#/session/process';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import { ToolAccesses } from '#/agent/tool';
|
||||
import { isAbortError } from '#/agent/loop/errors';
|
||||
import type {
|
||||
|
|
@ -25,12 +25,14 @@ import type {
|
|||
import { isUserCancellation } from '#/_base/utils/abort';
|
||||
import {
|
||||
AgentBackgroundTask,
|
||||
type IAgentBackgroundService,
|
||||
IAgentBackgroundService,
|
||||
type RegisterBackgroundTaskOptions,
|
||||
} from '#/agent/background';
|
||||
import type { IAgentProfileService } from '#/agent/profile';
|
||||
import type { IAgentLifecycleService } from '#/session/agentLifecycle';
|
||||
import type { ISessionMetadata } from '#/session/sessionMetadata';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext';
|
||||
import { IExecContext } from '#/session/execContext';
|
||||
import { IAgentLifecycleService } from '#/session/agentLifecycle';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import { matchesGlobRuleSubject } from '#/_base/tools/support/rule-match';
|
||||
import {
|
||||
|
|
@ -123,53 +125,40 @@ export interface AgentToolSubagentProfile {
|
|||
|
||||
export type AgentToolSubagentMap = Readonly<Record<string, AgentToolSubagentProfile>>;
|
||||
|
||||
export interface AgentToolOptions {
|
||||
readonly lifecycle: IAgentLifecycleService;
|
||||
readonly callerAgentId: string;
|
||||
readonly metadata?: ISessionMetadata;
|
||||
readonly background: IAgentBackgroundService;
|
||||
readonly profile: IAgentProfileService;
|
||||
readonly cwd: string;
|
||||
readonly processRunner: ISessionProcessRunner;
|
||||
readonly log?: ILogger;
|
||||
readonly runOverride?: AgentToolRunOverride;
|
||||
}
|
||||
|
||||
// ── AgentTool class ──────────────────────────────────────────────────
|
||||
|
||||
export class AgentTool implements BuiltinTool<AgentToolInput> {
|
||||
readonly name: string = 'Agent';
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(AgentToolInputSchema);
|
||||
|
||||
private readonly lifecycle: IAgentLifecycleService;
|
||||
private readonly callerAgentId: string;
|
||||
private readonly metadata?: ISessionMetadata;
|
||||
private readonly background: IAgentBackgroundService;
|
||||
private readonly log?: ILogger;
|
||||
private readonly runOverride?: AgentToolRunOverride;
|
||||
private readonly typeLines: string;
|
||||
private readonly gitContext: { cwd: string; runner: ISessionProcessRunner };
|
||||
private readonly typeLines: string;
|
||||
private readonly canRunInBackground: () => boolean;
|
||||
|
||||
constructor(options: AgentToolOptions) {
|
||||
this.lifecycle = options.lifecycle;
|
||||
this.callerAgentId = options.callerAgentId;
|
||||
this.metadata = options.metadata;
|
||||
this.background = options.background;
|
||||
this.log = options.log;
|
||||
this.runOverride = options.runOverride;
|
||||
this.gitContext = { cwd: options.cwd, runner: options.processRunner };
|
||||
constructor(
|
||||
private readonly runOverride: AgentToolRunOverride | undefined,
|
||||
@IAgentLifecycleService private readonly lifecycle: IAgentLifecycleService,
|
||||
@IAgentScopeContext scopeContext: IAgentScopeContext,
|
||||
@ISessionMetadata private readonly metadata: ISessionMetadata,
|
||||
@IAgentBackgroundService private readonly background: IAgentBackgroundService,
|
||||
@IAgentProfileService private readonly profile: IAgentProfileService,
|
||||
@IExecContext execContext: IExecContext,
|
||||
@ISessionProcessRunner processRunner: ISessionProcessRunner,
|
||||
@ILogService private readonly log: ILogService,
|
||||
) {
|
||||
this.callerAgentId = scopeContext.agentId;
|
||||
this.gitContext = { cwd: execContext.cwd, runner: processRunner };
|
||||
this.typeLines = buildSubagentDescriptions(DEFAULT_AGENT_SUBAGENT_PROFILES);
|
||||
this.canRunInBackground = () => {
|
||||
return (
|
||||
options.profile.isToolActive('TaskList') &&
|
||||
options.profile.isToolActive('TaskOutput') &&
|
||||
options.profile.isToolActive('TaskStop')
|
||||
this.profile.isToolActive('TaskList') &&
|
||||
this.profile.isToolActive('TaskOutput') &&
|
||||
this.profile.isToolActive('TaskStop')
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private readonly canRunInBackground: () => boolean;
|
||||
|
||||
private get run(): AgentToolRunOverride {
|
||||
return (
|
||||
this.runOverride ?? {
|
||||
|
|
|
|||
|
|
@ -1,29 +1,23 @@
|
|||
/**
|
||||
* `agentTool` domain (L5) — registers the `Agent` collaboration tool for an agent.
|
||||
*
|
||||
* Registers the `Agent` tool into the `toolRegistry` so the agent can spawn task
|
||||
* subagents, bound to this agent as the caller (`callerAgentId` from the agent
|
||||
* `scopeContext`). The optional first static `runner` argument is a test seam
|
||||
* (`AgentToolRunOverride`) that lets tests substitute the `runChildAgent`
|
||||
* helpers; the scoped registry supplies none. Bound at Agent scope; reads its
|
||||
* identity through `scopeContext`, creates child agents through
|
||||
* `agentLifecycle`, reads the parent check through `sessionMetadata`, gates
|
||||
* background execution through the agent `profile`, and gathers git context
|
||||
* through `kaos` (cwd) + `process` (runner).
|
||||
* Eager Agent-scope registration service for the `Agent` tool, which lets the
|
||||
* agent spawn task subagents. The tool is a DI class created via
|
||||
* `IInstantiationService.createInstance` (its dependencies — identity via
|
||||
* `scopeContext`, child creation via `agentLifecycle`, parent check via
|
||||
* `sessionMetadata`, background gating via `profile`, git context via
|
||||
* `execContext` + `process` — are injected) and registered into the agent
|
||||
* `IAgentToolRegistryService`. The optional leading static `runner` argument is a
|
||||
* test seam (`AgentToolRunOverride`) that lets tests substitute the
|
||||
* `runChildAgent` helpers; the scoped registry supplies none. Eager so the tool
|
||||
* is registered when the Agent scope is created, before the first turn.
|
||||
*/
|
||||
|
||||
import { Disposable } from '#/_base/di';
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentBackgroundService } from '#/agent/background';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import { IExecContext } from '#/session/execContext';
|
||||
import { ILogService } from '#/app/log';
|
||||
import { IAgentLifecycleService } from '#/session/agentLifecycle';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata';
|
||||
|
||||
import { AgentTool } from './agentTool';
|
||||
import { IAgentToolService } from './agentToolServiceToken';
|
||||
|
|
@ -34,31 +28,12 @@ export class AgentToolService extends Disposable implements IAgentToolService {
|
|||
|
||||
constructor(
|
||||
runner: AgentToolRunOverride | undefined,
|
||||
@IAgentScopeContext ctx: IAgentScopeContext,
|
||||
@IAgentLifecycleService lifecycle: IAgentLifecycleService,
|
||||
@ISessionMetadata metadata: ISessionMetadata,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
@IAgentBackgroundService background: IAgentBackgroundService,
|
||||
@IAgentProfileService profile: IAgentProfileService,
|
||||
@IExecContext execContext: IExecContext,
|
||||
@ISessionProcessRunner processRunner: ISessionProcessRunner,
|
||||
@ILogService log?: ILogService,
|
||||
) {
|
||||
super();
|
||||
this._register(
|
||||
toolRegistry.register(
|
||||
new AgentTool({
|
||||
lifecycle,
|
||||
callerAgentId: ctx.agentId,
|
||||
metadata,
|
||||
background,
|
||||
profile,
|
||||
cwd: execContext.cwd,
|
||||
processRunner,
|
||||
log,
|
||||
runOverride: runner,
|
||||
}),
|
||||
),
|
||||
toolRegistry.register(instantiationService.createInstance(AgentTool, runner)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +42,6 @@ registerScopedService(
|
|||
LifecycleScope.Agent,
|
||||
IAgentToolService,
|
||||
AgentToolService,
|
||||
InstantiationType.Delayed,
|
||||
InstantiationType.Eager,
|
||||
'agentTool',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ export {
|
|||
} from './agentTool';
|
||||
export type {
|
||||
AgentToolInput,
|
||||
AgentToolOptions,
|
||||
AgentToolOutput,
|
||||
AgentToolSubagentMap,
|
||||
AgentToolSubagentProfile,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import { IAgentPromptService } from '#/agent/prompt';
|
|||
import { IAgentUsageService } from '#/agent/usage';
|
||||
import type { Turn } from '#/agent/turn';
|
||||
|
||||
import { IAgentToolService } from './agentToolServiceToken';
|
||||
import { DEFAULT_AGENT_SUBAGENT_PROFILES, EXPLORE_ROLE_ADDITIONAL } from './profiles';
|
||||
import {
|
||||
DEFAULT_SUBAGENT_TIMEOUT_MS,
|
||||
|
|
@ -82,7 +81,6 @@ export async function spawnChildAgent(args: SpawnChildAgentArgs): Promise<Subage
|
|||
swarmItem: options.swarmItem,
|
||||
});
|
||||
configureChild(caller, child, options.profileName);
|
||||
ensureAgentTool(child);
|
||||
emitSpawned(caller, callerAgentId, child.id, options.profileName, options);
|
||||
const completion = runWithActiveChild(
|
||||
child,
|
||||
|
|
@ -143,16 +141,9 @@ async function requireAgent(
|
|||
): Promise<IAgentScopeHandle> {
|
||||
const handle = lifecycle.getHandle(agentId);
|
||||
if (handle === undefined) throw new Error(`Agent instance "${agentId}" does not exist`);
|
||||
ensureAgentTool(handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
function ensureAgentTool(child: IAgentScopeHandle): void {
|
||||
// Force-instantiate the child agent's `Agent` tool registrar so its `Agent`
|
||||
// tool is registered before the child's first turn builds its tool list.
|
||||
child.accessor.get(IAgentToolService);
|
||||
}
|
||||
|
||||
function configureChild(source: IAgentScopeHandle, child: IAgentScopeHandle, profileName: string): void {
|
||||
const sourceProfile = source.accessor.get(IAgentProfileService);
|
||||
const childProfile = child.accessor.get(IAgentProfileService);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import { IAgentPromptService } from '#/agent/prompt';
|
|||
import { ISessionContext } from '#/session/sessionContext';
|
||||
import { IAtomicDocumentStore, IStorageService } from '#/app/storage';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import { IAgentRecordService, type AgentRecord } from '#/agent/record';
|
||||
import {
|
||||
IAgentBackgroundService,
|
||||
|
|
@ -136,7 +135,6 @@ export class AgentBackgroundService extends Disposable implements IAgentBackgrou
|
|||
@IAgentPromptService private readonly prompt: IAgentPromptService,
|
||||
@IAgentExternalHooksService private readonly externalHooks: IAgentExternalHooksService,
|
||||
@IAgentContextMemoryService private readonly context: IAgentContextMemoryService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
@IConfigService private readonly config: IConfigService,
|
||||
@IAtomicDocumentStore atomicDocs: IAtomicDocumentStore,
|
||||
@IStorageService byteStore: IStorageService,
|
||||
|
|
@ -183,10 +181,6 @@ export class AgentBackgroundService extends Disposable implements IAgentBackgrou
|
|||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this._register(toolRegistry.register(new TaskListTool(this)));
|
||||
this._register(toolRegistry.register(new TaskOutputTool(this)));
|
||||
this._register(toolRegistry.register(new TaskStopTool(this)));
|
||||
}
|
||||
|
||||
registerTask(task: BackgroundTask, options: RegisterBackgroundTaskOptions = {}): string {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* `backgroundTools` domain (L4) — `IAgentBackgroundToolsService` registration contract.
|
||||
*
|
||||
* Marker service: its implementation registers the built-in background-task tools
|
||||
* (TaskList / TaskOutput / TaskStop) into the agent `IAgentToolRegistryService` on
|
||||
* construction. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export interface IAgentBackgroundToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IAgentBackgroundToolsService: ServiceIdentifier<IAgentBackgroundToolsService> =
|
||||
createDecorator<IAgentBackgroundToolsService>('agentBackgroundToolsService');
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* `backgroundTools` domain (L4) — `IAgentBackgroundToolsService` implementation.
|
||||
*
|
||||
* Eager Agent-scope registration service for the built-in background-task tools
|
||||
* (TaskList / TaskOutput / TaskStop). Each tool is a DI class created via
|
||||
* `IInstantiationService.createInstance` (they inject `IAgentBackgroundService`
|
||||
* themselves) and registered into the agent `IAgentToolRegistryService`. Eager so
|
||||
* the tools are registered when the Agent scope is created, before the first turn.
|
||||
*
|
||||
* Split out of `AgentBackgroundService` so the tools can inject
|
||||
* `IAgentBackgroundService` without forming a constructor-instantiation cycle.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
||||
import { IAgentBackgroundToolsService } from './backgroundTools';
|
||||
import { TaskListTool } from '#/agent/background/tools/task-list';
|
||||
import { TaskOutputTool } from '#/agent/background/tools/task-output';
|
||||
import { TaskStopTool } from '#/agent/background/tools/task-stop';
|
||||
|
||||
export class AgentBackgroundToolsService extends Disposable implements IAgentBackgroundToolsService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
) {
|
||||
super();
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(TaskListTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(TaskOutputTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(TaskStopTool)));
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
IAgentBackgroundToolsService,
|
||||
AgentBackgroundToolsService,
|
||||
InstantiationType.Eager,
|
||||
'background',
|
||||
);
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
/**
|
||||
* `background` domain barrel — re-exports the background contract
|
||||
* (`background`) and its scoped service (`backgroundService`). Importing this
|
||||
* barrel registers the `IAgentBackgroundService` binding into the scope registry.
|
||||
* (`background`) and its scoped service (`backgroundService`), plus the
|
||||
* `backgroundTools` registrar. Importing this barrel registers the
|
||||
* `IAgentBackgroundService` and `IAgentBackgroundToolsService` bindings into the
|
||||
* scope registry.
|
||||
*/
|
||||
|
||||
import './configSection';
|
||||
|
||||
export * from './background';
|
||||
export * from './backgroundService';
|
||||
export * from './backgroundTools';
|
||||
export * from './backgroundToolsService';
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
|||
import { matchesGlobRuleSubject } from '#/_base/tools/support/rule-match';
|
||||
import type { BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
|
||||
import type { BackgroundTaskInfo, IAgentBackgroundService } from '#/agent/background/background';
|
||||
import { IAgentBackgroundService } from '#/agent/background/background';
|
||||
import type { BackgroundTaskInfo } from '#/agent/background/background';
|
||||
import { formatPlainObject } from './format';
|
||||
import TASK_LIST_DESCRIPTION from './task-list.md?raw';
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ export class TaskListTool implements BuiltinTool<TaskListInput> {
|
|||
readonly description = TASK_LIST_DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(TaskListInputSchema);
|
||||
|
||||
constructor(private readonly background: IAgentBackgroundService) {}
|
||||
constructor(@IAgentBackgroundService private readonly background: IAgentBackgroundService) {}
|
||||
|
||||
resolveExecution(args: TaskListInput): ToolExecution {
|
||||
const listScope = (args.active_only ?? true) ? 'active' : 'all';
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
|||
import { matchesGlobRuleSubject } from '#/_base/tools/support/rule-match';
|
||||
import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
|
||||
import { IAgentBackgroundService } from '#/agent/background/background';
|
||||
import type {
|
||||
BackgroundTaskInfo,
|
||||
BackgroundTaskOutputSnapshot,
|
||||
IAgentBackgroundService,
|
||||
} from '#/agent/background/background';
|
||||
import { type BackgroundTaskStatus, TERMINAL_STATUSES } from '#/agent/background/task';
|
||||
import { formatPlainObject } from './format';
|
||||
|
|
@ -98,7 +98,7 @@ export class TaskOutputTool implements BuiltinTool<TaskOutputInput> {
|
|||
readonly description: string = TASK_OUTPUT_DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(TaskOutputInputSchema);
|
||||
|
||||
constructor(private readonly background: IAgentBackgroundService) {}
|
||||
constructor(@IAgentBackgroundService private readonly background: IAgentBackgroundService) {}
|
||||
|
||||
resolveExecution(args: TaskOutputInput): ToolExecution {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
|||
import { matchesGlobRuleSubject } from '#/_base/tools/support/rule-match';
|
||||
import type { BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
|
||||
import type { IAgentBackgroundService } from '#/agent/background/background';
|
||||
import { IAgentBackgroundService } from '#/agent/background/background';
|
||||
import { TERMINAL_STATUSES } from '#/agent/background/task';
|
||||
import TASK_STOP_DESCRIPTION from './task-stop.md?raw';
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ export class TaskStopTool implements BuiltinTool<TaskStopInput> {
|
|||
readonly description = TASK_STOP_DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(TaskStopInputSchema);
|
||||
|
||||
constructor(private readonly background: IAgentBackgroundService) {}
|
||||
constructor(@IAgentBackgroundService private readonly background: IAgentBackgroundService) {}
|
||||
|
||||
resolveExecution(args: TaskStopInput): ToolExecution {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import type { ContextMessage } from '#/agent/contextMemory';
|
|||
import { IAgentPromptService } from '#/agent/prompt';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import type { Turn } from '#/agent/turn';
|
||||
import { IAgentTurnService } from '#/agent/turn';
|
||||
import { ISessionContext } from '#/session/sessionContext';
|
||||
|
|
@ -160,7 +159,6 @@ export class AgentCronService extends Disposable implements IAgentCronService {
|
|||
@IAgentRecordService private readonly record: IAgentRecordService,
|
||||
@IAgentTurnService private readonly turnService: IAgentTurnService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
@IAgentToolRegistryService private readonly toolRegistry: IAgentToolRegistryService,
|
||||
@IConfigService private readonly config: IConfigService,
|
||||
@IAtomicDocumentStore private readonly atomicDocs: IAtomicDocumentStore,
|
||||
@ISessionContext private readonly session: ISessionContext,
|
||||
|
|
@ -222,10 +220,6 @@ export class AgentCronService extends Disposable implements IAgentCronService {
|
|||
);
|
||||
|
||||
if (this.enabled) {
|
||||
this._register(this.toolRegistry.register(new CronCreateTool(this, this.cronConfig.disabled)));
|
||||
this._register(this.toolRegistry.register(new CronListTool(this)));
|
||||
this._register(this.toolRegistry.register(new CronDeleteTool(this)));
|
||||
|
||||
this.start();
|
||||
}
|
||||
|
||||
|
|
|
|||
16
packages/agent-core-v2/src/agent/cron/cronTools.ts
Normal file
16
packages/agent-core-v2/src/agent/cron/cronTools.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* `cronTools` domain (L4) — `IAgentCronToolsService` registration contract.
|
||||
*
|
||||
* Marker service: its implementation registers the built-in cron tools
|
||||
* (CronCreate / CronList / CronDelete) into the agent `IAgentToolRegistryService`
|
||||
* on construction. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export interface IAgentCronToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IAgentCronToolsService: ServiceIdentifier<IAgentCronToolsService> =
|
||||
createDecorator<IAgentCronToolsService>('agentCronToolsService');
|
||||
61
packages/agent-core-v2/src/agent/cron/cronToolsService.ts
Normal file
61
packages/agent-core-v2/src/agent/cron/cronToolsService.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* `cronTools` domain (L4) — `IAgentCronToolsService` implementation.
|
||||
*
|
||||
* Eager Agent-scope registration service for the built-in cron tools
|
||||
* (CronCreate / CronList / CronDelete). Each tool is a DI class created via
|
||||
* `IInstantiationService.createInstance` (they inject `IAgentCronService`
|
||||
* themselves) and registered into the agent `IAgentToolRegistryService`.
|
||||
*
|
||||
* Registration is gated on `IAgentCronService.isEnabled` (cron only runs on the
|
||||
* main agent), matching the previous in-service behavior. The global
|
||||
* `cron.disabled` killswitch (`KIMI_DISABLE_CRON`) is read from config and passed
|
||||
* to `CronCreateTool` as a leading static argument so the tool can surface a
|
||||
* friendly error when scheduling is disabled.
|
||||
*
|
||||
* Split out of `AgentCronService` so the tools can inject `IAgentCronService`
|
||||
* without forming a constructor-instantiation cycle. Eager so the tools are
|
||||
* registered when the Agent scope is created, before the first turn.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IConfigService } from '#/app/config';
|
||||
import { IAgentCronService } from '#/agent/cron';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
||||
import { type CronConfig, CRON_SECTION, DEFAULT_CRON_CONFIG } from './configSection';
|
||||
import { IAgentCronToolsService } from './cronTools';
|
||||
import { CronCreateTool } from '#/agent/cron/tools/cron-create';
|
||||
import { CronDeleteTool } from '#/agent/cron/tools/cron-delete';
|
||||
import { CronListTool } from '#/agent/cron/tools/cron-list';
|
||||
|
||||
export class AgentCronToolsService extends Disposable implements IAgentCronToolsService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
@IAgentCronService cron: IAgentCronService,
|
||||
@IConfigService config: IConfigService,
|
||||
) {
|
||||
super();
|
||||
if (!cron.isEnabled) return;
|
||||
const disabled =
|
||||
config.get<CronConfig>(CRON_SECTION)?.disabled ?? DEFAULT_CRON_CONFIG.disabled;
|
||||
this._register(
|
||||
toolRegistry.register(instantiationService.createInstance(CronCreateTool, disabled)),
|
||||
);
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(CronListTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(CronDeleteTool)));
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
IAgentCronToolsService,
|
||||
AgentCronToolsService,
|
||||
InstantiationType.Eager,
|
||||
'cron',
|
||||
);
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
/**
|
||||
* `cron` domain barrel — re-exports the cron contract (`cron`) and its scoped
|
||||
* service (`cronService`). Importing this barrel registers the `IAgentCronService`
|
||||
* binding into the scope registry.
|
||||
* service (`cronService`), plus the `cronTools` registrar. Importing this barrel
|
||||
* registers the `IAgentCronService` and `IAgentCronToolsService` bindings into the
|
||||
* scope registry.
|
||||
*/
|
||||
|
||||
import './configSection';
|
||||
|
||||
export * from './cron';
|
||||
export * from './cronService';
|
||||
export * from './cronTools';
|
||||
export * from './cronToolsService';
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { z } from 'zod';
|
|||
import type { ExecutableTool as BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import { literalRulePattern } from '#/_base/tools/support/rule-match';
|
||||
import type { IAgentCronService } from '#/agent/cron';
|
||||
import { IAgentCronService } from '#/agent/cron';
|
||||
import {
|
||||
computeNextCronRun,
|
||||
cronToHuman,
|
||||
|
|
@ -121,8 +121,8 @@ export class CronCreateTool implements BuiltinTool<CronCreateInput> {
|
|||
);
|
||||
|
||||
constructor(
|
||||
private readonly cron: IAgentCronService,
|
||||
private readonly disabled: boolean = false,
|
||||
@IAgentCronService private readonly cron: IAgentCronService,
|
||||
) {}
|
||||
|
||||
resolveExecution(args: CronCreateInput): ToolExecution {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import { z } from 'zod';
|
|||
|
||||
import type { ExecutableTool as BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import type { IAgentCronService } from '#/agent/cron';
|
||||
import { IAgentCronService } from '#/agent/cron';
|
||||
import CRON_DELETE_DESCRIPTION from './cron-delete.md?raw';
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────
|
||||
|
|
@ -71,7 +71,7 @@ export class CronDeleteTool implements BuiltinTool<CronDeleteInput> {
|
|||
CronDeleteInputSchema,
|
||||
);
|
||||
|
||||
constructor(private readonly cron: IAgentCronService) {}
|
||||
constructor(@IAgentCronService private readonly cron: IAgentCronService) {}
|
||||
|
||||
resolveExecution(args: CronDeleteInput): ToolExecution {
|
||||
// Format check up front. The store would reject the lookup anyway,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ import { z } from 'zod';
|
|||
|
||||
import type { ExecutableTool as BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import type { CronTask, IAgentCronService } from '#/agent/cron';
|
||||
import { IAgentCronService } from '#/agent/cron';
|
||||
import type { CronTask } from '#/agent/cron';
|
||||
import {
|
||||
cronToHuman,
|
||||
parseCronExpression,
|
||||
|
|
@ -90,7 +91,7 @@ export class CronListTool implements BuiltinTool<CronListInput> {
|
|||
CronListInputSchema,
|
||||
);
|
||||
|
||||
constructor(private readonly cron: IAgentCronService) {}
|
||||
constructor(@IAgentCronService private readonly cron: IAgentCronService) {}
|
||||
|
||||
resolveExecution(_args: CronListInput): ToolExecution {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
/**
|
||||
* `fileTools` domain (L4) — `IAgentFileToolsService` implementation.
|
||||
*
|
||||
* Registers the built-in file tools (Read / Write / Edit / Grep / Glob) into
|
||||
* the agent `IAgentToolRegistryService` on construction, wiring each to the session
|
||||
* `ISessionAgentFileSystem` (file IO), `ISessionFsService` (workspace search/grep),
|
||||
* `ISessionProcessRunner` (rg subprocess for Glob), `IHostEnvironment`
|
||||
* (path semantics) and the session workspace. Bound at Agent scope.
|
||||
* Eager Agent-scope registration service for the built-in file tools
|
||||
* (Read / Write / Edit / Grep / Glob). Each tool is a DI class created via
|
||||
* `IInstantiationService.createInstance` (so its session/app dependencies are
|
||||
* injected) and registered into the agent `IAgentToolRegistryService`. Eager so
|
||||
* the tools are registered when the Agent scope is created, before the first turn.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import type { WorkspaceConfig } from '#/_base/tools/support/workspace';
|
||||
import { ISessionAgentFileSystem, ISessionFsService } from '#/session/agentFs';
|
||||
import { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
|
||||
import { IAgentFileToolsService } from './fileTools';
|
||||
import { EditTool } from '#/agent/fileTools/tools/edit';
|
||||
|
|
@ -25,27 +21,19 @@ import { GrepTool } from '#/agent/fileTools/tools/grep';
|
|||
import { ReadTool } from '#/agent/fileTools/tools/read';
|
||||
import { WriteTool } from '#/agent/fileTools/tools/write';
|
||||
|
||||
export class AgentFileToolsService implements IAgentFileToolsService {
|
||||
export class AgentFileToolsService extends Disposable implements IAgentFileToolsService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
@ISessionAgentFileSystem fs: ISessionAgentFileSystem,
|
||||
@IHostEnvironment env: IHostEnvironment,
|
||||
@ISessionWorkspaceContext workspace: ISessionWorkspaceContext,
|
||||
@ISessionFsService fsService: ISessionFsService,
|
||||
@ISessionProcessRunner runner: ISessionProcessRunner,
|
||||
@ITelemetryService telemetry: ITelemetryService,
|
||||
) {
|
||||
const workspaceConfig: WorkspaceConfig = {
|
||||
workspaceDir: workspace.workDir,
|
||||
additionalDirs: workspace.additionalDirs,
|
||||
};
|
||||
toolRegistry.register(new ReadTool(fs, env, workspaceConfig));
|
||||
toolRegistry.register(new WriteTool(fs, env, workspaceConfig));
|
||||
toolRegistry.register(new EditTool(fs, env, workspaceConfig));
|
||||
toolRegistry.register(new GrepTool(fsService, env, workspaceConfig));
|
||||
toolRegistry.register(new GlobTool(fs, env, runner, workspaceConfig, telemetry));
|
||||
super();
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(ReadTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(WriteTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(EditTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(GrepTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(GlobTool)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,6 +41,6 @@ registerScopedService(
|
|||
LifecycleScope.Agent,
|
||||
IAgentFileToolsService,
|
||||
AgentFileToolsService,
|
||||
InstantiationType.Delayed,
|
||||
InstantiationType.Eager,
|
||||
'fileTools',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import type { WorkspaceConfig } from '#/_base/tools/support/workspace';
|
|||
import { renderPrompt } from '#/_base/utils/render-prompt';
|
||||
import { ISessionAgentFileSystem } from '#/session/agentFs';
|
||||
import { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
import { ToolAccesses } from '#/agent/tool';
|
||||
import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
|
||||
|
|
@ -77,15 +78,22 @@ export class EditTool implements BuiltinTool<EditInput> {
|
|||
readonly parameters: Record<string, unknown> = toInputJsonSchema(EditInputSchema);
|
||||
|
||||
constructor(
|
||||
private readonly fs: ISessionAgentFileSystem,
|
||||
private readonly env: IHostEnvironment,
|
||||
private readonly workspace: WorkspaceConfig,
|
||||
@ISessionAgentFileSystem private readonly fs: ISessionAgentFileSystem,
|
||||
@IHostEnvironment private readonly env: IHostEnvironment,
|
||||
@ISessionWorkspaceContext private readonly workspaceCtx: ISessionWorkspaceContext,
|
||||
) {}
|
||||
|
||||
private get workspaceConfig(): WorkspaceConfig {
|
||||
return {
|
||||
workspaceDir: this.workspaceCtx.workDir,
|
||||
additionalDirs: this.workspaceCtx.additionalDirs,
|
||||
};
|
||||
}
|
||||
|
||||
resolveExecution(args: EditInput): ToolExecution {
|
||||
const path = resolvePathAccessPath(args.path, {
|
||||
env: this.env,
|
||||
workspace: this.workspace,
|
||||
workspace: this.workspaceConfig,
|
||||
operation: 'write',
|
||||
});
|
||||
return {
|
||||
|
|
@ -101,7 +109,7 @@ export class EditTool implements BuiltinTool<EditInput> {
|
|||
approvalRule: literalRulePattern(this.name, path),
|
||||
matchesRule: (ruleArgs) =>
|
||||
matchesPathRuleSubject(ruleArgs, path, {
|
||||
cwd: this.workspace.workspaceDir,
|
||||
cwd: this.workspaceConfig.workspaceDir,
|
||||
pathClass: this.env.pathClass,
|
||||
homeDir: this.env.homeDir,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -57,8 +57,9 @@ import {
|
|||
shouldRetryRipgrepEagain,
|
||||
} from '#/session/agentFs/runRg';
|
||||
import { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import { ITelemetryService, noopTelemetryService } from '#/app/telemetry';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { ToolAccesses } from '#/agent/tool';
|
||||
import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
import {
|
||||
|
|
@ -144,30 +145,35 @@ export class GlobTool implements BuiltinTool<GlobInput> {
|
|||
readonly name = 'Glob' as const;
|
||||
readonly description: string;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(GlobInputSchema);
|
||||
private readonly telemetry: ITelemetryService;
|
||||
constructor(
|
||||
private readonly fs: ISessionAgentFileSystem,
|
||||
private readonly env: IHostEnvironment,
|
||||
private readonly runner: ISessionProcessRunner,
|
||||
private readonly workspace: WorkspaceConfig,
|
||||
telemetry: ITelemetryService = noopTelemetryService,
|
||||
@ISessionAgentFileSystem private readonly fs: ISessionAgentFileSystem,
|
||||
@IHostEnvironment private readonly env: IHostEnvironment,
|
||||
@ISessionProcessRunner private readonly runner: ISessionProcessRunner,
|
||||
@ISessionWorkspaceContext private readonly workspaceCtx: ISessionWorkspaceContext,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
) {
|
||||
this.telemetry = telemetry;
|
||||
this.description =
|
||||
this.env.pathClass === 'win32' ? globDescription + WINDOWS_PATH_HINT : globDescription;
|
||||
}
|
||||
|
||||
private get workspaceConfig(): WorkspaceConfig {
|
||||
return {
|
||||
workspaceDir: this.workspaceCtx.workDir,
|
||||
additionalDirs: this.workspaceCtx.additionalDirs,
|
||||
};
|
||||
}
|
||||
|
||||
resolveExecution(args: GlobInput): ToolExecution {
|
||||
let path: string | undefined;
|
||||
if (args.path !== undefined) {
|
||||
path = resolvePathAccessPath(args.path, {
|
||||
env: this.env,
|
||||
workspace: this.workspace,
|
||||
workspace: this.workspaceConfig,
|
||||
operation: 'search',
|
||||
policy: { guardMode: 'absolute-outside-allowed', checkSensitive: false },
|
||||
});
|
||||
}
|
||||
const searchRoots = [path ?? this.workspace.workspaceDir];
|
||||
const searchRoots = [path ?? this.workspaceConfig.workspaceDir];
|
||||
|
||||
const detailParts: string[] = [`pattern: ${args.pattern}`];
|
||||
if (args.path !== undefined) {
|
||||
|
|
@ -197,7 +203,7 @@ export class GlobTool implements BuiltinTool<GlobInput> {
|
|||
signal: AbortSignal,
|
||||
searchRoots: readonly string[],
|
||||
): Promise<ExecutableToolResult> {
|
||||
const searchRoot = searchRoots[0] ?? this.workspace.workspaceDir;
|
||||
const searchRoot = searchRoots[0] ?? this.workspaceConfig.workspaceDir;
|
||||
|
||||
// `rg --files <file>` exits 0 and lists the file itself, so without this
|
||||
// check a file root would be returned as its own match instead of
|
||||
|
|
@ -340,7 +346,7 @@ export class GlobTool implements BuiltinTool<GlobInput> {
|
|||
// later resolved against workspaceDir, so additionalDir matches stay
|
||||
// absolute to keep follow-up Read/Edit calls on the same file.
|
||||
const pathClass = this.env.pathClass;
|
||||
const shouldRelativize = isWithinDirectory(searchRoot, this.workspace.workspaceDir, pathClass);
|
||||
const shouldRelativize = isWithinDirectory(searchRoot, this.workspaceConfig.workspaceDir, pathClass);
|
||||
const displayLines = limited.map((p) =>
|
||||
shouldRelativize ? relativizeIfUnder(p, searchRoot, pathClass) : p,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { z } from 'zod';
|
|||
import { ISessionFsService } from '#/session/agentFs';
|
||||
import { ErrorCodes, isKimiError } from '#/errors';
|
||||
import { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
import { ToolAccesses } from '#/agent/tool';
|
||||
import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
import { resolvePathAccessPath } from '#/_base/tools/policies/path-access';
|
||||
|
|
@ -147,23 +148,30 @@ export class GrepTool implements BuiltinTool<GrepInput> {
|
|||
readonly description = GREP_DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(GrepInputSchema);
|
||||
constructor(
|
||||
private readonly fs: ISessionFsService,
|
||||
private readonly env: IHostEnvironment,
|
||||
private readonly workspace: WorkspaceConfig,
|
||||
@ISessionFsService private readonly fs: ISessionFsService,
|
||||
@IHostEnvironment private readonly env: IHostEnvironment,
|
||||
@ISessionWorkspaceContext private readonly workspaceCtx: ISessionWorkspaceContext,
|
||||
) {}
|
||||
|
||||
private get workspaceConfig(): WorkspaceConfig {
|
||||
return {
|
||||
workspaceDir: this.workspaceCtx.workDir,
|
||||
additionalDirs: this.workspaceCtx.additionalDirs,
|
||||
};
|
||||
}
|
||||
|
||||
resolveExecution(args: GrepInput): ToolExecution {
|
||||
let searchPath: string | undefined;
|
||||
if (args.path !== undefined) {
|
||||
searchPath = resolvePathAccessPath(args.path, {
|
||||
env: this.env,
|
||||
workspace: this.workspace,
|
||||
workspace: this.workspaceConfig,
|
||||
operation: 'search',
|
||||
policy: { guardMode: 'absolute-outside-allowed', checkSensitive: false },
|
||||
});
|
||||
}
|
||||
const accessPath = searchPath ?? this.workspace.workspaceDir;
|
||||
const displayPath = args.path ?? this.workspace.workspaceDir;
|
||||
const accessPath = searchPath ?? this.workspaceConfig.workspaceDir;
|
||||
const displayPath = args.path ?? this.workspaceConfig.workspaceDir;
|
||||
return {
|
||||
accesses: ToolAccesses.searchTree(accessPath),
|
||||
description: `Searching for '${args.pattern}' in ${displayPath}`,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { z } from 'zod';
|
|||
|
||||
import { ISessionAgentFileSystem } from '#/session/agentFs';
|
||||
import { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
import { ToolAccesses } from '#/agent/tool';
|
||||
import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
import { resolvePathAccessPath } from '#/_base/tools/policies/path-access';
|
||||
|
|
@ -224,15 +225,22 @@ export class ReadTool implements BuiltinTool<ReadInput> {
|
|||
readonly description = READ_DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(ReadInputSchema);
|
||||
constructor(
|
||||
private readonly fs: ISessionAgentFileSystem,
|
||||
private readonly env: IHostEnvironment,
|
||||
private readonly workspace: WorkspaceConfig,
|
||||
@ISessionAgentFileSystem private readonly fs: ISessionAgentFileSystem,
|
||||
@IHostEnvironment private readonly env: IHostEnvironment,
|
||||
@ISessionWorkspaceContext private readonly workspaceCtx: ISessionWorkspaceContext,
|
||||
) {}
|
||||
|
||||
private get workspaceConfig(): WorkspaceConfig {
|
||||
return {
|
||||
workspaceDir: this.workspaceCtx.workDir,
|
||||
additionalDirs: this.workspaceCtx.additionalDirs,
|
||||
};
|
||||
}
|
||||
|
||||
resolveExecution(args: ReadInput): ToolExecution {
|
||||
const path = resolvePathAccessPath(args.path, {
|
||||
env: this.env,
|
||||
workspace: this.workspace,
|
||||
workspace: this.workspaceConfig,
|
||||
operation: 'read',
|
||||
});
|
||||
return {
|
||||
|
|
@ -242,7 +250,7 @@ export class ReadTool implements BuiltinTool<ReadInput> {
|
|||
approvalRule: literalRulePattern(this.name, path),
|
||||
matchesRule: (ruleArgs) =>
|
||||
matchesPathRuleSubject(ruleArgs, path, {
|
||||
cwd: this.workspace.workspaceDir,
|
||||
cwd: this.workspaceConfig.workspaceDir,
|
||||
pathClass: this.env.pathClass,
|
||||
homeDir: this.env.homeDir,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@
|
|||
import { dirname } from 'pathe';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { AgentFileStat, ISessionAgentFileSystem } from '#/session/agentFs';
|
||||
import { ISessionAgentFileSystem } from '#/session/agentFs';
|
||||
import type { AgentFileStat } from '#/session/agentFs';
|
||||
import { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
import { ToolAccesses } from '#/agent/tool';
|
||||
import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
import { resolvePathAccessPath } from '#/_base/tools/policies/path-access';
|
||||
|
|
@ -62,15 +64,22 @@ export class WriteTool implements BuiltinTool<WriteInput> {
|
|||
readonly parameters: Record<string, unknown> = toInputJsonSchema(WriteInputSchema);
|
||||
|
||||
constructor(
|
||||
private readonly fs: ISessionAgentFileSystem,
|
||||
private readonly env: IHostEnvironment,
|
||||
private readonly workspace: WorkspaceConfig,
|
||||
@ISessionAgentFileSystem private readonly fs: ISessionAgentFileSystem,
|
||||
@IHostEnvironment private readonly env: IHostEnvironment,
|
||||
@ISessionWorkspaceContext private readonly workspaceCtx: ISessionWorkspaceContext,
|
||||
) {}
|
||||
|
||||
private get workspaceConfig(): WorkspaceConfig {
|
||||
return {
|
||||
workspaceDir: this.workspaceCtx.workDir,
|
||||
additionalDirs: this.workspaceCtx.additionalDirs,
|
||||
};
|
||||
}
|
||||
|
||||
resolveExecution(args: WriteInput): ToolExecution {
|
||||
const path = resolvePathAccessPath(args.path, {
|
||||
env: this.env,
|
||||
workspace: this.workspace,
|
||||
workspace: this.workspaceConfig,
|
||||
operation: 'write',
|
||||
});
|
||||
return {
|
||||
|
|
@ -80,7 +89,7 @@ export class WriteTool implements BuiltinTool<WriteInput> {
|
|||
approvalRule: literalRulePattern(this.name, path),
|
||||
matchesRule: (ruleArgs) =>
|
||||
matchesPathRuleSubject(ruleArgs, path, {
|
||||
cwd: this.workspace.workspaceDir,
|
||||
cwd: this.workspaceConfig.workspaceDir,
|
||||
pathClass: this.env.pathClass,
|
||||
homeDir: this.env.homeDir,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import {
|
|||
type PromptOrigin,
|
||||
} from '#/agent/contextMemory';
|
||||
import { IAgentLoopService } from '#/agent/loop';
|
||||
import { IAgentPermissionModeService } from '#/agent/permissionMode';
|
||||
import { IAgentReplayBuilderService } from '#/agent/replayBuilder';
|
||||
import { IAgentSystemReminderService } from '#/agent/systemReminder';
|
||||
import {
|
||||
|
|
@ -41,7 +40,6 @@ import {
|
|||
} from '#/agent/turn';
|
||||
import type { TelemetryProperties } from '#/app/telemetry';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import { IAgentRecordService, type AgentRecord } from '#/agent/record';
|
||||
import {
|
||||
IAgentGoalService,
|
||||
|
|
@ -174,8 +172,6 @@ export class AgentGoalService extends Disposable implements IAgentGoalService {
|
|||
@IAgentContextMemoryService private readonly context: IAgentContextMemoryService,
|
||||
@IAgentTurnService private readonly turnService: IAgentTurnService,
|
||||
@IAgentLoopService loopService: IAgentLoopService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
@IAgentPermissionModeService private readonly permissionMode: IAgentPermissionModeService,
|
||||
) {
|
||||
super();
|
||||
this._register(
|
||||
|
|
@ -258,11 +254,6 @@ export class AgentGoalService extends Disposable implements IAgentGoalService {
|
|||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this._register(toolRegistry.register(new CreateGoalTool(this, this.permissionMode)));
|
||||
this._register(toolRegistry.register(new GetGoalTool(this)));
|
||||
this._register(toolRegistry.register(new SetGoalBudgetTool(this)));
|
||||
this._register(toolRegistry.register(new UpdateGoalTool(this)));
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
|
|
|
|||
16
packages/agent-core-v2/src/agent/goal/goalTools.ts
Normal file
16
packages/agent-core-v2/src/agent/goal/goalTools.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* `goalTools` domain (L4) — `IAgentGoalToolsService` registration contract.
|
||||
*
|
||||
* Marker service: its implementation registers the built-in goal tools
|
||||
* (CreateGoal / GetGoal / SetGoalBudget / UpdateGoal) into the agent
|
||||
* `IAgentToolRegistryService` on construction. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export interface IAgentGoalToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IAgentGoalToolsService: ServiceIdentifier<IAgentGoalToolsService> =
|
||||
createDecorator<IAgentGoalToolsService>('agentGoalToolsService');
|
||||
49
packages/agent-core-v2/src/agent/goal/goalToolsService.ts
Normal file
49
packages/agent-core-v2/src/agent/goal/goalToolsService.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* `goalTools` domain (L4) — `IAgentGoalToolsService` implementation.
|
||||
*
|
||||
* Eager Agent-scope registration service for the built-in goal tools
|
||||
* (CreateGoal / GetGoal / SetGoalBudget / UpdateGoal). Each tool is a DI class
|
||||
* created via `IInstantiationService.createInstance` (they inject
|
||||
* `IAgentGoalService`, and `CreateGoal` also injects
|
||||
* `IAgentPermissionModeService`, themselves) and registered into the agent
|
||||
* `IAgentToolRegistryService`. Eager so the tools are registered when the Agent
|
||||
* scope is created, before the first turn.
|
||||
*
|
||||
* Split out of `AgentGoalService` so the tools can inject `IAgentGoalService`
|
||||
* without forming a constructor-instantiation cycle.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
||||
import { IAgentGoalToolsService } from './goalTools';
|
||||
import { CreateGoalTool } from '#/agent/goal/tools/create-goal';
|
||||
import { GetGoalTool } from '#/agent/goal/tools/get-goal';
|
||||
import { SetGoalBudgetTool } from '#/agent/goal/tools/set-goal-budget';
|
||||
import { UpdateGoalTool } from '#/agent/goal/tools/update-goal';
|
||||
|
||||
export class AgentGoalToolsService extends Disposable implements IAgentGoalToolsService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
) {
|
||||
super();
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(CreateGoalTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(GetGoalTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(SetGoalBudgetTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(UpdateGoalTool)));
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
IAgentGoalToolsService,
|
||||
AgentGoalToolsService,
|
||||
InstantiationType.Eager,
|
||||
'goal',
|
||||
);
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
/**
|
||||
* `goal` domain barrel — re-exports the goal contract (`goal`) and its scoped
|
||||
* service (`goalService`). Importing this barrel registers the `IAgentGoalService`
|
||||
* binding into the scope registry.
|
||||
* service (`goalService`), plus the `goalTools` registrar. Importing this barrel
|
||||
* registers the `IAgentGoalService` and `IAgentGoalToolsService` bindings into the
|
||||
* scope registry.
|
||||
*/
|
||||
|
||||
export * from './goal';
|
||||
export * from './goalService';
|
||||
export * from './goalTools';
|
||||
export * from './goalToolsService';
|
||||
export * from './types';
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ import { z } from 'zod';
|
|||
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
|
||||
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import type { IAgentPermissionModeService } from '#/agent/permissionMode';
|
||||
import { IAgentPermissionModeService } from '#/agent/permissionMode';
|
||||
import type { BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
|
||||
import type { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import DESCRIPTION from './create-goal.md?raw';
|
||||
import { goalForModel } from './serialize';
|
||||
|
||||
|
|
@ -38,8 +38,8 @@ export class CreateGoalTool implements BuiltinTool<CreateGoalToolInput> {
|
|||
readonly parameters: Record<string, unknown> = toInputJsonSchema(CreateGoalToolInputSchema);
|
||||
|
||||
constructor(
|
||||
private readonly goal: IAgentGoalService,
|
||||
private readonly permissionMode: IAgentPermissionModeService,
|
||||
@IAgentGoalService private readonly goal: IAgentGoalService,
|
||||
@IAgentPermissionModeService private readonly permissionMode: IAgentPermissionModeService,
|
||||
) {}
|
||||
|
||||
resolveExecution(args: CreateGoalToolInput): ToolExecution {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { z } from 'zod';
|
|||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import type { BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
|
||||
import type { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import DESCRIPTION from './get-goal.md?raw';
|
||||
import { goalResultForModel } from './serialize';
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ export class GetGoalTool implements BuiltinTool<GetGoalToolInput> {
|
|||
readonly description: string = DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(GetGoalToolInputSchema);
|
||||
|
||||
constructor(private readonly goal: IAgentGoalService) {}
|
||||
constructor(@IAgentGoalService private readonly goal: IAgentGoalService) {}
|
||||
|
||||
resolveExecution(_args: GetGoalToolInput): ToolExecution {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { z } from 'zod';
|
|||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import type { BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
|
||||
import type { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import type { GoalBudgetLimits } from '#/agent/goal/types';
|
||||
import DESCRIPTION from './set-goal-budget.md?raw';
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ export class SetGoalBudgetTool implements BuiltinTool<SetGoalBudgetToolInput> {
|
|||
readonly description: string = DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(SetGoalBudgetToolInputSchema);
|
||||
|
||||
constructor(private readonly goal: IAgentGoalService) {}
|
||||
constructor(@IAgentGoalService private readonly goal: IAgentGoalService) {}
|
||||
|
||||
resolveExecution(args: SetGoalBudgetToolInput): ToolExecution {
|
||||
const normalizedArgs = normalizeBudgetInput(args);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { z } from 'zod';
|
|||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import type { BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
|
||||
import type { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import DESCRIPTION from './update-goal.md?raw';
|
||||
|
||||
export const UpdateGoalToolInputSchema = z
|
||||
|
|
@ -32,7 +32,7 @@ export class UpdateGoalTool implements BuiltinTool<UpdateGoalToolInput> {
|
|||
readonly description: string = DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(UpdateGoalToolInputSchema);
|
||||
|
||||
constructor(private readonly goal: IAgentGoalService) {}
|
||||
constructor(@IAgentGoalService private readonly goal: IAgentGoalService) {}
|
||||
|
||||
resolveExecution(args: UpdateGoalToolInput): ToolExecution {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
/**
|
||||
* `plan` domain barrel — re-exports the plan contract (`plan`) and its scoped
|
||||
* service (`planService`). Importing this barrel registers the `IAgentPlanService`
|
||||
* binding into the scope registry.
|
||||
* service (`planService`), plus the `planTools` registrar. Importing this barrel
|
||||
* registers the `IAgentPlanService` and `IAgentPlanToolsService` bindings into the
|
||||
* scope registry.
|
||||
*/
|
||||
|
||||
export * from './plan';
|
||||
export * from './planService';
|
||||
export * from './planTools';
|
||||
export * from './planToolsService';
|
||||
|
|
|
|||
|
|
@ -11,24 +11,16 @@ import {
|
|||
import {
|
||||
Disposable,
|
||||
} from "#/_base/di";
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import { generateHeroSlug } from "#/_base/utils/hero-slug";
|
||||
import { IAgentContextMemoryService, type ContextMessage } from '#/agent/contextMemory';
|
||||
import { IAgentContextInjectorService } from '#/agent/contextInjector';
|
||||
import { ISessionAgentFileSystem } from '#/session/agentFs';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
import type { ToolInputDisplay } from '@moonshot-ai/protocol';
|
||||
import type { ExecutableToolResult } from '#/agent/tool';
|
||||
import { EnterPlanModeInputSchema } from '#/agent/plan/tools/enter-plan-mode';
|
||||
import ENTER_PLAN_MODE_DESCRIPTION from './tools/enter-plan-mode.md?raw';
|
||||
import {
|
||||
ExitPlanModeInputSchema,
|
||||
type ExitPlanModeInput,
|
||||
} from '#/agent/plan/tools/exit-plan-mode';
|
||||
import EXIT_PLAN_MODE_DESCRIPTION from './tools/exit-plan-mode.md?raw';
|
||||
import { type ExitPlanModeInput } from '#/agent/plan/tools/exit-plan-mode';
|
||||
import {
|
||||
IAgentPlanService,
|
||||
type PlanData,
|
||||
|
|
@ -66,7 +58,6 @@ export class AgentPlanService extends Disposable implements IAgentPlanService {
|
|||
@IAgentRecordService private readonly record: IAgentRecordService,
|
||||
@ISessionAgentFileSystem private readonly agentFs: ISessionAgentFileSystem,
|
||||
@IAgentProfileService private readonly profile: IAgentProfileService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
@IAgentContextInjectorService dynamicInjector: IAgentContextInjectorService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
) {
|
||||
|
|
@ -96,37 +87,6 @@ export class AgentPlanService extends Disposable implements IAgentPlanService {
|
|||
}),
|
||||
);
|
||||
|
||||
this._register(
|
||||
toolRegistry.register({
|
||||
name: 'EnterPlanMode',
|
||||
description: ENTER_PLAN_MODE_DESCRIPTION,
|
||||
parameters: toInputJsonSchema(EnterPlanModeInputSchema),
|
||||
resolveExecution: () => {
|
||||
return {
|
||||
description: 'Requesting to enter plan mode',
|
||||
approvalRule: 'EnterPlanMode',
|
||||
execute: async () => this.enterPlanModeToolResult(),
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
this._register(
|
||||
toolRegistry.register({
|
||||
name: 'ExitPlanMode',
|
||||
description: EXIT_PLAN_MODE_DESCRIPTION,
|
||||
parameters: toInputJsonSchema(ExitPlanModeInputSchema),
|
||||
resolveExecution: async (args: unknown) => {
|
||||
const input = args as ExitPlanModeInput;
|
||||
return {
|
||||
description: 'Presenting plan and exiting plan mode',
|
||||
display: await this.resolvePlanReviewDisplay(input),
|
||||
approvalRule: 'ExitPlanMode',
|
||||
execute: async () => this.exitPlanModeToolResult(input),
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let wasActive = false;
|
||||
this._register(
|
||||
dynamicInjector.register(PLAN_MODE_INJECTION_VARIANT, async ({ lastInjectedAt: injectedAt }) => {
|
||||
|
|
|
|||
16
packages/agent-core-v2/src/agent/plan/planTools.ts
Normal file
16
packages/agent-core-v2/src/agent/plan/planTools.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* `planTools` domain (L4) — `IAgentPlanToolsService` registration contract.
|
||||
*
|
||||
* Marker service: its implementation registers the built-in plan-mode tools
|
||||
* (EnterPlanMode / ExitPlanMode) into the agent `IAgentToolRegistryService` on
|
||||
* construction. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export interface IAgentPlanToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IAgentPlanToolsService: ServiceIdentifier<IAgentPlanToolsService> =
|
||||
createDecorator<IAgentPlanToolsService>('agentPlanToolsService');
|
||||
45
packages/agent-core-v2/src/agent/plan/planToolsService.ts
Normal file
45
packages/agent-core-v2/src/agent/plan/planToolsService.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* `planTools` domain (L4) — `IAgentPlanToolsService` implementation.
|
||||
*
|
||||
* Eager Agent-scope registration service for the built-in plan-mode tools
|
||||
* (EnterPlanMode / ExitPlanMode). Each tool is a DI class created via
|
||||
* `IInstantiationService.createInstance` (they inject `IAgentPlanService` and
|
||||
* `ITelemetryService` themselves) and registered into the agent
|
||||
* `IAgentToolRegistryService`. Eager so the tools are registered when the Agent
|
||||
* scope is created, before the first turn.
|
||||
*
|
||||
* Split out of `AgentPlanService` so the tools can inject `IAgentPlanService`
|
||||
* without forming a constructor-instantiation cycle; this also replaces the
|
||||
* previous inline object-literal registrations with real DI tool classes.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
||||
import { IAgentPlanToolsService } from './planTools';
|
||||
import { EnterPlanModeTool } from '#/agent/plan/tools/enter-plan-mode';
|
||||
import { ExitPlanModeTool } from '#/agent/plan/tools/exit-plan-mode';
|
||||
|
||||
export class AgentPlanToolsService extends Disposable implements IAgentPlanToolsService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
) {
|
||||
super();
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(EnterPlanModeTool)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(ExitPlanModeTool)));
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
IAgentPlanToolsService,
|
||||
AgentPlanToolsService,
|
||||
InstantiationType.Eager,
|
||||
'plan',
|
||||
);
|
||||
|
|
@ -10,8 +10,8 @@ import { z } from 'zod';
|
|||
import type { BuiltinTool } from '#/agent/tool';
|
||||
import type { ToolExecution } from '#/agent/tool';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import type { ITelemetryService } from '#/app/telemetry';
|
||||
import type { IAgentPlanService } from '#/agent/plan/plan';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentPlanService } from '#/agent/plan/plan';
|
||||
import DESCRIPTION from './enter-plan-mode.md?raw';
|
||||
|
||||
// ── Input schema ─────────────────────────────────────────────────────
|
||||
|
|
@ -25,8 +25,8 @@ export class EnterPlanModeTool implements BuiltinTool<EnterPlanModeInput> {
|
|||
readonly parameters: Record<string, unknown> = toInputJsonSchema(EnterPlanModeInputSchema);
|
||||
|
||||
constructor(
|
||||
private readonly planMode: IAgentPlanService,
|
||||
private readonly telemetry: ITelemetryService,
|
||||
@IAgentPlanService private readonly planMode: IAgentPlanService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
) {}
|
||||
|
||||
resolveExecution(_args: EnterPlanModeInput): ToolExecution {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ import { z } from 'zod';
|
|||
import type { BuiltinTool } from '#/agent/tool';
|
||||
import type { ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import type { ITelemetryService } from '#/app/telemetry';
|
||||
import type { IAgentPlanService, PlanData } from '#/agent/plan/plan';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentPlanService } from '#/agent/plan/plan';
|
||||
import type { PlanData } from '#/agent/plan/plan';
|
||||
import DESCRIPTION from './exit-plan-mode.md?raw';
|
||||
|
||||
// ── Input schema ─────────────────────────────────────────────────────
|
||||
|
|
@ -85,8 +86,8 @@ export class ExitPlanModeTool implements BuiltinTool<ExitPlanModeInput> {
|
|||
readonly parameters: Record<string, unknown> = toInputJsonSchema(ExitPlanModeInputSchema);
|
||||
|
||||
constructor(
|
||||
private readonly planMode: IAgentPlanService,
|
||||
private readonly telemetry: ITelemetryService,
|
||||
@IAgentPlanService private readonly planMode: IAgentPlanService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
) {}
|
||||
|
||||
async resolveExecution(args: ExitPlanModeInput): Promise<ToolExecution> {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
/**
|
||||
* `questionTools` domain (L7) — `IAgentQuestionToolsService` implementation.
|
||||
*
|
||||
* Registers the built-in `AskUserQuestion` tool into the agent `IAgentToolRegistryService`
|
||||
* on construction, wiring it to the session `ISessionQuestionService` (ask-user
|
||||
* broker), the agent `IAgentBackgroundService` (background-question lifecycle) and
|
||||
* `ITelemetryService`. Bound at Agent scope.
|
||||
* Eager Agent-scope registration service for the built-in `AskUserQuestion` tool.
|
||||
* The tool is a DI class created via `IInstantiationService.createInstance` and
|
||||
* registered into the agent `IAgentToolRegistryService`. Eager so the tool is
|
||||
* registered when the Agent scope is created, before the first turn.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentBackgroundService } from '#/agent/background';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
||||
import { ISessionQuestionService } from '#/session/question/question';
|
||||
import { IAgentQuestionToolsService } from './questionTools';
|
||||
import { AskUserQuestionTool } from '#/agent/questionTools/tools/ask-user';
|
||||
|
||||
export class AgentQuestionToolsService implements IAgentQuestionToolsService {
|
||||
export class AgentQuestionToolsService extends Disposable implements IAgentQuestionToolsService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
@ISessionQuestionService question: ISessionQuestionService,
|
||||
@IAgentBackgroundService background: IAgentBackgroundService,
|
||||
@ITelemetryService telemetry: ITelemetryService,
|
||||
) {
|
||||
toolRegistry.register(new AskUserQuestionTool(question, background, telemetry));
|
||||
super();
|
||||
this._register(
|
||||
toolRegistry.register(instantiationService.createInstance(AskUserQuestionTool)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +34,6 @@ registerScopedService(
|
|||
LifecycleScope.Agent,
|
||||
IAgentQuestionToolsService,
|
||||
AgentQuestionToolsService,
|
||||
InstantiationType.Delayed,
|
||||
InstantiationType.Eager,
|
||||
'questionTools',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import { QuestionBackgroundTask, type IAgentBackgroundService } from '#/agent/background';
|
||||
import type { ITelemetryService, TelemetryProperties } from '#/app/telemetry';
|
||||
import { QuestionBackgroundTask, IAgentBackgroundService } from '#/agent/background';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import type { TelemetryProperties } from '#/app/telemetry';
|
||||
import type {
|
||||
BuiltinTool,
|
||||
ExecutableToolContext,
|
||||
|
|
@ -20,8 +21,8 @@ import type {
|
|||
ToolExecution,
|
||||
} from '#/agent/tool';
|
||||
|
||||
import { ISessionQuestionService } from '#/session/question/question';
|
||||
import type {
|
||||
ISessionQuestionService,
|
||||
QuestionAnswers,
|
||||
QuestionAnswerMethod,
|
||||
QuestionResponse,
|
||||
|
|
@ -99,9 +100,9 @@ export class AskUserQuestionTool implements BuiltinTool<AskUserQuestionInput> {
|
|||
);
|
||||
|
||||
constructor(
|
||||
private readonly question: ISessionQuestionService,
|
||||
private readonly background: IAgentBackgroundService,
|
||||
private readonly telemetry: ITelemetryService,
|
||||
@ISessionQuestionService private readonly question: ISessionQuestionService,
|
||||
@IAgentBackgroundService private readonly background: IAgentBackgroundService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
) {}
|
||||
|
||||
resolveExecution(args: AskUserQuestionInput): ToolExecution {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
|||
import { IAgentBackgroundService } from '#/agent/background';
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory';
|
||||
import { IAgentContextSizeService } from '#/agent/contextSize';
|
||||
import { IAgentFileToolsService } from '#/agent/fileTools';
|
||||
import { IAgentFullCompactionService } from '#/agent/fullCompaction';
|
||||
import { IAgentGoalService } from '#/agent/goal';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
|
|
@ -14,16 +13,11 @@ import { userCancellationReason } from '#/_base/utils/abort';
|
|||
import { IAgentPermissionGate } from '#/agent/permissionGate';
|
||||
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';
|
||||
import { IAgentPlanService } from '#/agent/plan';
|
||||
import { IExecContext } from '#/session/execContext';
|
||||
import { expandCommandArguments, IPluginService } from '#/app/plugin';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { IAgentPromptService } from '#/agent/prompt';
|
||||
import { IAgentQuestionToolsService } from '#/agent/questionTools';
|
||||
import { ISessionMetadata, type SessionMetaPatch } from '#/session/sessionMetadata';
|
||||
import { BashTool, IAgentShellToolsService } from '#/agent/shellTools';
|
||||
import { IAgentSkillService } from '#/agent/skill';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import { IAgentToolService } from '#/agent/agentTool';
|
||||
import { IAgentSwarmService } from '#/agent/swarm';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
|
@ -31,7 +25,6 @@ import type { ToolUpdate } from '#/agent/tool';
|
|||
import { IAgentTurnService } from '#/agent/turn';
|
||||
import { IAgentUsageService } from '#/agent/usage';
|
||||
import { IAgentUserToolService } from '#/agent/userTool';
|
||||
import { IAgentWebService } from '#/agent/web';
|
||||
import type {
|
||||
ActivatePluginCommandPayload,
|
||||
ActivateSkillPayload,
|
||||
|
|
@ -82,21 +75,14 @@ export class AgentRPCService implements IAgentRPCService {
|
|||
@IAgentFullCompactionService private readonly fullCompaction: IAgentFullCompactionService,
|
||||
@IAgentUserToolService private readonly userTools: IAgentUserToolService,
|
||||
@IAgentToolRegistryService private readonly toolRegistry: IAgentToolRegistryService,
|
||||
@IAgentFileToolsService private readonly fileTools: IAgentFileToolsService,
|
||||
@IAgentShellToolsService private readonly shellTools: IAgentShellToolsService,
|
||||
@ISessionProcessRunner private readonly processRunner: ISessionProcessRunner,
|
||||
@IExecContext private readonly execContext: IExecContext,
|
||||
@IAgentBackgroundService private readonly background: IAgentBackgroundService,
|
||||
@IAgentContextMemoryService private readonly context: IAgentContextMemoryService,
|
||||
@IAgentContextSizeService private readonly contextSize: IAgentContextSizeService,
|
||||
@IAgentSkillService private readonly skills: IAgentSkillService,
|
||||
@IAgentToolService private readonly agentTool: IAgentToolService,
|
||||
@IAgentUsageService private readonly usage: IAgentUsageService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
@IAgentGoalService private readonly goal: IAgentGoalService,
|
||||
@IAgentRecordService private readonly record: IAgentRecordService,
|
||||
@IAgentQuestionToolsService private readonly questionTools: IAgentQuestionToolsService,
|
||||
@IAgentWebService private readonly web: IAgentWebService,
|
||||
@IPluginService private readonly plugins: IPluginService,
|
||||
@ISessionMetadata private readonly metadata: ISessionMetadata,
|
||||
) { }
|
||||
|
|
@ -111,10 +97,10 @@ export class AgentRPCService implements IAgentRPCService {
|
|||
}
|
||||
|
||||
private ensureBashTool() {
|
||||
const existing = this.toolRegistry.resolve('Bash');
|
||||
if (existing !== undefined) return existing;
|
||||
const bash = new BashTool(this.processRunner, this.execContext, this.background);
|
||||
this.toolRegistry.register(bash);
|
||||
const bash = this.toolRegistry.resolve('Bash');
|
||||
if (bash === undefined) {
|
||||
throw new Error('Bash tool is not registered.');
|
||||
}
|
||||
return bash;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,39 +1,30 @@
|
|||
/**
|
||||
* `shellTools` domain (L4) — `IAgentShellToolsService` implementation.
|
||||
*
|
||||
* Registers the built-in Bash tool into the agent `IAgentToolRegistryService` on
|
||||
* construction, wiring it to the session `ISessionProcessRunner` (process spawn),
|
||||
* `IHostEnvironment` (OS / shell probe), `IExecContext` (session cwd) and
|
||||
* `IAgentBackgroundService` (background-task lifecycle). Bound at Agent scope.
|
||||
* Eager Agent-scope registration service for the built-in Bash tool. The tool is
|
||||
* a DI class created via `IInstantiationService.createInstance` and registered
|
||||
* into the agent `IAgentToolRegistryService`. Eager so Bash is registered when
|
||||
* the Agent scope is created, before the first turn.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentBackgroundService } from '#/agent/background';
|
||||
import { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { IExecContext } from '#/session/execContext';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
||||
import { IAgentShellToolsService } from './shellTools';
|
||||
import { BashTool } from '#/agent/shellTools/tools/bash';
|
||||
|
||||
export class AgentShellToolsService implements IAgentShellToolsService {
|
||||
export class AgentShellToolsService extends Disposable implements IAgentShellToolsService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
@ISessionProcessRunner runner: ISessionProcessRunner,
|
||||
@IHostEnvironment env: IHostEnvironment,
|
||||
@IExecContext ctx: IExecContext,
|
||||
@IAgentBackgroundService background: IAgentBackgroundService,
|
||||
@IAgentProfileService profile: IAgentProfileService,
|
||||
) {
|
||||
toolRegistry.register(new BashTool(runner, env, ctx, background, {
|
||||
allowBackground: () =>
|
||||
profile.isToolActive('TaskOutput') && profile.isToolActive('TaskStop'),
|
||||
}));
|
||||
super();
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(BashTool)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +32,6 @@ registerScopedService(
|
|||
LifecycleScope.Agent,
|
||||
IAgentShellToolsService,
|
||||
AgentShellToolsService,
|
||||
InstantiationType.Delayed,
|
||||
InstantiationType.Eager,
|
||||
'shellTools',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,11 +32,12 @@
|
|||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ProcessBackgroundTask } from '#/agent/background';
|
||||
import type { IAgentBackgroundService } from '#/agent/background';
|
||||
import type { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import type { IExecContext } from '#/session/execContext';
|
||||
import type { IProcess, ISessionProcessRunner } from '#/session/process';
|
||||
import { ProcessBackgroundTask, IAgentBackgroundService } from '#/agent/background';
|
||||
import { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { IExecContext } from '#/session/execContext';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import type { IProcess } from '#/session/process';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import type { BuiltinTool, ExecutableToolResult, ToolExecution, ToolUpdate } from '#/agent/tool';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import { literalRulePattern, matchesGlobRuleSubject } from '#/_base/tools/support/rule-match';
|
||||
|
|
@ -164,22 +165,22 @@ export class BashTool implements BuiltinTool<BashInput> {
|
|||
private readonly isWindowsBash: boolean;
|
||||
|
||||
private readonly renderedDescription: string;
|
||||
private readonly allowBackground: () => boolean;
|
||||
|
||||
constructor(
|
||||
private readonly runner: ISessionProcessRunner,
|
||||
private readonly env: IHostEnvironment,
|
||||
private readonly ctx: IExecContext,
|
||||
private readonly background: IAgentBackgroundService,
|
||||
options?: {
|
||||
allowBackground?: () => boolean;
|
||||
},
|
||||
@ISessionProcessRunner private readonly runner: ISessionProcessRunner,
|
||||
@IHostEnvironment private readonly env: IHostEnvironment,
|
||||
@IExecContext private readonly ctx: IExecContext,
|
||||
@IAgentBackgroundService private readonly background: IAgentBackgroundService,
|
||||
@IAgentProfileService private readonly profile: IAgentProfileService,
|
||||
) {
|
||||
this.isWindowsBash = this.env.osKind === 'Windows';
|
||||
this.allowBackground = options?.allowBackground ?? (() => true);
|
||||
this.renderedDescription = renderBashDescription(this.env.shellName);
|
||||
}
|
||||
|
||||
private allowBackground(): boolean {
|
||||
return this.profile.isToolActive('TaskOutput') && this.profile.isToolActive('TaskStop');
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.allowBackground()
|
||||
? this.renderedDescription
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
/**
|
||||
* `skill` domain barrel — re-exports the agent skill contract and its
|
||||
* Agent-scope service. Importing this barrel registers the
|
||||
* `IAgentSkillService` binding into the scope registry.
|
||||
* Agent-scope service, plus the `skillTools` registrar. Importing this barrel
|
||||
* registers the `IAgentSkillService` and `IAgentSkillToolsService` bindings into
|
||||
* the scope registry.
|
||||
*/
|
||||
|
||||
export * from './skill';
|
||||
export * from './skillService';
|
||||
export * from './skillTools';
|
||||
export * from './skillToolsService';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { createDecorator } from "#/_base/di";
|
||||
import type { SkillActivationOrigin } from '#/agent/contextMemory';
|
||||
import type { Turn } from '#/agent/turn';
|
||||
|
||||
export interface SkillActivationInput {
|
||||
|
|
@ -10,6 +11,14 @@ export interface IAgentSkillService {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
activate(input: SkillActivationInput): Promise<Turn>;
|
||||
|
||||
/**
|
||||
* Records a model-tool skill activation (an inline skill loaded through the
|
||||
* `Skill` tool) without opening a new turn — the tool builds and steers its
|
||||
* own message into the current turn. Publishes the activation and emits
|
||||
* telemetry, matching the user-slash `activate` path's side effects.
|
||||
*/
|
||||
recordModelToolActivation(origin: SkillActivationOrigin): void;
|
||||
}
|
||||
|
||||
export const IAgentSkillService =
|
||||
|
|
|
|||
|
|
@ -11,12 +11,10 @@ import { ErrorCodes, KimiError } from "#/errors";
|
|||
import { isUserActivatableSkillType, type SkillDefinition } from '#/app/globalSkillCatalog/types';
|
||||
import { IAgentPromptService } from '#/agent/prompt';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import type { Turn } from '#/agent/turn';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
import { IAgentSkillService, type SkillActivationInput } from './skill';
|
||||
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog';
|
||||
import { SkillTool, type SkillToolDeps } from '#/agent/skill/tools/skill';
|
||||
|
||||
declare module '#/agent/wireRecord' {
|
||||
interface WireRecordMap {
|
||||
|
|
@ -34,7 +32,6 @@ export class AgentSkillService extends Disposable implements IAgentSkillService
|
|||
@IAgentPromptService private readonly prompt: IAgentPromptService,
|
||||
@IAgentRecordService private readonly records: IAgentRecordService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
) {
|
||||
super();
|
||||
this._register(
|
||||
|
|
@ -44,7 +41,6 @@ export class AgentSkillService extends Disposable implements IAgentSkillService
|
|||
},
|
||||
}),
|
||||
);
|
||||
this._register(toolRegistry.register(new SkillTool(this.skillToolDeps())));
|
||||
}
|
||||
|
||||
async activate(input: SkillActivationInput): Promise<Turn> {
|
||||
|
|
@ -90,12 +86,8 @@ export class AgentSkillService extends Disposable implements IAgentSkillService
|
|||
)!;
|
||||
}
|
||||
|
||||
protected skillToolDeps(): SkillToolDeps {
|
||||
return {
|
||||
catalog: this.skillCatalog,
|
||||
prompt: this.prompt,
|
||||
recordActivation: (origin) => this.recordActivation(origin),
|
||||
};
|
||||
recordModelToolActivation(origin: SkillActivationOrigin): void {
|
||||
this.recordActivation(origin);
|
||||
}
|
||||
|
||||
private recordActivation(
|
||||
|
|
|
|||
16
packages/agent-core-v2/src/agent/skill/skillTools.ts
Normal file
16
packages/agent-core-v2/src/agent/skill/skillTools.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* `skillTools` domain (L4) — `IAgentSkillToolsService` registration contract.
|
||||
*
|
||||
* Marker service: its implementation registers the built-in `Skill` collaboration
|
||||
* tool into the agent `IAgentToolRegistryService` on construction. Bound at Agent
|
||||
* scope.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export interface IAgentSkillToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IAgentSkillToolsService: ServiceIdentifier<IAgentSkillToolsService> =
|
||||
createDecorator<IAgentSkillToolsService>('agentSkillToolsService');
|
||||
43
packages/agent-core-v2/src/agent/skill/skillToolsService.ts
Normal file
43
packages/agent-core-v2/src/agent/skill/skillToolsService.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* `skillTools` domain (L4) — `IAgentSkillToolsService` implementation.
|
||||
*
|
||||
* Eager Agent-scope registration service for the built-in `Skill` collaboration
|
||||
* tool. The tool is a DI class created via `IInstantiationService.createInstance`
|
||||
* (it injects `ISessionSkillCatalog`, `IAgentPromptService` and
|
||||
* `IAgentSkillService` itself) and registered into the agent
|
||||
* `IAgentToolRegistryService`. Eager so the tool is registered when the Agent
|
||||
* scope is created, before the first turn.
|
||||
*
|
||||
* Split out of `AgentSkillService` so the tool can inject `IAgentSkillService`
|
||||
* without forming a constructor-instantiation cycle, and so the previous
|
||||
* `recordActivation` closure can be replaced by a direct service call.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
||||
import { IAgentSkillToolsService } from './skillTools';
|
||||
import { SkillTool } from '#/agent/skill/tools/skill';
|
||||
|
||||
export class AgentSkillToolsService extends Disposable implements IAgentSkillToolsService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
) {
|
||||
super();
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(SkillTool)));
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
IAgentSkillToolsService,
|
||||
AgentSkillToolsService,
|
||||
InstantiationType.Eager,
|
||||
'skill',
|
||||
);
|
||||
|
|
@ -23,12 +23,13 @@ import { randomUUID } from 'node:crypto';
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { ContextMessage, SkillActivationOrigin } from '#/agent/contextMemory';
|
||||
import type { IAgentPromptService } from '#/agent/prompt';
|
||||
import { IAgentPromptService } from '#/agent/prompt';
|
||||
import { IAgentSkillService } from '#/agent/skill/skill';
|
||||
import { renderModelToolSkillPrompt } from '#/agent/skill/prompt';
|
||||
import type { BuiltinTool } from '#/agent/tool';
|
||||
import type { ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
import { isInlineSkillType } from '#/app/globalSkillCatalog/types';
|
||||
import type { ISessionSkillCatalog } from '#/session/sessionSkillCatalog';
|
||||
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog';
|
||||
import { renderPrompt } from '#/_base/utils/render-prompt';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import { matchesGlobRuleSubject } from '#/_base/tools/support/rule-match';
|
||||
|
|
@ -61,24 +62,6 @@ export const SkillToolInputSchema: z.ZodType<SkillToolInput> = z.object({
|
|||
args: z.string().optional(),
|
||||
});
|
||||
|
||||
export interface SkillToolOptions {
|
||||
/**
|
||||
* Current inline skill recursion depth.
|
||||
*/
|
||||
readonly queryDepth?: number;
|
||||
/**
|
||||
* Alias for `queryDepth`. Kept so older call sites can seed the
|
||||
* inline recursion depth without knowing the internal field name.
|
||||
*/
|
||||
readonly initialQueryDepth?: number;
|
||||
}
|
||||
|
||||
export interface SkillToolDeps {
|
||||
readonly catalog: ISessionSkillCatalog;
|
||||
readonly prompt: IAgentPromptService;
|
||||
readonly recordActivation: (origin: SkillActivationOrigin) => void;
|
||||
}
|
||||
|
||||
export class SkillTool implements BuiltinTool<SkillToolInput> {
|
||||
readonly name = 'Skill';
|
||||
readonly description: string = renderPrompt(skillDescriptionTemplate, {
|
||||
|
|
@ -86,9 +69,17 @@ export class SkillTool implements BuiltinTool<SkillToolInput> {
|
|||
});
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(SkillToolInputSchema);
|
||||
|
||||
/**
|
||||
* Current inline-skill recursion depth. Zero for the root tool; set on clones
|
||||
* produced by `withInitialQueryDepth` so a Skill→Skill chain cannot recurse
|
||||
* past `MAX_SKILL_QUERY_DEPTH`.
|
||||
*/
|
||||
private queryDepth: number = 0;
|
||||
|
||||
constructor(
|
||||
private readonly deps: SkillToolDeps,
|
||||
private readonly options: SkillToolOptions = {},
|
||||
@ISessionSkillCatalog private readonly catalog: ISessionSkillCatalog,
|
||||
@IAgentPromptService private readonly prompt: IAgentPromptService,
|
||||
@IAgentSkillService private readonly skill: IAgentSkillService,
|
||||
) {}
|
||||
|
||||
resolveExecution(args: SkillToolInput): ToolExecution {
|
||||
|
|
@ -102,34 +93,35 @@ export class SkillTool implements BuiltinTool<SkillToolInput> {
|
|||
}
|
||||
|
||||
withInitialQueryDepth(initialQueryDepth: number): SkillTool {
|
||||
return new SkillTool(this.deps, {
|
||||
...this.options,
|
||||
initialQueryDepth,
|
||||
});
|
||||
const clone = new SkillTool(this.catalog, this.prompt, this.skill);
|
||||
clone.queryDepth = initialQueryDepth;
|
||||
return clone;
|
||||
}
|
||||
|
||||
private async execution(args: SkillToolInput): Promise<ExecutableToolResult> {
|
||||
return executeModelSkill(this.deps, args, this.options);
|
||||
return executeModelSkill(this.catalog, this.prompt, this.skill, args, this.queryDepth);
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeModelSkill(
|
||||
deps: SkillToolDeps,
|
||||
catalog: ISessionSkillCatalog,
|
||||
prompt: IAgentPromptService,
|
||||
skillService: IAgentSkillService,
|
||||
args: SkillToolInput,
|
||||
options: SkillToolOptions,
|
||||
queryDepth: number,
|
||||
): Promise<ExecutableToolResult> {
|
||||
// Recursion hard cap. Once `currentDepth` has reached
|
||||
// MAX_SKILL_QUERY_DEPTH, firing another Skill call would push the
|
||||
// child to depth+1 which violates the invariant. Throw a structured
|
||||
// error (rather than a soft tool-error) so Runtime can distinguish
|
||||
// "LLM mis-dispatched" from "safety net fired".
|
||||
const currentDepth = options.initialQueryDepth ?? options.queryDepth ?? 0;
|
||||
const currentDepth = queryDepth;
|
||||
if (currentDepth >= MAX_SKILL_QUERY_DEPTH) {
|
||||
throw new NestedSkillTooDeepError(MAX_SKILL_QUERY_DEPTH, args.skill);
|
||||
}
|
||||
|
||||
await deps.catalog.ready;
|
||||
const skill = deps.catalog.catalog.getSkill(args.skill);
|
||||
await catalog.ready;
|
||||
const skill = catalog.catalog.getSkill(args.skill);
|
||||
if (skill === undefined) {
|
||||
return errorResult(`Skill "${args.skill}" not found in the current skill listing.`);
|
||||
}
|
||||
|
|
@ -158,7 +150,7 @@ export async function executeModelSkill(
|
|||
skillPath: skill.path,
|
||||
skillSource: skill.source,
|
||||
};
|
||||
const skillContent = deps.catalog.catalog.renderSkillPrompt(skill, skillArgs);
|
||||
const skillContent = catalog.catalog.renderSkillPrompt(skill, skillArgs);
|
||||
const message: ContextMessage = {
|
||||
role: 'user',
|
||||
content: [
|
||||
|
|
@ -177,8 +169,8 @@ export async function executeModelSkill(
|
|||
toolCalls: [],
|
||||
origin,
|
||||
};
|
||||
deps.recordActivation(origin);
|
||||
deps.prompt.steer(message);
|
||||
skillService.recordModelToolActivation(origin);
|
||||
prompt.steer(message);
|
||||
return {
|
||||
output: `Skill "${skill.name}" loaded inline. Follow its instructions.`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
/**
|
||||
* `swarm` domain barrel — re-exports the swarm contract (`swarm`) and its
|
||||
* scoped service (`swarmService`). Importing this barrel registers the
|
||||
* `IAgentSwarmService` binding into the scope registry.
|
||||
* scoped service (`swarmService`), plus the `swarmTools` registrar. Importing
|
||||
* this barrel registers the `IAgentSwarmService` and `IAgentSwarmToolsService`
|
||||
* bindings into the scope registry.
|
||||
*/
|
||||
|
||||
export * from './swarm';
|
||||
export * from './swarmService';
|
||||
export * from './swarmTools';
|
||||
export * from './swarmToolsService';
|
||||
|
|
|
|||
|
|
@ -2,24 +2,18 @@
|
|||
* `swarm` domain (L4) — `IAgentSwarmService` implementation.
|
||||
*
|
||||
* Tracks swarm-mode enter/exit (mirroring it into `wireRecord` and
|
||||
* `systemReminder`), auto-exits on turn end, and registers the `AgentSwarm`
|
||||
* tool bound to this agent as the caller. Bound at Agent scope; reads its
|
||||
* identity through `scopeContext`, registers the tool through `toolRegistry`,
|
||||
* and delegates batch runs to the Session-scoped `sessionSwarm` service.
|
||||
* `systemReminder`) and auto-exits on turn end. Bound at Agent scope. The
|
||||
* `AgentSwarm` tool is registered separately by `AgentSwarmToolsService`.
|
||||
*/
|
||||
|
||||
import { Disposable } from '#/_base/di';
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext';
|
||||
import { IAgentSystemReminderService } from '#/agent/systemReminder';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import { IAgentTurnService } from '#/agent/turn';
|
||||
import { ISessionSwarmService } from '#/session/swarm';
|
||||
import SWARM_MODE_ENTER_REMINDER from './enter-reminder.md?raw';
|
||||
import SWARM_MODE_EXIT_REMINDER from './exit-reminder.md?raw';
|
||||
import { AgentSwarmTool } from '#/agent/swarm/tools/agent-swarm';
|
||||
import {
|
||||
IAgentSwarmService,
|
||||
type SwarmModeTrigger,
|
||||
|
|
@ -43,9 +37,6 @@ export class AgentSwarmService extends Disposable implements IAgentSwarmService
|
|||
@IAgentRecordService private readonly record: IAgentRecordService,
|
||||
@IAgentSystemReminderService private readonly reminders: IAgentSystemReminderService,
|
||||
@IAgentTurnService turnService: IAgentTurnService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
@IAgentScopeContext ctx: IAgentScopeContext,
|
||||
@ISessionSwarmService swarmService: ISessionSwarmService,
|
||||
) {
|
||||
super();
|
||||
this._register(
|
||||
|
|
@ -71,11 +62,6 @@ export class AgentSwarmService extends Disposable implements IAgentSwarmService
|
|||
return done;
|
||||
}),
|
||||
);
|
||||
this._register(
|
||||
toolRegistry.register(
|
||||
new AgentSwarmTool({ swarmService, callerAgentId: ctx.agentId }, this),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
enter(trigger: SwarmModeTrigger): void {
|
||||
|
|
|
|||
16
packages/agent-core-v2/src/agent/swarm/swarmTools.ts
Normal file
16
packages/agent-core-v2/src/agent/swarm/swarmTools.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* `swarmTools` domain (L4) — `IAgentSwarmToolsService` registration contract.
|
||||
*
|
||||
* Marker service: its implementation registers the built-in `AgentSwarm`
|
||||
* collaboration tool into the agent `IAgentToolRegistryService` on construction.
|
||||
* Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export interface IAgentSwarmToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IAgentSwarmToolsService: ServiceIdentifier<IAgentSwarmToolsService> =
|
||||
createDecorator<IAgentSwarmToolsService>('agentSwarmToolsService');
|
||||
43
packages/agent-core-v2/src/agent/swarm/swarmToolsService.ts
Normal file
43
packages/agent-core-v2/src/agent/swarm/swarmToolsService.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* `swarmTools` domain (L4) — `IAgentSwarmToolsService` implementation.
|
||||
*
|
||||
* Eager Agent-scope registration service for the built-in `AgentSwarm`
|
||||
* collaboration tool. The tool is a DI class created via
|
||||
* `IInstantiationService.createInstance` (it injects `ISessionSwarmService` for
|
||||
* batch runs, `IAgentScopeContext` for the caller identity, and
|
||||
* `IAgentSwarmService` to enter swarm mode) and registered into the agent
|
||||
* `IAgentToolRegistryService`. Eager so the tool is registered when the Agent
|
||||
* scope is created, before the first turn.
|
||||
*
|
||||
* Split out of `AgentSwarmService` so the tool can inject `IAgentSwarmService`
|
||||
* without forming a constructor-instantiation cycle.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
||||
import { IAgentSwarmToolsService } from './swarmTools';
|
||||
import { AgentSwarmTool } from '#/agent/swarm/tools/agent-swarm';
|
||||
|
||||
export class AgentSwarmToolsService extends Disposable implements IAgentSwarmToolsService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
) {
|
||||
super();
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(AgentSwarmTool)));
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
IAgentSwarmToolsService,
|
||||
AgentSwarmToolsService,
|
||||
InstantiationType.Eager,
|
||||
'swarm',
|
||||
);
|
||||
|
|
@ -17,7 +17,10 @@ import {
|
|||
import { ToolAccesses } from '#/agent/tool';
|
||||
import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import type { ISessionSwarmService, SessionSwarmTask } from '#/session/swarm';
|
||||
import { ISessionSwarmService } from '#/session/swarm';
|
||||
import type { SessionSwarmTask } from '#/session/swarm';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext';
|
||||
import { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
import AGENT_SWARM_DESCRIPTION from './agent-swarm.md?raw';
|
||||
|
||||
const DEFAULT_SUBAGENT_TYPE = 'coder';
|
||||
|
|
@ -93,24 +96,20 @@ interface SwarmRunResult {
|
|||
readonly error?: string;
|
||||
}
|
||||
|
||||
interface AgentSwarmMode {
|
||||
enter(trigger: 'tool'): void;
|
||||
}
|
||||
|
||||
export interface AgentSwarmToolHost {
|
||||
readonly swarmService: ISessionSwarmService;
|
||||
readonly callerAgentId: string;
|
||||
}
|
||||
|
||||
export class AgentSwarmTool implements BuiltinTool<AgentSwarmToolInput> {
|
||||
readonly name = 'AgentSwarm' as const;
|
||||
readonly description = AGENT_SWARM_DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(AgentSwarmToolInputSchema);
|
||||
|
||||
private readonly callerAgentId: string;
|
||||
|
||||
constructor(
|
||||
private readonly host: AgentSwarmToolHost,
|
||||
private readonly swarmMode: AgentSwarmMode,
|
||||
) {}
|
||||
@ISessionSwarmService private readonly swarmService: ISessionSwarmService,
|
||||
@IAgentScopeContext scopeContext: IAgentScopeContext,
|
||||
@IAgentSwarmService private readonly swarmMode: IAgentSwarmService,
|
||||
) {
|
||||
this.callerAgentId = scopeContext.agentId;
|
||||
}
|
||||
|
||||
resolveExecution(args: AgentSwarmToolInput): ToolExecution {
|
||||
const agentCount = (args.items?.length ?? 0) + Object.keys(args.resume_agent_ids ?? {}).length;
|
||||
|
|
@ -178,8 +177,8 @@ export class AgentSwarmTool implements BuiltinTool<AgentSwarmToolInput> {
|
|||
kind: 'spawn' as const,
|
||||
};
|
||||
});
|
||||
const results = await this.host.swarmService.run({
|
||||
callerAgentId: this.host.callerAgentId,
|
||||
const results = await this.swarmService.run({
|
||||
callerAgentId: this.callerAgentId,
|
||||
tasks,
|
||||
});
|
||||
for (const result of results) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
Disposable,
|
||||
} from "#/_base/di";
|
||||
import { IInstantiationService } from "#/_base/di/instantiation";
|
||||
import {
|
||||
TODO_LIST_TOOL_NAME,
|
||||
TODO_STORE_KEY,
|
||||
|
|
@ -30,9 +31,10 @@ export class AgentTodoListService extends Disposable implements IAgentTodoListSe
|
|||
@IAgentToolStoreService private readonly toolStore: IAgentToolStoreService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
@IAgentContextInjectorService dynamicInjector: IAgentContextInjectorService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
this._register(toolRegistry.register(new TodoListTool(toolStore)));
|
||||
this._register(toolRegistry.register(instantiationService.createInstance(TodoListTool)));
|
||||
this._register(
|
||||
dynamicInjector.register(TODO_LIST_REMINDER_VARIANT, () => this.staleReminder()),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { z } from 'zod';
|
|||
import type { BuiltinTool } from '#/agent/tool';
|
||||
import type { ToolExecution } from '#/agent/tool';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import type { ToolStore } from '#/agent/toolStore';
|
||||
import { IAgentToolStoreService } from '#/agent/toolStore';
|
||||
import DESCRIPTION from './todo-list.md?raw';
|
||||
import TODO_LIST_WRITE_REMINDER from './todo-list-write-reminder.md?raw';
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ export class TodoListTool implements BuiltinTool<TodoListInput> {
|
|||
readonly description: string = DESCRIPTION;
|
||||
readonly parameters: Record<string, unknown> = toInputJsonSchema(TodoListInputSchema);
|
||||
|
||||
constructor(private readonly store: ToolStore) {}
|
||||
constructor(@IAgentToolStoreService private readonly store: IAgentToolStoreService) {}
|
||||
|
||||
resolveExecution(args: TodoListInput): ToolExecution {
|
||||
const description =
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
/**
|
||||
* `web` domain (L4) — `IAgentWebService` implementation.
|
||||
*
|
||||
* Registers the built-in web tools into the agent `IAgentToolRegistryService` on
|
||||
* construction: `FetchURL` is always registered (using the injected
|
||||
* `UrlFetcher` or the built-in `LocalFetchURLProvider` fallback); `WebSearch`
|
||||
* is registered only when a `WebSearchProvider` is supplied via options, since
|
||||
* there is no local search backend. Bound at Agent scope.
|
||||
* Eager Agent-scope registration service for the built-in web tools: `FetchURL`
|
||||
* is always registered (using the host-injected `UrlFetcher` or the built-in
|
||||
* `LocalFetchURLProvider` fallback); `WebSearch` is registered only when a
|
||||
* `WebSearchProvider` is supplied via options. Each tool is created via
|
||||
* `IInstantiationService.createInstance` (the provider is passed as a leading
|
||||
* static argument) and registered into the agent `IAgentToolRegistryService`.
|
||||
* Eager so the tools are registered when the Agent scope is created.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
||||
|
|
@ -17,17 +21,25 @@ import { FetchURLTool } from '#/agent/web/tools/fetch-url';
|
|||
import { WebSearchTool } from '#/agent/web/tools/web-search';
|
||||
import { IAgentWebService, type WebServiceOptions } from './web';
|
||||
|
||||
export class AgentWebService implements IAgentWebService {
|
||||
export class AgentWebService extends Disposable implements IAgentWebService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
private readonly options: WebServiceOptions = {},
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IAgentToolRegistryService toolRegistry: IAgentToolRegistryService,
|
||||
) {
|
||||
super();
|
||||
const fetcher = options.urlFetcher ?? new LocalFetchURLProvider();
|
||||
toolRegistry.register(new FetchURLTool(fetcher));
|
||||
this._register(
|
||||
toolRegistry.register(instantiationService.createInstance(FetchURLTool, fetcher)),
|
||||
);
|
||||
if (options.webSearcher !== undefined) {
|
||||
toolRegistry.register(new WebSearchTool(options.webSearcher));
|
||||
this._register(
|
||||
toolRegistry.register(
|
||||
instantiationService.createInstance(WebSearchTool, options.webSearcher),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +48,6 @@ registerScopedService(
|
|||
LifecycleScope.Agent,
|
||||
IAgentWebService,
|
||||
AgentWebService,
|
||||
InstantiationType.Delayed,
|
||||
InstantiationType.Eager,
|
||||
'web',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
|
||||
import { userCancellationReason } from '#/_base/utils/abort';
|
||||
import { IAgentBackgroundService } from '#/agent/background';
|
||||
import type { ILogger, LogPayload } from '#/app/log';
|
||||
import { ILogService } from '#/app/log';
|
||||
import type { LogPayload } from '#/app/log';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { createExecContext } from '#/session/execContext';
|
||||
import {
|
||||
AgentTool,
|
||||
AgentToolInputSchema,
|
||||
|
|
@ -12,6 +14,7 @@ import {
|
|||
} from '#/agent/agentTool';
|
||||
import { ToolAccesses } from '#/agent/tool';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata';
|
||||
import { executeTool } from '../tools/fixtures/execute-tool';
|
||||
import {
|
||||
agentToolServices,
|
||||
|
|
@ -33,16 +36,20 @@ function context<Input>(args: Input, toolCallId = 'call_agent') {
|
|||
}
|
||||
|
||||
function createLogCapture(): {
|
||||
readonly logger: ILogger;
|
||||
readonly logger: ILogService;
|
||||
readonly entries: CapturedLogEntry[];
|
||||
} {
|
||||
const entries: CapturedLogEntry[] = [];
|
||||
const logger: ILogger = {
|
||||
const logger: ILogService = {
|
||||
_serviceBrand: undefined,
|
||||
level: 'info',
|
||||
error: (message, payload) => entries.push({ level: 'error', message, payload }),
|
||||
warn: (message, payload) => entries.push({ level: 'warn', message, payload }),
|
||||
info: (message, payload) => entries.push({ level: 'info', message, payload }),
|
||||
debug: (message, payload) => entries.push({ level: 'debug', message, payload }),
|
||||
child: () => logger,
|
||||
setLevel: () => {},
|
||||
flush: () => Promise.resolve(),
|
||||
};
|
||||
return { logger, entries };
|
||||
}
|
||||
|
|
@ -98,7 +105,7 @@ describe('AgentTool direct contract', () => {
|
|||
readonly run?: AgentToolRunOverride;
|
||||
readonly maxRunningTasks?: number;
|
||||
readonly isToolActive?: (name: string) => boolean;
|
||||
readonly log?: ILogger;
|
||||
readonly log?: ILogService;
|
||||
} = {}): {
|
||||
readonly ctx: TestAgentContext;
|
||||
readonly background: IAgentBackgroundService;
|
||||
|
|
@ -113,20 +120,22 @@ describe('AgentTool direct contract', () => {
|
|||
});
|
||||
contexts.push(ctx);
|
||||
const background = ctx.get(IAgentBackgroundService);
|
||||
const logService = log ?? ctx.get(ILogService);
|
||||
return {
|
||||
ctx,
|
||||
background,
|
||||
run,
|
||||
tool: new AgentTool({
|
||||
lifecycle: fakeLifecycle(),
|
||||
callerAgentId: PARENT_AGENT_ID,
|
||||
tool: new AgentTool(
|
||||
run,
|
||||
fakeLifecycle(),
|
||||
{ _serviceBrand: undefined, agentId: PARENT_AGENT_ID },
|
||||
ctx.get(ISessionMetadata),
|
||||
background,
|
||||
profile: fakeProfile(isToolActive),
|
||||
cwd: '/repo',
|
||||
processRunner: fakeProcessRunner(),
|
||||
log,
|
||||
runOverride: run,
|
||||
}),
|
||||
fakeProfile(isToolActive),
|
||||
createExecContext('/repo'),
|
||||
fakeProcessRunner(),
|
||||
logService,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ describe('Cron — session E2E (P1.9)', () => {
|
|||
// bypass `emitScheduled` telemetry and skip the byte-length /
|
||||
// expression checks; that would not be the production code path
|
||||
// this commit is meant to smoke.
|
||||
const createTool = new CronCreateTool(cron);
|
||||
const createTool = new CronCreateTool(false, cron);
|
||||
const execution = createTool.resolveExecution({
|
||||
cron: '*/5 * * * *',
|
||||
prompt: 'cron-fired prompt',
|
||||
|
|
@ -149,7 +149,7 @@ describe('Cron — session E2E (P1.9)', () => {
|
|||
// Optional second case from the P1.9 plan: prove the three-tool
|
||||
// surface composes correctly end-to-end on the real manager. No
|
||||
// clock manipulation needed — list/delete are time-invariant.
|
||||
const createTool = new CronCreateTool(cron);
|
||||
const createTool = new CronCreateTool(false, cron);
|
||||
const listTool = new CronListTool(cron);
|
||||
const deleteTool = new CronDeleteTool(cron);
|
||||
const ctxArgs = {
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ function pad(value: number): string {
|
|||
describe('CronCreateTool', () => {
|
||||
it('schedules a recurring task and emits scheduled telemetry through the manager', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron);
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
|
||||
const out = assertSuccess(
|
||||
await runTool<CronCreateInput>(tool, {
|
||||
|
|
@ -217,7 +217,7 @@ describe('CronCreateTool', () => {
|
|||
|
||||
it('stores explicit one-shot tasks with recurring=false', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron);
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
|
||||
const out = assertSuccess(
|
||||
await runTool<CronCreateInput>(tool, {
|
||||
|
|
@ -237,7 +237,7 @@ describe('CronCreateTool', () => {
|
|||
|
||||
it('returns an error when scheduling is disabled', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron, true);
|
||||
const tool = new CronCreateTool(true, harness.cron);
|
||||
|
||||
const output = assertError(
|
||||
await runTool<CronCreateInput>(tool, {
|
||||
|
|
@ -253,7 +253,7 @@ describe('CronCreateTool', () => {
|
|||
|
||||
it('rejects an unparseable cron expression', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron);
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
|
||||
const output = assertError(
|
||||
await runTool<CronCreateInput>(tool, {
|
||||
|
|
@ -269,7 +269,7 @@ describe('CronCreateTool', () => {
|
|||
|
||||
it('rejects a legal expression that has no fire inside the supported window', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron);
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
|
||||
const output = assertError(
|
||||
await runTool<CronCreateInput>(tool, {
|
||||
|
|
@ -284,7 +284,7 @@ describe('CronCreateTool', () => {
|
|||
|
||||
it('refuses to schedule past the session cap', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron);
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
|
||||
for (let i = 0; i < MAX_CRON_JOBS_PER_SESSION; i++) {
|
||||
harness.store.add({ cron: '*/5 * * * *', prompt: `seed-${i}`, recurring: true }, harness.now());
|
||||
|
|
@ -303,7 +303,7 @@ describe('CronCreateTool', () => {
|
|||
|
||||
it('rechecks the session cap inside execute', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron);
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
|
||||
for (let i = 0; i < MAX_CRON_JOBS_PER_SESSION - 1; i++) {
|
||||
harness.store.add({ cron: '*/5 * * * *', prompt: `seed-${i}`, recurring: true }, harness.now());
|
||||
|
|
@ -340,7 +340,7 @@ describe('CronCreateTool', () => {
|
|||
|
||||
it('rejects prompts over the UTF-8 byte budget', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron);
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
const prompt = '\u4f60'.repeat(3000);
|
||||
|
||||
const output = assertError(
|
||||
|
|
@ -356,7 +356,7 @@ describe('CronCreateTool', () => {
|
|||
|
||||
it('normalizes cron field whitespace before storing and rendering', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron);
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
|
||||
const out = assertSuccess(
|
||||
await runTool<CronCreateInput>(tool, {
|
||||
|
|
@ -373,7 +373,7 @@ describe('CronCreateTool', () => {
|
|||
|
||||
it('uses the execution-time clock for createdAt', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron);
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
const execution = await tool.resolveExecution({
|
||||
cron: '*/5 * * * *',
|
||||
prompt: 'delayed approval',
|
||||
|
|
@ -393,7 +393,7 @@ describe('CronCreateTool', () => {
|
|||
|
||||
it('includes the normalized payload in the approval rule', async () => {
|
||||
const harness = createToolHarness();
|
||||
const tool = new CronCreateTool(harness.cron);
|
||||
const tool = new CronCreateTool(false, harness.cron);
|
||||
|
||||
const a = await tool.resolveExecution({
|
||||
cron: '*/5\n* * * *',
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { PathSecurityError } from '../../src/_base/tools/policies/path-access';
|
||||
import type { WorkspaceConfig } from '../../src/_base/tools/support/workspace';
|
||||
import { stubWorkspaceContext } from './stub-workspace-context';
|
||||
import type { ISessionAgentFileSystem } from '#/session/agentFs';
|
||||
import { type EditInput, EditInputSchema, EditTool } from '#/agent/fileTools/tools/edit';
|
||||
import type { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
const PERMISSIVE_WORKSPACE: WorkspaceConfig = { workspaceDir: '/', additionalDirs: [] };
|
||||
const PERMISSIVE_WORKSPACE = stubWorkspaceContext('/');
|
||||
|
||||
function createTestEnv(home = '/home'): IHostEnvironment {
|
||||
return {
|
||||
|
|
@ -391,10 +391,7 @@ describe('EditTool', () => {
|
|||
it('rejects relative traversal edits before reading', async () => {
|
||||
const readText = vi.fn().mockResolvedValue('secret');
|
||||
const { fs } = createSpiedEditFs({ readText });
|
||||
const tool = new EditTool(fs, createTestEnv(), {
|
||||
workspaceDir: '/workspace/project',
|
||||
additionalDirs: [],
|
||||
});
|
||||
const tool = new EditTool(fs, createTestEnv(), stubWorkspaceContext('/workspace/project'));
|
||||
|
||||
const result = await execute(tool, {
|
||||
path: '../outside.txt',
|
||||
|
|
@ -491,10 +488,7 @@ describe('EditTool', () => {
|
|||
readText: vi.fn().mockResolvedValue('old content'),
|
||||
writeText,
|
||||
});
|
||||
const tool = new EditTool(fs, createTestEnv(), {
|
||||
workspaceDir: '/workspace',
|
||||
additionalDirs: [],
|
||||
});
|
||||
const tool = new EditTool(fs, createTestEnv(), stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, {
|
||||
path: '/tmp/outside.txt',
|
||||
|
|
@ -514,10 +508,7 @@ describe('EditTool', () => {
|
|||
readText: vi.fn().mockResolvedValue('content'),
|
||||
writeText,
|
||||
});
|
||||
const tool = new EditTool(fs, createTestEnv(), {
|
||||
workspaceDir: '/workspace',
|
||||
additionalDirs: [],
|
||||
});
|
||||
const tool = new EditTool(fs, createTestEnv(), stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, {
|
||||
path: '/workspace-sneaky/test.txt',
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ISessionAgentFileSystem, ISessionFsService } from '#/session/agentFs';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { ISessionAgentFileSystem, ISessionFsService } from '#/session/agentFs';
|
||||
import { AgentFileToolsService } from '#/agent/fileTools';
|
||||
import type { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import type { ISessionProcessRunner } from '#/session/process';
|
||||
import { noopTelemetryService } from '#/app/telemetry';
|
||||
import { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import { ITelemetryService, noopTelemetryService } from '#/app/telemetry';
|
||||
import type { IDisposable } from '#/_base/di';
|
||||
import type { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import type { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
|
||||
function fakeToolRegistry(): { registry: IAgentToolRegistryService; names: () => string[] } {
|
||||
const tools = new Map<string, unknown>();
|
||||
|
|
@ -44,15 +45,14 @@ const fakeWorkspace = {
|
|||
describe('AgentFileToolsService', () => {
|
||||
it('registers Read/Write/Edit/Grep/Glob into the tool registry', () => {
|
||||
const { registry, names } = fakeToolRegistry();
|
||||
new AgentFileToolsService(
|
||||
registry,
|
||||
fakeFs,
|
||||
fakeEnv,
|
||||
fakeWorkspace,
|
||||
fakeFsService,
|
||||
fakeRunner,
|
||||
noopTelemetryService,
|
||||
);
|
||||
const ix = new TestInstantiationService();
|
||||
ix.set(ISessionAgentFileSystem, fakeFs);
|
||||
ix.set(ISessionFsService, fakeFsService);
|
||||
ix.set(IHostEnvironment, fakeEnv);
|
||||
ix.set(ISessionProcessRunner, fakeRunner);
|
||||
ix.set(ITelemetryService, noopTelemetryService);
|
||||
ix.set(ISessionWorkspaceContext, fakeWorkspace);
|
||||
new AgentFileToolsService(ix, registry);
|
||||
expect(names()).toEqual(['Edit', 'Glob', 'Grep', 'Read', 'Write']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
|
||||
import { ensureRgPath } from '#/session/agentFs/rgLocator';
|
||||
import { PathSecurityError, type PathClass } from '../../src/_base/tools/policies/path-access';
|
||||
import type { WorkspaceConfig } from '../../src/_base/tools/support/workspace';
|
||||
import { noopTelemetryService } from '#/app/telemetry';
|
||||
import type { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
import { stubWorkspaceContext } from './stub-workspace-context';
|
||||
import type { ISessionAgentFileSystem } from '#/session/agentFs';
|
||||
import { SessionAgentFileSystem } from '#/session/agentFs/agentFsService';
|
||||
import {
|
||||
|
|
@ -54,7 +56,7 @@ vi.mock('#/session/agentFs/rgLocator', () => ({
|
|||
const RG_AVAILABLE = spawnSync('rg', ['--version'], { stdio: 'ignore' }).status === 0;
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
const workspace: WorkspaceConfig = { workspaceDir: '/workspace', additionalDirs: ['/extra'] };
|
||||
const workspace = stubWorkspaceContext('/workspace', ['/extra']);
|
||||
|
||||
/** Fake fs with a spied `readdir` for the directory pre-check. */
|
||||
function createTestFs(opts: { readdir?: ReturnType<typeof vi.fn> } = {}) {
|
||||
|
|
@ -184,16 +186,20 @@ function toolContentString(result: ExecutableToolResult): string {
|
|||
|
||||
/** Build a `GlobTool` with the given exec spy, using a fake env + runner. */
|
||||
function makeTool(
|
||||
workspaceConfig: WorkspaceConfig,
|
||||
workspaceConfig: ISessionWorkspaceContext,
|
||||
opts: { home?: string; pathClass?: PathClass; exec?: ReturnType<typeof vi.fn>; readdir?: ReturnType<typeof vi.fn>; telemetry?: ITelemetryService } = {},
|
||||
): { tool: GlobTool; exec: ReturnType<typeof vi.fn>; withCwd: ReturnType<typeof withCwdOf> } {
|
||||
const exec = opts.exec ?? execReturning('');
|
||||
const { fs } = createTestFs({ readdir: opts.readdir });
|
||||
const runner = createTestRunner(exec);
|
||||
const env = createTestEnv({ home: opts.home, pathClass: opts.pathClass });
|
||||
const tool = opts.telemetry !== undefined
|
||||
? new GlobTool(fs, env, runner, workspaceConfig, opts.telemetry)
|
||||
: new GlobTool(fs, env, runner, workspaceConfig);
|
||||
const tool = new GlobTool(
|
||||
fs,
|
||||
env,
|
||||
runner,
|
||||
workspaceConfig,
|
||||
opts.telemetry ?? noopTelemetryService,
|
||||
);
|
||||
return { tool, exec, withCwd: withCwdOf(exec) };
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +260,7 @@ describe('GlobTool', () => {
|
|||
it('uses the backend path class when displaying paths relative to a windows root', async () => {
|
||||
const exec = execReturning('C:\\workspace\\src\\old.ts\n');
|
||||
const { tool, withCwd } = makeTool(
|
||||
{ workspaceDir: 'C:\\workspace', additionalDirs: [] },
|
||||
stubWorkspaceContext('C:\\workspace'),
|
||||
{ pathClass: 'win32', exec },
|
||||
);
|
||||
|
||||
|
|
@ -353,7 +359,7 @@ describe('GlobTool', () => {
|
|||
Array.from({ length: MAX_MATCHES + 1 }, (_, i) => `/workspace/${String(i)}.ts`).join('\n') +
|
||||
'\n';
|
||||
const exec = execReturning(stdout);
|
||||
const { tool } = makeTool({ workspaceDir: '/workspace', additionalDirs: [] }, { exec });
|
||||
const { tool } = makeTool(stubWorkspaceContext('/workspace'), { exec });
|
||||
|
||||
const result = await execute(tool, { pattern: '*.ts' });
|
||||
|
||||
|
|
@ -368,7 +374,7 @@ describe('GlobTool', () => {
|
|||
'\n',
|
||||
) + '\n';
|
||||
const exec = execReturning(stdout);
|
||||
const { tool } = makeTool({ workspaceDir: '/workspace', additionalDirs: [] }, { exec });
|
||||
const { tool } = makeTool(stubWorkspaceContext('/workspace'), { exec });
|
||||
|
||||
const result = await execute(tool, { pattern: '*.txt' });
|
||||
|
||||
|
|
@ -380,7 +386,7 @@ describe('GlobTool', () => {
|
|||
Array.from({ length: MAX_MATCHES }, (_, i) => `/workspace/test_${String(i)}.py`).join('\n') +
|
||||
'\n';
|
||||
const exec = execReturning(stdout);
|
||||
const { tool } = makeTool({ workspaceDir: '/workspace', additionalDirs: [] }, { exec });
|
||||
const { tool } = makeTool(stubWorkspaceContext('/workspace'), { exec });
|
||||
|
||||
const result = await execute(tool, { pattern: '*.py' });
|
||||
|
||||
|
|
@ -468,10 +474,7 @@ describe('GlobTool', () => {
|
|||
});
|
||||
|
||||
describe('skills / additional dirs', () => {
|
||||
const skillsWorkspace: WorkspaceConfig = {
|
||||
workspaceDir: '/workspace',
|
||||
additionalDirs: ['/skills'],
|
||||
};
|
||||
const skillsWorkspace = stubWorkspaceContext('/workspace', ['/skills']);
|
||||
|
||||
it('searches inside a registered additionalDir entry', async () => {
|
||||
const exec = execReturning('/skills/read_content.py\n/skills/utils.py\n');
|
||||
|
|
@ -501,7 +504,7 @@ describe('GlobTool', () => {
|
|||
it('rejects a relative path that escapes both workspace and additionalDirs', async () => {
|
||||
const exec = vi.fn();
|
||||
const { tool, withCwd } = makeTool(
|
||||
{ workspaceDir: '/workspace/project', additionalDirs: ['/skills'] },
|
||||
stubWorkspaceContext('/workspace/project', ['/skills']),
|
||||
{ exec },
|
||||
);
|
||||
|
||||
|
|
@ -673,7 +676,7 @@ describe('GlobTool', () => {
|
|||
it('shows absolute paths when explicit search root is outside all workspace roots', async () => {
|
||||
const exec = execReturning('/extra/test.py\n');
|
||||
const { tool, withCwd } = makeTool(
|
||||
{ workspaceDir: '/workspace', additionalDirs: [] },
|
||||
stubWorkspaceContext('/workspace'),
|
||||
{ exec },
|
||||
);
|
||||
|
||||
|
|
@ -684,7 +687,7 @@ describe('GlobTool', () => {
|
|||
});
|
||||
|
||||
it('keeps absolute paths when explicit search root is an additionalDir', async () => {
|
||||
const registered: WorkspaceConfig = { workspaceDir: '/workspace', additionalDirs: ['/extra'] };
|
||||
const registered = stubWorkspaceContext('/workspace', ['/extra']);
|
||||
const exec = execReturning('/extra/test.py\n');
|
||||
const { tool } = makeTool(registered, { exec });
|
||||
|
||||
|
|
@ -708,7 +711,7 @@ describe('GlobTool', () => {
|
|||
it('expands a leading "~/" path before searching outside the workspace', async () => {
|
||||
const exec = execReturning('');
|
||||
const { tool, withCwd } = makeTool(
|
||||
{ workspaceDir: '/workspace', additionalDirs: [] },
|
||||
stubWorkspaceContext('/workspace'),
|
||||
{ home: '/home/test', exec },
|
||||
);
|
||||
|
||||
|
|
@ -723,7 +726,7 @@ describe('GlobTool', () => {
|
|||
it('allows a path sharing the workspace prefix when it is absolute', async () => {
|
||||
const exec = execReturning('');
|
||||
const { tool, withCwd } = makeTool(
|
||||
{ workspaceDir: '/parent/workdir', additionalDirs: [] },
|
||||
stubWorkspaceContext('/parent/workdir'),
|
||||
{ exec },
|
||||
);
|
||||
|
||||
|
|
@ -747,7 +750,7 @@ describe('GlobTool', () => {
|
|||
|
||||
it('mentions Windows path forms in the description on win32 backends', () => {
|
||||
const { tool } = makeTool(
|
||||
{ workspaceDir: 'C:\\workspace', additionalDirs: [] },
|
||||
stubWorkspaceContext('C:\\workspace'),
|
||||
{ pathClass: 'win32' },
|
||||
);
|
||||
|
||||
|
|
@ -825,13 +828,13 @@ describe.skipIf(!RG_AVAILABLE)('GlobTool integration (real ripgrep)', () => {
|
|||
await fs.utimes(full, mtime, mtime);
|
||||
}
|
||||
|
||||
const ws = (): WorkspaceConfig => ({ workspaceDir: tmpDir!, additionalDirs: [] });
|
||||
const ws = () => stubWorkspaceContext(tmpDir!);
|
||||
|
||||
it('returns files newest-first by modification time (--sortr=modified)', async () => {
|
||||
await touch('old.ts', new Date('2020-01-01T00:00:00Z'));
|
||||
await touch('mid.ts', new Date('2022-01-01T00:00:00Z'));
|
||||
await touch('new.ts', new Date('2024-01-01T00:00:00Z'));
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws());
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws(), noopTelemetryService);
|
||||
|
||||
const result = await execute(tool, { pattern: '*.ts', path: tmpDir! });
|
||||
|
||||
|
|
@ -842,7 +845,7 @@ describe.skipIf(!RG_AVAILABLE)('GlobTool integration (real ripgrep)', () => {
|
|||
await touch('root.ts', new Date('2024-01-01T00:00:00Z'));
|
||||
await touch('src/a.ts', new Date('2023-01-01T00:00:00Z'));
|
||||
await touch('src/sub/b.ts', new Date('2022-01-01T00:00:00Z'));
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws());
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws(), noopTelemetryService);
|
||||
|
||||
const result = await execute(tool, { pattern: '*.ts', path: tmpDir! });
|
||||
|
||||
|
|
@ -855,7 +858,7 @@ describe.skipIf(!RG_AVAILABLE)('GlobTool integration (real ripgrep)', () => {
|
|||
await touch('src/a.ts', new Date('2024-01-01T00:00:00Z'));
|
||||
await touch('test/a.ts', new Date('2023-01-01T00:00:00Z'));
|
||||
await touch('other/a.ts', new Date('2022-01-01T00:00:00Z'));
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws());
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws(), noopTelemetryService);
|
||||
|
||||
const result = await execute(tool, { pattern: '{src,test}/*.ts', path: tmpDir! });
|
||||
|
||||
|
|
@ -868,7 +871,7 @@ describe.skipIf(!RG_AVAILABLE)('GlobTool integration (real ripgrep)', () => {
|
|||
await touch('src/a.ts', new Date('2024-01-01T00:00:00Z'));
|
||||
await touch('src/sub/b.ts', new Date('2023-01-01T00:00:00Z'));
|
||||
await touch('other/c.ts', new Date('2022-01-01T00:00:00Z'));
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws());
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws(), noopTelemetryService);
|
||||
|
||||
const result = await execute(tool, { pattern: 'src/**/*.ts', path: tmpDir! });
|
||||
|
||||
|
|
@ -879,7 +882,7 @@ describe.skipIf(!RG_AVAILABLE)('GlobTool integration (real ripgrep)', () => {
|
|||
|
||||
it('treats an escaped brace as a literal filename', async () => {
|
||||
await touch('{a,b}.ts', new Date('2024-01-01T00:00:00Z'));
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws());
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws(), noopTelemetryService);
|
||||
|
||||
const result = await execute(tool, { pattern: '\\{a,b\\}.ts', path: tmpDir! });
|
||||
|
||||
|
|
@ -891,7 +894,7 @@ describe.skipIf(!RG_AVAILABLE)('GlobTool integration (real ripgrep)', () => {
|
|||
try {
|
||||
const extFile = path.join(externalDir, 'pkg.ts');
|
||||
await fs.writeFile(extFile, '');
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws());
|
||||
const tool = new GlobTool(realFs, realEnv, realRunner, ws(), noopTelemetryService);
|
||||
|
||||
const result = await execute(tool, { pattern: '*.ts', path: externalDir });
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import type { FsGrepFileHit, FsGrepRequest, FsGrepResponse } from '@moonshot-ai/protocol';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { WorkspaceConfig } from '../../src/_base/tools/support/workspace';
|
||||
import { stubWorkspaceContext } from './stub-workspace-context';
|
||||
import type { ISessionFsService } from '#/session/agentFs';
|
||||
import {
|
||||
type GrepInput,
|
||||
|
|
@ -24,7 +24,7 @@ import type { IHostEnvironment } from '#/app/hostEnvironment';
|
|||
import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
const workspace: WorkspaceConfig = { workspaceDir: '/workspace', additionalDirs: ['/extra'] };
|
||||
const workspace = stubWorkspaceContext('/workspace', ['/extra']);
|
||||
|
||||
function fileHit(path: string, lines: number[] = [1], text = 'hit'): FsGrepFileHit {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|||
|
||||
import { PathSecurityError } from '../../src/_base/tools/policies/path-access';
|
||||
import { MEDIA_SNIFF_BYTES } from '../../src/_base/tools/support/file-type';
|
||||
import type { WorkspaceConfig } from '../../src/_base/tools/support/workspace';
|
||||
import { stubWorkspaceContext } from './stub-workspace-context';
|
||||
import type { ISessionAgentFileSystem } from '#/session/agentFs';
|
||||
import {
|
||||
MAX_BYTES,
|
||||
|
|
@ -30,7 +30,7 @@ import type { IHostEnvironment } from '#/app/hostEnvironment';
|
|||
import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
const PERMISSIVE_WORKSPACE: WorkspaceConfig = { workspaceDir: '/', additionalDirs: [] };
|
||||
const PERMISSIVE_WORKSPACE = stubWorkspaceContext('/');
|
||||
|
||||
function linesFromContent(content: string): string[] {
|
||||
if (content === '') return [];
|
||||
|
|
@ -136,7 +136,7 @@ function createSpiedMapFs(files: Record<string, FakeFile>) {
|
|||
return { fs, readBytes, readLines, readText, stat };
|
||||
}
|
||||
|
||||
function toolWithContent(content: string, workspace: WorkspaceConfig = PERMISSIVE_WORKSPACE) {
|
||||
function toolWithContent(content: string, workspace = PERMISSIVE_WORKSPACE) {
|
||||
return new ReadTool(createSpiedFs(content).fs, createTestEnv(), workspace);
|
||||
}
|
||||
|
||||
|
|
@ -305,10 +305,7 @@ describe('ReadTool', () => {
|
|||
|
||||
it('rejects relative traversal before reading', async () => {
|
||||
const { fs, readText } = createSpiedFs('secret');
|
||||
const tool = new ReadTool(fs, createTestEnv(), {
|
||||
workspaceDir: '/workspace/project',
|
||||
additionalDirs: [],
|
||||
});
|
||||
const tool = new ReadTool(fs, createTestEnv(), stubWorkspaceContext('/workspace/project'));
|
||||
|
||||
const result = await execute(tool, { path: '../../outside.txt' });
|
||||
|
||||
|
|
@ -319,10 +316,7 @@ describe('ReadTool', () => {
|
|||
|
||||
it('allows explicit absolute paths outside the workspace', async () => {
|
||||
const { fs, readBytes, readLines } = createSpiedFs('external');
|
||||
const tool = new ReadTool(fs, createTestEnv(), {
|
||||
workspaceDir: '/workspace',
|
||||
additionalDirs: [],
|
||||
});
|
||||
const tool = new ReadTool(fs, createTestEnv(), stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, { path: '/tmp/external.txt' });
|
||||
|
||||
|
|
@ -338,10 +332,7 @@ describe('ReadTool', () => {
|
|||
|
||||
it('returns a friendly error for missing files before sniffing bytes', async () => {
|
||||
const { fs, readBytes, readLines } = createSpiedMapFs({});
|
||||
const tool = new ReadTool(fs, createTestEnv(), {
|
||||
workspaceDir: '/workspace',
|
||||
additionalDirs: [],
|
||||
});
|
||||
const tool = new ReadTool(fs, createTestEnv(), stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, { path: '/workspace/missing.txt' });
|
||||
|
||||
|
|
@ -357,10 +348,7 @@ describe('ReadTool', () => {
|
|||
const { fs, readBytes, readLines } = createSpiedMapFs({
|
||||
'/workspace/src': { bytes: Buffer.alloc(0), isFile: false, isDirectory: true },
|
||||
});
|
||||
const tool = new ReadTool(fs, createTestEnv(), {
|
||||
workspaceDir: '/workspace',
|
||||
additionalDirs: [],
|
||||
});
|
||||
const tool = new ReadTool(fs, createTestEnv(), stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, { path: '/workspace/src' });
|
||||
|
||||
|
|
@ -374,10 +362,7 @@ describe('ReadTool', () => {
|
|||
|
||||
it('expands leading tilde paths using the kaos home directory', async () => {
|
||||
const { fs, readBytes, readLines } = createSpiedFs('home note');
|
||||
const tool = new ReadTool(fs, createTestEnv('/home/test'), {
|
||||
workspaceDir: '/workspace',
|
||||
additionalDirs: [],
|
||||
});
|
||||
const tool = new ReadTool(fs, createTestEnv('/home/test'), stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, { path: '~/notes/today.txt' });
|
||||
|
||||
|
|
@ -393,10 +378,7 @@ describe('ReadTool', () => {
|
|||
|
||||
it('blocks sensitive files independently from workspace access', async () => {
|
||||
const { fs, readText } = createSpiedFs('SECRET=value');
|
||||
const tool = new ReadTool(fs, createTestEnv(), {
|
||||
workspaceDir: '/workspace',
|
||||
additionalDirs: [],
|
||||
});
|
||||
const tool = new ReadTool(fs, createTestEnv(), stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, { path: '/workspace/.env' });
|
||||
|
||||
|
|
@ -672,10 +654,7 @@ describe('ReadTool', () => {
|
|||
|
||||
it('reads files inside additional_dirs via absolute path', async () => {
|
||||
const { fs } = createSpiedFs('extra-dir note');
|
||||
const tool = new ReadTool(fs, createTestEnv(), {
|
||||
workspaceDir: '/workspace',
|
||||
additionalDirs: ['/extra'],
|
||||
});
|
||||
const tool = new ReadTool(fs, createTestEnv(), stubWorkspaceContext('/workspace', ['/extra']));
|
||||
|
||||
const result = await execute(tool, { path: '/extra/notes.txt' });
|
||||
|
||||
|
|
@ -685,10 +664,7 @@ describe('ReadTool', () => {
|
|||
|
||||
it('reports nonexistent files with the expected does-not-exist phrasing', async () => {
|
||||
const { fs } = createSpiedMapFs({});
|
||||
const tool = new ReadTool(fs, createTestEnv(), {
|
||||
workspaceDir: '/workspace',
|
||||
additionalDirs: [],
|
||||
});
|
||||
const tool = new ReadTool(fs, createTestEnv(), stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, { path: '/workspace/ghost.txt' });
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import type { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
|
||||
/**
|
||||
* Builds a minimal `ISessionWorkspaceContext` stub for file-tool unit tests.
|
||||
*
|
||||
* The file tools only read `workDir` / `additionalDirs`; the remaining members
|
||||
* are no-op stubs so tests can construct tools without standing up a full
|
||||
* session scope.
|
||||
*/
|
||||
export function stubWorkspaceContext(
|
||||
workDir: string,
|
||||
additionalDirs: readonly string[] = [],
|
||||
): ISessionWorkspaceContext {
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
workDir,
|
||||
additionalDirs,
|
||||
setWorkDir: () => {},
|
||||
resolve: (rel) => `${workDir}/${rel}`,
|
||||
isWithin: () => true,
|
||||
assertAllowed: (absPath) => absPath,
|
||||
addAdditionalDir: () => {},
|
||||
removeAdditionalDir: () => {},
|
||||
};
|
||||
}
|
||||
|
|
@ -16,13 +16,13 @@ import { describe, expect, it, vi } from 'vitest';
|
|||
|
||||
import { PathSecurityError } from '../../src/_base/tools/policies/path-access';
|
||||
import type { AgentFileStat, ISessionAgentFileSystem } from '#/session/agentFs';
|
||||
import type { WorkspaceConfig } from '../../src/_base/tools/support/workspace';
|
||||
import { stubWorkspaceContext } from './stub-workspace-context';
|
||||
import { type WriteInput, WriteInputSchema, WriteTool } from '#/agent/fileTools/tools/write';
|
||||
import type { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
const PERMISSIVE_WORKSPACE: WorkspaceConfig = { workspaceDir: '/', additionalDirs: [] };
|
||||
const PERMISSIVE_WORKSPACE = stubWorkspaceContext('/');
|
||||
|
||||
function toolContentString(result: ExecutableToolResult): string {
|
||||
const c = result.output;
|
||||
|
|
@ -80,7 +80,7 @@ function createWriteFs(options: WriteFsOptions = {}) {
|
|||
return { fs, readText, writeText, stat, mkdir };
|
||||
}
|
||||
|
||||
function makeTool(options: WriteFsOptions = {}, workspace: WorkspaceConfig = PERMISSIVE_WORKSPACE) {
|
||||
function makeTool(options: WriteFsOptions = {}, workspace = PERMISSIVE_WORKSPACE) {
|
||||
const fakes = createWriteFs(options);
|
||||
const tool = new WriteTool(fakes.fs, createTestEnv(), workspace);
|
||||
return { tool, ...fakes };
|
||||
|
|
@ -178,7 +178,7 @@ describe('WriteTool', () => {
|
|||
});
|
||||
|
||||
it('matches permission args with negated glob path semantics', () => {
|
||||
const { tool } = makeTool({}, { workspaceDir: '/workspace', additionalDirs: [] });
|
||||
const { tool } = makeTool({}, stubWorkspaceContext('/workspace'));
|
||||
const insideSrc = tool.resolveExecution({ path: './src/a.ts', content: 'x' });
|
||||
const outsideSrc = tool.resolveExecution({ path: './README.md', content: 'x' });
|
||||
if (insideSrc.isError === true || outsideSrc.isError === true) {
|
||||
|
|
@ -343,7 +343,7 @@ describe('WriteTool', () => {
|
|||
});
|
||||
|
||||
it('allows explicit absolute writes outside the workspace', async () => {
|
||||
const { tool, writeText } = makeTool({}, { workspaceDir: '/workspace', additionalDirs: [] });
|
||||
const { tool, writeText } = makeTool({}, stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, { path: '/tmp/pwned.txt', content: 'x' });
|
||||
|
||||
|
|
@ -354,7 +354,7 @@ describe('WriteTool', () => {
|
|||
it('rejects relative traversal writes before fs I/O', async () => {
|
||||
const { tool, writeText } = makeTool(
|
||||
{},
|
||||
{ workspaceDir: '/workspace/project', additionalDirs: [] },
|
||||
stubWorkspaceContext('/workspace/project'),
|
||||
);
|
||||
|
||||
const result = await execute(tool, { path: '../outside.txt', content: 'x' });
|
||||
|
|
@ -365,7 +365,7 @@ describe('WriteTool', () => {
|
|||
});
|
||||
|
||||
it('blocks sensitive file writes', async () => {
|
||||
const { tool, writeText } = makeTool({}, { workspaceDir: '/workspace', additionalDirs: [] });
|
||||
const { tool, writeText } = makeTool({}, stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, { path: '/workspace/id_rsa', content: 'key' });
|
||||
|
||||
|
|
@ -433,7 +433,7 @@ describe('WriteTool', () => {
|
|||
it('allows absolute writes to a sibling dir that merely shares the work-dir prefix', async () => {
|
||||
// Path policy must distinguish "shares a prefix with workspaceDir" from
|
||||
// "is inside workspaceDir". /workspace-sneaky/* is outside /workspace.
|
||||
const { tool, writeText } = makeTool({}, { workspaceDir: '/workspace', additionalDirs: [] });
|
||||
const { tool, writeText } = makeTool({}, stubWorkspaceContext('/workspace'));
|
||||
|
||||
const result = await execute(tool, { path: '/workspace-sneaky/file.txt', content: 'content' });
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
} from '#/agent/background';
|
||||
import type { BackgroundTaskSettlement } from '#/agent/background/task';
|
||||
import type { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import type { IAgentProfileService } from '#/agent/profile';
|
||||
import { createExecContext, type IExecContext } from '#/session/execContext';
|
||||
import type { IProcess, ISessionProcessRunner } from '#/session/process';
|
||||
import { type BashInput, BashInputSchema, BashTool } from '#/agent/shellTools/tools/bash';
|
||||
|
|
@ -673,14 +674,21 @@ async function executeTool(
|
|||
return execution.execute(executionContext as ExecutableToolContext);
|
||||
}
|
||||
|
||||
function stubProfile(isToolActive: (name: string) => boolean = () => true): IAgentProfileService {
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
isToolActive,
|
||||
} as unknown as IAgentProfileService;
|
||||
}
|
||||
|
||||
function bashTool(
|
||||
runner: ISessionProcessRunner,
|
||||
env: IHostEnvironment = createTestEnv(),
|
||||
ctx: IExecContext = createTestCtx(),
|
||||
background: IAgentBackgroundService = createFakeBackgroundService().service,
|
||||
options?: ConstructorParameters<typeof BashTool>[4],
|
||||
profile: IAgentProfileService = stubProfile(),
|
||||
): BashTool {
|
||||
return new BashTool(runner, env, ctx, background, options);
|
||||
return new BashTool(runner, env, ctx, background, profile);
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────
|
||||
|
|
@ -1243,7 +1251,7 @@ describe('BashTool background mode', () => {
|
|||
const { proc, finish } = pendingProcess();
|
||||
const { runner } = createTestRunner(proc);
|
||||
const { service } = createFakeBackgroundService();
|
||||
const tool = bashTool(runner, createTestEnv(), createTestCtx(), service, { allowBackground: () => false });
|
||||
const tool = bashTool(runner, createTestEnv(), createTestCtx(), service, stubProfile(() => false));
|
||||
|
||||
const running = executeTool(tool, context({ command: 'sleep 10', timeout: 60 }));
|
||||
await vi.waitFor(() => {
|
||||
|
|
@ -1314,7 +1322,7 @@ describe('BashTool background mode', () => {
|
|||
runner,
|
||||
createTestEnv(), createTestCtx(),
|
||||
createFakeBackgroundService().service,
|
||||
{ allowBackground: () => false },
|
||||
stubProfile(() => false),
|
||||
);
|
||||
|
||||
const unavailable = await executeTool(
|
||||
|
|
@ -1589,9 +1597,7 @@ describe('BashTool prompt / runtime consistency', () => {
|
|||
[...enabledTool.description.matchAll(/`(Task[A-Za-z]+)`/g)].map((match) => match[1]),
|
||||
);
|
||||
|
||||
const tool = bashTool(runner, createTestEnv(), createTestCtx(), createFakeBackgroundService().service, {
|
||||
allowBackground: () => false,
|
||||
});
|
||||
const tool = bashTool(runner, createTestEnv(), createTestCtx(), createFakeBackgroundService().service, stubProfile(() => false));
|
||||
const result = await executeTool(
|
||||
tool,
|
||||
context({ command: 'sleep 10', run_in_background: true, description: 'watch' }),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { IAgentBackgroundService } from '#/agent/background';
|
||||
import { IAgentBackgroundService } from '#/agent/background';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import type { IDisposable } from '#/_base/di';
|
||||
import type { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { createExecContext, type IExecContext } from '#/session/execContext';
|
||||
import type { ISessionProcessRunner } from '#/session/process';
|
||||
import type { IAgentProfileService } from '#/agent/profile';
|
||||
import { IHostEnvironment } from '#/app/hostEnvironment';
|
||||
import { createExecContext, IExecContext } from '#/session/execContext';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { AgentShellToolsService } from '#/agent/shellTools';
|
||||
import type { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
||||
|
|
@ -43,7 +44,13 @@ const fakeProfile = {
|
|||
describe('AgentShellToolsService', () => {
|
||||
it('registers Bash into the tool registry', () => {
|
||||
const { registry, names } = fakeToolRegistry();
|
||||
new AgentShellToolsService(registry, fakeRunner, fakeEnv, fakeCtx, fakeBackground, fakeProfile);
|
||||
const ix = new TestInstantiationService();
|
||||
ix.set(ISessionProcessRunner, fakeRunner);
|
||||
ix.set(IHostEnvironment, fakeEnv);
|
||||
ix.set(IExecContext, fakeCtx);
|
||||
ix.set(IAgentBackgroundService, fakeBackground);
|
||||
ix.set(IAgentProfileService, fakeProfile);
|
||||
new AgentShellToolsService(ix, registry);
|
||||
expect(names()).toEqual(['Bash']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { SyncDescriptor } from '#/_base/di/descriptors';
|
|||
import { DisposableStore } from '#/_base/di/lifecycle';
|
||||
import { createServices, type TestInstantiationService } from '#/_base/di/test';
|
||||
import type { ContextMessage } from '#/agent/contextMemory';
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import { IAgentPromptService } from '#/agent/prompt';
|
||||
import { IAgentSkillService } from '#/agent/skill';
|
||||
import { InMemorySkillCatalog } from '#/app/globalSkillCatalog';
|
||||
|
|
@ -14,7 +13,6 @@ import {
|
|||
MAX_SKILL_QUERY_DEPTH,
|
||||
NestedSkillTooDeepError,
|
||||
SkillTool,
|
||||
type SkillToolDeps,
|
||||
} from '#/agent/skill/tools/skill';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
|
@ -68,10 +66,6 @@ describe('AgentSkillService', () => {
|
|||
undo: () => 0,
|
||||
clear: () => {},
|
||||
});
|
||||
reg.definePartialInstance(IAgentEventSinkService, {
|
||||
emit: () => {},
|
||||
on: () => ({ dispose: () => {} }),
|
||||
});
|
||||
reg.defineInstance(IAgentWireRecordService, stubWireRecord());
|
||||
reg.definePartialInstance(IAgentReplayBuilderService, {
|
||||
push: () => {},
|
||||
|
|
@ -175,10 +169,6 @@ describe('SkillTool', () => {
|
|||
undo: () => 0,
|
||||
clear: () => {},
|
||||
});
|
||||
reg.definePartialInstance(IAgentEventSinkService, {
|
||||
emit: () => {},
|
||||
on: () => ({ dispose: () => {} }),
|
||||
});
|
||||
reg.defineInstance(IAgentWireRecordService, stubWireRecord());
|
||||
reg.definePartialInstance(IAgentReplayBuilderService, {
|
||||
push: () => {},
|
||||
|
|
@ -215,16 +205,25 @@ describe('SkillTool', () => {
|
|||
};
|
||||
}
|
||||
|
||||
function skillToolDeps(ix: TestInstantiationService): SkillToolDeps {
|
||||
function stubSkillService(): IAgentSkillService {
|
||||
return {
|
||||
catalog: ix.get(ISessionSkillCatalog),
|
||||
prompt: ix.get(IAgentPromptService),
|
||||
recordActivation: () => {},
|
||||
_serviceBrand: undefined,
|
||||
activate: () => Promise.reject(new Error('not implemented')),
|
||||
recordModelToolActivation: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
function makeTool(ix: TestInstantiationService, depth?: number): SkillTool {
|
||||
const tool = new SkillTool(
|
||||
ix.get(ISessionSkillCatalog),
|
||||
ix.get(IAgentPromptService),
|
||||
stubSkillService(),
|
||||
);
|
||||
return depth === undefined ? tool : tool.withInitialQueryDepth(depth);
|
||||
}
|
||||
|
||||
it('exposes metadata and schema for model-invoked skills', () => {
|
||||
const tool = new SkillTool(skillToolDeps(ix));
|
||||
const tool = makeTool(ix);
|
||||
|
||||
expect(tool.name).toBe('Skill');
|
||||
expect(tool.description).toContain('Invoke a registered skill');
|
||||
|
|
@ -242,7 +241,7 @@ describe('SkillTool', () => {
|
|||
|
||||
it('returns a tool error when the skill is unknown', async () => {
|
||||
const result = await executeTool(
|
||||
new SkillTool(skillToolDeps(ix)),
|
||||
makeTool(ix),
|
||||
toolContext({ skill: 'missing' }),
|
||||
);
|
||||
|
||||
|
|
@ -256,7 +255,7 @@ describe('SkillTool', () => {
|
|||
skills.register(stubSkill('private', { metadata: { disableModelInvocation: true } }));
|
||||
|
||||
const result = await executeTool(
|
||||
new SkillTool(skillToolDeps(ix)),
|
||||
makeTool(ix),
|
||||
toolContext({ skill: 'private' }),
|
||||
);
|
||||
|
||||
|
|
@ -270,7 +269,7 @@ describe('SkillTool', () => {
|
|||
skills.register(stubSkill('flow-only', { metadata: { type: 'flow' } }));
|
||||
|
||||
const result = await executeTool(
|
||||
new SkillTool(skillToolDeps(ix)),
|
||||
makeTool(ix),
|
||||
toolContext({ skill: 'flow-only' }),
|
||||
);
|
||||
|
||||
|
|
@ -282,7 +281,7 @@ describe('SkillTool', () => {
|
|||
|
||||
it('loads inline skills through the model-tool wrapper without exposing the body in output', async () => {
|
||||
const result = await executeTool(
|
||||
new SkillTool(skillToolDeps(ix)),
|
||||
makeTool(ix),
|
||||
toolContext({ skill: 'commit', args: 'src/app.ts' }),
|
||||
);
|
||||
|
||||
|
|
@ -310,11 +309,11 @@ describe('SkillTool', () => {
|
|||
|
||||
it('honors initialQueryDepth as an alias for queryDepth', async () => {
|
||||
await executeTool(
|
||||
new SkillTool(skillToolDeps(ix), { initialQueryDepth: 2 }),
|
||||
makeTool(ix, 2),
|
||||
toolContext({ skill: 'commit' }),
|
||||
);
|
||||
await executeTool(
|
||||
new SkillTool(skillToolDeps(ix), { initialQueryDepth: 0 }),
|
||||
makeTool(ix, 0),
|
||||
toolContext({ skill: 'commit' }),
|
||||
);
|
||||
|
||||
|
|
@ -332,7 +331,7 @@ describe('SkillTool', () => {
|
|||
it('throws a structured recursion error when nested skill invocation is too deep', async () => {
|
||||
await expect(
|
||||
executeTool(
|
||||
new SkillTool(skillToolDeps(ix), { initialQueryDepth: MAX_SKILL_QUERY_DEPTH }),
|
||||
makeTool(ix, MAX_SKILL_QUERY_DEPTH),
|
||||
toolContext({ skill: 'commit' }),
|
||||
),
|
||||
).rejects.toBeInstanceOf(NestedSkillTooDeepError);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { SyncDescriptor } from '#/_base/di/descriptors';
|
|||
import { DisposableStore, toDisposable } from '#/_base/di/lifecycle';
|
||||
import { TestInstantiationService } from '#/_base/di/test';
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory';
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
import {
|
||||
DEFAULT_SUBAGENT_TIMEOUT_MS,
|
||||
} from '#/agent/agentTool';
|
||||
|
|
@ -51,7 +51,7 @@ function mockSwarmHost({
|
|||
}
|
||||
|
||||
function mockSwarmMode() {
|
||||
return { enter: vi.fn() };
|
||||
return { _serviceBrand: undefined, isActive: false, enter: vi.fn(), exit: vi.fn() };
|
||||
}
|
||||
|
||||
describe('AgentSwarmService', () => {
|
||||
|
|
@ -63,7 +63,12 @@ describe('AgentSwarmService', () => {
|
|||
ix = disposables.add(new TestInstantiationService());
|
||||
ix.stub(IAgentContextMemoryService, stubContextMemory());
|
||||
ix.stub(IAgentWireRecordService, stubWireRecord());
|
||||
ix.stub(IAgentEventSinkService, { emit: () => {}, on: () => toDisposable(() => {}) });
|
||||
ix.stub(IAgentRecordService, {
|
||||
append: () => {},
|
||||
signal: () => {},
|
||||
on: () => toDisposable(() => {}),
|
||||
define: () => toDisposable(() => {}),
|
||||
});
|
||||
ix.stub(IAgentTurnService, stubTurnWithHooks());
|
||||
ix.set(IAgentToolRegistryService, new SyncDescriptor(AgentToolRegistryService));
|
||||
ix.stub(IAgentLifecycleService, {});
|
||||
|
|
@ -129,7 +134,7 @@ describe('AgentSwarmTool', () => {
|
|||
]),
|
||||
});
|
||||
const swarmMode = mockSwarmMode();
|
||||
const tool = new AgentSwarmTool(host, swarmMode);
|
||||
const tool = new AgentSwarmTool(host.swarmService, { _serviceBrand: undefined, agentId: host.callerAgentId }, swarmMode);
|
||||
const input = {
|
||||
description: 'Review files',
|
||||
prompt_template: 'Review {{item}}',
|
||||
|
|
@ -215,7 +220,8 @@ describe('AgentSwarmTool', () => {
|
|||
});
|
||||
|
||||
it('does not expose permission rule argument matching', () => {
|
||||
const tool = new AgentSwarmTool(mockSwarmHost(), mockSwarmMode());
|
||||
const host = mockSwarmHost();
|
||||
const tool = new AgentSwarmTool(host.swarmService, { _serviceBrand: undefined, agentId: host.callerAgentId }, mockSwarmMode());
|
||||
const execution = tool.resolveExecution({
|
||||
description: 'Review files',
|
||||
prompt_template: 'Review {{item}}',
|
||||
|
|
@ -274,7 +280,7 @@ describe('AgentSwarmTool', () => {
|
|||
|
||||
for (const testCase of cases) {
|
||||
const host = mockSwarmHost();
|
||||
const tool = new AgentSwarmTool(host, mockSwarmMode());
|
||||
const tool = new AgentSwarmTool(host.swarmService, { _serviceBrand: undefined, agentId: host.callerAgentId }, mockSwarmMode());
|
||||
|
||||
const result = await executeTool(tool, context(testCase.input));
|
||||
|
||||
|
|
@ -307,7 +313,7 @@ describe('AgentSwarmTool', () => {
|
|||
},
|
||||
);
|
||||
const host = mockSwarmHost({ run });
|
||||
const tool = new AgentSwarmTool(host, mockSwarmMode());
|
||||
const tool = new AgentSwarmTool(host.swarmService, { _serviceBrand: undefined, agentId: host.callerAgentId }, mockSwarmMode());
|
||||
// Seed the module-level swarm item map so resume_agent_ids can recover the original items.
|
||||
await executeTool(
|
||||
tool,
|
||||
|
|
@ -434,7 +440,7 @@ describe('AgentSwarmTool', () => {
|
|||
},
|
||||
);
|
||||
const host = mockSwarmHost({ run });
|
||||
const tool = new AgentSwarmTool(host, mockSwarmMode());
|
||||
const tool = new AgentSwarmTool(host.swarmService, { _serviceBrand: undefined, agentId: host.callerAgentId }, mockSwarmMode());
|
||||
// Seed the module-level swarm item map so resume_agent_ids can recover the original item.
|
||||
await executeTool(
|
||||
tool,
|
||||
|
|
@ -502,7 +508,7 @@ describe('AgentSwarmTool', () => {
|
|||
},
|
||||
]),
|
||||
});
|
||||
const tool = new AgentSwarmTool(host, mockSwarmMode());
|
||||
const tool = new AgentSwarmTool(host.swarmService, { _serviceBrand: undefined, agentId: host.callerAgentId }, mockSwarmMode());
|
||||
|
||||
const result = await executeTool(
|
||||
tool,
|
||||
|
|
@ -541,7 +547,7 @@ describe('AgentSwarmTool', () => {
|
|||
},
|
||||
]),
|
||||
});
|
||||
const tool = new AgentSwarmTool(host, mockSwarmMode());
|
||||
const tool = new AgentSwarmTool(host.swarmService, { _serviceBrand: undefined, agentId: host.callerAgentId }, mockSwarmMode());
|
||||
|
||||
const result = await executeTool(
|
||||
tool,
|
||||
|
|
@ -588,7 +594,7 @@ describe('AgentSwarmTool', () => {
|
|||
},
|
||||
]),
|
||||
});
|
||||
const tool = new AgentSwarmTool(host, mockSwarmMode());
|
||||
const tool = new AgentSwarmTool(host.swarmService, { _serviceBrand: undefined, agentId: host.callerAgentId }, mockSwarmMode());
|
||||
|
||||
const result = await executeTool(
|
||||
tool,
|
||||
|
|
|
|||
|
|
@ -7,25 +7,28 @@ import {
|
|||
TodoListTool,
|
||||
type TodoItem,
|
||||
} from '#/agent/todoList/tools/todo-list';
|
||||
import type { ToolStore } from '#/agent/toolStore';
|
||||
import type { IAgentToolStoreService } from '#/agent/toolStore';
|
||||
import { executeTool } from '../tools/fixtures/execute-tool';
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
function makeStore(initial: readonly TodoItem[] = []): {
|
||||
readonly store: ToolStore;
|
||||
readonly store: IAgentToolStoreService;
|
||||
readonly getTodos: () => readonly TodoItem[];
|
||||
} {
|
||||
let todos = [...initial];
|
||||
return {
|
||||
store: {
|
||||
get: (key) => (key === TODO_STORE_KEY ? todos : undefined),
|
||||
set: (key, value) => {
|
||||
_serviceBrand: undefined,
|
||||
get: (key: string) => (key === TODO_STORE_KEY ? todos : undefined),
|
||||
set: (key: string, value: unknown) => {
|
||||
if (key === TODO_STORE_KEY) {
|
||||
todos = [...(value as readonly TodoItem[])];
|
||||
}
|
||||
},
|
||||
},
|
||||
data: () => ({ [TODO_STORE_KEY]: todos }),
|
||||
hooks: { onUpdated: { register: () => ({ dispose: () => {} }) } },
|
||||
} as unknown as IAgentToolStoreService,
|
||||
getTodos: () => todos,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue