fix: preserve signed thinking turns during anthropic replay validation

This commit is contained in:
Shakker
2026-04-12 03:36:26 +01:00
committed by Shakker
parent 2d1f4af67a
commit 0b95510ec5
2 changed files with 58 additions and 0 deletions

View File

@@ -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([
{

View File

@@ -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<AgentMessage, { role: "assistant" }>).length ===
0