From e2d4803f8b0219999366cf77eb13aa91a84828b9 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Tue, 10 Feb 2026 19:33:44 +0800 Subject: [PATCH 1/3] fix(agent): sanitize invalid tool call ids in context --- packages/core/src/agent/runner.ts | 8 ++++ .../session/session-transcript-repair.test.ts | 38 +++++++++++++++++++ .../session/session-transcript-repair.ts | 6 ++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index 8738163b..ddfd3ac1 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -38,6 +38,10 @@ import { type SystemPromptMode, } from "./system-prompt/index.js"; import type { AuthProfileFailureReason } from "./auth-profiles/index.js"; +import { + sanitizeToolCallInputs, + sanitizeToolUseResultPairing, +} from "./session/session-transcript-repair.js"; // ============================================================ // Error classification for auth profile rotation @@ -181,6 +185,10 @@ export class Agent { } return this.currentApiKey; }, + transformContext: async (messages) => { + const sanitizedInputs = sanitizeToolCallInputs(messages); + return sanitizeToolUseResultPairing(sanitizedInputs); + }, }); // Load Agent Profile (if profileId is specified) diff --git a/packages/core/src/agent/session/session-transcript-repair.test.ts b/packages/core/src/agent/session/session-transcript-repair.test.ts index d162dbd4..39282e24 100644 --- a/packages/core/src/agent/session/session-transcript-repair.test.ts +++ b/packages/core/src/agent/session/session-transcript-repair.test.ts @@ -112,6 +112,28 @@ describe("sanitizeToolUseResultPairing", () => { expect(out.some((m) => m.role === "toolResult")).toBe(false); expect(out.map((m) => m.role)).toEqual(["user", "assistant"]); }); + + it("drops tool results with empty tool call id", () => { + const input = [ + { + role: "assistant", + content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }], + }, + { + role: "toolResult", + toolCallId: "", + toolName: "read", + content: [{ type: "text", text: "invalid id" }], + isError: true, + }, + { role: "user", content: "next" }, + ] as AgentMessage[]; + + const out = sanitizeToolUseResultPairing(input); + const toolResults = out.filter((m) => m.role === "toolResult") as Array<{ toolCallId?: string }>; + expect(toolResults).toHaveLength(1); + expect(toolResults[0]?.toolCallId).toBe("call_1"); + }); }); describe("sanitizeToolCallInputs", () => { @@ -147,4 +169,20 @@ describe("sanitizeToolCallInputs", () => { : []; expect(types).toEqual(["text", "toolUse"]); }); + + it("drops tool calls with empty id even when input exists", () => { + const input = [ + { + role: "assistant", + content: [ + { type: "toolCall", id: "", name: "read", arguments: { path: "a" } }, + { type: "toolUse", id: " ", name: "exec", input: { cmd: "pwd" } }, + ], + }, + { role: "user", content: "hello" }, + ] as AgentMessage[]; + + const out = sanitizeToolCallInputs(input); + expect(out.map((m) => m.role)).toEqual(["user"]); + }); }); diff --git a/packages/core/src/agent/session/session-transcript-repair.ts b/packages/core/src/agent/session/session-transcript-repair.ts index 956653b2..d8417e60 100644 --- a/packages/core/src/agent/session/session-transcript-repair.ts +++ b/packages/core/src/agent/session/session-transcript-repair.ts @@ -58,6 +58,10 @@ function hasToolCallInput(block: ToolCallBlock): boolean { return hasInput || hasArguments; } +function hasValidToolCallId(block: ToolCallBlock): boolean { + return typeof block.id === "string" && block.id.trim().length > 0; +} + function extractToolResultId(msg: Extract): string | null { const toolCallId = (msg as { toolCallId?: unknown }).toolCallId; if (typeof toolCallId === "string" && toolCallId) { @@ -118,7 +122,7 @@ export function repairToolCallInputs(messages: AgentMessage[]): ToolCallInputRep let droppedInMessage = 0; for (const block of msg.content) { - if (isToolCallBlock(block) && !hasToolCallInput(block)) { + if (isToolCallBlock(block) && (!hasToolCallInput(block) || !hasValidToolCallId(block))) { droppedToolCalls += 1; droppedInMessage += 1; changed = true; From db25f8f44aa1ee79623496dc6674b723318873f5 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Tue, 10 Feb 2026 19:43:49 +0800 Subject: [PATCH 2/3] chore(agent): add debug hook to inject invalid tool call id --- packages/core/src/agent/runner.ts | 47 ++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index ddfd3ac1..b27d8fc3 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -39,8 +39,8 @@ import { } from "./system-prompt/index.js"; import type { AuthProfileFailureReason } from "./auth-profiles/index.js"; import { - sanitizeToolCallInputs, - sanitizeToolUseResultPairing, + repairToolCallInputs, + repairToolUseResultPairing, } from "./session/session-transcript-repair.js"; // ============================================================ @@ -75,6 +75,10 @@ export function isRotatableError(reason: AuthProfileFailureReason): boolean { return reason === "auth" || reason === "rate_limit" || reason === "billing" || reason === "timeout"; } +function shouldInjectInvalidToolCallIdForDebug(): boolean { + return process.env.MULTICA_DEBUG_INJECT_INVALID_TOOL_CALL_ID === "1"; +} + export class Agent { private readonly agent: PiAgentCore; private output; @@ -186,8 +190,43 @@ export class Agent { return this.currentApiKey; }, transformContext: async (messages) => { - const sanitizedInputs = sanitizeToolCallInputs(messages); - return sanitizeToolUseResultPairing(sanitizedInputs); + let workingMessages = messages; + + // Debug-only fault injection: + // Simulate a poisoned transcript with an empty toolCallId so we can + // verify sanitize logic prevents provider-side tool_call_id failures. + if (shouldInjectInvalidToolCallIdForDebug()) { + workingMessages = [ + ...workingMessages, + { + role: "toolResult", + toolCallId: "", + toolName: "debug-invalid-tool-call-id", + content: [{ type: "text", text: "[debug] injected invalid toolCallId" }], + isError: true, + timestamp: Date.now(), + } as AgentMessage, + ]; + } + + const inputRepair = repairToolCallInputs(workingMessages); + const pairingRepair = repairToolUseResultPairing(inputRepair.messages); + + if ( + shouldInjectInvalidToolCallIdForDebug() + || inputRepair.droppedToolCalls > 0 + || pairingRepair.droppedOrphanCount > 0 + || pairingRepair.droppedDuplicateCount > 0 + ) { + console.error( + `[Agent] context sanitize: droppedToolCalls=${inputRepair.droppedToolCalls}, ` + + `droppedOrphanToolResults=${pairingRepair.droppedOrphanCount}, ` + + `droppedDuplicateToolResults=${pairingRepair.droppedDuplicateCount}, ` + + `insertedMissingToolResults=${pairingRepair.added.length}`, + ); + } + + return pairingRepair.messages; }, }); From d78f8480bf389dcc2584723bce7ca07ca79742dc Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Tue, 10 Feb 2026 19:53:33 +0800 Subject: [PATCH 3/3] chore(agent): remove debug invalid tool-call injection --- packages/core/src/agent/runner.ts | 47 +++---------------------------- 1 file changed, 4 insertions(+), 43 deletions(-) diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index b27d8fc3..ddfd3ac1 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -39,8 +39,8 @@ import { } from "./system-prompt/index.js"; import type { AuthProfileFailureReason } from "./auth-profiles/index.js"; import { - repairToolCallInputs, - repairToolUseResultPairing, + sanitizeToolCallInputs, + sanitizeToolUseResultPairing, } from "./session/session-transcript-repair.js"; // ============================================================ @@ -75,10 +75,6 @@ export function isRotatableError(reason: AuthProfileFailureReason): boolean { return reason === "auth" || reason === "rate_limit" || reason === "billing" || reason === "timeout"; } -function shouldInjectInvalidToolCallIdForDebug(): boolean { - return process.env.MULTICA_DEBUG_INJECT_INVALID_TOOL_CALL_ID === "1"; -} - export class Agent { private readonly agent: PiAgentCore; private output; @@ -190,43 +186,8 @@ export class Agent { return this.currentApiKey; }, transformContext: async (messages) => { - let workingMessages = messages; - - // Debug-only fault injection: - // Simulate a poisoned transcript with an empty toolCallId so we can - // verify sanitize logic prevents provider-side tool_call_id failures. - if (shouldInjectInvalidToolCallIdForDebug()) { - workingMessages = [ - ...workingMessages, - { - role: "toolResult", - toolCallId: "", - toolName: "debug-invalid-tool-call-id", - content: [{ type: "text", text: "[debug] injected invalid toolCallId" }], - isError: true, - timestamp: Date.now(), - } as AgentMessage, - ]; - } - - const inputRepair = repairToolCallInputs(workingMessages); - const pairingRepair = repairToolUseResultPairing(inputRepair.messages); - - if ( - shouldInjectInvalidToolCallIdForDebug() - || inputRepair.droppedToolCalls > 0 - || pairingRepair.droppedOrphanCount > 0 - || pairingRepair.droppedDuplicateCount > 0 - ) { - console.error( - `[Agent] context sanitize: droppedToolCalls=${inputRepair.droppedToolCalls}, ` + - `droppedOrphanToolResults=${pairingRepair.droppedOrphanCount}, ` + - `droppedDuplicateToolResults=${pairingRepair.droppedDuplicateCount}, ` + - `insertedMissingToolResults=${pairingRepair.added.length}`, - ); - } - - return pairingRepair.messages; + const sanitizedInputs = sanitizeToolCallInputs(messages); + return sanitizeToolUseResultPairing(sanitizedInputs); }, });