Skip to content

Commit 3bde24f

Browse files
Merge pull request #1252 from corbitsdev/cl-9539-raw-wire-name-comparisons-break-director-and-tui-tool
fix(agent): canonicalize wire tool names across director and tui tracking
2 parents 4c93965 + 7c91b0c commit 3bde24f

8 files changed

Lines changed: 34 additions & 13 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: 2 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 } from "arktype";
23
import type { ToolCall, ToolResult } from "@intx/types/runtime";
34
import { isLiveWaitStatus, type WaitJSONStatus } from "./lifecycle.js";
@@ -40,7 +41,7 @@ export function isPollOnlyPendingBatch(
4041
return calls.every((call, index) => {
4142
const result = results[index];
4243
if (result === undefined) return false;
43-
if (call.name !== "wait_agents") return false;
44+
if (canonicalToolName(call.name) !== "wait_agents") return false;
4445
const payload = resultPayload(result);
4546
if (payload === undefined) return false;
4647
return isWaitAgentsPending(payload);

‎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: 4 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 { TurnStatus } from "./chrome-state.js";
23

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

8083
function isStallBoundedInFlightTool(args: ShouldAbortForStallArgs): boolean {

‎src/tui/tool-execution-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 { AsyncLocalStorage } from "node:async_hooks";
23
import {
34
formatMcpToolTimeoutMessage,
@@ -112,10 +113,12 @@ 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
) {
120123
return undefined;
121124
}

‎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,
@@ -166,7 +167,11 @@ function finalizeResumeToolBlocks(
166167
// so an errored or missing result must not leave the raw rows behind.
167168
for (let i = 0; i < blocks.length; i += 1) {
168169
const call = blocks[i];
169-
if (call?.type !== "tool_call" || call.name !== "manage_tasks") continue;
170+
if (
171+
call?.type !== "tool_call" ||
172+
canonicalToolName(call.name) !== "manage_tasks"
173+
)
174+
continue;
170175
indicesToRemove.add(i);
171176
const resultIndex =
172177
call.callId !== undefined

0 commit comments

Comments
 (0)