From 67c7c9b117af7464fd5d583f719bf6214a5508a8 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 14 Sep 2026 04:11:52 -0700 Subject: [PATCH] feat: Add tool names to tool calls (#1128) Supports the new tool call name RFD --- docs/permission-extension.md | 4 ++- src/acp-agent.ts | 10 +++++-- src/native-subagents.ts | 3 ++ src/tests/native-subagents.test.ts | 1 + src/tests/tools.test.ts | 44 ++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/docs/permission-extension.md b/docs/permission-extension.md index dd39fc56..5cbdce53 100644 --- a/docs/permission-extension.md +++ b/docs/permission-extension.md @@ -71,7 +71,9 @@ with `Reason: `. The SDK `description` operation subtitle is not copied there. The permission request carries the same standard ACP tool information used for normal tool updates: -- `name`, `kind`, `title`, `content`, and `locations` come from `toolInfoFromToolUse`; +- `name` is the SDK tool name (for example `Read` or `mcp__server__tool`), matching the standard + `name` field on the initial `tool_call` update; +- `kind`, `title`, `content`, and `locations` come from `toolInfoFromToolUse`; - `status` is `pending`; - `rawInput` is the original SDK input object; - `blockedPath` is appended to `locations` when it is valid and not already present; diff --git a/src/acp-agent.ts b/src/acp-agent.ts index f8700c58..b8003d8b 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -1252,7 +1252,9 @@ type ProviderConfig = { export type ToolUpdateMeta = { contextCompaction?: ContextCompactionMetadata; claudeCode?: { - /* The name of the tool that was used in Claude Code. */ + /* The name of the tool that was used in Claude Code. Also carried as the + standard ACP `name` field on the initial `tool_call`; kept here so every + `tool_call_update` stays self-describing for clients that key off it. */ toolName: string; /* A human-readable title supplied by Claude Code for the tool call. */ title?: string; @@ -9358,7 +9360,10 @@ function resolveSkillPath(skillName: string, cwd?: string): string | undefined { * refine) and the permission flow (`ensureToolCallEmitted`), so they can't * drift. The initial `tool_call` carries `status: "pending"` and, for Bash, the * `terminal_info` _meta that the later `terminal_output`/`terminal_exit` - * updates key off of; a refining `tool_call_update` carries neither. */ + * updates key off of, and the programmatic tool `name` (ACP's tool-call-name + * RFD); a refining `tool_call_update` carries none of these. `name` is set + * once at first report — on a v1 update, omitting it means "unchanged", and + * the tool behind a `toolCallId` never changes. */ function toolCallNotification( toolUse: { id: string; name: string; input: unknown }, rawInput: unknown, @@ -9384,6 +9389,7 @@ function toolCallNotification( } satisfies ToolUpdateMeta, toolCallId: toolUse.id, sessionUpdate: "tool_call", + name: toolUse.name, rawInput, status: "pending", ...toolInfoFromToolUse(toolUse, supportsTerminalOutput, cwd), diff --git a/src/native-subagents.ts b/src/native-subagents.ts index a13f39dc..bef41528 100644 --- a/src/native-subagents.ts +++ b/src/native-subagents.ts @@ -422,6 +422,9 @@ function failedControlFallback( ...terminal.update, sessionUpdate: "tool_call", status: "failed", + // The synthesized tool_call is this call's first report, so give it + // the standard `name` the initial one would have carried. + ...(typeof claudeMeta?.toolName === "string" ? { name: claudeMeta.toolName } : {}), title: typeof terminal.update.title === "string" && terminal.update.title.length > 0 ? terminal.update.title diff --git a/src/tests/native-subagents.test.ts b/src/tests/native-subagents.test.ts index 4b99c21e..cdd07a09 100644 --- a/src/tests/native-subagents.test.ts +++ b/src/tests/native-subagents.test.ts @@ -140,6 +140,7 @@ describe("NativeSubagentRuntime lifecycle", () => { update: { sessionUpdate: "tool_call", toolCallId: "agent-tool", + name: "Agent", title: "Agent", status: "failed", _meta: { claudeCode: { toolName: "Agent" } }, diff --git a/src/tests/tools.test.ts b/src/tests/tools.test.ts index 0abf83f5..9aefe242 100644 --- a/src/tests/tools.test.ts +++ b/src/tests/tools.test.ts @@ -3420,6 +3420,50 @@ describe("Skill tool rendering", () => { }); }); + // ACP tool-call-name RFD: the initial tool_call carries the programmatic + // tool name as the standard `name` field, alongside `_meta.claudeCode.toolName`. + describe("standard `name` on tool_call notifications", () => { + it.each([ + ["Read", { file_path: "/tmp/a.ts" }], + ["Bash", { command: "ls" }], + ["mcp__github__list_issues", { repo: "acp" }], + ])("reports %s as `name` on the initial tool_call", (name, input) => { + const notifications = toAcpNotifications( + [{ type: "tool_use", id: "toolu_name", name, input }] as any, + "assistant", + "test-session", + {}, + {} as AcpClient, + mockLogger, + ); + expect(notifications[0]?.update).toMatchObject({ + sessionUpdate: "tool_call", + toolCallId: "toolu_name", + name, + _meta: { claudeCode: { toolName: name } }, + }); + }); + + it("does not re-send `name` on the refining tool_call_update", () => { + const notifications = toAcpNotifications( + [ + { type: "tool_use", id: "toolu_refine", name: "Read", input: { file_path: "/tmp/a" } }, + ] as any, + "assistant", + "test-session", + {}, + {} as AcpClient, + mockLogger, + { emittedToolCalls: new Set(["toolu_refine"]) }, + ); + expect(notifications[0]?.update).toMatchObject({ + sessionUpdate: "tool_call_update", + toolCallId: "toolu_refine", + }); + expect((notifications[0]?.update as any).name).toBeUndefined(); + }); + }); + describe("_meta.claudeCode.skillPath", () => { const skillMeta = (skill: string, cwd?: string) => (