Skip to content

Commit 20f031c

Browse files
committed
fix(agent): canonicalize wire tool names in director and tui tracking
Raw wire tool names from incoming tool calls were compared directly against internal engine names across director triggers, watchdogs, and TUI event handlers. Resolve them through canonicalToolName so aliased wire names (e.g. read/edit, manage_tasks) match correctly.
1 parent e78c9be commit 20f031c

8 files changed

Lines changed: 39 additions & 17 deletions

File tree

‎src/agent/director.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,6 +1901,9 @@ describe("chatDirector LSP auto-activation", () => {
19011901
test.each([
19021902
{ tool: "read_file", path: "src/foo.ts", expected: [lspEmit] },
19031903
{ tool: "edit_file", path: "lib/bar.rs", expected: [lspEmit] },
1904+
// Wire names the model actually emits; the director must resolve them.
1905+
{ tool: "read", path: "src/foo.ts", expected: [lspEmit] },
1906+
{ tool: "edit", path: "lib/bar.rs", expected: [lspEmit] },
19041907
// Non-code file.
19051908
{ tool: "read_file", path: "README.md", expected: [] },
19061909
// Failed result never activates.

‎src/agent/director.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { canonicalToolName } from "./canonical-tool-name.js";
12
import {
23
DefaultDirector,
34
type ExtendedInferenceOptions,
@@ -446,7 +447,7 @@ function applyManageTasksToolCall(
446447
tasks: Task[],
447448
block: { name: string; arguments: unknown },
448449
): Task[] | null {
449-
if (block.name !== "manage_tasks") return null;
450+
if (canonicalToolName(block.name) !== "manage_tasks") return null;
450451
const taskArgs = parseManageTasksArgs(block.arguments);
451452
return taskArgs !== null ? applyManageTasks(tasks, taskArgs) : null;
452453
}
@@ -1173,7 +1174,8 @@ class ChatDirectorImpl extends DefaultDirector {
11731174
}
11741175
for (const block of event.turn.content) {
11751176
if (block.type !== "tool_call") continue;
1176-
if (block.name === "manage_tasks") {
1177+
const engineName = canonicalToolName(block.name);
1178+
if (engineName === "manage_tasks") {
11771179
const next = applyManageTasksToolCall(this.tasks, block);
11781180
if (next !== null) {
11791181
this.tasks = next;
@@ -1183,7 +1185,7 @@ class ChatDirectorImpl extends DefaultDirector {
11831185
}),
11841186
);
11851187
}
1186-
} else if (block.name === "read_file" || block.name === "edit_file") {
1188+
} else if (engineName === "read_file" || engineName === "edit_file") {
11871189
const pathResult = PathArgSchema(block.arguments);
11881190
const path = pathResult instanceof type.errors ? "" : pathResult.path;
11891191
if (isCodeFile(path)) this.lspTriggerCalls.add(block.id);
@@ -1194,7 +1196,7 @@ class ChatDirectorImpl extends DefaultDirector {
11941196
args: block.arguments,
11951197
});
11961198
}
1197-
if (block.name === "ask_operator") {
1199+
if (engineName === "ask_operator") {
11981200
this.askOperatorCalls.add(block.id);
11991201
}
12001202
}

‎src/subagent/poll-exempt.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { canonicalToolName } from "../agent/canonical-tool-name.js";
12
import { type } from "arktype";
23
import type { ToolCall, ToolResult } from "@intx/types/runtime";
34
import { isLiveWaitStatus, type WaitJSONStatus } from "./lifecycle.js";
@@ -50,12 +51,13 @@ export function isPollOnlyPendingBatch(
5051
return calls.every((call, index) => {
5152
const result = results[index];
5253
if (result === undefined) return false;
53-
if (call.name !== "wait_agents" && call.name !== "shell_collect") {
54+
const name = canonicalToolName(call.name);
55+
if (name !== "wait_agents" && name !== "shell_collect") {
5456
return false;
5557
}
5658
const payload = resultPayload(result);
5759
if (payload === undefined) return false;
58-
return call.name === "wait_agents"
60+
return name === "wait_agents"
5961
? isWaitAgentsPending(payload)
6062
: isShellCollectPending(payload);
6163
});

‎src/tui/mcp-view.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* for the pagination cursor.
1616
*/
1717

18+
import { canonicalToolName } from "../agent/canonical-tool-name.js";
1819
import {
1920
fg as fgChunk,
2021
bold as boldChunk,
@@ -501,7 +502,7 @@ const USE_SKILL_TOOL = "use_skill";
501502
* future wording) simply does not collapse.
502503
*/
503504
function loadedSkillName(name: string, content: string): string | undefined {
504-
if (name !== USE_SKILL_TOOL) return undefined;
505+
if (canonicalToolName(name) !== USE_SKILL_TOOL) return undefined;
505506
return /^Skill "([^"]+)"/.exec(content)?.[1];
506507
}
507508

‎src/tui/runtime-bridge.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* later waves can bind real agent APIs). Not the production CLI entry.
77
*/
88

9+
import { canonicalToolName } from "../agent/canonical-tool-name.js";
910
import {
1011
cancelItem,
1112
createSessionQueue,
@@ -1005,8 +1006,9 @@ export function flushStreamRowUpdates(shell: AppShell): void {
10051006
function applyToolCall(
10061007
shell: AppShell,
10071008
bag: BridgeBag,
1008-
event: Extract<BridgeInboundEvent, { type: "tool_call" }>,
1009+
raw: Extract<BridgeInboundEvent, { type: "tool_call" }>,
10091010
): void {
1011+
const event = { ...raw, name: canonicalToolName(raw.name) };
10101012
if (event.name === MANAGE_TASKS_TOOL_NAME) {
10111013
// Remembered so the matching result is dropped too — suppressing only the
10121014
// call would leave its result to land as an unpaired row. Checklist lives
@@ -1057,8 +1059,9 @@ function applyToolCall(
10571059
function applyToolResult(
10581060
shell: AppShell,
10591061
bag: BridgeBag,
1060-
event: Extract<BridgeInboundEvent, { type: "tool_result" }>,
1062+
raw: Extract<BridgeInboundEvent, { type: "tool_result" }>,
10611063
): void {
1064+
const event = { ...raw, name: canonicalToolName(raw.name) };
10621065
if (event.callId !== undefined && bag.panelOnlyCallIds.delete(event.callId))
10631066
return;
10641067
const result = toolResultRow({

‎src/tui/stall-watchdog.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { canonicalToolName } from "../agent/canonical-tool-name.js";
12
import type { TurnStatus } from "./chrome-state.js";
23

34
// How long the run can be continuously awaiting a response with no new content
@@ -74,10 +75,12 @@ function silentPastThreshold(
7475
* mapping-owning sibling resolves first and clears the shared slot.
7576
*/
7677
function isStallBoundedToolName(name: string | null | undefined): boolean {
78+
if (name === null || name === undefined) return false;
79+
const engine = canonicalToolName(name);
7780
return (
78-
name === "shell_collect" ||
79-
name === "wait_agents" ||
80-
name === "ask_director"
81+
engine === "shell_collect" ||
82+
engine === "wait_agents" ||
83+
engine === "ask_director"
8184
);
8285
}
8386

‎src/tui/tool-execution-watchdog.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { canonicalToolName } from "../agent/canonical-tool-name.js";
12
import { AsyncLocalStorage } from "node:async_hooks";
23
import {
34
formatMcpToolTimeoutMessage,
@@ -112,13 +113,15 @@ export function resolveToolExecutionTimeoutMs(
112113
config?: ToolWatchdogConfig,
113114
call?: ToolCall,
114115
): number | undefined {
116+
const engineName =
117+
call !== undefined ? canonicalToolName(call.name) : undefined;
115118
if (
116-
call?.name === "spawn_agent" ||
117-
call?.name === "wait_agents" ||
118-
call?.name === "ask_director" ||
119+
engineName === "spawn_agent" ||
120+
engineName === "wait_agents" ||
121+
engineName === "ask_director" ||
119122
// shell_collect with wait_ms is a capped poll over a background shell that
120123
// outlives the turn; aborting the collect would not stop the process.
121-
call?.name === "shell_collect"
124+
engineName === "shell_collect"
122125
) {
123126
return undefined;
124127
}

‎src/tui/turns-to-blocks.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { canonicalToolName } from "../agent/canonical-tool-name.js";
12
import type {
23
ContentBlock as RuntimeContentBlock,
34
ConversationTurn,
@@ -165,7 +166,11 @@ function finalizeResumeToolBlocks(
165166
// so an errored or missing result must not leave the raw rows behind.
166167
for (let i = 0; i < blocks.length; i += 1) {
167168
const call = blocks[i];
168-
if (call?.type !== "tool_call" || call.name !== "manage_tasks") continue;
169+
if (
170+
call?.type !== "tool_call" ||
171+
canonicalToolName(call.name) !== "manage_tasks"
172+
)
173+
continue;
169174
indicesToRemove.add(i);
170175
const resultIndex =
171176
call.callId !== undefined

0 commit comments

Comments
 (0)