From c9939063f5143614e3638960898cd2dfa7c19d27 Mon Sep 17 00:00:00 2001 From: Kira Date: Fri, 30 Jan 2026 03:05:56 -0500 Subject: [PATCH] fix: skip tool calls from aborted assistant messages in transcript repair When a streaming response is aborted mid-tool-call, the tool call may be incomplete (indicated by partialJson in the message). The transcript repair logic was still extracting tool calls from these aborted messages and inserting synthetic tool_result blocks for them. When sent to Anthropic API, the aborted tool_use block may be invalid/incomplete, causing the API to reject the request with: 'unexpected tool_use_id found in tool_result blocks' This permanently corrupts the session until manually repaired. Fix: Check for stopReason === 'aborted' in extractToolCallsFromAssistant and return early with an empty array, skipping synthetic result insertion. Fixes #4475 --- src/agents/session-transcript-repair.test.ts | 34 ++++++++++++++++++++ src/agents/session-transcript-repair.ts | 5 +++ 2 files changed, 39 insertions(+) diff --git a/src/agents/session-transcript-repair.test.ts b/src/agents/session-transcript-repair.test.ts index ccc63ec7f..b4effd736 100644 --- a/src/agents/session-transcript-repair.test.ts +++ b/src/agents/session-transcript-repair.test.ts @@ -109,4 +109,38 @@ describe("sanitizeToolUseResultPairing", () => { expect(out.some((m) => m.role === "toolResult")).toBe(false); expect(out.map((m) => m.role)).toEqual(["user", "assistant"]); }); + + it("skips tool calls from aborted assistant messages", () => { + // When a streaming response is aborted mid-tool-call, the tool call may be incomplete. + // Inserting synthetic results for aborted tool calls causes API validation failures + // because the original tool_use block may be malformed. + const input = [ + { + role: "assistant", + content: [{ type: "toolCall", id: "call_aborted", name: "process", arguments: {} }], + stopReason: "aborted", + }, + { role: "user", content: "next message" }, + ] as AgentMessage[]; + + const out = sanitizeToolUseResultPairing(input); + // Should NOT insert a synthetic tool result for the aborted tool call + expect(out.some((m) => m.role === "toolResult")).toBe(false); + expect(out.map((m) => m.role)).toEqual(["assistant", "user"]); + }); + + it("processes tool calls normally when stopReason is not aborted", () => { + const input = [ + { + role: "assistant", + content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }], + stopReason: "toolUse", + }, + { role: "user", content: "next" }, + ] as AgentMessage[]; + + const out = sanitizeToolUseResultPairing(input); + // Should insert synthetic result for the missing tool result + expect(out.filter((m) => m.role === "toolResult")).toHaveLength(1); + }); }); diff --git a/src/agents/session-transcript-repair.ts b/src/agents/session-transcript-repair.ts index 2728bb89c..406824eaf 100644 --- a/src/agents/session-transcript-repair.ts +++ b/src/agents/session-transcript-repair.ts @@ -8,6 +8,11 @@ type ToolCallLike = { function extractToolCallsFromAssistant( msg: Extract, ): ToolCallLike[] { + // Skip extracting tool calls from aborted messages - they may be incomplete + // and inserting synthetic results for them causes API validation failures + const stopReason = (msg as { stopReason?: unknown }).stopReason; + if (stopReason === "aborted") return []; + const content = msg.content; if (!Array.isArray(content)) return [];