From d0614b4b4e950516ad4f1abc0516667a01f5f177 Mon Sep 17 00:00:00 2001 From: Shakker Date: Sun, 12 Apr 2026 03:43:25 +0100 Subject: [PATCH] fix: drop dangling signed-thinking tool turns during anthropic validation --- ...pi-embedded-helpers.validate-turns.test.ts | 44 +++++++++++++++---- src/agents/pi-embedded-helpers/turns.ts | 26 +++++++++-- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/agents/pi-embedded-helpers.validate-turns.test.ts b/src/agents/pi-embedded-helpers.validate-turns.test.ts index 7730844dc0a..725249778f4 100644 --- a/src/agents/pi-embedded-helpers.validate-turns.test.ts +++ b/src/agents/pi-embedded-helpers.validate-turns.test.ts @@ -525,7 +525,37 @@ describe("validateAnthropicTurns strips dangling tool_use blocks", () => { ]); }); - it("preserves assistant turns that include signed thinking blocks", () => { + it("preserves signed-thinking turns whose sibling tool calls still resolve", () => { + const msgs = asMessages([ + { role: "user", content: [{ type: "text", text: "Use tool" }] }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "internal", thinkingSignature: "sig_1" }, + { type: "toolCall", id: "tool-1", name: "gateway", arguments: {} }, + ], + }, + { + role: "toolResult", + toolCallId: "tool-1", + toolName: "gateway", + content: [{ type: "text", text: "ok" }], + isError: false, + }, + { role: "user", content: [{ type: "text", text: "Continue" }] }, + ]); + + const result = validateAnthropicTurns(msgs); + + expect(result).toHaveLength(4); + const assistantContent = (result[1] as { content?: unknown[] }).content; + expect(assistantContent).toEqual([ + { type: "thinking", thinking: "internal", thinkingSignature: "sig_1" }, + { type: "toolCall", id: "tool-1", name: "gateway", arguments: {} }, + ]); + }); + + it("drops signed-thinking turns whose sibling tool calls are dangling", () => { const msgs = asMessages([ { role: "user", content: [{ type: "text", text: "Use tool" }] }, { @@ -541,14 +571,13 @@ describe("validateAnthropicTurns strips dangling tool_use blocks", () => { const result = validateAnthropicTurns(msgs); expect(result).toHaveLength(3); - const assistantContent = (result[1] as { content?: unknown[] }).content; - expect(assistantContent).toEqual([ - { type: "thinking", thinking: "internal", thinkingSignature: "sig_1" }, - { type: "toolCall", id: "tool-1", name: "gateway", arguments: {} }, + expect((result[1] as { role?: unknown }).role).toBe("assistant"); + expect((result[1] as { content?: unknown[] }).content).toEqual([ + { type: "text", text: "[tool calls omitted]" }, ]); }); - it("preserves assistant turns that include redacted thinking blocks", () => { + it("drops redacted-thinking turns whose sibling tool calls are dangling", () => { const msgs = asMessages([ { role: "user", content: [{ type: "text", text: "Use tool" }] }, { @@ -566,8 +595,7 @@ describe("validateAnthropicTurns strips dangling tool_use blocks", () => { expect(result).toHaveLength(3); const assistantContent = (result[1] as { content?: unknown[] }).content; expect(assistantContent).toEqual([ - { type: "redacted_thinking", data: "blob", thinkingSignature: "sig_1" }, - { type: "toolUse", id: "tool-1", name: "gateway", arguments: {} }, + { type: "text", text: "[tool calls omitted]" }, ]); }); diff --git a/src/agents/pi-embedded-helpers/turns.ts b/src/agents/pi-embedded-helpers/turns.ts index 43bcf77e79e..ea40dedb5b3 100644 --- a/src/agents/pi-embedded-helpers/turns.ts +++ b/src/agents/pi-embedded-helpers/turns.ts @@ -125,10 +125,6 @@ function stripDanglingAnthropicToolUses(messages: AgentMessage[]): AgentMessage[ result.push(msg); continue; } - if (originalContent.some((block) => isThinkingLikeBlock(block))) { - result.push(msg); - continue; - } if ( extractToolCallsFromAssistant(msg as Extract).length === 0 @@ -136,8 +132,30 @@ function stripDanglingAnthropicToolUses(messages: AgentMessage[]): AgentMessage[ result.push(msg); continue; } + const hasThinking = originalContent.some((block) => isThinkingLikeBlock(block)); const validToolUseIds = collectFutureToolResultIds(messages, i); + if (hasThinking) { + const allToolCallsResolvable = originalContent.every((block) => { + if (!block || !isToolCallBlock(block)) { + return true; + } + const blockId = normalizeOptionalString(block.id); + return blockId ? validToolUseIds.has(blockId) : false; + }); + if (allToolCallsResolvable) { + result.push(msg); + } else { + result.push({ + ...assistantMsg, + content: isAbortedAssistantTurn(msg) + ? [] + : ([{ type: "text", text: "[tool calls omitted]" }] as AnthropicContentBlock[]), + } as AgentMessage); + } + continue; + } + const filteredContent = originalContent.filter((block) => { if (!block) { return false;