diff --git a/.changeset/tool-websearch-not-found-filter.md b/.changeset/tool-websearch-not-found-filter.md new file mode 100644 index 00000000..7a9c1c2c --- /dev/null +++ b/.changeset/tool-websearch-not-found-filter.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +--- + +Filter unregistered profile tools from active set and warn when they require configuration. diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts index 550cfeba..ffd3651a 100644 --- a/packages/agent-core/src/agent/tool/index.ts +++ b/packages/agent-core/src/agent/tool/index.ts @@ -38,6 +38,13 @@ export class ToolManager { protected enabledTools: Set = new Set(); /** Glob patterns (e.g. `mcp__*`, `mcp__github__*`) gating which MCP tools the profile exposes. */ private mcpAccessPatterns: string[] = []; + /** + * Profile-requested builtin tool names that could not be resolved because + * `initializeBuiltinTools()` had not run yet. Replayed once builtins are + * available so tools such as `WebSearch` — which require a service that may + * be present at init time — are not silently dropped on first profile apply. + */ + private pendingBuiltinToolNames: string[] = []; protected readonly store: Partial = {}; private mcpToolStatusUnsubscribe: (() => void) | undefined; @@ -297,7 +304,26 @@ export class ToolManager { }); // MCP entries are glob patterns gated separately; the rest are exact // builtin/user tool names. The split keeps every caller on one string[]. - this.enabledTools = new Set(names.filter((name) => !isMcpToolName(name))); + const nonMcpNames = names.filter((name) => !isMcpToolName(name)); + const availableNames = nonMcpNames.filter( + (name) => this.builtinTools.has(name) || this.userTools.has(name), + ); + const missingTools = nonMcpNames.filter((name) => !availableNames.includes(name)); + if (missingTools.length > 0) { + if (this.builtinTools.size > 0) { + // Builtins are fully initialized — missing tools are genuinely unavailable. + this.agent.log.warn( + `The following tools listed in the active profile are not available and will be omitted: ${missingTools.join(', ')}. ` + + `They may require additional service configuration.`, + ); + } + // Save pending builtin names so they can be re-applied once builtins + // are initialized (e.g. when the model is configured after profile apply). + this.pendingBuiltinToolNames = missingTools; + } else { + this.pendingBuiltinToolNames = []; + } + this.enabledTools = new Set(availableNames); this.mcpAccessPatterns = names.filter((name) => isMcpToolName(name)); } @@ -399,6 +425,28 @@ export class ToolManager { .filter((tool) => !!tool) .map((tool) => [tool.name, tool] as const), ); + // Re-apply pending profile tool names that were deferred because builtins + // had not been initialized when `setActiveTools` was first called. + if (this.pendingBuiltinToolNames.length > 0) { + const nowAvailable = this.pendingBuiltinToolNames.filter( + (name) => this.builtinTools.has(name) || this.userTools.has(name), + ); + if (nowAvailable.length > 0) { + for (const name of nowAvailable) { + this.enabledTools.add(name); + } + } + const stillMissing = this.pendingBuiltinToolNames.filter( + (name) => !nowAvailable.includes(name), + ); + if (stillMissing.length > 0) { + this.agent.log.warn( + `The following tools listed in the active profile are not available and will be omitted: ${stillMissing.join(', ')}. ` + + `They may require additional service configuration.`, + ); + } + this.pendingBuiltinToolNames = []; + } } private createVideoUploader(provider: ChatProvider): b.VideoUploader | undefined { diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 068d5626..d63ec739 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -803,7 +803,7 @@ function telemetryToolOutcome(result: ToolTelemetryResult): 'success' | 'error' function telemetryToolErrorType(result: ToolTelemetryResult): string { const text = toolResultText(result); - if (text.startsWith('Tool "') && text.includes('" not found')) return 'ToolNotFound'; + if (text.startsWith('Tool "') && (text.includes('" not found') || text.includes('is not available'))) return 'ToolNotFound'; if (text.startsWith('Invalid args for tool "')) return 'ToolInputError'; if (text.includes('prepareToolExecution hook failed')) return 'HookError'; if (text.includes('finalizeToolResult hook failed')) return 'HookError'; diff --git a/packages/agent-core/src/loop/tool-call.ts b/packages/agent-core/src/loop/tool-call.ts index c86a5705..92151138 100644 --- a/packages/agent-core/src/loop/tool-call.ts +++ b/packages/agent-core/src/loop/tool-call.ts @@ -167,7 +167,10 @@ function preflightToolCall( toolCall, toolName, args, - output: `Tool "${toolName}" not found`, + output: + `Tool "${toolName}" is not available. The tool was not found in the current session's tool list. ` + + `It may require configuration or have been removed from the active profile. ` + + `Use the available tools listed in your tool list instead.`, }; } if (!parsedArgs.success) { diff --git a/packages/agent-core/test/agent/compaction.test.ts b/packages/agent-core/test/agent/compaction.test.ts index cbd9c437..9379e042 100644 --- a/packages/agent-core/test/agent/compaction.test.ts +++ b/packages/agent-core/test/agent/compaction.test.ts @@ -1499,8 +1499,8 @@ describe('Agent compaction', () => { [wire] context.append_loop_event { "event": { "type": "content.part", "uuid": "", "turnId": "0", "step": 1, "stepUuid": "", "part": { "type": "text", "text": "I need a tool." } }, "time": "