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;