diff --git a/packages/agent-core-v2/src/agent/agentTool/agentTool.ts b/packages/agent-core-v2/src/agent/agentTool/agentTool.ts index 828d9b338..a37f9cf5d 100644 --- a/packages/agent-core-v2/src/agent/agentTool/agentTool.ts +++ b/packages/agent-core-v2/src/agent/agentTool/agentTool.ts @@ -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>; -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 { readonly name: string = 'Agent'; readonly parameters: Record = 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 ?? { diff --git a/packages/agent-core-v2/src/agent/agentTool/agentToolService.ts b/packages/agent-core-v2/src/agent/agentTool/agentToolService.ts index 77573ce62..447a97a87 100644 --- a/packages/agent-core-v2/src/agent/agentTool/agentToolService.ts +++ b/packages/agent-core-v2/src/agent/agentTool/agentToolService.ts @@ -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', ); diff --git a/packages/agent-core-v2/src/agent/agentTool/index.ts b/packages/agent-core-v2/src/agent/agentTool/index.ts index 97d636480..698ee536b 100644 --- a/packages/agent-core-v2/src/agent/agentTool/index.ts +++ b/packages/agent-core-v2/src/agent/agentTool/index.ts @@ -18,7 +18,6 @@ export { } from './agentTool'; export type { AgentToolInput, - AgentToolOptions, AgentToolOutput, AgentToolSubagentMap, AgentToolSubagentProfile, diff --git a/packages/agent-core-v2/src/agent/agentTool/runChildAgent.ts b/packages/agent-core-v2/src/agent/agentTool/runChildAgent.ts index 2b1b63792..c2b5008a2 100644 --- a/packages/agent-core-v2/src/agent/agentTool/runChildAgent.ts +++ b/packages/agent-core-v2/src/agent/agentTool/runChildAgent.ts @@ -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 { 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); diff --git a/packages/agent-core-v2/src/agent/background/backgroundService.ts b/packages/agent-core-v2/src/agent/background/backgroundService.ts index 45d5607bc..6cc000cbc 100644 --- a/packages/agent-core-v2/src/agent/background/backgroundService.ts +++ b/packages/agent-core-v2/src/agent/background/backgroundService.ts @@ -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 { diff --git a/packages/agent-core-v2/src/agent/background/backgroundTools.ts b/packages/agent-core-v2/src/agent/background/backgroundTools.ts new file mode 100644 index 000000000..af46263ec --- /dev/null +++ b/packages/agent-core-v2/src/agent/background/backgroundTools.ts @@ -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 = + createDecorator('agentBackgroundToolsService'); diff --git a/packages/agent-core-v2/src/agent/background/backgroundToolsService.ts b/packages/agent-core-v2/src/agent/background/backgroundToolsService.ts new file mode 100644 index 000000000..2df0e8d57 --- /dev/null +++ b/packages/agent-core-v2/src/agent/background/backgroundToolsService.ts @@ -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', +); diff --git a/packages/agent-core-v2/src/agent/background/index.ts b/packages/agent-core-v2/src/agent/background/index.ts index c52f87b40..614172249 100644 --- a/packages/agent-core-v2/src/agent/background/index.ts +++ b/packages/agent-core-v2/src/agent/background/index.ts @@ -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'; diff --git a/packages/agent-core-v2/src/agent/background/tools/task-list.ts b/packages/agent-core-v2/src/agent/background/tools/task-list.ts index 3c83c4e14..1dd300ea0 100644 --- a/packages/agent-core-v2/src/agent/background/tools/task-list.ts +++ b/packages/agent-core-v2/src/agent/background/tools/task-list.ts @@ -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 { readonly description = TASK_LIST_DESCRIPTION; readonly parameters: Record = 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'; diff --git a/packages/agent-core-v2/src/agent/background/tools/task-output.ts b/packages/agent-core-v2/src/agent/background/tools/task-output.ts index 3d748b017..fd0fd2f9d 100644 --- a/packages/agent-core-v2/src/agent/background/tools/task-output.ts +++ b/packages/agent-core-v2/src/agent/background/tools/task-output.ts @@ -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 { readonly description: string = TASK_OUTPUT_DESCRIPTION; readonly parameters: Record = toInputJsonSchema(TaskOutputInputSchema); - constructor(private readonly background: IAgentBackgroundService) {} + constructor(@IAgentBackgroundService private readonly background: IAgentBackgroundService) {} resolveExecution(args: TaskOutputInput): ToolExecution { return { diff --git a/packages/agent-core-v2/src/agent/background/tools/task-stop.ts b/packages/agent-core-v2/src/agent/background/tools/task-stop.ts index 7200f4a22..920e68caa 100644 --- a/packages/agent-core-v2/src/agent/background/tools/task-stop.ts +++ b/packages/agent-core-v2/src/agent/background/tools/task-stop.ts @@ -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 { readonly description = TASK_STOP_DESCRIPTION; readonly parameters: Record = toInputJsonSchema(TaskStopInputSchema); - constructor(private readonly background: IAgentBackgroundService) {} + constructor(@IAgentBackgroundService private readonly background: IAgentBackgroundService) {} resolveExecution(args: TaskStopInput): ToolExecution { return { diff --git a/packages/agent-core-v2/src/agent/cron/cronService.ts b/packages/agent-core-v2/src/agent/cron/cronService.ts index d78a678d3..d20dfe222 100644 --- a/packages/agent-core-v2/src/agent/cron/cronService.ts +++ b/packages/agent-core-v2/src/agent/cron/cronService.ts @@ -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(); } diff --git a/packages/agent-core-v2/src/agent/cron/cronTools.ts b/packages/agent-core-v2/src/agent/cron/cronTools.ts new file mode 100644 index 000000000..eae7c6533 --- /dev/null +++ b/packages/agent-core-v2/src/agent/cron/cronTools.ts @@ -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 = + createDecorator('agentCronToolsService'); diff --git a/packages/agent-core-v2/src/agent/cron/cronToolsService.ts b/packages/agent-core-v2/src/agent/cron/cronToolsService.ts new file mode 100644 index 000000000..81c818b03 --- /dev/null +++ b/packages/agent-core-v2/src/agent/cron/cronToolsService.ts @@ -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(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', +); diff --git a/packages/agent-core-v2/src/agent/cron/index.ts b/packages/agent-core-v2/src/agent/cron/index.ts index 19ceac14b..ad5046775 100644 --- a/packages/agent-core-v2/src/agent/cron/index.ts +++ b/packages/agent-core-v2/src/agent/cron/index.ts @@ -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'; diff --git a/packages/agent-core-v2/src/agent/cron/tools/cron-create.ts b/packages/agent-core-v2/src/agent/cron/tools/cron-create.ts index f4dc47c80..4390d91f1 100644 --- a/packages/agent-core-v2/src/agent/cron/tools/cron-create.ts +++ b/packages/agent-core-v2/src/agent/cron/tools/cron-create.ts @@ -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 { ); constructor( - private readonly cron: IAgentCronService, private readonly disabled: boolean = false, + @IAgentCronService private readonly cron: IAgentCronService, ) {} resolveExecution(args: CronCreateInput): ToolExecution { diff --git a/packages/agent-core-v2/src/agent/cron/tools/cron-delete.ts b/packages/agent-core-v2/src/agent/cron/tools/cron-delete.ts index 2b9d5d830..ed052435f 100644 --- a/packages/agent-core-v2/src/agent/cron/tools/cron-delete.ts +++ b/packages/agent-core-v2/src/agent/cron/tools/cron-delete.ts @@ -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 { 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, diff --git a/packages/agent-core-v2/src/agent/cron/tools/cron-list.ts b/packages/agent-core-v2/src/agent/cron/tools/cron-list.ts index dc57fee56..e4c2a101b 100644 --- a/packages/agent-core-v2/src/agent/cron/tools/cron-list.ts +++ b/packages/agent-core-v2/src/agent/cron/tools/cron-list.ts @@ -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 { CronListInputSchema, ); - constructor(private readonly cron: IAgentCronService) {} + constructor(@IAgentCronService private readonly cron: IAgentCronService) {} resolveExecution(_args: CronListInput): ToolExecution { return { diff --git a/packages/agent-core-v2/src/agent/fileTools/fileToolsService.ts b/packages/agent-core-v2/src/agent/fileTools/fileToolsService.ts index 26cbc9a61..63632ddb3 100644 --- a/packages/agent-core-v2/src/agent/fileTools/fileToolsService.ts +++ b/packages/agent-core-v2/src/agent/fileTools/fileToolsService.ts @@ -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', ); diff --git a/packages/agent-core-v2/src/agent/fileTools/tools/edit.ts b/packages/agent-core-v2/src/agent/fileTools/tools/edit.ts index 253b0db34..c5fdc7826 100644 --- a/packages/agent-core-v2/src/agent/fileTools/tools/edit.ts +++ b/packages/agent-core-v2/src/agent/fileTools/tools/edit.ts @@ -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 { readonly parameters: Record = 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 { 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, }), diff --git a/packages/agent-core-v2/src/agent/fileTools/tools/glob.ts b/packages/agent-core-v2/src/agent/fileTools/tools/glob.ts index 063cad560..e23adfeaf 100644 --- a/packages/agent-core-v2/src/agent/fileTools/tools/glob.ts +++ b/packages/agent-core-v2/src/agent/fileTools/tools/glob.ts @@ -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 { readonly name = 'Glob' as const; readonly description: string; readonly parameters: Record = 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 { signal: AbortSignal, searchRoots: readonly string[], ): Promise { - const searchRoot = searchRoots[0] ?? this.workspace.workspaceDir; + const searchRoot = searchRoots[0] ?? this.workspaceConfig.workspaceDir; // `rg --files ` 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 { // 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, ); diff --git a/packages/agent-core-v2/src/agent/fileTools/tools/grep.ts b/packages/agent-core-v2/src/agent/fileTools/tools/grep.ts index bc89cd94e..3e5f0513d 100644 --- a/packages/agent-core-v2/src/agent/fileTools/tools/grep.ts +++ b/packages/agent-core-v2/src/agent/fileTools/tools/grep.ts @@ -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 { readonly description = GREP_DESCRIPTION; readonly parameters: Record = 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}`, diff --git a/packages/agent-core-v2/src/agent/fileTools/tools/read.ts b/packages/agent-core-v2/src/agent/fileTools/tools/read.ts index d281ebbaa..d91f175b8 100644 --- a/packages/agent-core-v2/src/agent/fileTools/tools/read.ts +++ b/packages/agent-core-v2/src/agent/fileTools/tools/read.ts @@ -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 { readonly description = READ_DESCRIPTION; readonly parameters: Record = 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 { 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, }), diff --git a/packages/agent-core-v2/src/agent/fileTools/tools/write.ts b/packages/agent-core-v2/src/agent/fileTools/tools/write.ts index ede805731..e933e7a05 100644 --- a/packages/agent-core-v2/src/agent/fileTools/tools/write.ts +++ b/packages/agent-core-v2/src/agent/fileTools/tools/write.ts @@ -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 { readonly parameters: Record = 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 { 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, }), diff --git a/packages/agent-core-v2/src/agent/goal/goalService.ts b/packages/agent-core-v2/src/agent/goal/goalService.ts index 655a544de..d5d2c2703 100644 --- a/packages/agent-core-v2/src/agent/goal/goalService.ts +++ b/packages/agent-core-v2/src/agent/goal/goalService.ts @@ -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 { diff --git a/packages/agent-core-v2/src/agent/goal/goalTools.ts b/packages/agent-core-v2/src/agent/goal/goalTools.ts new file mode 100644 index 000000000..0a2a5ed3f --- /dev/null +++ b/packages/agent-core-v2/src/agent/goal/goalTools.ts @@ -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 = + createDecorator('agentGoalToolsService'); diff --git a/packages/agent-core-v2/src/agent/goal/goalToolsService.ts b/packages/agent-core-v2/src/agent/goal/goalToolsService.ts new file mode 100644 index 000000000..833775602 --- /dev/null +++ b/packages/agent-core-v2/src/agent/goal/goalToolsService.ts @@ -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', +); diff --git a/packages/agent-core-v2/src/agent/goal/index.ts b/packages/agent-core-v2/src/agent/goal/index.ts index 5ce944034..f9e69d73d 100644 --- a/packages/agent-core-v2/src/agent/goal/index.ts +++ b/packages/agent-core-v2/src/agent/goal/index.ts @@ -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'; diff --git a/packages/agent-core-v2/src/agent/goal/tools/create-goal.ts b/packages/agent-core-v2/src/agent/goal/tools/create-goal.ts index 0cce5ae96..ce7ceff76 100644 --- a/packages/agent-core-v2/src/agent/goal/tools/create-goal.ts +++ b/packages/agent-core-v2/src/agent/goal/tools/create-goal.ts @@ -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 { readonly parameters: Record = 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 { diff --git a/packages/agent-core-v2/src/agent/goal/tools/get-goal.ts b/packages/agent-core-v2/src/agent/goal/tools/get-goal.ts index 30663eaf6..1db432349 100644 --- a/packages/agent-core-v2/src/agent/goal/tools/get-goal.ts +++ b/packages/agent-core-v2/src/agent/goal/tools/get-goal.ts @@ -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 { readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(GetGoalToolInputSchema); - constructor(private readonly goal: IAgentGoalService) {} + constructor(@IAgentGoalService private readonly goal: IAgentGoalService) {} resolveExecution(_args: GetGoalToolInput): ToolExecution { return { diff --git a/packages/agent-core-v2/src/agent/goal/tools/set-goal-budget.ts b/packages/agent-core-v2/src/agent/goal/tools/set-goal-budget.ts index c267f264d..a2e377ac3 100644 --- a/packages/agent-core-v2/src/agent/goal/tools/set-goal-budget.ts +++ b/packages/agent-core-v2/src/agent/goal/tools/set-goal-budget.ts @@ -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 { readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(SetGoalBudgetToolInputSchema); - constructor(private readonly goal: IAgentGoalService) {} + constructor(@IAgentGoalService private readonly goal: IAgentGoalService) {} resolveExecution(args: SetGoalBudgetToolInput): ToolExecution { const normalizedArgs = normalizeBudgetInput(args); diff --git a/packages/agent-core-v2/src/agent/goal/tools/update-goal.ts b/packages/agent-core-v2/src/agent/goal/tools/update-goal.ts index 28a5d9bae..7084962b1 100644 --- a/packages/agent-core-v2/src/agent/goal/tools/update-goal.ts +++ b/packages/agent-core-v2/src/agent/goal/tools/update-goal.ts @@ -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 { readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(UpdateGoalToolInputSchema); - constructor(private readonly goal: IAgentGoalService) {} + constructor(@IAgentGoalService private readonly goal: IAgentGoalService) {} resolveExecution(args: UpdateGoalToolInput): ToolExecution { return { diff --git a/packages/agent-core-v2/src/agent/plan/index.ts b/packages/agent-core-v2/src/agent/plan/index.ts index f138f333f..7a32b3a0c 100644 --- a/packages/agent-core-v2/src/agent/plan/index.ts +++ b/packages/agent-core-v2/src/agent/plan/index.ts @@ -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'; diff --git a/packages/agent-core-v2/src/agent/plan/planService.ts b/packages/agent-core-v2/src/agent/plan/planService.ts index 5dd646d14..70e16ba20 100644 --- a/packages/agent-core-v2/src/agent/plan/planService.ts +++ b/packages/agent-core-v2/src/agent/plan/planService.ts @@ -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 }) => { diff --git a/packages/agent-core-v2/src/agent/plan/planTools.ts b/packages/agent-core-v2/src/agent/plan/planTools.ts new file mode 100644 index 000000000..155d01aa3 --- /dev/null +++ b/packages/agent-core-v2/src/agent/plan/planTools.ts @@ -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 = + createDecorator('agentPlanToolsService'); diff --git a/packages/agent-core-v2/src/agent/plan/planToolsService.ts b/packages/agent-core-v2/src/agent/plan/planToolsService.ts new file mode 100644 index 000000000..2e7ca65a6 --- /dev/null +++ b/packages/agent-core-v2/src/agent/plan/planToolsService.ts @@ -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', +); diff --git a/packages/agent-core-v2/src/agent/plan/tools/enter-plan-mode.ts b/packages/agent-core-v2/src/agent/plan/tools/enter-plan-mode.ts index 8ce53a0d6..e89da80cc 100644 --- a/packages/agent-core-v2/src/agent/plan/tools/enter-plan-mode.ts +++ b/packages/agent-core-v2/src/agent/plan/tools/enter-plan-mode.ts @@ -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 { readonly parameters: Record = 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 { diff --git a/packages/agent-core-v2/src/agent/plan/tools/exit-plan-mode.ts b/packages/agent-core-v2/src/agent/plan/tools/exit-plan-mode.ts index 63dca4943..8d77f53ec 100644 --- a/packages/agent-core-v2/src/agent/plan/tools/exit-plan-mode.ts +++ b/packages/agent-core-v2/src/agent/plan/tools/exit-plan-mode.ts @@ -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 { readonly parameters: Record = 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 { diff --git a/packages/agent-core-v2/src/agent/questionTools/questionToolsService.ts b/packages/agent-core-v2/src/agent/questionTools/questionToolsService.ts index 56cabd87f..c4ec31988 100644 --- a/packages/agent-core-v2/src/agent/questionTools/questionToolsService.ts +++ b/packages/agent-core-v2/src/agent/questionTools/questionToolsService.ts @@ -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', ); diff --git a/packages/agent-core-v2/src/agent/questionTools/tools/ask-user.ts b/packages/agent-core-v2/src/agent/questionTools/tools/ask-user.ts index 0573b75e6..308e1ae58 100644 --- a/packages/agent-core-v2/src/agent/questionTools/tools/ask-user.ts +++ b/packages/agent-core-v2/src/agent/questionTools/tools/ask-user.ts @@ -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 { ); 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 { diff --git a/packages/agent-core-v2/src/agent/rpc/rpcService.ts b/packages/agent-core-v2/src/agent/rpc/rpcService.ts index 88e2862a0..b51788949 100644 --- a/packages/agent-core-v2/src/agent/rpc/rpcService.ts +++ b/packages/agent-core-v2/src/agent/rpc/rpcService.ts @@ -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; } diff --git a/packages/agent-core-v2/src/agent/shellTools/shellToolsService.ts b/packages/agent-core-v2/src/agent/shellTools/shellToolsService.ts index 01a396497..48a34f963 100644 --- a/packages/agent-core-v2/src/agent/shellTools/shellToolsService.ts +++ b/packages/agent-core-v2/src/agent/shellTools/shellToolsService.ts @@ -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', ); diff --git a/packages/agent-core-v2/src/agent/shellTools/tools/bash.ts b/packages/agent-core-v2/src/agent/shellTools/tools/bash.ts index 35457ba51..865f0c558 100644 --- a/packages/agent-core-v2/src/agent/shellTools/tools/bash.ts +++ b/packages/agent-core-v2/src/agent/shellTools/tools/bash.ts @@ -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 { 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 diff --git a/packages/agent-core-v2/src/agent/skill/index.ts b/packages/agent-core-v2/src/agent/skill/index.ts index 7958b471c..9cb6fed8e 100644 --- a/packages/agent-core-v2/src/agent/skill/index.ts +++ b/packages/agent-core-v2/src/agent/skill/index.ts @@ -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'; diff --git a/packages/agent-core-v2/src/agent/skill/skill.ts b/packages/agent-core-v2/src/agent/skill/skill.ts index 653a87669..840fb04f9 100644 --- a/packages/agent-core-v2/src/agent/skill/skill.ts +++ b/packages/agent-core-v2/src/agent/skill/skill.ts @@ -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; + + /** + * 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 = diff --git a/packages/agent-core-v2/src/agent/skill/skillService.ts b/packages/agent-core-v2/src/agent/skill/skillService.ts index 1b26435f3..17e9bff36 100644 --- a/packages/agent-core-v2/src/agent/skill/skillService.ts +++ b/packages/agent-core-v2/src/agent/skill/skillService.ts @@ -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 { @@ -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( diff --git a/packages/agent-core-v2/src/agent/skill/skillTools.ts b/packages/agent-core-v2/src/agent/skill/skillTools.ts new file mode 100644 index 000000000..3dc21d033 --- /dev/null +++ b/packages/agent-core-v2/src/agent/skill/skillTools.ts @@ -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 = + createDecorator('agentSkillToolsService'); diff --git a/packages/agent-core-v2/src/agent/skill/skillToolsService.ts b/packages/agent-core-v2/src/agent/skill/skillToolsService.ts new file mode 100644 index 000000000..da9ff99f8 --- /dev/null +++ b/packages/agent-core-v2/src/agent/skill/skillToolsService.ts @@ -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', +); diff --git a/packages/agent-core-v2/src/agent/skill/tools/skill.ts b/packages/agent-core-v2/src/agent/skill/tools/skill.ts index 9914d5320..1f8573e17 100644 --- a/packages/agent-core-v2/src/agent/skill/tools/skill.ts +++ b/packages/agent-core-v2/src/agent/skill/tools/skill.ts @@ -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 = 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 { readonly name = 'Skill'; readonly description: string = renderPrompt(skillDescriptionTemplate, { @@ -86,9 +69,17 @@ export class SkillTool implements BuiltinTool { }); readonly parameters: Record = 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 { } 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 { - 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 { // 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.`, }; diff --git a/packages/agent-core-v2/src/agent/swarm/index.ts b/packages/agent-core-v2/src/agent/swarm/index.ts index 831d46c42..ab4772689 100644 --- a/packages/agent-core-v2/src/agent/swarm/index.ts +++ b/packages/agent-core-v2/src/agent/swarm/index.ts @@ -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'; diff --git a/packages/agent-core-v2/src/agent/swarm/swarmService.ts b/packages/agent-core-v2/src/agent/swarm/swarmService.ts index a5633b4a7..46b3e3c9a 100644 --- a/packages/agent-core-v2/src/agent/swarm/swarmService.ts +++ b/packages/agent-core-v2/src/agent/swarm/swarmService.ts @@ -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 { diff --git a/packages/agent-core-v2/src/agent/swarm/swarmTools.ts b/packages/agent-core-v2/src/agent/swarm/swarmTools.ts new file mode 100644 index 000000000..f07d85108 --- /dev/null +++ b/packages/agent-core-v2/src/agent/swarm/swarmTools.ts @@ -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 = + createDecorator('agentSwarmToolsService'); diff --git a/packages/agent-core-v2/src/agent/swarm/swarmToolsService.ts b/packages/agent-core-v2/src/agent/swarm/swarmToolsService.ts new file mode 100644 index 000000000..7721873bc --- /dev/null +++ b/packages/agent-core-v2/src/agent/swarm/swarmToolsService.ts @@ -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', +); diff --git a/packages/agent-core-v2/src/agent/swarm/tools/agent-swarm.ts b/packages/agent-core-v2/src/agent/swarm/tools/agent-swarm.ts index bd5f80194..2b40b1c0b 100644 --- a/packages/agent-core-v2/src/agent/swarm/tools/agent-swarm.ts +++ b/packages/agent-core-v2/src/agent/swarm/tools/agent-swarm.ts @@ -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 { readonly name = 'AgentSwarm' as const; readonly description = AGENT_SWARM_DESCRIPTION; readonly parameters: Record = 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 { 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) { diff --git a/packages/agent-core-v2/src/agent/todoList/todoListService.ts b/packages/agent-core-v2/src/agent/todoList/todoListService.ts index e0fc7e248..f50caabb2 100644 --- a/packages/agent-core-v2/src/agent/todoList/todoListService.ts +++ b/packages/agent-core-v2/src/agent/todoList/todoListService.ts @@ -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()), ); diff --git a/packages/agent-core-v2/src/agent/todoList/tools/todo-list.ts b/packages/agent-core-v2/src/agent/todoList/tools/todo-list.ts index 00a70f203..fca96e9e6 100644 --- a/packages/agent-core-v2/src/agent/todoList/tools/todo-list.ts +++ b/packages/agent-core-v2/src/agent/todoList/tools/todo-list.ts @@ -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 { readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(TodoListInputSchema); - constructor(private readonly store: ToolStore) {} + constructor(@IAgentToolStoreService private readonly store: IAgentToolStoreService) {} resolveExecution(args: TodoListInput): ToolExecution { const description = diff --git a/packages/agent-core-v2/src/agent/web/webService.ts b/packages/agent-core-v2/src/agent/web/webService.ts index e3f89ef58..1f404531b 100644 --- a/packages/agent-core-v2/src/agent/web/webService.ts +++ b/packages/agent-core-v2/src/agent/web/webService.ts @@ -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', ); diff --git a/packages/agent-core-v2/test/agentTool/agentTool.test.ts b/packages/agent-core-v2/test/agentTool/agentTool.test.ts index 69a44b2cd..45a69948d 100644 --- a/packages/agent-core-v2/test/agentTool/agentTool.test.ts +++ b/packages/agent-core-v2/test/agentTool/agentTool.test.ts @@ -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(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, + ), }; } diff --git a/packages/agent-core-v2/test/cron/cron.e2e.test.ts b/packages/agent-core-v2/test/cron/cron.e2e.test.ts index 72ae29ab6..30d4796e4 100644 --- a/packages/agent-core-v2/test/cron/cron.e2e.test.ts +++ b/packages/agent-core-v2/test/cron/cron.e2e.test.ts @@ -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 = { diff --git a/packages/agent-core-v2/test/cron/tools.test.ts b/packages/agent-core-v2/test/cron/tools.test.ts index 5ea2efe34..6af27ce07 100644 --- a/packages/agent-core-v2/test/cron/tools.test.ts +++ b/packages/agent-core-v2/test/cron/tools.test.ts @@ -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(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(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(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(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(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(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* * * *', diff --git a/packages/agent-core-v2/test/fileTools/edit.test.ts b/packages/agent-core-v2/test/fileTools/edit.test.ts index 0d1756068..3e7d1e39a 100644 --- a/packages/agent-core-v2/test/fileTools/edit.test.ts +++ b/packages/agent-core-v2/test/fileTools/edit.test.ts @@ -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', diff --git a/packages/agent-core-v2/test/fileTools/fileToolsService.test.ts b/packages/agent-core-v2/test/fileTools/fileToolsService.test.ts index 73b8fe620..072feed38 100644 --- a/packages/agent-core-v2/test/fileTools/fileToolsService.test.ts +++ b/packages/agent-core-v2/test/fileTools/fileToolsService.test.ts @@ -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(); @@ -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']); }); }); diff --git a/packages/agent-core-v2/test/fileTools/glob.test.ts b/packages/agent-core-v2/test/fileTools/glob.test.ts index f084f422d..51de6bfd5 100644 --- a/packages/agent-core-v2/test/fileTools/glob.test.ts +++ b/packages/agent-core-v2/test/fileTools/glob.test.ts @@ -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 } = {}) { @@ -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; readdir?: ReturnType; telemetry?: ITelemetryService } = {}, ): { tool: GlobTool; exec: ReturnType; withCwd: ReturnType } { 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 }); diff --git a/packages/agent-core-v2/test/fileTools/grep.test.ts b/packages/agent-core-v2/test/fileTools/grep.test.ts index ecbce63a5..c5fe4344d 100644 --- a/packages/agent-core-v2/test/fileTools/grep.test.ts +++ b/packages/agent-core-v2/test/fileTools/grep.test.ts @@ -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 { diff --git a/packages/agent-core-v2/test/fileTools/read.test.ts b/packages/agent-core-v2/test/fileTools/read.test.ts index d2f96a0b4..dfae90c06 100644 --- a/packages/agent-core-v2/test/fileTools/read.test.ts +++ b/packages/agent-core-v2/test/fileTools/read.test.ts @@ -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) { 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' }); diff --git a/packages/agent-core-v2/test/fileTools/stub-workspace-context.ts b/packages/agent-core-v2/test/fileTools/stub-workspace-context.ts new file mode 100644 index 000000000..176eff7b5 --- /dev/null +++ b/packages/agent-core-v2/test/fileTools/stub-workspace-context.ts @@ -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: () => {}, + }; +} diff --git a/packages/agent-core-v2/test/fileTools/write.test.ts b/packages/agent-core-v2/test/fileTools/write.test.ts index 00f757421..6048514a8 100644 --- a/packages/agent-core-v2/test/fileTools/write.test.ts +++ b/packages/agent-core-v2/test/fileTools/write.test.ts @@ -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' }); diff --git a/packages/agent-core-v2/test/shellTools/bash.test.ts b/packages/agent-core-v2/test/shellTools/bash.test.ts index 5fe745678..d765d962e 100644 --- a/packages/agent-core-v2/test/shellTools/bash.test.ts +++ b/packages/agent-core-v2/test/shellTools/bash.test.ts @@ -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[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' }), diff --git a/packages/agent-core-v2/test/shellTools/shellToolsService.test.ts b/packages/agent-core-v2/test/shellTools/shellToolsService.test.ts index 03fe7373a..4d60ddd8c 100644 --- a/packages/agent-core-v2/test/shellTools/shellToolsService.test.ts +++ b/packages/agent-core-v2/test/shellTools/shellToolsService.test.ts @@ -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']); }); }); diff --git a/packages/agent-core-v2/test/skill/skill.test.ts b/packages/agent-core-v2/test/skill/skill.test.ts index fd6b38f93..cfe518c32 100644 --- a/packages/agent-core-v2/test/skill/skill.test.ts +++ b/packages/agent-core-v2/test/skill/skill.test.ts @@ -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); diff --git a/packages/agent-core-v2/test/swarm/swarm.test.ts b/packages/agent-core-v2/test/swarm/swarm.test.ts index 47e36e5c4..742b3e4b1 100644 --- a/packages/agent-core-v2/test/swarm/swarm.test.ts +++ b/packages/agent-core-v2/test/swarm/swarm.test.ts @@ -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, diff --git a/packages/agent-core-v2/test/todoList/todo-list.test.ts b/packages/agent-core-v2/test/todoList/todo-list.test.ts index 6fb8a1724..aa99ccc90 100644 --- a/packages/agent-core-v2/test/todoList/todo-list.test.ts +++ b/packages/agent-core-v2/test/todoList/todo-list.test.ts @@ -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, }; }