diff --git a/src/agent/director.test.ts b/src/agent/director.test.ts index 042a9efc3..6501f98f7 100644 --- a/src/agent/director.test.ts +++ b/src/agent/director.test.ts @@ -1901,6 +1901,9 @@ describe("chatDirector LSP auto-activation", () => { test.each([ { tool: "read_file", path: "src/foo.ts", expected: [lspEmit] }, { tool: "edit_file", path: "lib/bar.rs", expected: [lspEmit] }, + // Wire names the model actually emits; the director must resolve them. + { tool: "read", path: "src/foo.ts", expected: [lspEmit] }, + { tool: "edit", path: "lib/bar.rs", expected: [lspEmit] }, // Non-code file. { tool: "read_file", path: "README.md", expected: [] }, // Failed result never activates. diff --git a/src/agent/director.ts b/src/agent/director.ts index 429f05c52..9be7c0604 100644 --- a/src/agent/director.ts +++ b/src/agent/director.ts @@ -1,3 +1,4 @@ +import { canonicalToolName } from "./canonical-tool-name.js"; import { DefaultDirector, type ExtendedInferenceOptions, @@ -446,7 +447,7 @@ function applyManageTasksToolCall( tasks: Task[], block: { name: string; arguments: unknown }, ): Task[] | null { - if (block.name !== "manage_tasks") return null; + if (canonicalToolName(block.name) !== "manage_tasks") return null; const taskArgs = parseManageTasksArgs(block.arguments); return taskArgs !== null ? applyManageTasks(tasks, taskArgs) : null; } @@ -1173,7 +1174,8 @@ class ChatDirectorImpl extends DefaultDirector { } for (const block of event.turn.content) { if (block.type !== "tool_call") continue; - if (block.name === "manage_tasks") { + const engineName = canonicalToolName(block.name); + if (engineName === "manage_tasks") { const next = applyManageTasksToolCall(this.tasks, block); if (next !== null) { this.tasks = next; @@ -1183,7 +1185,7 @@ class ChatDirectorImpl extends DefaultDirector { }), ); } - } else if (block.name === "read_file" || block.name === "edit_file") { + } else if (engineName === "read_file" || engineName === "edit_file") { const pathResult = PathArgSchema(block.arguments); const path = pathResult instanceof type.errors ? "" : pathResult.path; if (isCodeFile(path)) this.lspTriggerCalls.add(block.id); @@ -1194,7 +1196,7 @@ class ChatDirectorImpl extends DefaultDirector { args: block.arguments, }); } - if (block.name === "ask_operator") { + if (engineName === "ask_operator") { this.askOperatorCalls.add(block.id); } } diff --git a/src/subagent/poll-exempt.ts b/src/subagent/poll-exempt.ts index 63e7a1b1d..35a733422 100644 --- a/src/subagent/poll-exempt.ts +++ b/src/subagent/poll-exempt.ts @@ -1,3 +1,4 @@ +import { canonicalToolName } from "../agent/canonical-tool-name.js"; import { type } from "arktype"; import type { ToolCall, ToolResult } from "@intx/types/runtime"; import { isLiveWaitStatus, type WaitJSONStatus } from "./lifecycle.js"; @@ -40,7 +41,7 @@ export function isPollOnlyPendingBatch( return calls.every((call, index) => { const result = results[index]; if (result === undefined) return false; - if (call.name !== "wait_agents") return false; + if (canonicalToolName(call.name) !== "wait_agents") return false; const payload = resultPayload(result); if (payload === undefined) return false; return isWaitAgentsPending(payload); diff --git a/src/tui/mcp-view.ts b/src/tui/mcp-view.ts index 36a5069fe..fa24c3756 100644 --- a/src/tui/mcp-view.ts +++ b/src/tui/mcp-view.ts @@ -15,6 +15,7 @@ * for the pagination cursor. */ +import { canonicalToolName } from "../agent/canonical-tool-name.js"; import { fg as fgChunk, bold as boldChunk, @@ -501,7 +502,7 @@ const USE_SKILL_TOOL = "use_skill"; * future wording) simply does not collapse. */ function loadedSkillName(name: string, content: string): string | undefined { - if (name !== USE_SKILL_TOOL) return undefined; + if (canonicalToolName(name) !== USE_SKILL_TOOL) return undefined; return /^Skill "([^"]+)"/.exec(content)?.[1]; } diff --git a/src/tui/runtime-bridge.ts b/src/tui/runtime-bridge.ts index 409e8f6ab..4119d2aa8 100644 --- a/src/tui/runtime-bridge.ts +++ b/src/tui/runtime-bridge.ts @@ -6,6 +6,7 @@ * later waves can bind real agent APIs). Not the production CLI entry. */ +import { canonicalToolName } from "../agent/canonical-tool-name.js"; import { cancelItem, createSessionQueue, @@ -1005,8 +1006,9 @@ export function flushStreamRowUpdates(shell: AppShell): void { function applyToolCall( shell: AppShell, bag: BridgeBag, - event: Extract, + raw: Extract, ): void { + const event = { ...raw, name: canonicalToolName(raw.name) }; if (event.name === MANAGE_TASKS_TOOL_NAME) { // Remembered so the matching result is dropped too — suppressing only the // call would leave its result to land as an unpaired row. Checklist lives @@ -1057,8 +1059,9 @@ function applyToolCall( function applyToolResult( shell: AppShell, bag: BridgeBag, - event: Extract, + raw: Extract, ): void { + const event = { ...raw, name: canonicalToolName(raw.name) }; if (event.callId !== undefined && bag.panelOnlyCallIds.delete(event.callId)) return; const result = toolResultRow({ diff --git a/src/tui/stall-watchdog.ts b/src/tui/stall-watchdog.ts index 1323f9e4a..bf1dae45a 100644 --- a/src/tui/stall-watchdog.ts +++ b/src/tui/stall-watchdog.ts @@ -1,3 +1,4 @@ +import { canonicalToolName } from "../agent/canonical-tool-name.js"; import type { TurnStatus } from "./chrome-state.js"; // How long the run can be continuously awaiting a response with no new content @@ -74,7 +75,9 @@ function silentPastThreshold( * mapping-owning sibling resolves first and clears the shared slot. */ function isStallBoundedToolName(name: string | null | undefined): boolean { - return name === "wait_agents" || name === "ask_director"; + if (name === null || name === undefined) return false; + const engine = canonicalToolName(name); + return engine === "wait_agents" || engine === "ask_director"; } function isStallBoundedInFlightTool(args: ShouldAbortForStallArgs): boolean { diff --git a/src/tui/tool-execution-watchdog.ts b/src/tui/tool-execution-watchdog.ts index 46e4bbe60..0bd16ecc4 100644 --- a/src/tui/tool-execution-watchdog.ts +++ b/src/tui/tool-execution-watchdog.ts @@ -1,3 +1,4 @@ +import { canonicalToolName } from "../agent/canonical-tool-name.js"; import { AsyncLocalStorage } from "node:async_hooks"; import { formatMcpToolTimeoutMessage, @@ -112,10 +113,12 @@ export function resolveToolExecutionTimeoutMs( config?: ToolWatchdogConfig, call?: ToolCall, ): number | undefined { + const engineName = + call !== undefined ? canonicalToolName(call.name) : undefined; if ( - call?.name === "spawn_agent" || - call?.name === "wait_agents" || - call?.name === "ask_director" + engineName === "spawn_agent" || + engineName === "wait_agents" || + engineName === "ask_director" ) { return undefined; } diff --git a/src/tui/turns-to-blocks.ts b/src/tui/turns-to-blocks.ts index 0583c1c0b..f2505e191 100644 --- a/src/tui/turns-to-blocks.ts +++ b/src/tui/turns-to-blocks.ts @@ -1,3 +1,4 @@ +import { canonicalToolName } from "../agent/canonical-tool-name.js"; import type { ContentBlock as RuntimeContentBlock, ConversationTurn, @@ -166,7 +167,11 @@ function finalizeResumeToolBlocks( // so an errored or missing result must not leave the raw rows behind. for (let i = 0; i < blocks.length; i += 1) { const call = blocks[i]; - if (call?.type !== "tool_call" || call.name !== "manage_tasks") continue; + if ( + call?.type !== "tool_call" || + canonicalToolName(call.name) !== "manage_tasks" + ) + continue; indicesToRemove.add(i); const resultIndex = call.callId !== undefined