Skip to content
6 changes: 6 additions & 0 deletions .changeset/tool-websearch-not-found-filter.md
Original file line number Diff line number Diff line change
@@ -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.
68 changes: 64 additions & 4 deletions packages/agent-core/src/agent/tool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ export class ToolManager {
protected enabledTools: Set<string> = 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.
*/
protected pendingBuiltinToolNames: string[] = [];
protected readonly store: Partial<ToolStoreData> = {};
private mcpToolStatusUnsubscribe: (() => void) | undefined;

Expand Down Expand Up @@ -297,7 +304,30 @@ 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.`,
);
}
// Replace pending with the current missing set. The most recent
// pre-init call expressing intent about builtin tools wins, so stale
// names from an earlier profile are not re-enabled at init time.
this.pendingBuiltinToolNames = [...missingTools];
} else {
// All requested tools resolved against known builtin/user tools.
// This is a replacement call: the new set explicitly replaces the
// previous one, so any prior deferred names are no longer relevant.
this.pendingBuiltinToolNames = [];
}
this.enabledTools = new Set(availableNames);
Comment thread
LifeJiggy marked this conversation as resolved.
this.mcpAccessPatterns = names.filter((name) => isMcpToolName(name));
}

Expand Down Expand Up @@ -355,10 +385,15 @@ export class ToolManager {
},
this.agent.skills?.registry.getSkillRoots() ?? [],
);
const pendingIncludesBackgroundTools =
this.pendingBuiltinToolNames.includes('TaskList') &&
this.pendingBuiltinToolNames.includes('TaskOutput') &&
this.pendingBuiltinToolNames.includes('TaskStop');
const allowBackground =
this.enabledTools.has('TaskList') &&
this.enabledTools.has('TaskOutput') &&
this.enabledTools.has('TaskStop');
(this.enabledTools.has('TaskList') &&
this.enabledTools.has('TaskOutput') &&
this.enabledTools.has('TaskStop')) ||
pendingIncludesBackgroundTools;
this.builtinTools = new Map(
[
new b.ReadTool(kaos, workspace),
Expand Down Expand Up @@ -399,6 +434,31 @@ 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);
Comment thread
LifeJiggy marked this conversation as resolved.
}
this.agent.log.info(
`Re-applied pending tool names that are now available: ${nowAvailable.join(', ')}.`,
);
}
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 = stillMissing;
}
}

private createVideoUploader(provider: ChatProvider): b.VideoUploader | undefined {
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/agent/turn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,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';
Expand Down
5 changes: 4 additions & 1 deletion packages/agent-core/src/loop/tool-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,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) {
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-core/test/agent/compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1499,8 +1499,8 @@ describe('Agent compaction', () => {
[wire] context.append_loop_event { "event": { "type": "content.part", "uuid": "<uuid-2>", "turnId": "0", "step": 1, "stepUuid": "<uuid-1>", "part": { "type": "text", "text": "I need a tool." } }, "time": "<time>" }
[wire] context.append_loop_event { "event": { "type": "tool.call", "uuid": "call_missing", "turnId": "0", "step": 1, "stepUuid": "<uuid-1>", "toolCallId": "call_missing", "name": "MissingTool", "args": {} }, "time": "<time>" }
[emit] tool.call.started { "turnId": 0, "toolCallId": "call_missing", "name": "MissingTool", "args": {} }
[wire] context.append_loop_event { "event": { "type": "tool.result", "parentUuid": "call_missing", "toolCallId": "call_missing", "result": { "output": "Tool \\"MissingTool\\" not found", "isError": true } }, "time": "<time>" }
[emit] tool.result { "turnId": 0, "toolCallId": "call_missing", "output": "Tool \\"MissingTool\\" not found", "isError": true }
[wire] context.append_loop_event { "event": { "type": "tool.result", "parentUuid": "call_missing", "toolCallId": "call_missing", "result": { "output": "Tool \\"MissingTool\\" 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.", "isError": true } }, "time": "<time>" }
[emit] tool.result { "turnId": 0, "toolCallId": "call_missing", "output": "Tool \\"MissingTool\\" 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.", "isError": true }
[wire] context.append_loop_event { "event": { "type": "step.end", "uuid": "<uuid-1>", "turnId": "0", "step": 1, "usage": { "inputOther": 9, "output": 11, "inputCacheRead": 0, "inputCacheCreation": 0 }, "finishReason": "tool_use" }, "time": "<time>" }
[emit] turn.step.completed { "turnId": 0, "step": 1, "stepId": "<uuid-1>", "usage": { "inputOther": 9, "output": 11, "inputCacheRead": 0, "inputCacheCreation": 0 }, "finishReason": "tool_use" }
[wire] usage.record { "model": "mock-model", "usage": { "inputOther": 9, "output": 11, "inputCacheRead": 0, "inputCacheCreation": 0 }, "usageScope": "turn", "time": "<time>" }
Expand Down
Loading