From eb10555ebaa3ad93f16e729fdcae8c4d5d48e4e5 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Sun, 26 Jul 2026 08:40:25 +0800 Subject: [PATCH 1/4] refactor(agent-core-v2): register agent tools as DI services with profile-aware activation - replace module-level registerTool + Eager AgentBuiltinToolsRegistrar with registerAgentTool double registration (Agent-scope DI service + contribution table) - add toolActivation domain: AgentToolActivationService filters contributions by the bound Profile's tool policy using declared names, resolves instances lazily via accessor.get, and re-activates on agent.status.updated - rename BuiltinTool to AgentTool service interface; tools become Agent-scope services with decorator-injected dependencies (e.g. AgentTool -> SubagentTool/ISubagentTool) - AgentLifecycleService.create runs one activation pass after restore and profile binding so tools reflect the Profile before the first turn - update tool registrations, scripts, and tests; add toolActivationService tests --- .../scripts/check-domain-layers.mjs | 4 + packages/agent-core-v2/scripts/debarrel.mjs | 2 +- .../src/agent/goal/tools/create-goal.ts | 15 +- .../src/agent/goal/tools/get-goal.ts | 15 +- .../src/agent/goal/tools/set-goal-budget.ts | 15 +- .../src/agent/goal/tools/update-goal.ts | 15 +- .../src/agent/media/mediaToolsRegistrar.ts | 8 +- .../src/agent/media/tools/read-media.ts | 12 +- .../src/agent/plan/tools/enter-plan-mode.ts | 15 +- .../src/agent/plan/tools/exit-plan-mode.ts | 15 +- .../src/agent/profile/profileService.ts | 5 + .../src/agent/questionTools/tools/ask-user.ts | 18 +- .../src/agent/skill/tools/skill.ts | 13 +- .../src/agent/swarm/swarmService.ts | 2 +- .../src/agent/swarm/tools/agent-swarm.ts | 13 +- .../src/agent/task/tools/task-list.ts | 13 +- .../src/agent/task/tools/task-output.ts | 13 +- .../src/agent/task/tools/task-stop.ts | 13 +- .../agent/toolActivation/toolActivation.ts | 27 +++ .../toolActivation/toolActivationService.ts | 82 +++++++++ .../toolRegistry/builtinToolsRegistrar.ts | 74 -------- .../agent/toolRegistry/toolContribution.ts | 81 +++++---- .../agent/toolSelect/tools/select-tools.ts | 13 +- .../agentProfileCatalog.ts | 2 +- .../app/agentProfileCatalog/contribution.ts | 4 +- .../app/auth/webSearch/tools/web-search.ts | 46 +++-- .../app/auth/webSearch/webSearchService.ts | 7 +- .../agent-core-v2/src/app/edit/tools/edit.ts | 15 +- .../src/app/web/tools/fetch-url.ts | 29 ++-- packages/agent-core-v2/src/index.ts | 8 +- .../src/os/backends/node-local/tools/bash.ts | 13 +- .../src/os/backends/node-local/tools/glob.ts | 13 +- .../src/os/backends/node-local/tools/grep.ts | 13 +- .../src/os/backends/node-local/tools/read.ts | 13 +- .../src/os/backends/node-local/tools/write.ts | 13 +- .../agentLifecycle/agentLifecycleService.ts | 5 + .../session/cron/sessionCronServiceImpl.ts | 14 +- .../src/session/cron/tools/cron-create.ts | 14 +- .../src/session/cron/tools/cron-delete.ts | 14 +- .../src/session/cron/tools/cron-list.ts | 14 +- .../src/session/subagent/tools/agent.ts | 59 +++++-- .../src/session/todo/tools/todo-list.ts | 27 ++- .../agent-core-v2/src/tool/toolContract.ts | 7 +- .../test/agent/goal/tools/goal-tools.test.ts | 4 +- .../toolActivationService.test.ts | 161 ++++++++++++++++++ .../test/app/web/tools/fetch-url.test.ts | 20 ++- packages/agent-core-v2/test/harness/agent.ts | 8 +- .../os/backends/node-local/tools/grep.test.ts | 44 +++-- .../agentLifecycle/agentLifecycle.test.ts | 5 +- packages/agent-core-v2/test/tool/tool.test.ts | 22 +-- 50 files changed, 774 insertions(+), 303 deletions(-) create mode 100644 packages/agent-core-v2/src/agent/toolActivation/toolActivation.ts create mode 100644 packages/agent-core-v2/src/agent/toolActivation/toolActivationService.ts delete mode 100644 packages/agent-core-v2/src/agent/toolRegistry/builtinToolsRegistrar.ts create mode 100644 packages/agent-core-v2/test/agent/toolActivation/toolActivationService.test.ts diff --git a/packages/agent-core-v2/scripts/check-domain-layers.mjs b/packages/agent-core-v2/scripts/check-domain-layers.mjs index 000e95598e..e7caca46d5 100644 --- a/packages/agent-core-v2/scripts/check-domain-layers.mjs +++ b/packages/agent-core-v2/scripts/check-domain-layers.mjs @@ -177,6 +177,10 @@ const DOMAIN_LAYER = new Map([ ['toolDedupe', 4], ['toolSelect', 4], ['toolPolicy', 4], + // `toolActivation` turns the `toolRegistry` (L3) contribution table into + // per-agent runtime registrations, filtered by the bound Profile's tool + // policy (`profile`, L4) — the reason it cannot live in L3 itself. + ['toolActivation', 4], ['contextMemory', 4], ['contextInjector', 4], ['agentPlugin', 4], diff --git a/packages/agent-core-v2/scripts/debarrel.mjs b/packages/agent-core-v2/scripts/debarrel.mjs index d6a2a96a08..3336da74da 100644 --- a/packages/agent-core-v2/scripts/debarrel.mjs +++ b/packages/agent-core-v2/scripts/debarrel.mjs @@ -365,7 +365,7 @@ function regenerateEntry() { // --------------------------------------------------------------------------- const REGISTER_NAMES = new Set([ 'registerScopedService', - 'registerTool', + 'registerAgentTool', 'registerErrorDomain', 'registerConfigSection', 'registerAgentProfile', 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 5e5dd2fbc1..071a454ae3 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,11 +9,12 @@ import { z } from 'zod'; import type { ToolInputDisplay } from '#/tool/toolInputDisplay'; +import { createDecorator } from '#/_base/di/instantiation'; import { toInputJsonSchema } from '#/tool/input-schema'; import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; -import type { BuiltinTool, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { type AgentTool, type ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { IAgentGoalService } from '#/agent/goal/goal'; import DESCRIPTION from './create-goal.md?raw'; @@ -35,7 +36,11 @@ export const CreateGoalToolInputSchema = z export type CreateGoalToolInput = z.infer; -export class CreateGoalTool implements BuiltinTool { +export interface ICreateGoalTool extends AgentTool { readonly _serviceBrand: undefined } +export const ICreateGoalTool = createDecorator('createGoalTool'); + +export class CreateGoalTool implements ICreateGoalTool { + declare readonly _serviceBrand: undefined; readonly name = 'CreateGoal' as const; readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(CreateGoalToolInputSchema); @@ -84,6 +89,8 @@ export class CreateGoalTool implements BuiltinTool { } } -registerTool(CreateGoalTool, { +registerAgentTool(ICreateGoalTool, CreateGoalTool, { + name: 'CreateGoal', + domain: 'goal', when: (accessor) => accessor.get(IAgentScopeContext).agentId === 'main', }); 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 ad912a9086..45a79bac6b 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 @@ -7,10 +7,11 @@ import { z } from 'zod'; +import { createDecorator } from '#/_base/di/instantiation'; import { toInputJsonSchema } from '#/tool/input-schema'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; -import type { BuiltinTool, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { type AgentTool, type ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { IAgentGoalService } from '#/agent/goal/goal'; import DESCRIPTION from './get-goal.md?raw'; @@ -19,7 +20,11 @@ import { goalResultForModel } from './serialize'; export const GetGoalToolInputSchema = z.object({}).strict(); export type GetGoalToolInput = z.infer; -export class GetGoalTool implements BuiltinTool { +export interface IGetGoalTool extends AgentTool { readonly _serviceBrand: undefined } +export const IGetGoalTool = createDecorator('getGoalTool'); + +export class GetGoalTool implements IGetGoalTool { + declare readonly _serviceBrand: undefined; readonly name = 'GetGoal' as const; readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(GetGoalToolInputSchema); @@ -38,6 +43,8 @@ export class GetGoalTool implements BuiltinTool { } } -registerTool(GetGoalTool, { +registerAgentTool(IGetGoalTool, GetGoalTool, { + name: 'GetGoal', + domain: 'goal', when: (accessor) => accessor.get(IAgentScopeContext).agentId === 'main', }); 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 d9b2288230..ffc330e09e 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 @@ -8,10 +8,11 @@ import { z } from 'zod'; +import { createDecorator } from '#/_base/di/instantiation'; import { toInputJsonSchema } from '#/tool/input-schema'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; -import type { BuiltinTool, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { type AgentTool, type ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { IAgentGoalService } from '#/agent/goal/goal'; import type { GoalBudgetLimits, GoalSnapshot } from '#/agent/goal/types'; @@ -30,7 +31,11 @@ export const SetGoalBudgetToolInputSchema = z export type SetGoalBudgetToolInput = z.infer; -export class SetGoalBudgetTool implements BuiltinTool { +export interface ISetGoalBudgetTool extends AgentTool { readonly _serviceBrand: undefined } +export const ISetGoalBudgetTool = createDecorator('setGoalBudgetTool'); + +export class SetGoalBudgetTool implements ISetGoalBudgetTool { + declare readonly _serviceBrand: undefined; readonly name = 'SetGoalBudget' as const; readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(SetGoalBudgetToolInputSchema); @@ -99,7 +104,9 @@ export class SetGoalBudgetTool implements BuiltinTool { } } -registerTool(SetGoalBudgetTool, { +registerAgentTool(ISetGoalBudgetTool, SetGoalBudgetTool, { + name: 'SetGoalBudget', + domain: 'goal', when: (accessor) => accessor.get(IAgentScopeContext).agentId === 'main', }); 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 f058126b30..00d6c972f6 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 @@ -11,10 +11,11 @@ import { z } from 'zod'; +import { createDecorator } from '#/_base/di/instantiation'; import { toInputJsonSchema } from '#/tool/input-schema'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; -import type { BuiltinTool, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { type AgentTool, type ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { IAgentGoalService } from '#/agent/goal/goal'; import { @@ -35,7 +36,11 @@ export const UpdateGoalToolInputSchema = z export type UpdateGoalToolInput = z.infer; -export class UpdateGoalTool implements BuiltinTool { +export interface IUpdateGoalTool extends AgentTool { readonly _serviceBrand: undefined } +export const IUpdateGoalTool = createDecorator('updateGoalTool'); + +export class UpdateGoalTool implements IUpdateGoalTool { + declare readonly _serviceBrand: undefined; readonly name = 'UpdateGoal' as const; readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(UpdateGoalToolInputSchema); @@ -112,6 +117,8 @@ function changedGoalOutput(status: UpdateGoalToolInput['status']): string { return 'Goal not blocked: the current goal changed.'; } -registerTool(UpdateGoalTool, { +registerAgentTool(IUpdateGoalTool, UpdateGoalTool, { + name: 'UpdateGoal', + domain: 'goal', when: (accessor) => accessor.get(IAgentScopeContext).agentId === 'main', }); diff --git a/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts b/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts index 861cf30ce2..7b2dbdee7a 100644 --- a/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts +++ b/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts @@ -2,10 +2,10 @@ * Media tool production registration — the Eager Agent-scope service that * keeps `ReadMediaFile` in the tool registry in sync with the bound model. * - * Media tools cannot ride the module-level `registerTool(...)` contribution - * table: its `when` predicates run once, when the Agent's tool registry is - * constructed, and at that point no model is bound yet — the capabilities are - * still `UNKNOWN_CAPABILITY`, so a capability gate would permanently skip the + * Media tools cannot ride the module-level `registerAgentTool(...)` + * contribution table: its activation runs when the Agent is created, and at + * that point no model is bound yet — the capabilities are still + * `UNKNOWN_CAPABILITY`, so a capability gate would permanently skip the * tool. Registration instead re-runs whenever the resolved model changes: * every profile/model update publishes `agent.status.updated`, and this * service re-invokes {@link registerMediaTools} when the model alias or its diff --git a/packages/agent-core-v2/src/agent/media/tools/read-media.ts b/packages/agent-core-v2/src/agent/media/tools/read-media.ts index 5d4d79f0a5..fa9db5ede9 100644 --- a/packages/agent-core-v2/src/agent/media/tools/read-media.ts +++ b/packages/agent-core-v2/src/agent/media/tools/read-media.ts @@ -43,6 +43,13 @@ * * Registration is capability-gated by `registerMediaTools`: this tool is * only registered when the active model supports image or video input. + * + * This tool is a deliberate exception to the `registerAgentTool` contribution + * table: its constructor depends on runtime model capabilities (capability + * profile, video uploader, protocol flags), so it cannot be a static + * Agent-scope Service and is instead `new`ed by `AgentMediaToolsRegistrar` + * whenever the bound model changes. It still satisfies the `AgentTool` + * contract. */ import type { ModelCapability } from '#/kosong/contract/capability'; @@ -57,7 +64,7 @@ import { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { IHostEnvironment } from '#/os/interface/hostEnvironment'; import { ToolAccesses, - type BuiltinTool, + type AgentTool, type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; @@ -263,7 +270,8 @@ function shouldSurfaceVideoUploadError(error: unknown, inlineVideoSupported: boo return isVideoUploadAuthError(error); } -export class ReadMediaFileTool implements BuiltinTool { +export class ReadMediaFileTool implements AgentTool { + declare readonly _serviceBrand: undefined; readonly name = 'ReadMediaFile' as const; readonly description: string; readonly parameters: Record = toInputJsonSchema(ReadMediaFileInputSchema); 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 c3d1778a22..3a9448daa6 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 @@ -7,8 +7,9 @@ import { z } from 'zod'; -import type { BuiltinTool, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { createDecorator } from '#/_base/di/instantiation'; +import type { AgentTool, ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { toInputJsonSchema } from '#/tool/input-schema'; import { ITelemetryService } from '#/app/telemetry/telemetry'; import { IAgentPlanService } from '#/agent/plan/plan'; @@ -18,7 +19,13 @@ import DESCRIPTION from './enter-plan-mode.md?raw'; export const EnterPlanModeInputSchema = z.object({}).strict(); export type EnterPlanModeInput = z.infer; -export class EnterPlanModeTool implements BuiltinTool { +export interface IEnterPlanModeTool extends AgentTool { + readonly _serviceBrand: undefined; +} +export const IEnterPlanModeTool = createDecorator('enterPlanModeTool'); + +export class EnterPlanModeTool implements IEnterPlanModeTool { + declare readonly _serviceBrand: undefined; readonly name = 'EnterPlanMode' as const; readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(EnterPlanModeInputSchema); @@ -58,7 +65,7 @@ export class EnterPlanModeTool implements BuiltinTool { } } -registerTool(EnterPlanModeTool); +registerAgentTool(IEnterPlanModeTool, EnterPlanModeTool, { name: 'EnterPlanMode', domain: 'plan' }); function enteredPlanModeMessage(planPath: string | null): string { if (planPath === null) { 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 810e1fcfa6..909e576c32 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 @@ -23,8 +23,9 @@ import type { ToolInputDisplay } from '#/tool/toolInputDisplay'; import { z } from 'zod'; -import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { createDecorator } from '#/_base/di/instantiation'; +import type { AgentTool, ExecutableToolResult, ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { toInputJsonSchema } from '#/tool/input-schema'; import { ITelemetryService } from '#/app/telemetry/telemetry'; import { IAgentPlanService } from '#/agent/plan/plan'; @@ -87,7 +88,13 @@ type ResolvePlanResult = | { readonly ok: false; readonly error: ExecutableToolResult }; -export class ExitPlanModeTool implements BuiltinTool { +export interface IExitPlanModeTool extends AgentTool { + readonly _serviceBrand: undefined; +} +export const IExitPlanModeTool = createDecorator('exitPlanModeTool'); + +export class ExitPlanModeTool implements IExitPlanModeTool { + declare readonly _serviceBrand: undefined; readonly name = 'ExitPlanMode' as const; readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(ExitPlanModeInputSchema); @@ -223,7 +230,7 @@ export class ExitPlanModeTool implements BuiltinTool { } } -registerTool(ExitPlanModeTool); +registerAgentTool(IExitPlanModeTool, ExitPlanModeTool, { name: 'ExitPlanMode', domain: 'plan' }); function hasUniqueOptionLabels(options: readonly ExitPlanModeOption[]): boolean { const labels = new Set(); diff --git a/packages/agent-core-v2/src/agent/profile/profileService.ts b/packages/agent-core-v2/src/agent/profile/profileService.ts index b3aaabcfc7..e7a8f02422 100644 --- a/packages/agent-core-v2/src/agent/profile/profileService.ts +++ b/packages/agent-core-v2/src/agent/profile/profileService.ts @@ -109,6 +109,7 @@ import { IAgentProfileService, ProfileError, ProfileErrors } from './profile'; import { TOOLS_SECTION, type ToolsConfig } from '#/agent/toolPolicy/configSection'; import { isToolActiveComposed, findInactiveToolPatterns, literalToolNames, type InactiveToolPattern } from '#/agent/toolPolicy/evaluate'; import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; +import { getAgentToolContributions } from '#/agent/toolRegistry/toolContribution'; import { ActiveToolsModel, configUpdate, @@ -778,6 +779,10 @@ export class AgentProfileService extends Disposable implements IAgentProfileServ private publishToolPatternWarnings(profile?: ResolvedAgentProfile): void { const known = new Set(); + // The registry only holds tools the bound Profile activated; the + // contribution table is the full static universe, so a valid-but-inactive + // name never trips the unknown-tool warning. + for (const contribution of getAgentToolContributions()) known.add(contribution.options.name); for (const ref of this.toolRegistry.listReferences()) known.add(ref.name); for (const builtin of this.builtinProfiles.list()) { for (const name of literalToolNames([ 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 dd6a3ab7e1..0c1400032e 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 @@ -20,13 +20,14 @@ import { IAgentTaskService } from '#/agent/task/task'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; import { ITelemetryService } from '#/app/telemetry/telemetry'; import type { QuestionAnsweredEvent, QuestionDismissedEvent } from '#/app/telemetry/events'; +import { createDecorator } from '#/_base/di/instantiation'; import type { - BuiltinTool, + AgentTool, ExecutableToolContext, ExecutableToolResult, ToolExecution, } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { ISessionQuestionService } from '#/session/question/question'; import type { @@ -130,7 +131,13 @@ const QUESTION_UNSUPPORTED_FAILURE_MESSAGE = 'The connected client does not support interactive questions. Do NOT call this tool again. Ask the user directly in your text response instead.'; -export class AskUserQuestionTool implements BuiltinTool { +export interface IAskUserQuestionTool extends AgentTool { + readonly _serviceBrand: undefined; +} +export const IAskUserQuestionTool = createDecorator('askUserQuestionTool'); + +export class AskUserQuestionTool implements IAskUserQuestionTool { + declare readonly _serviceBrand: undefined; readonly name = 'AskUserQuestion' as const; readonly description: string; readonly parameters: Record; @@ -283,7 +290,10 @@ export class AskUserQuestionTool implements BuiltinTool { } } -registerTool(AskUserQuestionTool); +registerAgentTool(IAskUserQuestionTool, AskUserQuestionTool, { + name: 'AskUserQuestion', + domain: 'questionTools', +}); function questionDescription(questions: AskUserQuestionInput['questions']): string { const first = questions[0]?.question.trim(); 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 8c01a0bc90..74eab244be 100644 --- a/packages/agent-core-v2/src/agent/skill/tools/skill.ts +++ b/packages/agent-core-v2/src/agent/skill/tools/skill.ts @@ -28,8 +28,9 @@ import { z } from 'zod'; import type { SkillActivationOrigin } from '#/agent/contextMemory/types'; import { IAgentSkillService } from '#/agent/skill/skill'; import { renderModelToolSkillPrompt } from '#/agent/skill/prompt'; -import type { BuiltinTool, ExecutableToolResult, ToolDeliveryMessage, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import type { AgentTool, ExecutableToolResult, ToolDeliveryMessage, ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; +import { createDecorator } from '#/_base/di/instantiation'; import { isInlineSkillType } from '#/app/skillCatalog/types'; import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog'; import { ISessionContext } from '#/session/sessionContext/sessionContext'; @@ -74,7 +75,11 @@ export const SkillToolInputSchema: z.ZodType = z.object({ ), }); -export class SkillTool implements BuiltinTool { +export interface ISkillTool extends AgentTool { readonly _serviceBrand: undefined } +export const ISkillTool = createDecorator('skillTool'); + +export class SkillTool implements ISkillTool { + declare readonly _serviceBrand: undefined; readonly name = 'Skill'; readonly description: string = renderPrompt(skillDescriptionTemplate, { MAX_SKILL_QUERY_DEPTH, @@ -116,7 +121,7 @@ export class SkillTool implements BuiltinTool { } } -registerTool(SkillTool); +registerAgentTool(ISkillTool, SkillTool, { name: 'Skill', domain: 'skill' }); export async function executeModelSkill( catalog: ISessionSkillCatalog, diff --git a/packages/agent-core-v2/src/agent/swarm/swarmService.ts b/packages/agent-core-v2/src/agent/swarm/swarmService.ts index f3cfbcdd8d..0bf7eb2717 100644 --- a/packages/agent-core-v2/src/agent/swarm/swarmService.ts +++ b/packages/agent-core-v2/src/agent/swarm/swarmService.ts @@ -12,7 +12,7 @@ * live-only `context.spliced` event for that pop (so injector bookkeeping * stays in step) and appends the exit reminder when nothing was * popped. Bound at Agent scope. The `AgentSwarm` tool self-registers via - * `registerTool(...)` in `tools/agent-swarm.ts`. The service also guards + * `registerAgentTool(...)` in `tools/agent-swarm.ts`. The service also guards * AgentSwarm batch exclusivity through an `onBeforeExecuteTool` veto * listener: an AgentSwarm call must be the only tool call in its batch, * anything else is vetoed with a `toolApproval.formatDenyMessage`-formatted 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 3d11c001d4..2b88e15754 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 @@ -14,14 +14,15 @@ import { z } from 'zod'; +import { createDecorator } from '#/_base/di/instantiation'; import { ToolAccesses, - type BuiltinTool, + type AgentTool, type ExecutableToolContext, type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { toInputJsonSchema } from '#/tool/input-schema'; import { IConfigService } from '#/app/config/config'; import { IFlagService } from '#/app/flag/flag'; @@ -118,7 +119,11 @@ interface SwarmRunResult { readonly error?: string; } -export class AgentSwarmTool implements BuiltinTool { +export interface IAgentSwarmTool extends AgentTool { readonly _serviceBrand: undefined } +export const IAgentSwarmTool = createDecorator('agentSwarmTool'); + +export class AgentSwarmTool implements IAgentSwarmTool { + declare readonly _serviceBrand: undefined; readonly name = 'AgentSwarm' as const; readonly parameters: Record = toInputJsonSchema(AgentSwarmToolInputSchema); @@ -248,7 +253,7 @@ export class AgentSwarmTool implements BuiltinTool { } } -registerTool(AgentSwarmTool); +registerAgentTool(IAgentSwarmTool, AgentSwarmTool, { name: 'AgentSwarm', domain: 'swarm' }); async function createAgentSwarmSpecs( args: AgentSwarmToolInput, diff --git a/packages/agent-core-v2/src/agent/task/tools/task-list.ts b/packages/agent-core-v2/src/agent/task/tools/task-list.ts index 2753cb1241..d5e7a5b0b9 100644 --- a/packages/agent-core-v2/src/agent/task/tools/task-list.ts +++ b/packages/agent-core-v2/src/agent/task/tools/task-list.ts @@ -6,8 +6,9 @@ import { z } from 'zod'; import { toInputJsonSchema } from '#/tool/input-schema'; import { matchesGlobRuleSubject } from '#/tool/rule-match'; -import type { BuiltinTool, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { createDecorator } from '#/_base/di/instantiation'; +import { type AgentTool, type ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { IAgentTaskService } from '#/agent/task/task'; import type { AgentTaskInfo } from '#/agent/task/task'; @@ -41,7 +42,11 @@ export function formatTaskList(tasks: readonly AgentTaskInfo[], activeOnly: bool return `${header}\n${tasks.map((task) => formatPlainObject(task)).join('\n---\n')}`; } -export class TaskListTool implements BuiltinTool { +export interface ITaskListTool extends AgentTool { readonly _serviceBrand: undefined } +export const ITaskListTool = createDecorator('taskListTool'); + +export class TaskListTool implements ITaskListTool { + declare readonly _serviceBrand: undefined; readonly name = 'TaskList' as const; readonly description = TASK_LIST_DESCRIPTION; readonly parameters: Record = toInputJsonSchema(TaskListInputSchema); @@ -66,4 +71,4 @@ export class TaskListTool implements BuiltinTool { } } -registerTool(TaskListTool); +registerAgentTool(ITaskListTool, TaskListTool, { name: 'TaskList', domain: 'agentTask' }); diff --git a/packages/agent-core-v2/src/agent/task/tools/task-output.ts b/packages/agent-core-v2/src/agent/task/tools/task-output.ts index caf610bc08..fded462414 100644 --- a/packages/agent-core-v2/src/agent/task/tools/task-output.ts +++ b/packages/agent-core-v2/src/agent/task/tools/task-output.ts @@ -16,8 +16,9 @@ import { z } from 'zod'; import { toInputJsonSchema } from '#/tool/input-schema'; import { matchesGlobRuleSubject } from '#/tool/rule-match'; -import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { createDecorator } from '#/_base/di/instantiation'; +import { type AgentTool, type ExecutableToolResult, type ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { IAgentTaskService } from '#/agent/task/task'; import type { @@ -88,7 +89,11 @@ function fullOutputHint(output: AgentTaskOutputSnapshot): string | undefined { ); } -export class TaskOutputTool implements BuiltinTool { +export interface ITaskOutputTool extends AgentTool { readonly _serviceBrand: undefined } +export const ITaskOutputTool = createDecorator('taskOutputTool'); + +export class TaskOutputTool implements ITaskOutputTool { + declare readonly _serviceBrand: undefined; readonly name = 'TaskOutput' as const; readonly description: string = TASK_OUTPUT_DESCRIPTION; readonly parameters: Record = toInputJsonSchema(TaskOutputInputSchema); @@ -161,4 +166,4 @@ export class TaskOutputTool implements BuiltinTool { } } -registerTool(TaskOutputTool); +registerAgentTool(ITaskOutputTool, TaskOutputTool, { name: 'TaskOutput', domain: 'agentTask' }); diff --git a/packages/agent-core-v2/src/agent/task/tools/task-stop.ts b/packages/agent-core-v2/src/agent/task/tools/task-stop.ts index 4d1b835fad..27cc66353f 100644 --- a/packages/agent-core-v2/src/agent/task/tools/task-stop.ts +++ b/packages/agent-core-v2/src/agent/task/tools/task-stop.ts @@ -6,8 +6,9 @@ import { z } from 'zod'; import { toInputJsonSchema } from '#/tool/input-schema'; import { matchesGlobRuleSubject } from '#/tool/rule-match'; -import type { BuiltinTool, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { createDecorator } from '#/_base/di/instantiation'; +import { type AgentTool, type ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { IAgentTaskService } from '#/agent/task/task'; import { TERMINAL_STATUSES } from '#/agent/task/types'; @@ -26,7 +27,11 @@ export const TaskStopInputSchema = z.object({ export type TaskStopInput = z.infer; -export class TaskStopTool implements BuiltinTool { +export interface ITaskStopTool extends AgentTool { readonly _serviceBrand: undefined } +export const ITaskStopTool = createDecorator('taskStopTool'); + +export class TaskStopTool implements ITaskStopTool { + declare readonly _serviceBrand: undefined; readonly name = 'TaskStop' as const; readonly description = TASK_STOP_DESCRIPTION; readonly parameters: Record = toInputJsonSchema(TaskStopInputSchema); @@ -78,7 +83,7 @@ export class TaskStopTool implements BuiltinTool { } } -registerTool(TaskStopTool); +registerAgentTool(ITaskStopTool, TaskStopTool, { name: 'TaskStop', domain: 'agentTask' }); function terminalStopReason(reason: string | undefined): string { const trimmed = reason?.trim(); diff --git a/packages/agent-core-v2/src/agent/toolActivation/toolActivation.ts b/packages/agent-core-v2/src/agent/toolActivation/toolActivation.ts new file mode 100644 index 0000000000..cb7e533e89 --- /dev/null +++ b/packages/agent-core-v2/src/agent/toolActivation/toolActivation.ts @@ -0,0 +1,27 @@ +/** + * `toolActivation` domain (L4) — `IAgentToolActivationService` contract. + * + * Owns the activation pass that turns the module-level `registerAgentTool` + * contributions (`toolRegistry`, L3) into entries of the per-agent runtime + * registry: a contribution activates only when its `when` predicate holds + * and its declared `name` is allowed by the bound Profile's tool policy + * (`profile`, L4). `AgentLifecycleService.create` awaits one activation pass + * after restore and profile binding, so an Agent's tools reflect the Profile + * before the first turn. Bound at Agent scope. + */ + +import { createDecorator } from '#/_base/di/instantiation'; + +export interface IAgentToolActivationService { + readonly _serviceBrand: undefined; + + /** + * Idempotent: instantiates and registers every contribution that is allowed + * by the current Profile and not yet registered. Never unregisters — + * restriction stays the request-time tool policy's job. + */ + activate(): Promise; +} + +export const IAgentToolActivationService = + createDecorator('agentToolActivationService'); diff --git a/packages/agent-core-v2/src/agent/toolActivation/toolActivationService.ts b/packages/agent-core-v2/src/agent/toolActivation/toolActivationService.ts new file mode 100644 index 0000000000..0512075197 --- /dev/null +++ b/packages/agent-core-v2/src/agent/toolActivation/toolActivationService.ts @@ -0,0 +1,82 @@ +/** + * `toolActivation` domain (L4) — `IAgentToolActivationService` implementation. + * + * Iterates the `toolRegistry` contribution table and, for each entry allowed + * by the bound Profile's tool policy (`profile`), resolves the Agent-scope + * service through the container — nothing constructs the tool before this + * `accessor.get` — and registers the real instance into the runtime + * registry. + * + * Activation runs once explicitly from `AgentLifecycleService.create` (after + * restore and profile binding) and re-runs on every `agent.status.updated` + * from `event`, so tools newly allowed by a runtime re-bind or + * `setActiveTools` are activated without a restart. Already-registered names + * are skipped, and nothing is ever unregistered here: restricting visibility + * remains the request-time tool policy's job. + * + * Resolving contributions lazily inside `activate()` — never from the + * constructor — keeps the historical cycle broken: some tools (SkillTool → + * `prompt` → `loop` → `toolRegistry`) transitively depend on the tool + * registry, which by activation time has long finished constructing. Bound + * at Agent scope; the lifecycle's explicit `activate()` is the only + * resolution path. + */ + +import { Disposable } from '#/_base/di/lifecycle'; +import { InstantiationType } from '#/_base/di/extensions'; +import { IInstantiationService } from '#/_base/di/instantiation'; +import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; +import { IEventBus } from '#/app/event/eventBus'; +import { IAgentProfileService } from '#/agent/profile/profile'; +import { isToolActive } from '#/agent/toolPolicy/evaluate'; +import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; +import { getAgentToolContributions } from '#/agent/toolRegistry/toolContribution'; + +import { IAgentToolActivationService } from './toolActivation'; + +export class AgentToolActivationService extends Disposable implements IAgentToolActivationService { + declare readonly _serviceBrand: undefined; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IAgentToolRegistryService private readonly toolRegistry: IAgentToolRegistryService, + @IAgentProfileService private readonly profile: IAgentProfileService, + @IEventBus eventBus: IEventBus, + ) { + super(); + this._register( + eventBus.subscribe('agent.status.updated', () => { + void this.activate(); + }), + ); + } + + activate(): Promise { + const data = this.profile.data(); + const policy = { tools: data.activeToolNames, disallowedTools: data.disallowedTools }; + this.instantiationService.invokeFunction((accessor) => { + for (const { id, options } of getAgentToolContributions()) { + const source = options.source ?? 'builtin'; + if (this.toolRegistry.resolve(options.name) !== undefined) continue; + if (!isToolActive(policy, options.name, source)) continue; + if (options.when !== undefined && !options.when(accessor)) continue; + const tool = accessor.get(id); + this._register( + this.toolRegistry.register(tool, { + source: options.source, + disclosure: options.disclosure, + }), + ); + } + }); + return Promise.resolve(); + } +} + +registerScopedService( + LifecycleScope.Agent, + IAgentToolActivationService, + AgentToolActivationService, + InstantiationType.Eager, + 'toolActivation', +); diff --git a/packages/agent-core-v2/src/agent/toolRegistry/builtinToolsRegistrar.ts b/packages/agent-core-v2/src/agent/toolRegistry/builtinToolsRegistrar.ts deleted file mode 100644 index eea747232d..0000000000 --- a/packages/agent-core-v2/src/agent/toolRegistry/builtinToolsRegistrar.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * `toolRegistry` domain (L3) — the Eager Agent-scope side-effect service that - * consumes every module-level `registerTool(...)` contribution. - * - * Why this is separate from `AgentToolRegistryService`: - * - * Instantiating a tool pulls in that tool's `@IX` dependencies. Some tools - * (SkillTool → IAgentPromptService → IAgentLoopService → IAgentToolRegistryService) - * transitively depend on the tool registry itself. If contributions were consumed - * inside `AgentToolRegistryService`'s constructor, the container would treat the - * cascade as a recursive instantiation of the registry and throw - * `illegal state - RECURSIVELY instantiating service 'agentToolRegistryService'`. - * - * Splitting the "iterate contributions and instantiate" step into its own - * Eager service that injects the already-constructed registry breaks the cycle: - * by the time we call `createInstance(SkillTool)`, `IAgentToolRegistryService` - * has finished its own constructor and downstream `@IAgentToolRegistryService` - * resolutions hit the cached instance instead of re-entering construction. - * - * Agent scope creation eagerly instantiates this service, so builtin tools - * land in the registry before the first turn. - */ - -import { Disposable } from '#/_base/di/lifecycle'; -import { InstantiationType } from '#/_base/di/extensions'; -import { IInstantiationService } from '#/_base/di/instantiation'; -import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; - -import { IAgentToolRegistryService } from './toolRegistry'; -import { getToolContributions } from './toolContribution'; - -export interface IAgentBuiltinToolsRegistrar { - readonly _serviceBrand: undefined; -} - -export const IAgentBuiltinToolsRegistrar: ServiceIdentifier = - createDecorator('agentBuiltinToolsRegistrar'); - -export class AgentBuiltinToolsRegistrar extends Disposable implements IAgentBuiltinToolsRegistrar { - declare readonly _serviceBrand: undefined; - - constructor( - @IInstantiationService instantiationService: IInstantiationService, - @IAgentToolRegistryService toolRegistry: IAgentToolRegistryService, - ) { - super(); - instantiationService.invokeFunction((accessor) => { - for (const contribution of getToolContributions()) { - const { ctor, options } = contribution; - if (options.when !== undefined && !options.when(accessor)) continue; - const staticArgs = options.staticArgs?.(accessor) ?? []; - const tool = instantiationService.createInstance( - ctor, - ...(staticArgs as []), - ); - this._register( - toolRegistry.register(tool, { - source: options.source, - disclosure: options.disclosure, - }), - ); - } - }); - } -} - -registerScopedService( - LifecycleScope.Agent, - IAgentBuiltinToolsRegistrar, - AgentBuiltinToolsRegistrar, - InstantiationType.Eager, - 'toolRegistry', -); diff --git a/packages/agent-core-v2/src/agent/toolRegistry/toolContribution.ts b/packages/agent-core-v2/src/agent/toolRegistry/toolContribution.ts index 1e97a05b37..9f6cd2f653 100644 --- a/packages/agent-core-v2/src/agent/toolRegistry/toolContribution.ts +++ b/packages/agent-core-v2/src/agent/toolRegistry/toolContribution.ts @@ -1,63 +1,82 @@ /** - * `toolRegistry` domain (L3) — module-level tool contribution registry. + * `toolRegistry` domain (L3) — module-level agent-tool contribution registry. * - * Tools contribute themselves at module load via `registerTool(ctor, options?)` - * — the same "import = register" pattern used by `registerScopedService` for - * DI services and by `registerConfigSection` for config. `AgentToolRegistryService` - * (Agent scope) consumes the accumulated contributions on construction: for each - * contribution whose `when` predicate holds, it uses `IInstantiationService.createInstance` - * to build the tool (passing any `staticArgs` before the injected DI dependencies) - * and registers it into the per-agent runtime registry. + * Tools contribute themselves at module load via + * `registerAgentTool(identifier, ctor, options?)` — a double registration: + * the tool is registered as an Agent-scope DI service + * (`registerScopedService`) and recorded in this contribution table. No tool + * is constructed at scope creation — `InstantiationType.Eager` only skips + * the lazy proxy at resolve time in this DI, so the runtime registry always + * holds real instances, never proxies. + * `AgentToolActivationService` (`toolActivation`, L4) consumes the table when + * an Agent is created: for each contribution whose `when` predicate holds and + * whose `name` the bound Profile's tool policy allows, it resolves the + * service through the container (`accessor.get`, triggering the Delayed + * instantiation) and registers it into the per-agent runtime registry. The + * declared `name` is what lets activation filter without instantiating. * - * `registerTool` is deliberately not "builtin"-scoped: the same API is what - * external contributors (plugins, SDK consumers) will use once the surface is - * public. The tool's origin is carried by `options.source` (`'builtin'` / - * `'user'` / `'mcp'` / …), not by the registration API. + * `registerAgentTool` is deliberately not "builtin"-scoped: the same API is + * what external contributors (plugins, SDK consumers) will use once the + * surface is public. The tool's origin is carried by `options.source` + * (`'builtin'` / `'user'` / `'mcp'` / …), not by the registration API. * - * Tools are always Agent-scoped instances (each Agent has its own tool + * Tools are always Agent-scoped services (each Agent has its own tool * registry, and tool constructors inject Agent-scope services), so no `scope` * parameter is exposed. If tools at other scopes are ever needed, add it * optionally without breaking existing callers. */ -import type { ServicesAccessor } from '#/_base/di/instantiation'; +import type { ServiceIdentifier, ServicesAccessor } from '#/_base/di/instantiation'; +import { InstantiationType } from '#/_base/di/extensions'; +import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; import type { - ExecutableTool, + AgentTool, ToolDisclosure, ToolSource, } from '#/tool/toolContract'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AnyExecutableTool = ExecutableTool; +export type AnyAgentTool = AgentTool; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ToolCtor = new (...args: any[]) => T; +export type AgentToolCtor = new (...args: any[]) => T; -export interface ToolContributionOptions { +export interface AgentToolContributionOptions { + /** Model-facing tool name, declared so activation can filter without instantiating. */ + readonly name: string; readonly source?: ToolSource; readonly disclosure?: ToolDisclosure; readonly when?: (accessor: ServicesAccessor) => boolean; - readonly staticArgs?: (accessor: ServicesAccessor) => readonly unknown[]; + readonly domain?: string; } -export interface ToolContribution { - readonly ctor: ToolCtor; - readonly options: ToolContributionOptions; +export interface AgentToolContribution { + readonly id: ServiceIdentifier; + readonly ctor: AgentToolCtor; + readonly options: AgentToolContributionOptions; } -const _toolContributions: ToolContribution[] = []; +const _agentToolContributions: AgentToolContribution[] = []; -export function registerTool( - ctor: ToolCtor, - options: ToolContributionOptions = {}, +export function registerAgentTool( + id: ServiceIdentifier, + ctor: AgentToolCtor, + options: AgentToolContributionOptions, ): void { - _toolContributions.push({ ctor: ctor as ToolCtor, options }); + registerScopedService( + LifecycleScope.Agent, + id, + ctor, + InstantiationType.Eager, + options.domain ?? 'unknown', + ); + _agentToolContributions.push({ id, ctor, options }); } -export function getToolContributions(): readonly ToolContribution[] { - return _toolContributions; +export function getAgentToolContributions(): readonly AgentToolContribution[] { + return _agentToolContributions; } -export function _clearToolContributionsForTests(): void { - _toolContributions.length = 0; +export function _clearAgentToolContributionsForTests(): void { + _agentToolContributions.length = 0; } diff --git a/packages/agent-core-v2/src/agent/toolSelect/tools/select-tools.ts b/packages/agent-core-v2/src/agent/toolSelect/tools/select-tools.ts index 90083a0f29..cfb822c9db 100644 --- a/packages/agent-core-v2/src/agent/toolSelect/tools/select-tools.ts +++ b/packages/agent-core-v2/src/agent/toolSelect/tools/select-tools.ts @@ -11,8 +11,9 @@ import { z } from 'zod'; import { toInputJsonSchema } from '#/tool/input-schema'; -import type { BuiltinTool, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import type { AgentTool, ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; +import { createDecorator } from '#/_base/di/instantiation'; import { IAgentToolSelectService, SELECT_TOOLS_TOOL_NAME } from '../toolSelect'; @@ -34,7 +35,11 @@ const DESCRIPTION = 'Pass the exact name(s) you need; their full definitions become available immediately, ' + 'so you can call them directly in your next tool call.'; -export class SelectToolsTool implements BuiltinTool { +export interface ISelectToolsTool extends AgentTool { readonly _serviceBrand: undefined } +export const ISelectToolsTool = createDecorator('selectToolsTool'); + +export class SelectToolsTool implements ISelectToolsTool { + declare readonly _serviceBrand: undefined; readonly name = SELECT_TOOLS_TOOL_NAME; readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(SelectToolsInputSchema); @@ -71,4 +76,4 @@ export class SelectToolsTool implements BuiltinTool { } } -registerTool(SelectToolsTool); +registerAgentTool(ISelectToolsTool, SelectToolsTool, { name: SELECT_TOOLS_TOOL_NAME, domain: 'toolSelect' }); diff --git a/packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts b/packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts index 66aac1cde2..2f131eafc0 100644 --- a/packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts +++ b/packages/agent-core-v2/src/app/agentProfileCatalog/agentProfileCatalog.ts @@ -23,7 +23,7 @@ * (`undefined` = any type). * * Profiles are contributed at module load via `registerAgentProfile(...)`, the - * same "import = register" pattern used by `registerTool` and + * same "import = register" pattern used by `registerAgentTool` and * `registerConfigSection`. `AgentProfileCatalogService` consumes the accumulated * contributions on construction and exposes `get(name)` / `getDefault()` / * `list()` to callers (the `Agent` tool, the swarm scheduler, and the per-agent diff --git a/packages/agent-core-v2/src/app/agentProfileCatalog/contribution.ts b/packages/agent-core-v2/src/app/agentProfileCatalog/contribution.ts index cfdebf9625..2d004550b9 100644 --- a/packages/agent-core-v2/src/app/agentProfileCatalog/contribution.ts +++ b/packages/agent-core-v2/src/app/agentProfileCatalog/contribution.ts @@ -2,8 +2,8 @@ * `agentProfileCatalog` domain (L3) — module-level profile contribution registry. * * Profiles contribute themselves at module load via `registerAgentProfile(def)`, - * the same "import = register" pattern used by `registerTool` for tools and - * `registerScopedService` for DI. `AgentProfileCatalogService` consumes the + * the same "import = register" pattern used by `registerAgentTool` for tools + * and `registerScopedService` for DI. `AgentProfileCatalogService` consumes the * accumulated list on construction. Uniqueness is enforced by `name`: * later-registered profiles with the same name replace earlier ones, so tests * can override built-ins by re-registering. diff --git a/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts b/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts index f5ce5da372..154b4b28be 100644 --- a/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts +++ b/packages/agent-core-v2/src/app/auth/webSearch/tools/web-search.ts @@ -4,26 +4,27 @@ * * Defines the `WebSearch` tool and the host-injected `WebSearchProvider` * interface (plus `WebSearchResult`). Web search needs an authenticated - * Moonshot backend, so the tool lives in the KimiOAuth `auth` domain: it reads - * its provider from the App-scope `IWebSearchProviderService` at - * registry-construction time and self-registers via `registerTool(...)` at - * module load, but only when a provider is configured (there is no local - * search backend). + * Moonshot backend, so the tool lives in the KimiOAuth `auth` domain: it is + * contributed via `registerAgentTool(...)` at module load and reads its + * provider from the App-scope `IWebSearchProviderService` at activation time, + * but only activates when a provider is configured (there is no local search + * backend). */ import { z } from 'zod'; +import { createDecorator } from '#/_base/di/instantiation'; import { toInputJsonSchema } from '#/tool/input-schema'; import { literalRulePattern, matchesGlobRuleSubject } from '#/tool/rule-match'; import { + type AgentTool, ToolAccesses, - type BuiltinTool, type ExecutableToolContext, type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; import { ToolResultBuilder } from '#/tool/result-builder'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { IWebSearchProviderService } from '../webSearch'; import DESCRIPTION from './web-search.md?raw'; @@ -55,12 +56,28 @@ export const WebSearchInputSchema = z.object({ export type WebSearchInput = z.infer; -export class WebSearchTool implements BuiltinTool { +export interface IWebSearchTool extends AgentTool { + readonly _serviceBrand: undefined; +} +export const IWebSearchTool = createDecorator('webSearchTool'); + +export class WebSearchTool implements IWebSearchTool { + declare readonly _serviceBrand: undefined; readonly name = 'WebSearch' as const; readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(WebSearchInputSchema); - constructor(private readonly provider: WebSearchProvider) {} + private readonly provider: WebSearchProvider; + + constructor( + @IWebSearchProviderService providerService: IWebSearchProviderService, + ) { + const provider = providerService.getWebSearchProvider(); + if (provider === undefined) { + throw new Error('WebSearchProviderService returned no provider during tool activation.'); + } + this.provider = provider; + } resolveExecution(args: WebSearchInput): ToolExecution { const preview = args.query.length > 40 ? `${args.query.slice(0, 40)}…` : args.query; @@ -140,13 +157,8 @@ function classifySearchError(error: unknown): string { return `Search failed: ${message}`; } -registerTool(WebSearchTool, { +registerAgentTool(IWebSearchTool, WebSearchTool, { + name: 'WebSearch', + domain: 'auth', when: (accessor) => accessor.get(IWebSearchProviderService).getWebSearchProvider() !== undefined, - staticArgs: (accessor) => { - const provider = accessor.get(IWebSearchProviderService).getWebSearchProvider(); - if (provider === undefined) { - throw new Error('WebSearchProviderService returned no provider during tool registration.'); - } - return [provider]; - }, }); diff --git a/packages/agent-core-v2/src/app/auth/webSearch/webSearchService.ts b/packages/agent-core-v2/src/app/auth/webSearch/webSearchService.ts index 051b729b49..bc2d3f689d 100644 --- a/packages/agent-core-v2/src/app/auth/webSearch/webSearchService.ts +++ b/packages/agent-core-v2/src/app/auth/webSearch/webSearchService.ts @@ -12,9 +12,10 @@ * the provider's `baseUrl`. The explicit config wins over the managed * derivation. Both use the host's Kimi identity headers (`IHostRequestHeaders`, * mirroring v1's `kimiRequestHeaders`) as default headers. When neither source - * is configured it yields `undefined` so the self-registering `WebSearch` tool - * stays hidden. Owns no tool registration — the `WebSearch` tool self-registers - * via `registerTool(...)` and reads this service from the Agent-scope accessor. + * is configured it yields `undefined` so the contributed `WebSearch` tool + * stays hidden. Owns no tool registration — the `WebSearch` tool contributes + * itself via `registerAgentTool(...)` and reads this service from the + * Agent-scope accessor. * Tests and hosts that need a custom backend bind `IWebSearchProviderService` * directly. Bound at App scope. */ diff --git a/packages/agent-core-v2/src/app/edit/tools/edit.ts b/packages/agent-core-v2/src/app/edit/tools/edit.ts index 8eda6b50a0..5f90926bf0 100644 --- a/packages/agent-core-v2/src/app/edit/tools/edit.ts +++ b/packages/agent-core-v2/src/app/edit/tools/edit.ts @@ -20,6 +20,7 @@ import { z } from 'zod'; +import { createDecorator } from '#/_base/di/instantiation'; import { extendWorkspaceWithSkillRoots, resolvePathAccessPath, @@ -32,12 +33,12 @@ import { IHostEnvironment } from '#/os/interface/hostEnvironment'; import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog'; import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext'; import { + type AgentTool, ToolAccesses, - type BuiltinTool, type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import editDescriptionTemplate from './edit.md?raw'; @@ -66,7 +67,13 @@ export const EditInputSchema = z.object({ export type EditInput = z.infer; -export class EditTool implements BuiltinTool { +export interface IEditTool extends AgentTool { + readonly _serviceBrand: undefined; +} +export const IEditTool = createDecorator('editTool'); + +export class EditTool implements IEditTool { + declare readonly _serviceBrand: undefined; readonly name = 'Edit' as const; readonly description = editDescriptionTemplate; readonly parameters: Record = toInputJsonSchema(EditInputSchema); @@ -139,4 +146,4 @@ export class EditTool implements BuiltinTool { } } -registerTool(EditTool); +registerAgentTool(IEditTool, EditTool, { name: 'Edit', domain: 'edit' }); diff --git a/packages/agent-core-v2/src/app/web/tools/fetch-url.ts b/packages/agent-core-v2/src/app/web/tools/fetch-url.ts index 6c847b0d04..7686741885 100644 --- a/packages/agent-core-v2/src/app/web/tools/fetch-url.ts +++ b/packages/agent-core-v2/src/app/web/tools/fetch-url.ts @@ -2,25 +2,26 @@ * `web` domain (L4) — `FetchURL` builtin tool. * * Defines the `FetchURL` tool. The host-injected `UrlFetcher` contract lives - * in `fetch-url-types`; the tool reads its fetcher from the App-scope - * `IWebFetchService` at registry-construction time and self-registers via - * `registerTool(...)` at module load. The default service falls back to the + * in `fetch-url-types`; the tool receives the App-scope `IWebFetchService` + * via DI and self-registers via `registerAgentTool(...)` at module load. The + * default service falls back to the * built-in `LocalFetchURLProvider`, so `FetchURL` is always available without OAuth. */ import { z } from 'zod'; +import { createDecorator } from '#/_base/di/instantiation'; import { toInputJsonSchema } from '#/tool/input-schema'; import { literalRulePattern, matchesGlobRuleSubject } from '#/tool/rule-match'; import { + type AgentTool, ToolAccesses, - type BuiltinTool, type ExecutableToolContext, type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; import { ToolResultBuilder } from '#/tool/result-builder'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { IWebFetchService } from '../web'; import { HttpFetchError, type UrlFetcher } from './fetch-url-types'; @@ -34,12 +35,22 @@ export const FetchURLInputSchema = z.object({ export type FetchURLInput = z.infer; -export class FetchURLTool implements BuiltinTool { +export interface IFetchURLTool extends AgentTool { + readonly _serviceBrand: undefined; +} +export const IFetchURLTool = createDecorator('fetchURLTool'); + +export class FetchURLTool implements IFetchURLTool { + declare readonly _serviceBrand: undefined; readonly name = 'FetchURL' as const; readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(FetchURLInputSchema); - constructor(private readonly fetcher: UrlFetcher) {} + private readonly fetcher: UrlFetcher; + + constructor(@IWebFetchService webFetch: IWebFetchService) { + this.fetcher = webFetch.getUrlFetcher(); + } resolveExecution(args: FetchURLInput): ToolExecution { const preview = args.url.length > 50 ? `${args.url.slice(0, 50)}…` : args.url; @@ -93,6 +104,4 @@ export class FetchURLTool implements BuiltinTool { } } -registerTool(FetchURLTool, { - staticArgs: (accessor) => [accessor.get(IWebFetchService).getUrlFetcher()], -}); +registerAgentTool(IFetchURLTool, FetchURLTool, { name: 'FetchURL', domain: 'web' }); diff --git a/packages/agent-core-v2/src/index.ts b/packages/agent-core-v2/src/index.ts index 1a76e63919..a75fff9f6c 100644 --- a/packages/agent-core-v2/src/index.ts +++ b/packages/agent-core-v2/src/index.ts @@ -519,14 +519,14 @@ export * from '#/agent/toolExecutor/toolExecutor'; export * from '#/agent/toolExecutor/toolExecutorService'; export * from '#/agent/toolResultTruncation/toolResultTruncation'; import '#/agent/toolResultTruncation/toolResultTruncationService'; -import '#/agent/toolRegistry/builtinToolsRegistrar'; +import '#/agent/toolActivation/toolActivationService'; import '#/agent/toolRegistry/toolContribution'; import '#/agent/toolRegistry/toolRegistry'; import '#/agent/toolRegistry/toolRegistryService'; -export { IAgentBuiltinToolsRegistrar } from '#/agent/toolRegistry/builtinToolsRegistrar'; +export { IAgentToolActivationService } from '#/agent/toolActivation/toolActivation'; export { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; -export { registerTool } from '#/agent/toolRegistry/toolContribution'; -export type { ToolContribution, ToolContributionOptions } from '#/agent/toolRegistry/toolContribution'; +export { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; +export type { AgentToolContribution, AgentToolContributionOptions } from '#/agent/toolRegistry/toolContribution'; export * from '#/agent/userTool/userTool'; export * from '#/agent/userTool/userToolOps'; export * from '#/agent/userTool/userToolService'; diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/bash.ts b/packages/agent-core-v2/src/os/backends/node-local/tools/bash.ts index d28c7bebd6..0623316a2c 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/bash.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/bash.ts @@ -41,12 +41,13 @@ import { IHostEnvironment } from '#/os/interface/hostEnvironment'; import { ISessionContext } from '#/session/sessionContext/sessionContext'; import { ISessionProcessRunner, type IProcess } from '#/session/process/processRunner'; import { IAgentToolPolicyService } from '#/agent/toolPolicy/toolPolicy'; -import type { BuiltinTool, ExecutableToolResult, ToolExecution, ToolUpdate } from '#/tool/toolContract'; +import { createDecorator } from '#/_base/di/instantiation'; +import type { AgentTool, ExecutableToolResult, ToolExecution, ToolUpdate } from '#/tool/toolContract'; import { type ExecutableToolResultBuilderResult, ToolResultBuilder, } from '#/tool/result-builder'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { toInputJsonSchema } from '#/tool/input-schema'; import { literalRulePattern, matchesGlobRuleSubject } from '#/tool/rule-match'; import { renderPrompt } from '#/_base/utils/render-prompt'; @@ -172,7 +173,11 @@ function withoutAutoBackgroundOnTimeout(description: string): string { ); } -export class BashTool implements BuiltinTool { +export interface IBashTool extends AgentTool { readonly _serviceBrand: undefined } +export const IBashTool = createDecorator('bashTool'); + +export class BashTool implements IBashTool { + declare readonly _serviceBrand: undefined; readonly name = 'Bash' as const; readonly parameters: Record = toInputJsonSchema(BashInputSchema); @@ -500,7 +505,7 @@ export class BashTool implements BuiltinTool { } } -registerTool(BashTool); +registerAgentTool(IBashTool, BashTool, { name: 'Bash', domain: 'os/backends' }); function formatTimeoutLabel(timeoutMs: number): string { return timeoutMs % 1000 === 0 ? `${String(timeoutMs / 1000)}s` : `${String(timeoutMs)}ms`; diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/glob.ts b/packages/agent-core-v2/src/os/backends/node-local/tools/glob.ts index dfaff3d474..db403798da 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/glob.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/glob.ts @@ -61,13 +61,14 @@ import { unwrapErrorCause } from '#/_base/errors/errors'; import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog'; import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext'; import { ITelemetryService } from '#/app/telemetry/telemetry'; +import { createDecorator } from '#/_base/di/instantiation'; import { + type AgentTool, ToolAccesses, - type BuiltinTool, type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { extendWorkspaceWithSkillRoots, isWithinDirectory, @@ -129,7 +130,11 @@ export const WINDOWS_PATH_HINT = 'returned in Windows backslash form; convert them to forward slashes before ' + 'using them in a Bash command.'; -export class GlobTool implements BuiltinTool { +export interface IGlobTool extends AgentTool { readonly _serviceBrand: undefined } +export const IGlobTool = createDecorator('globTool'); + +export class GlobTool implements IGlobTool { + declare readonly _serviceBrand: undefined; readonly name = 'Glob' as const; readonly description: string; readonly parameters: Record = toInputJsonSchema(GlobInputSchema); @@ -331,7 +336,7 @@ export class GlobTool implements BuiltinTool { } } -registerTool(GlobTool); +registerAgentTool(IGlobTool, GlobTool, { name: 'Glob', domain: 'os/backends' }); function createRgProbe(processService: IHostProcessService): RgProbe { return { diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/grep.ts b/packages/agent-core-v2/src/os/backends/node-local/tools/grep.ts index 12990abd56..69b64ca601 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/grep.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/grep.ts @@ -22,14 +22,15 @@ import { normalize } from 'pathe'; import { z } from 'zod'; import { ToolResultBuilder } from '#/tool/result-builder'; +import { createDecorator } from '#/_base/di/instantiation'; import { + type AgentTool, ToolAccesses, - type BuiltinTool, type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; import { ITelemetryService } from '#/app/telemetry/telemetry'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { IHostEnvironment } from '#/os/interface/hostEnvironment'; import { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { IHostProcessService } from '#/os/interface/hostProcess'; @@ -182,7 +183,11 @@ const SENSITIVE_GLOBS_TO_EXCLUDE = [ const CONTENT_LINE_RE = /^(.*?)([:-])(\d+)\2/; -export class GrepTool implements BuiltinTool { +export interface IGrepTool extends AgentTool { readonly _serviceBrand: undefined } +export const IGrepTool = createDecorator('grepTool'); + +export class GrepTool implements IGrepTool { + declare readonly _serviceBrand: undefined; readonly name = 'Grep' as const; readonly description = GREP_DESCRIPTION; readonly parameters: Record = toInputJsonSchema(GrepInputSchema); @@ -445,7 +450,7 @@ export class GrepTool implements BuiltinTool { } } -registerTool(GrepTool); +registerAgentTool(IGrepTool, GrepTool, { name: 'Grep', domain: 'os/backends' }); function formatSpawnError(error: unknown): string { return errorCode(error) === 'ENOENT' diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/read.ts b/packages/agent-core-v2/src/os/backends/node-local/tools/read.ts index 95220a9e5e..3182708879 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/read.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/read.ts @@ -30,13 +30,14 @@ import { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { unwrapErrorCause } from '#/_base/errors/errors'; import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog'; import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext'; +import { createDecorator } from '#/_base/di/instantiation'; import { + type AgentTool, ToolAccesses, - type BuiltinTool, type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { extendWorkspaceWithSkillRoots, resolvePathAccessPath, @@ -233,7 +234,11 @@ const READ_DESCRIPTION = renderPrompt(readDescriptionTemplate, { MAX_LINE_LENGTH, }); -export class ReadTool implements BuiltinTool { +export interface IReadTool extends AgentTool { readonly _serviceBrand: undefined } +export const IReadTool = createDecorator('readTool'); + +export class ReadTool implements IReadTool { + declare readonly _serviceBrand: undefined; readonly name = 'Read' as const; readonly description = READ_DESCRIPTION; readonly parameters: Record = toInputJsonSchema(ReadInputSchema); @@ -521,4 +526,4 @@ export class ReadTool implements BuiltinTool { } } -registerTool(ReadTool); +registerAgentTool(IReadTool, ReadTool, { name: 'Read', domain: 'os/backends' }); diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/write.ts b/packages/agent-core-v2/src/os/backends/node-local/tools/write.ts index 821cfb80b4..d2f88ccb86 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/write.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/write.ts @@ -25,13 +25,14 @@ import { type HostFileStat, IHostFileSystem } from '#/os/interface/hostFileSyste import { unwrapErrorCause } from '#/_base/errors/errors'; import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog'; import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext'; +import { createDecorator } from '#/_base/di/instantiation'; import { + type AgentTool, ToolAccesses, - type BuiltinTool, type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { extendWorkspaceWithSkillRoots, resolvePathAccessPath, @@ -67,7 +68,11 @@ export const WriteOutputSchema = z.object({ export type WriteInput = z.infer; export type WriteOutput = z.infer; -export class WriteTool implements BuiltinTool { +export interface IWriteTool extends AgentTool { readonly _serviceBrand: undefined } +export const IWriteTool = createDecorator('writeTool'); + +export class WriteTool implements IWriteTool { + declare readonly _serviceBrand: undefined; readonly name = 'Write' as const; readonly description = WRITE_DESCRIPTION; readonly parameters: Record = toInputJsonSchema(WriteInputSchema); @@ -166,4 +171,4 @@ export class WriteTool implements BuiltinTool { } } -registerTool(WriteTool); +registerAgentTool(IWriteTool, WriteTool, { name: 'Write', domain: 'os/backends' }); diff --git a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts index 070ab10f68..158f3dbb5a 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts @@ -48,6 +48,7 @@ import { abortError } from '#/_base/utils/abort'; import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; import { IAgentFullCompactionService } from '#/agent/fullCompaction/fullCompaction'; +import { IAgentToolActivationService } from '#/agent/toolActivation/toolActivation'; import { ISessionInteractionService } from '#/session/interaction/interaction'; import { IWireService } from '#/wire/wire'; import { ITelemetryService } from '#/app/telemetry/telemetry'; @@ -191,6 +192,10 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle await mcpReady; await wire.restore(); await this.bindBootstrap(handle, opts); + // Activate the AgentTool contributions allowed by the bound Profile + // before the handle admits turns: restore and binding own the final + // `activeToolNames`, so this must run after both. + await handle.accessor.get(IAgentToolActivationService).activate(); return handle; } catch (error) { // Startup failed: drop the half-built agent so the next `create` starts diff --git a/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts b/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts index d3698a0a3c..293f13a63a 100644 --- a/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts +++ b/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts @@ -24,7 +24,6 @@ import type { CronJobOrigin, CronMissedOrigin } from '#/agent/contextMemory/type import { Disposable, toDisposable } from '#/_base/di/lifecycle'; import { InstantiationType } from '#/_base/di/extensions'; -import { IInstantiationService } from '#/_base/di/instantiation'; import { type IAgentScopeHandle, LifecycleScope, registerScopedService } from '#/_base/di/scope'; import { defineState } from '#/_base/state/stateRegistry'; import { IntervalTimer } from '#/_base/utils/timer'; @@ -50,9 +49,9 @@ import { type DomainEvent, IEventBus } from '#/app/event/eventBus'; import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; import { IAgentLoopService, type Turn } from '#/agent/loop/loop'; -import { CronCreateTool } from './tools/cron-create'; -import { CronListTool } from './tools/cron-list'; -import { CronDeleteTool } from './tools/cron-delete'; +import { ICronCreateTool } from './tools/cron-create'; +import { ICronListTool } from './tools/cron-list'; +import { ICronDeleteTool } from './tools/cron-delete'; import { CronModel, cronAdd, cronDelete, cronCursor } from './cronOps'; import { ISessionCronService, type CronLoadOptions } from './sessionCronService'; @@ -173,12 +172,11 @@ export class SessionCronServiceImpl extends Disposable implements ISessionCronSe } private registerCronTools(handle: IAgentScopeHandle): void { - const instantiation = handle.accessor.get(IInstantiationService); const registry = handle.accessor.get(IAgentToolRegistryService); const tools = [ - instantiation.createInstance(CronCreateTool), - instantiation.createInstance(CronListTool), - instantiation.createInstance(CronDeleteTool), + handle.accessor.get(ICronCreateTool), + handle.accessor.get(ICronListTool), + handle.accessor.get(ICronDeleteTool), ]; for (const tool of tools) { this._register(registry.register(tool, { source: 'builtin' })); diff --git a/packages/agent-core-v2/src/session/cron/tools/cron-create.ts b/packages/agent-core-v2/src/session/cron/tools/cron-create.ts index 3215a6cf20..bccbf5a761 100644 --- a/packages/agent-core-v2/src/session/cron/tools/cron-create.ts +++ b/packages/agent-core-v2/src/session/cron/tools/cron-create.ts @@ -25,7 +25,10 @@ import { z } from 'zod'; -import type { ExecutableTool as BuiltinTool, ToolExecution } from '#/tool/toolContract'; +import { createDecorator } from '#/_base/di/instantiation'; +import { InstantiationType } from '#/_base/di/extensions'; +import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; +import type { AgentTool, ToolExecution } from '#/tool/toolContract'; import { toInputJsonSchema } from '#/tool/input-schema'; import { literalRulePattern } from '#/tool/rule-match'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; @@ -74,7 +77,12 @@ interface CronCreateOutput { } -export class CronCreateTool implements BuiltinTool { +export interface ICronCreateTool extends AgentTool { readonly _serviceBrand: undefined } +export const ICronCreateTool = createDecorator('cronCreateTool'); + +export class CronCreateTool implements ICronCreateTool { + declare readonly _serviceBrand: undefined; + readonly name = 'CronCreate' as const; readonly description = CRON_CREATE_DESCRIPTION; readonly parameters: Record = toInputJsonSchema( @@ -223,3 +231,5 @@ function formatOutput(o: CronCreateOutput): string { ]; return lines.join('\n'); } + +registerScopedService(LifecycleScope.Agent, ICronCreateTool, CronCreateTool, InstantiationType.Eager, 'cron'); diff --git a/packages/agent-core-v2/src/session/cron/tools/cron-delete.ts b/packages/agent-core-v2/src/session/cron/tools/cron-delete.ts index e1d80e7657..60f31e8233 100644 --- a/packages/agent-core-v2/src/session/cron/tools/cron-delete.ts +++ b/packages/agent-core-v2/src/session/cron/tools/cron-delete.ts @@ -37,7 +37,10 @@ import { z } from 'zod'; -import type { ExecutableTool as BuiltinTool, ToolExecution } from '#/tool/toolContract'; +import { createDecorator } from '#/_base/di/instantiation'; +import { InstantiationType } from '#/_base/di/extensions'; +import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; +import type { AgentTool, ToolExecution } from '#/tool/toolContract'; import { toInputJsonSchema } from '#/tool/input-schema'; import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; import { ISessionCronService } from '#/session/cron/sessionCronService'; @@ -55,7 +58,12 @@ export const CronDeleteInputSchema = z.object({ export type CronDeleteInput = z.infer; -export class CronDeleteTool implements BuiltinTool { +export interface ICronDeleteTool extends AgentTool { readonly _serviceBrand: undefined } +export const ICronDeleteTool = createDecorator('cronDeleteTool'); + +export class CronDeleteTool implements ICronDeleteTool { + declare readonly _serviceBrand: undefined; + readonly name = 'CronDelete' as const; readonly description = CRON_DELETE_DESCRIPTION; readonly parameters: Record = toInputJsonSchema( @@ -99,3 +107,5 @@ export class CronDeleteTool implements BuiltinTool { }; } } + +registerScopedService(LifecycleScope.Agent, ICronDeleteTool, CronDeleteTool, InstantiationType.Eager, 'cron'); diff --git a/packages/agent-core-v2/src/session/cron/tools/cron-list.ts b/packages/agent-core-v2/src/session/cron/tools/cron-list.ts index 05189c51ea..d0c7a7f900 100644 --- a/packages/agent-core-v2/src/session/cron/tools/cron-list.ts +++ b/packages/agent-core-v2/src/session/cron/tools/cron-list.ts @@ -42,7 +42,10 @@ import { z } from 'zod'; -import type { ExecutableTool as BuiltinTool, ToolExecution } from '#/tool/toolContract'; +import { createDecorator } from '#/_base/di/instantiation'; +import { InstantiationType } from '#/_base/di/extensions'; +import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; +import type { AgentTool, ToolExecution } from '#/tool/toolContract'; import { toInputJsonSchema } from '#/tool/input-schema'; import { ISessionCronService } from '#/session/cron/sessionCronService'; import { cronToHuman, parseCronExpression } from '#/app/cron/cron-expr'; @@ -68,7 +71,12 @@ function previewPrompt(prompt: string): string { } -export class CronListTool implements BuiltinTool { +export interface ICronListTool extends AgentTool { readonly _serviceBrand: undefined } +export const ICronListTool = createDecorator('cronListTool'); + +export class CronListTool implements ICronListTool { + declare readonly _serviceBrand: undefined; + readonly name = 'CronList' as const; readonly description = CRON_LIST_DESCRIPTION; readonly parameters: Record = toInputJsonSchema( @@ -132,3 +140,5 @@ export class CronListTool implements BuiltinTool { ].join('\n'); } } + +registerScopedService(LifecycleScope.Agent, ICronListTool, CronListTool, InstantiationType.Eager, 'cron'); diff --git a/packages/agent-core-v2/src/session/subagent/tools/agent.ts b/packages/agent-core-v2/src/session/subagent/tools/agent.ts index 275d76a29e..3cab7ee288 100644 --- a/packages/agent-core-v2/src/session/subagent/tools/agent.ts +++ b/packages/agent-core-v2/src/session/subagent/tools/agent.ts @@ -18,8 +18,12 @@ * models there is no "child follows the parent's current model" invariant to * enforce. * - * Registered via the module-level `registerTool(AgentTool)` at the bottom of - * this file — the same "import = register" pattern used by every builtin tool. + * Registered via the module-level `registerAgentTool(ISubagentTool, + * SubagentTool)` at the bottom of this file — the same "import = register" + * pattern used by every agent tool. The per-profile tool listings in the + * description read the full contribution table (not the runtime registry, + * which only holds tools the caller's own Profile activated), plus any + * dynamically registered tools. */ import { z } from 'zod'; @@ -48,12 +52,16 @@ import { IAgentLoopService } from '#/agent/loop/loop'; import { IAgentUserToolService } from '#/agent/userTool/userTool'; import { ToolAccesses, - type BuiltinTool, + type AgentTool, type ExecutableToolContext, type ExecutableToolResult, type ToolExecution, } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { createDecorator } from '#/_base/di/instantiation'; +import { + getAgentToolContributions, + registerAgentTool, +} from '#/agent/toolRegistry/toolContribution'; import { IAgentToolRegistryService, type ToolReference } from '#/agent/toolRegistry/toolRegistry'; import { type AgentProfile } from '#/app/agentProfileCatalog/agentProfileCatalog'; import { ISessionAgentProfileCatalog } from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog'; @@ -91,7 +99,7 @@ import AGENT_DESCRIPTION_BASE from './agent.md?raw'; const DEFAULT_PROFILE_NAME = 'coder'; const RESUMED_LABEL = 'subagent'; -export const AgentToolInputSchema = z.preprocess( +export const SubagentToolInputSchema = z.preprocess( (input) => { if (typeof input !== 'object' || input === null || Array.isArray(input)) { return input; @@ -139,10 +147,10 @@ export const AgentToolInputSchema = z.preprocess( }), ); -export type AgentToolInput = z.infer; +export type SubagentToolInput = z.infer; -export const AgentToolOutputSchema = z.object({ +export const SubagentToolOutputSchema = z.object({ result: z.string().describe('Aggregated text output from the subagent'), usage: z .object({ @@ -154,7 +162,7 @@ export const AgentToolOutputSchema = z.object({ .describe('Cumulative token usage'), }); -export type AgentToolOutput = z.infer; +export type SubagentToolOutput = z.infer; const BACKGROUND_AGENT_UNAVAILABLE = 'Background agent execution is not available for this agent because TaskList, TaskOutput, and TaskStop are not enabled.'; @@ -165,9 +173,16 @@ const USER_INTERRUPTED_SUBAGENT_MESSAGE = const SUBAGENT_STOPPED_MESSAGE = 'The subagent was stopped before it finished.'; -export class AgentTool implements BuiltinTool { +export interface ISubagentTool extends AgentTool { + readonly _serviceBrand: undefined; +} + +export const ISubagentTool = createDecorator('subagentTool'); + +export class SubagentTool implements ISubagentTool { + declare readonly _serviceBrand: undefined; readonly name: string = 'Agent'; - readonly parameters: Record = toInputJsonSchema(AgentToolInputSchema); + readonly parameters: Record = toInputJsonSchema(SubagentToolInputSchema); private readonly callerAgentId: string; private readonly canRunInBackground: () => boolean; @@ -209,7 +224,7 @@ export class AgentTool implements BuiltinTool { : this.catalog.list().filter((profile) => allowlist.includes(profile.name)); const typeLines = buildProfileDescriptions( profiles, - this.toolRegistry.listReferences(), + this.knownToolReferences(), (profile, name, source) => this.toolPolicy.isToolActiveForProfile(profile, name, source), this.flags.enabled(SECONDARY_MODEL_FLAG_ID), @@ -228,7 +243,21 @@ export class AgentTool implements BuiltinTool { return description; } - async resolveExecution(args: AgentToolInput): Promise { + private knownToolReferences(): ToolReference[] { + const refs = new Map(); + for (const contribution of getAgentToolContributions()) { + refs.set(contribution.options.name, { + name: contribution.options.name, + source: contribution.options.source ?? 'builtin', + }); + } + for (const ref of this.toolRegistry.listReferences()) { + if (!refs.has(ref.name)) refs.set(ref.name, ref); + } + return [...refs.values()]; + } + + async resolveExecution(args: SubagentToolInput): Promise { const requestedProfileName = args.subagent_type?.length ? args.subagent_type : undefined; const resumeAgentId = args.resume?.trim(); @@ -267,7 +296,7 @@ export class AgentTool implements BuiltinTool { } private async launch( - args: AgentToolInput, + args: SubagentToolInput, toolCallId: string, controller: AbortController, ): Promise { @@ -387,7 +416,7 @@ export class AgentTool implements BuiltinTool { } private async execution( - args: AgentToolInput, + args: SubagentToolInput, { toolCallId, signal }: ExecutableToolContext, ): Promise { try { @@ -503,7 +532,7 @@ export class AgentTool implements BuiltinTool { } } -registerTool(AgentTool); +registerAgentTool(ISubagentTool, SubagentTool, { name: 'Agent', domain: 'subagent' }); function buildProfileDescriptions( diff --git a/packages/agent-core-v2/src/session/todo/tools/todo-list.ts b/packages/agent-core-v2/src/session/todo/tools/todo-list.ts index da0abe1a82..ed2e4cc052 100644 --- a/packages/agent-core-v2/src/session/todo/tools/todo-list.ts +++ b/packages/agent-core-v2/src/session/todo/tools/todo-list.ts @@ -9,17 +9,20 @@ * * The list is session-shared: the tool reads/writes `ISessionTodoService`, * which persists every change as a `tools.update_store` (`key: 'todo'`) wire record on the main agent. - * Self-registers via `registerTool(TodoListTool)` at module load; the Eager - * `AgentBuiltinToolsRegistrar` instantiates one per agent (resolving the - * Session-scope `ISessionTodoService` from the parent scope) and registers it - * into that agent's tool registry — never from a service constructor, which - * would re-enter `ISessionTodoService` while it is still being constructed. + * Self-registers via `registerAgentTool(ITodoListTool, TodoListTool, ...)` at + * module load as a Delayed Agent-scope service contribution; + * `AgentToolActivationService` activates it per agent when the profile allows + * (resolving the Session-scope `ISessionTodoService` from the parent scope) + * and registers it into that agent's tool registry — never from a service + * constructor, which would re-enter `ISessionTodoService` while it is still + * being constructed. */ import { z } from 'zod'; -import type { BuiltinTool, ToolExecution } from '#/tool/toolContract'; -import { registerTool } from '#/agent/toolRegistry/toolContribution'; +import { createDecorator } from '#/_base/di/instantiation'; +import type { AgentTool, ToolExecution } from '#/tool/toolContract'; +import { registerAgentTool } from '#/agent/toolRegistry/toolContribution'; import { toInputJsonSchema } from '#/tool/input-schema'; import { ISessionTodoService } from '#/session/todo/sessionTodo'; @@ -51,7 +54,13 @@ export const TodoListInputSchema: z.ZodType = z.object({ ), }); -export class TodoListTool implements BuiltinTool { +export interface ITodoListTool extends AgentTool { + readonly _serviceBrand: undefined; +} +export const ITodoListTool = createDecorator('todoListTool'); + +export class TodoListTool implements ITodoListTool { + declare readonly _serviceBrand: undefined; readonly name = TODO_LIST_TOOL_NAME; readonly description: string = DESCRIPTION; readonly parameters: Record = toInputJsonSchema(TodoListInputSchema); @@ -89,4 +98,4 @@ export class TodoListTool implements BuiltinTool { } } -registerTool(TodoListTool); +registerAgentTool(ITodoListTool, TodoListTool, { name: 'TodoList', domain: 'todo' }); diff --git a/packages/agent-core-v2/src/tool/toolContract.ts b/packages/agent-core-v2/src/tool/toolContract.ts index 1367f3df17..ac4c0a539d 100644 --- a/packages/agent-core-v2/src/tool/toolContract.ts +++ b/packages/agent-core-v2/src/tool/toolContract.ts @@ -6,7 +6,8 @@ * contract every tool implements (`resolveExecution` → `ToolExecution` → * `execute(ctx)`), the `ExecutableToolContext` it runs against, the raw and * finalized results (`ExecutableToolResult` / `ToolResult`), the streaming - * `ToolUpdate`, and the `BuiltinTool` alias. Also owns the `ToolAccesses` + * `ToolUpdate`, and the `AgentTool` service interface every DI-registered + * agent tool implements. Also owns the `ToolAccesses` * resource-access declarations an execution emits so the host scheduler can * run non-conflicting calls concurrently (together with their conflict * semantics), and the `isMcpToolName` name predicate. The `stopTurn` / @@ -107,7 +108,9 @@ export interface ToolInfo extends ToolDefinition { readonly source: ToolSource; } -export type BuiltinTool = ExecutableTool; +export interface AgentTool extends ExecutableTool { + readonly _serviceBrand: undefined; +} export type ToolResult = ExecutableToolResult & { readonly description?: string; diff --git a/packages/agent-core-v2/test/agent/goal/tools/goal-tools.test.ts b/packages/agent-core-v2/test/agent/goal/tools/goal-tools.test.ts index 70ab5e0642..28007c5b4d 100644 --- a/packages/agent-core-v2/test/agent/goal/tools/goal-tools.test.ts +++ b/packages/agent-core-v2/test/agent/goal/tools/goal-tools.test.ts @@ -28,7 +28,7 @@ import { IAgentToolExecutorService, type ToolExecutionResult, } from '#/agent/toolExecutor/toolExecutor'; -import { getToolContributions } from '#/agent/toolRegistry/toolContribution'; +import { getAgentToolContributions } from '#/agent/toolRegistry/toolContribution'; import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; import { IEventBus } from '#/app/event/eventBus'; @@ -446,7 +446,7 @@ describe('goal tool main-agent gating', () => { } it.each(gatedTools)('%s is contributed with a main-agent-only guard', (name, ctor) => { - const contribution = getToolContributions().find((c) => c.ctor === ctor); + const contribution = getAgentToolContributions().find((c) => c.ctor === ctor); expect(contribution, `${name} contribution`).toBeDefined(); const when = contribution?.options.when; expect(when, `${name} must gate on agent identity`).toBeDefined(); diff --git a/packages/agent-core-v2/test/agent/toolActivation/toolActivationService.test.ts b/packages/agent-core-v2/test/agent/toolActivation/toolActivationService.test.ts new file mode 100644 index 0000000000..321c51b6cb --- /dev/null +++ b/packages/agent-core-v2/test/agent/toolActivation/toolActivationService.test.ts @@ -0,0 +1,161 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { DisposableStore, toDisposable } from '#/_base/di/lifecycle'; +import { createDecorator } from '#/_base/di/instantiation'; +import { createServices } from '#/_base/di/test'; +import { IEventBus } from '#/app/event/eventBus'; +import { IAgentProfileService, type ProfileData } from '#/agent/profile/profile'; +import { IAgentToolActivationService } from '#/agent/toolActivation/toolActivation'; +import { AgentToolActivationService } from '#/agent/toolActivation/toolActivationService'; +import { + _clearAgentToolContributionsForTests, + getAgentToolContributions, + registerAgentTool, + type AgentToolContribution, +} from '#/agent/toolRegistry/toolContribution'; +import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; +import { AgentToolRegistryService } from '#/agent/toolRegistry/toolRegistryService'; +import type { AgentTool, ToolExecution } from '#/tool/toolContract'; + +class StubTool implements AgentTool { + declare readonly _serviceBrand: undefined; + readonly description = 'stub'; + readonly parameters: Record = {}; + constructor(readonly name: string) {} + resolveExecution(): ToolExecution { + return { isError: true, output: 'stub' }; + } +} + +const IAlphaTool = createDecorator('activationTestAlphaTool'); +const IBetaTool = createDecorator('activationTestBetaTool'); +const IGammaTool = createDecorator('activationTestGammaTool'); + +class AlphaTool extends StubTool { + constructor() { + super('Alpha'); + } +} + +class BetaTool extends StubTool { + constructor() { + super('Beta'); + } +} + +class GammaTool extends StubTool { + constructor() { + super('Gamma'); + } +} + +describe('AgentToolActivationService', () => { + let savedContributions: readonly AgentToolContribution[]; + let disposables: DisposableStore; + const profileData: { + activeToolNames?: readonly string[]; + disallowedTools?: readonly string[]; + } = {}; + + function createActivationHost() { + disposables = new DisposableStore(); + return createServices(disposables, { + strict: true, + additionalServices: (reg) => { + reg.definePartialInstance(IAgentProfileService, { + data: () => profileData as ProfileData, + }); + reg.definePartialInstance(IEventBus, { + subscribe: () => toDisposable(() => {}), + }); + reg.define(IAgentToolRegistryService, AgentToolRegistryService); + reg.define(IAgentToolActivationService, AgentToolActivationService); + reg.define(IAlphaTool, AlphaTool); + reg.define(IBetaTool, BetaTool); + reg.define(IGammaTool, GammaTool); + }, + }); + } + + beforeEach(() => { + savedContributions = [...getAgentToolContributions()]; + _clearAgentToolContributionsForTests(); + delete profileData.activeToolNames; + delete profileData.disallowedTools; + }); + + afterEach(() => { + disposables.dispose(); + _clearAgentToolContributionsForTests(); + for (const contribution of savedContributions) { + registerAgentTool(contribution.id, contribution.ctor, contribution.options); + } + }); + + it('activates every contribution when the profile has no allowlist', async () => { + registerAgentTool(IAlphaTool, AlphaTool, { name: 'Alpha' }); + registerAgentTool(IBetaTool, BetaTool, { name: 'Beta' }); + const ix = createActivationHost(); + + await ix.get(IAgentToolActivationService).activate(); + + const registry = ix.get(IAgentToolRegistryService); + expect(registry.resolve('Alpha')).toBeInstanceOf(AlphaTool); + expect(registry.resolve('Beta')).toBeInstanceOf(BetaTool); + }); + + it('activates only the tools allowed by the profile allowlist', async () => { + profileData.activeToolNames = ['Alpha']; + registerAgentTool(IAlphaTool, AlphaTool, { name: 'Alpha' }); + registerAgentTool(IBetaTool, BetaTool, { name: 'Beta' }); + const ix = createActivationHost(); + + await ix.get(IAgentToolActivationService).activate(); + + const registry = ix.get(IAgentToolRegistryService); + expect(registry.resolve('Alpha')).toBeInstanceOf(AlphaTool); + expect(registry.resolve('Beta')).toBeUndefined(); + }); + + it('honors the profile disallowedTools', async () => { + profileData.disallowedTools = ['Beta']; + registerAgentTool(IAlphaTool, AlphaTool, { name: 'Alpha' }); + registerAgentTool(IBetaTool, BetaTool, { name: 'Beta' }); + const ix = createActivationHost(); + + await ix.get(IAgentToolActivationService).activate(); + + const registry = ix.get(IAgentToolRegistryService); + expect(registry.resolve('Alpha')).toBeInstanceOf(AlphaTool); + expect(registry.resolve('Beta')).toBeUndefined(); + }); + + it('skips contributions whose when predicate fails', async () => { + registerAgentTool(IGammaTool, GammaTool, { name: 'Gamma', when: () => false }); + const ix = createActivationHost(); + + await ix.get(IAgentToolActivationService).activate(); + + expect(ix.get(IAgentToolRegistryService).resolve('Gamma')).toBeUndefined(); + }); + + it('is idempotent and picks up newly allowed tools on re-activation', async () => { + profileData.activeToolNames = ['Alpha']; + registerAgentTool(IAlphaTool, AlphaTool, { name: 'Alpha' }); + registerAgentTool(IBetaTool, BetaTool, { name: 'Beta' }); + const ix = createActivationHost(); + const activation = ix.get(IAgentToolActivationService); + const registry = ix.get(IAgentToolRegistryService); + + await activation.activate(); + const alpha = registry.resolve('Alpha'); + expect(alpha).toBeInstanceOf(AlphaTool); + expect(registry.resolve('Beta')).toBeUndefined(); + + profileData.activeToolNames = ['Alpha', 'Beta']; + await activation.activate(); + + expect(registry.resolve('Alpha')).toBe(alpha); + expect(registry.resolve('Beta')).toBeInstanceOf(BetaTool); + }); +}); diff --git a/packages/agent-core-v2/test/app/web/tools/fetch-url.test.ts b/packages/agent-core-v2/test/app/web/tools/fetch-url.test.ts index 233833062d..02203a86b1 100644 --- a/packages/agent-core-v2/test/app/web/tools/fetch-url.test.ts +++ b/packages/agent-core-v2/test/app/web/tools/fetch-url.test.ts @@ -53,7 +53,10 @@ describe('FetchURLTool abort signal', () => { const fetch = vi .fn() .mockResolvedValue({ content: 'hello', kind: 'passthrough' } satisfies UrlFetchResult); - const tool = new FetchURLTool({ fetch }); + const tool = new FetchURLTool({ + _serviceBrand: undefined, + getUrlFetcher: () => ({ fetch }), + }); await execute(tool, 'https://example.com', controller.signal); @@ -69,7 +72,10 @@ describe('FetchURLTool abort signal', () => { controller.abort(new Error('Aborted by the user')); throw abortError(); }); - const tool = new FetchURLTool({ fetch }); + const tool = new FetchURLTool({ + _serviceBrand: undefined, + getUrlFetcher: () => ({ fetch }), + }); await expect(execute(tool, 'https://example.com', controller.signal)).rejects.toThrow(); }); @@ -77,7 +83,10 @@ describe('FetchURLTool abort signal', () => { it('returns a normal error result when fetch fails without abort', async () => { const controller = new AbortController(); const fetch = vi.fn().mockRejectedValue(new Error('boom')); - const tool = new FetchURLTool({ fetch }); + const tool = new FetchURLTool({ + _serviceBrand: undefined, + getUrlFetcher: () => ({ fetch }), + }); const result = await execute(tool, 'https://example.com', controller.signal); @@ -94,7 +103,10 @@ describe('FetchURLTool output note', () => { const fetch = vi .fn() .mockResolvedValue({ content: 'BODY', kind } satisfies UrlFetchResult); - const tool = new FetchURLTool({ fetch }); + const tool = new FetchURLTool({ + _serviceBrand: undefined, + getUrlFetcher: () => ({ fetch }), + }); const result = await execute(tool, 'https://example.com', new AbortController().signal); expect(result.isError).toBe(false); if (typeof result.output !== 'string') throw new Error('expected string output'); diff --git a/packages/agent-core-v2/test/harness/agent.ts b/packages/agent-core-v2/test/harness/agent.ts index 1cd7388ba5..2b7fa58939 100644 --- a/packages/agent-core-v2/test/harness/agent.ts +++ b/packages/agent-core-v2/test/harness/agent.ts @@ -119,7 +119,7 @@ import { ITelemetryService, IHostTerminalService, IAgentToolRegistryService, - IAgentBuiltinToolsRegistrar, + IAgentToolActivationService, IAgentUserToolService, IAgentUsageService, ISessionWorkspaceContext, @@ -1333,7 +1333,11 @@ export class AgentTestContext { const permissionRules = this.get(IAgentPermissionRulesService); const cron = this.get(ISessionCronService); const plan = this.get(IAgentPlanService); - this.get(IAgentBuiltinToolsRegistrar); + // Activate the AgentTool contributions before any profile allowlist is + // applied by `configure()` — at this point `activeToolNames` is still + // undefined, so every contribution whose `when` holds lands in the + // registry, matching the harness's historical all-tools behavior. + void this.get(IAgentToolActivationService).activate(); this.get(IAgentToolDedupeService); this.get(IAgentExternalHooksService); this.get(IAgentStepRetryService); diff --git a/packages/agent-core-v2/test/os/backends/node-local/tools/grep.test.ts b/packages/agent-core-v2/test/os/backends/node-local/tools/grep.test.ts index fb7669bc1b..5344e588e5 100644 --- a/packages/agent-core-v2/test/os/backends/node-local/tools/grep.test.ts +++ b/packages/agent-core-v2/test/os/backends/node-local/tools/grep.test.ts @@ -2,7 +2,7 @@ import { Readable, type Writable } from 'node:stream'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { DisposableStore } from '#/_base/di/lifecycle'; +import { DisposableStore, toDisposable } from '#/_base/di/lifecycle'; import { createServices } from '#/_base/di/test'; import type { ExecutableTool, @@ -10,17 +10,17 @@ import type { ExecutableToolResult, ToolExecution, } from '#/tool/toolContract'; +import { IAgentToolActivationService } from '#/agent/toolActivation/toolActivation'; +import { AgentToolActivationService } from '#/agent/toolActivation/toolActivationService'; +import { IAgentProfileService, type ProfileData } from '#/agent/profile/profile'; import { - AgentBuiltinToolsRegistrar, - IAgentBuiltinToolsRegistrar, -} from '#/agent/toolRegistry/builtinToolsRegistrar'; -import { - _clearToolContributionsForTests, - getToolContributions, - registerTool, + _clearAgentToolContributionsForTests, + getAgentToolContributions, + registerAgentTool, } from '#/agent/toolRegistry/toolContribution'; import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; import { AgentToolRegistryService } from '#/agent/toolRegistry/toolRegistryService'; +import { IEventBus } from '#/app/event/eventBus'; import { ITelemetryService, noopTelemetryService } from '#/app/telemetry/telemetry'; import type { PathClass } from '#/_base/execEnv/environmentProbe'; import { @@ -37,6 +37,7 @@ import { type GrepInput, GrepInputSchema, GrepTool as ProductionGrepTool, + IGrepTool, } from '#/os/backends/node-local/tools/grep'; import { ensureRgPath } from '#/os/backends/node-local/tools/rgLocator'; import { stubWorkspaceContext } from '../../../../session/workspaceContext/stub-workspace-context'; @@ -282,12 +283,16 @@ afterEach(() => { }); describe('GrepTool', () => { - it('registers contribution metadata through the production DI path', () => { - const savedContributions = [...getToolContributions()]; + it('registers contribution metadata through the production DI path', async () => { + const savedContributions = [...getAgentToolContributions()]; const disposables = new DisposableStore(); try { - _clearToolContributionsForTests(); - registerTool(ProductionGrepTool, { source: 'user', disclosure: 'deferred' }); + _clearAgentToolContributionsForTests(); + registerAgentTool(IGrepTool, ProductionGrepTool, { + name: 'Grep', + source: 'user', + disclosure: 'deferred', + }); const ix = createServices(disposables, { strict: true, @@ -303,12 +308,19 @@ describe('GrepTool', () => { _serviceBrand: undefined, catalog: { getSkillRoots: () => [] }, } as unknown as ISessionSkillCatalog); + reg.define(IGrepTool, ProductionGrepTool); reg.define(IAgentToolRegistryService, AgentToolRegistryService); - reg.define(IAgentBuiltinToolsRegistrar, AgentBuiltinToolsRegistrar); + reg.define(IAgentToolActivationService, AgentToolActivationService); + reg.definePartialInstance(IAgentProfileService, { + data: () => ({}) as unknown as ProfileData, + }); + reg.definePartialInstance(IEventBus, { + subscribe: () => toDisposable(() => {}), + }); }, }); - ix.get(IAgentBuiltinToolsRegistrar); + await ix.get(IAgentToolActivationService).activate(); const tool = ix.get(IAgentToolRegistryService).resolve('Grep'); const info = ix.get(IAgentToolRegistryService).list().find((entry) => entry.name === 'Grep'); @@ -317,9 +329,9 @@ describe('GrepTool', () => { expect(info).toMatchObject({ source: 'user', disclosure: 'deferred' }); } finally { disposables.dispose(); - _clearToolContributionsForTests(); + _clearAgentToolContributionsForTests(); for (const contribution of savedContributions) { - registerTool(contribution.ctor, contribution.options); + registerAgentTool(contribution.id, contribution.ctor, contribution.options); } } }); diff --git a/packages/agent-core-v2/test/session/agentLifecycle/agentLifecycle.test.ts b/packages/agent-core-v2/test/session/agentLifecycle/agentLifecycle.test.ts index 8a415cc822..8ba39f86d5 100644 --- a/packages/agent-core-v2/test/session/agentLifecycle/agentLifecycle.test.ts +++ b/packages/agent-core-v2/test/session/agentLifecycle/agentLifecycle.test.ts @@ -58,8 +58,9 @@ import { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { ISessionAgentProfileCatalog } from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog'; import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog'; import { ISessionToolPolicy } from '#/session/sessionToolPolicy/sessionToolPolicy'; -import { _clearToolContributionsForTests } from '#/agent/toolRegistry/toolContribution'; +import { _clearAgentToolContributionsForTests } from '#/agent/toolRegistry/toolContribution'; import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; +import '#/agent/toolActivation/toolActivationService'; import { IAgentMediaToolsRegistrar } from '#/agent/media/mediaTools'; import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext'; import type { OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js'; @@ -161,7 +162,7 @@ describe('AgentLifecycleService', () => { let didExecuteHookIds: string[]; beforeEach(() => { - _clearToolContributionsForTests(); + _clearAgentToolContributionsForTests(); disposables = new DisposableStore(); ix = disposables.add(new TestInstantiationService()); ix.set(IAgentStateService, new AgentStateService()); diff --git a/packages/agent-core-v2/test/tool/tool.test.ts b/packages/agent-core-v2/test/tool/tool.test.ts index f6fe70cc23..dfa5e8d284 100644 --- a/packages/agent-core-v2/test/tool/tool.test.ts +++ b/packages/agent-core-v2/test/tool/tool.test.ts @@ -37,8 +37,8 @@ import { type AgentSwarmToolInput, } from '#/agent/swarm/tools/agent-swarm'; import { - AgentToolInputSchema, - type AgentToolInput, + SubagentToolInputSchema, + type SubagentToolInput, } from '#/session/subagent/tools/agent'; import { DEFAULT_SUBAGENT_TIMEOUT_MS } from '#/session/subagent/configSection'; import { Error2, ErrorCodes } from '#/errors'; @@ -95,7 +95,7 @@ function secondaryModelFlags(enabled = true): TestAgentServiceOverride { function agentSchemaProperties(): Record { return ( - toInputJsonSchema(AgentToolInputSchema) as { properties: Record } + toInputJsonSchema(SubagentToolInputSchema) as { properties: Record } ).properties; } @@ -361,10 +361,10 @@ function createAgentLifecycleStub(options: AgentLifecycleStubOptions = {}): Agen return lifecycle; } -function agentTool(ctx: TestAgentContext): ExecutableTool { +function agentTool(ctx: TestAgentContext): ExecutableTool { const tool = ctx.get(IAgentToolRegistryService).resolve('Agent'); expect(tool).toBeDefined(); - return tool! as ExecutableTool; + return tool! as ExecutableTool; } function agentSwarmTool(ctx: TestAgentContext): ExecutableTool { @@ -375,7 +375,7 @@ function agentSwarmTool(ctx: TestAgentContext): ExecutableTool { +describe('SubagentToolInputSchema', () => { it('accepts the snake_case background parameter', () => { - const parsed = AgentToolInputSchema.parse({ + const parsed = SubagentToolInputSchema.parse({ prompt: 'Investigate', description: 'Find cause', subagent_type: 'explore', @@ -483,20 +483,20 @@ describe('AgentToolInputSchema', () => { it('normalizes the default subagent type into tool args', () => { expect( - AgentToolInputSchema.parse({ + SubagentToolInputSchema.parse({ prompt: 'Investigate', description: 'Find cause', }).subagent_type, ).toBe('coder'); expect( - AgentToolInputSchema.parse({ + SubagentToolInputSchema.parse({ prompt: 'Investigate', description: 'Find cause', subagent_type: '', }).subagent_type, ).toBe('coder'); expect( - AgentToolInputSchema.parse({ + SubagentToolInputSchema.parse({ prompt: 'Continue', description: 'Continue work', resume: 'agent-existing', From 9d2099e074ac4e6bf4c0ed1f4464581739c08377 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Sun, 26 Jul 2026 10:11:05 +0800 Subject: [PATCH 2/4] refactor(agent-core-v2): centralize builtin tools under agent/tools - move builtin tools from scattered domain folders (plan/tools, goal/tools, os/backends/node-local/tools, task/tools, etc.) into a unified agent/tools/ directory - split each tool into a kebab-case definition file (bash.ts) and a registration file (bashTool.ts) pairing with its prompt markdown - update imports across src, tests, kap-server, and TUI comments --- .../src/tui/components/messages/tool-call.ts | 5 +- .../scripts/check-domain-layers.mjs | 20 +++ .../src/agent/media/registerMediaTools.ts | 3 +- .../src/agent/task/taskService.ts | 6 +- .../agent-swarm}/agent-swarm.md | 0 .../agent/tools/agent-swarm/agent-swarm.ts | 69 +++++++++ .../agent-swarm/agentSwarmTool.ts} | 91 ++++-------- .../tools/agent}/agent-background-disabled.md | 0 .../tools/agent}/agent-background-enabled.md | 0 .../tools => agent/tools/agent}/agent.md | 0 .../src/agent/tools/agent/agent.ts | 98 +++++++++++++ .../tools/agent/agentTool.ts} | 113 +++------------ .../tools/agent}/subagent-task.ts | 0 .../ask-user-question/ask-user-question.ts | 108 ++++++++++++++ .../ask-user-question}/ask-user.md | 0 .../ask-user-question/askUserQuestionTool.ts} | 121 +++------------- .../question-background-task.ts | 0 .../tools/cron/cron-create}/cron-create.md | 0 .../tools/cron/cron-create/cron-create.ts | 52 +++++++ .../tools/cron/cron-create/cronCreateTool.ts} | 61 +++----- .../tools/cron/cron-delete}/cron-delete.md | 0 .../tools/cron/cron-delete/cron-delete.ts | 24 ++++ .../tools/cron/cron-delete/cronDeleteTool.ts} | 25 ++-- .../tools/cron/cron-list}/cron-list.md | 0 .../agent/tools/cron/cron-list/cron-list.ts | 20 +++ .../tools/cron/cron-list/cronListTool.ts} | 22 ++- .../edit/tools => agent/tools/edit}/edit.md | 0 .../src/agent/tools/edit/edit.ts | 48 +++++++ .../edit.ts => agent/tools/edit/editTool.ts} | 50 ++----- .../tools/fetch-url}/fetch-url.md | 0 .../src/agent/tools/fetch-url/fetch-url.ts | 27 ++++ .../tools/fetch-url/fetchUrlTool.ts} | 35 ++--- .../goal/create-goal}/create-goal.md | 0 .../tools/goal/create-goal/create-goal.ts | 34 +++++ .../goal/create-goal/createGoalTool.ts} | 44 +++--- .../tools => tools/goal/get-goal}/get-goal.md | 0 .../src/agent/tools/goal/get-goal/get-goal.ts | 21 +++ .../goal/get-goal/getGoalTool.ts} | 26 ++-- .../goal/set-goal-budget}/set-goal-budget.md | 0 .../goal/set-goal-budget/set-goal-budget.ts | 28 ++++ .../set-goal-budget/setGoalBudgetTool.ts} | 37 ++--- .../goal/update-goal}/update-goal.md | 0 .../tools/goal/update-goal/update-goal.ts | 30 ++++ .../goal/update-goal/updateGoalTool.ts} | 43 +++--- .../tools => agent/tools/os/bash}/bash.md | 0 .../src/agent/tools/os/bash/bash.ts | 92 ++++++++++++ .../tools/os/bash/bashTool.ts} | 109 ++++----------- .../tools/os/bash}/process-task.ts | 0 .../tools => agent/tools/os/glob}/glob.md | 0 .../src/agent/tools/os/glob/glob.ts | 53 +++++++ .../tools/os/glob/globTool.ts} | 77 +++++----- .../tools => agent/tools/os/grep}/grep.md | 0 .../src/agent/tools/os/grep/grep.ts | 119 ++++++++++++++++ .../tools/os/grep/grepTool.ts} | 132 +++--------------- .../tools => agent/tools/os/read}/read.md | 0 .../src/agent/tools/os/read/read.ts | 64 +++++++++ .../tools/os/read/readTool.ts} | 75 +++------- .../tools => agent/tools/os/write}/write.md | 0 .../src/agent/tools/os/write/write.ts | 48 +++++++ .../tools/os/write/writeTool.ts} | 54 ++----- .../plan/enter-plan-mode}/enter-plan-mode.md | 0 .../plan/enter-plan-mode/enter-plan-mode.ts | 22 +++ .../enter-plan-mode/enterPlanModeTool.ts} | 29 ++-- .../plan/exit-plan-mode}/exit-plan-mode.md | 0 .../plan/exit-plan-mode/exit-plan-mode.ts | 87 ++++++++++++ .../plan/exit-plan-mode/exitPlanModeTool.ts} | 112 +++------------ .../tools/read-media-file/read-media-file.ts | 61 ++++++++ .../read-media-file}/read-media.md | 0 .../read-media-file/readMediaFileTool.ts} | 62 ++------ .../agent/tools/select-tools/select-tools.ts | 29 ++++ .../select-tools/selectToolsTool.ts} | 43 +++--- .../{skill/tools => tools/skill}/skill.md | 0 .../src/agent/tools/skill/skill.ts | 56 ++++++++ .../skill.ts => tools/skill/skillTool.ts} | 70 +++------- .../task/task-list}/task-list.md | 0 .../agent/tools/task/task-list/task-list.ts | 35 +++++ .../task/task-list/taskListTool.ts} | 41 ++---- .../task/task-output}/task-output.md | 0 .../tools/task/task-output/task-output.ts | 39 ++++++ .../task/task-output/taskOutputTool.ts} | 50 ++----- .../task/task-stop}/task-stop.md | 0 .../agent/tools/task/task-stop/task-stop.ts | 28 ++++ .../task/task-stop/taskStopTool.ts} | 33 ++--- .../todo-list}/todo-list-write-reminder.md | 0 .../tools/todo-list}/todo-list.md | 0 .../src/agent/tools/todo-list/todo-list.ts | 45 ++++++ .../tools/todo-list/todoListTool.ts} | 59 +++----- .../tools/web-search}/web-search.md | 0 .../src/agent/tools/web-search/web-search.ts | 48 +++++++ .../tools/web-search/webSearchTool.ts} | 63 +++------ .../providers/moonshot-web-search.ts | 2 +- .../src/app/auth/webSearch/webSearch.ts | 4 +- .../app/auth/webSearch/webSearchService.ts | 2 +- packages/agent-core-v2/src/index.ts | 77 ++++++---- .../session/cron/sessionCronServiceImpl.ts | 6 +- .../test/agent/goal/goal.test.ts | 3 +- .../test/agent/goal/tools/goal-tools.test.ts | 12 +- .../test/agent/media/tools/read-media.test.ts | 4 +- .../agent/plan/tools/exit-plan-mode.test.ts | 4 +- .../plan/tools/plan-tools-telemetry.test.ts | 8 +- .../questionTools/tools/ask-user.test.ts | 6 +- .../test/agent/skill/skill.test.ts | 4 +- .../test/agent/swarm/swarm.test.ts | 3 +- .../agent/task/foreground-persistence.test.ts | 2 +- .../task/idle-notification-repro.test.ts | 2 +- .../agent-core-v2/test/agent/task/ids.test.ts | 4 +- .../test/agent/task/output-access.test.ts | 4 +- .../test/agent/task/rpc-events.test.ts | 6 +- .../test/agent/task/subagent-timeout.test.ts | 2 +- .../test/agent/task/taskManager.test.ts | 4 +- .../test/agent/task/taskService.test.ts | 2 +- .../test/agent/task/tools/task-tools.test.ts | 22 ++- .../agent/toolSelect/toolSelect.e2e.test.ts | 2 +- .../toolSelect/toolSelectService.test.ts | 2 +- .../test/app/edit/tools/edit.test.ts | 3 +- .../test/app/web/tools/fetch-url.test.ts | 2 +- .../os/backends/node-local/tools/bash.test.ts | 5 +- .../os/backends/node-local/tools/glob.test.ts | 5 +- .../os/backends/node-local/tools/grep.test.ts | 4 +- .../os/backends/node-local/tools/read.test.ts | 4 +- .../backends/node-local/tools/write.test.ts | 3 +- .../test/session/cron/cron-tools.test.ts | 10 +- .../test/session/todo/tools/todo-list.test.ts | 3 +- packages/agent-core-v2/test/tool/tool.test.ts | 4 +- packages/kap-server/src/routes/transcript.ts | 2 +- 125 files changed, 1993 insertions(+), 1335 deletions(-) rename packages/agent-core-v2/src/agent/{swarm/tools => tools/agent-swarm}/agent-swarm.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/agent-swarm/agent-swarm.ts rename packages/agent-core-v2/src/agent/{swarm/tools/agent-swarm.ts => tools/agent-swarm/agentSwarmTool.ts} (78%) rename packages/agent-core-v2/src/{session/subagent/tools => agent/tools/agent}/agent-background-disabled.md (100%) rename packages/agent-core-v2/src/{session/subagent/tools => agent/tools/agent}/agent-background-enabled.md (100%) rename packages/agent-core-v2/src/{session/subagent/tools => agent/tools/agent}/agent.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/agent/agent.ts rename packages/agent-core-v2/src/{session/subagent/tools/agent.ts => agent/tools/agent/agentTool.ts} (84%) rename packages/agent-core-v2/src/{session/subagent/tools => agent/tools/agent}/subagent-task.ts (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/ask-user-question/ask-user-question.ts rename packages/agent-core-v2/src/agent/{questionTools/tools => tools/ask-user-question}/ask-user.md (100%) rename packages/agent-core-v2/src/agent/{questionTools/tools/ask-user.ts => tools/ask-user-question/askUserQuestionTool.ts} (69%) rename packages/agent-core-v2/src/agent/{questionTools/tools => tools/ask-user-question}/question-background-task.ts (100%) rename packages/agent-core-v2/src/{session/cron/tools => agent/tools/cron/cron-create}/cron-create.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/cron/cron-create/cron-create.ts rename packages/agent-core-v2/src/{session/cron/tools/cron-create.ts => agent/tools/cron/cron-create/cronCreateTool.ts} (80%) rename packages/agent-core-v2/src/{session/cron/tools => agent/tools/cron/cron-delete}/cron-delete.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/cron/cron-delete/cron-delete.ts rename packages/agent-core-v2/src/{session/cron/tools/cron-delete.ts => agent/tools/cron/cron-delete/cronDeleteTool.ts} (86%) rename packages/agent-core-v2/src/{session/cron/tools => agent/tools/cron/cron-list}/cron-list.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/cron/cron-list/cron-list.ts rename packages/agent-core-v2/src/{session/cron/tools/cron-list.ts => agent/tools/cron/cron-list/cronListTool.ts} (91%) rename packages/agent-core-v2/src/{app/edit/tools => agent/tools/edit}/edit.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/edit/edit.ts rename packages/agent-core-v2/src/{app/edit/tools/edit.ts => agent/tools/edit/editTool.ts} (71%) rename packages/agent-core-v2/src/{app/web/tools => agent/tools/fetch-url}/fetch-url.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/fetch-url/fetch-url.ts rename packages/agent-core-v2/src/{app/web/tools/fetch-url.ts => agent/tools/fetch-url/fetchUrlTool.ts} (75%) rename packages/agent-core-v2/src/agent/{goal/tools => tools/goal/create-goal}/create-goal.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/goal/create-goal/create-goal.ts rename packages/agent-core-v2/src/agent/{goal/tools/create-goal.ts => tools/goal/create-goal/createGoalTool.ts} (65%) rename packages/agent-core-v2/src/agent/{goal/tools => tools/goal/get-goal}/get-goal.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/goal/get-goal/get-goal.ts rename packages/agent-core-v2/src/agent/{goal/tools/get-goal.ts => tools/goal/get-goal/getGoalTool.ts} (58%) rename packages/agent-core-v2/src/agent/{goal/tools => tools/goal/set-goal-budget}/set-goal-budget.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/goal/set-goal-budget/set-goal-budget.ts rename packages/agent-core-v2/src/agent/{goal/tools/set-goal-budget.ts => tools/goal/set-goal-budget/setGoalBudgetTool.ts} (82%) rename packages/agent-core-v2/src/agent/{goal/tools => tools/goal/update-goal}/update-goal.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/goal/update-goal/update-goal.ts rename packages/agent-core-v2/src/agent/{goal/tools/update-goal.ts => tools/goal/update-goal/updateGoalTool.ts} (73%) rename packages/agent-core-v2/src/{os/backends/node-local/tools => agent/tools/os/bash}/bash.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/os/bash/bash.ts rename packages/agent-core-v2/src/{os/backends/node-local/tools/bash.ts => agent/tools/os/bash/bashTool.ts} (85%) rename packages/agent-core-v2/src/{os/backends/node-local/tools => agent/tools/os/bash}/process-task.ts (100%) rename packages/agent-core-v2/src/{os/backends/node-local/tools => agent/tools/os/glob}/glob.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/os/glob/glob.ts rename packages/agent-core-v2/src/{os/backends/node-local/tools/glob.ts => agent/tools/os/glob/globTool.ts} (87%) rename packages/agent-core-v2/src/{os/backends/node-local/tools => agent/tools/os/grep}/grep.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/os/grep/grep.ts rename packages/agent-core-v2/src/{os/backends/node-local/tools/grep.ts => agent/tools/os/grep/grepTool.ts} (84%) rename packages/agent-core-v2/src/{os/backends/node-local/tools => agent/tools/os/read}/read.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/os/read/read.ts rename packages/agent-core-v2/src/{os/backends/node-local/tools/read.ts => agent/tools/os/read/readTool.ts} (86%) rename packages/agent-core-v2/src/{os/backends/node-local/tools => agent/tools/os/write}/write.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/os/write/write.ts rename packages/agent-core-v2/src/{os/backends/node-local/tools/write.ts => agent/tools/os/write/writeTool.ts} (72%) rename packages/agent-core-v2/src/agent/{plan/tools => tools/plan/enter-plan-mode}/enter-plan-mode.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/plan/enter-plan-mode/enter-plan-mode.ts rename packages/agent-core-v2/src/agent/{plan/tools/enter-plan-mode.ts => tools/plan/enter-plan-mode/enterPlanModeTool.ts} (81%) rename packages/agent-core-v2/src/agent/{plan/tools => tools/plan/exit-plan-mode}/exit-plan-mode.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/plan/exit-plan-mode/exit-plan-mode.ts rename packages/agent-core-v2/src/agent/{plan/tools/exit-plan-mode.ts => tools/plan/exit-plan-mode/exitPlanModeTool.ts} (66%) create mode 100644 packages/agent-core-v2/src/agent/tools/read-media-file/read-media-file.ts rename packages/agent-core-v2/src/agent/{media/tools => tools/read-media-file}/read-media.md (100%) rename packages/agent-core-v2/src/agent/{media/tools/read-media.ts => tools/read-media-file/readMediaFileTool.ts} (91%) create mode 100644 packages/agent-core-v2/src/agent/tools/select-tools/select-tools.ts rename packages/agent-core-v2/src/agent/{toolSelect/tools/select-tools.ts => tools/select-tools/selectToolsTool.ts} (68%) rename packages/agent-core-v2/src/agent/{skill/tools => tools/skill}/skill.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/skill/skill.ts rename packages/agent-core-v2/src/agent/{skill/tools/skill.ts => tools/skill/skillTool.ts} (69%) rename packages/agent-core-v2/src/agent/{task/tools => tools/task/task-list}/task-list.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/task/task-list/task-list.ts rename packages/agent-core-v2/src/agent/{task/tools/task-list.ts => tools/task/task-list/taskListTool.ts} (68%) rename packages/agent-core-v2/src/agent/{task/tools => tools/task/task-output}/task-output.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/task/task-output/task-output.ts rename packages/agent-core-v2/src/agent/{task/tools/task-output.ts => tools/task/task-output/taskOutputTool.ts} (78%) rename packages/agent-core-v2/src/agent/{task/tools => tools/task/task-stop}/task-stop.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/task/task-stop/task-stop.ts rename packages/agent-core-v2/src/agent/{task/tools/task-stop.ts => tools/task/task-stop/taskStopTool.ts} (75%) rename packages/agent-core-v2/src/{session/todo/tools => agent/tools/todo-list}/todo-list-write-reminder.md (100%) rename packages/agent-core-v2/src/{session/todo/tools => agent/tools/todo-list}/todo-list.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/todo-list/todo-list.ts rename packages/agent-core-v2/src/{session/todo/tools/todo-list.ts => agent/tools/todo-list/todoListTool.ts} (52%) rename packages/agent-core-v2/src/{app/auth/webSearch/tools => agent/tools/web-search}/web-search.md (100%) create mode 100644 packages/agent-core-v2/src/agent/tools/web-search/web-search.ts rename packages/agent-core-v2/src/{app/auth/webSearch/tools/web-search.ts => agent/tools/web-search/webSearchTool.ts} (73%) diff --git a/apps/kimi-code/src/tui/components/messages/tool-call.ts b/apps/kimi-code/src/tui/components/messages/tool-call.ts index 9ffed46b4e..01f76053a0 100644 --- a/apps/kimi-code/src/tui/components/messages/tool-call.ts +++ b/apps/kimi-code/src/tui/components/messages/tool-call.ts @@ -196,9 +196,8 @@ const PLAN_SAVED_TO_RE = /\nPlan saved to: ([^\n]+)\n/; /** * Parses the ExitPlanMode result content string to recover the approval outcome * and optional plan path. Core-side templates live in - * `packages/agent-core-v2/src/agent/plan/tools/exit-plan-mode.ts` (auto-approved - * path) and `.../permissionPolicy/policies/exit-plan-mode-review-ask.ts` - * (user-reviewed path): + * `packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts` and + * `.../agent/permission/policies/exit-plan-mode-review-ask.ts`: * - Approved output starts with 'Exited plan mode.' and selected options * are reported as 'Selected approach: