diff --git a/src/agents/pi-embedded-helpers.validate-turns.test.ts b/src/agents/pi-embedded-helpers.validate-turns.test.ts index a073d5b1f3d..7730844dc0a 100644 --- a/src/agents/pi-embedded-helpers.validate-turns.test.ts +++ b/src/agents/pi-embedded-helpers.validate-turns.test.ts @@ -525,6 +525,52 @@ describe("validateAnthropicTurns strips dangling tool_use blocks", () => { ]); }); + it("preserves assistant turns that include signed thinking blocks", () => { + 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: "user", content: [{ type: "text", text: "Continue" }] }, + ]); + + 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: {} }, + ]); + }); + + it("preserves assistant turns that include redacted thinking blocks", () => { + const msgs = asMessages([ + { role: "user", content: [{ type: "text", text: "Use tool" }] }, + { + role: "assistant", + content: [ + { type: "redacted_thinking", data: "blob", thinkingSignature: "sig_1" }, + { type: "toolUse", id: "tool-1", name: "gateway", arguments: {} }, + ], + }, + { role: "user", content: [{ type: "text", text: "Continue" }] }, + ]); + + const result = validateAnthropicTurns(msgs); + + 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: {} }, + ]); + }); + it("is replay-safe across repeated validation passes", () => { const msgs = makeDualToolAnthropicTurns([ { diff --git a/src/agents/pi-embedded-helpers/turns.ts b/src/agents/pi-embedded-helpers/turns.ts index e5687faa83d..43bcf77e79e 100644 --- a/src/agents/pi-embedded-helpers/turns.ts +++ b/src/agents/pi-embedded-helpers/turns.ts @@ -15,6 +15,14 @@ function isToolCallBlock(block: AnthropicContentBlock): boolean { return block.type === "toolUse" || block.type === "toolCall" || block.type === "functionCall"; } +function isThinkingLikeBlock(block: unknown): boolean { + if (!block || typeof block !== "object") { + return false; + } + const type = (block as { type?: unknown }).type; + return type === "thinking" || type === "redacted_thinking"; +} + function isAbortedAssistantTurn(message: AgentMessage): boolean { const stopReason = (message as { stopReason?: unknown }).stopReason; return stopReason === "aborted" || stopReason === "error"; @@ -117,6 +125,10 @@ 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