diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 1a0f9c51a..93416b98d 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -2296,11 +2296,15 @@ export class AgentSession { * * The signature covers: * 1. Active tool names in order (the prompt renders them in this order). - * 2. Active tool labels and descriptions (both are echoed in the prompt body; - * see `system-prompt.md` `{{label}}: \`{{name}}\`` rendering). - * 3. When MCP discovery is on, every registry tool's name+label+description, - * since `rebuildSystemPrompt` summarizes discoverable MCP tools that are - * not in the active set. + * 2. Active tool labels, descriptions, and wire-visible names — all are + * rendered into the prompt body (see `system-prompt.md` `{{label}}: \`{{name}}\`` + * and `toolPromptNames` in `buildSystemPrompt`). The wire name comes from + * `tool.customWireName` and overrides the internal name on the model wire + * (e.g. `edit` exposes itself as `apply_patch` to GPT-5 in apply_patch mode); + * a stale wire name would desync prompt guidance from actual tool routing. + * 3. When MCP discovery is on, every registry tool's name+label+description+ + * customWireName, since `rebuildSystemPrompt` summarizes discoverable MCP + * tools that are not in the active set. * 4. MCP server instructions text (per server), since `rebuildSystemPrompt` * embeds these in the appended prompt under "## MCP Server Instructions". * A server upgrade can change instructions while keeping tools identical. @@ -2314,7 +2318,8 @@ export class AgentSession { // Order-preserving join: any reorder must produce a different signature so // the rebuild fires and the new tool list reaches the API. const nameSegment = toolNames.join("\u0001"); - const describeTool = (tool: AgentTool): string => `${tool.name}=${tool.label ?? ""}|${tool.description ?? ""}`; + const describeTool = (tool: AgentTool): string => + `${tool.name}=${tool.label ?? ""}|${tool.description ?? ""}|${tool.customWireName ?? ""}`; const descriptionSegment = tools.map(describeTool).join("\u0002"); let registrySegment = ""; if (this.#mcpDiscoveryEnabled) { diff --git a/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts b/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts index c19e149f5..afa4f5aaf 100644 --- a/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts +++ b/packages/coding-agent/test/agent-session-tool-rebuild-skip.test.ts @@ -300,4 +300,38 @@ describe("AgentSession refreshMCPTools rebuild skipping", () => { await session.refreshMCPTools([active, discoverableV2]); expect(rebuildCount).toBe(baseline + 1); }); + it("rebuilds when an MCP tool's customWireName changes", async () => { + // `customWireName` overrides the model-facing tool name (e.g. `edit` exposes + // itself as `apply_patch` to GPT-5). The wire name is rendered into the prompt + // body via `toolPromptNames`, so a wire-name flip with the rest of the metadata + // constant would otherwise leave a stale system prompt that advertises the wrong + // callable name to the model. The signature must catch this. + let rebuildCount = 0; + const { session } = newSession(async toolNames => { + rebuildCount++; + return `tools:${toolNames.join(",")}`; + }); + + // Attach a custom wire name to the MCP tool. `applyToolProxy` forwards arbitrary + // properties from the underlying CustomTool to the wrapper, so the AgentTool the + // signature inspects exposes `customWireName` as if it were declared on the type. + const v1 = createMcpCustomTool("mcp__nucleus_search", "nucleus", "search", "Search"); + const v1WithWire = { ...v1, customWireName: "wire_v1" } as typeof v1 & { customWireName: string }; + await session.refreshMCPTools([v1WithWire]); + expect(rebuildCount).toBe(1); + + // Same wire name: skip. + await session.refreshMCPTools([v1WithWire]); + expect(rebuildCount).toBe(1); + + // Wire name changes while name/label/description stay constant: must rebuild. + const v2WithWire = { ...v1, customWireName: "wire_v2" } as typeof v1 & { customWireName: string }; + await session.refreshMCPTools([v2WithWire]); + expect(rebuildCount).toBe(2); + + // Drop wire name entirely: must rebuild (signature must differ from `wire_v2`). + await session.refreshMCPTools([v1]); + expect(rebuildCount).toBe(3); + }); + });