fix: trust embedded anthropic tool results for signed replay

This commit is contained in:
Shakker
2026-04-12 04:37:34 +01:00
committed by Shakker
parent b1a228fc3a
commit 539a95fc7a
2 changed files with 56 additions and 5 deletions

View File

@@ -555,6 +555,34 @@ describe("validateAnthropicTurns strips dangling tool_use blocks", () => {
]);
});
it("preserves signed-thinking turns when the matching tool result is embedded in user content", () => {
const msgs = asMessages([
{ role: "user", content: [{ type: "text", text: "Use tool" }] },
{
role: "assistant",
content: [
{ type: "thinking", thinking: "internal", thinkingSignature: "sig_1" },
{ type: "toolUse", id: "tool-1", name: "gateway", arguments: {} },
],
},
{
role: "user",
content: [
{ type: "toolResult", toolUseId: "tool-1", content: [{ type: "text", text: "ok" }] },
{ type: "text", text: "Continue" },
],
},
]);
const result = validateAnthropicTurns(msgs);
expect(result).toHaveLength(3);
expect((result[1] as { content?: unknown[] }).content).toEqual([
{ type: "thinking", thinking: "internal", thinkingSignature: "sig_1" },
{ type: "toolUse", 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" }] },

View File

@@ -88,11 +88,13 @@ function collectTrustedToolResultMatches(message: AgentMessage): Map<string, Set
const matches = new Map<string, Set<string>>();
const role = (message as { role?: unknown }).role;
const addMatch = (id: string | null, toolName: string | null) => {
if (!id || !toolName) {
if (!id) {
return;
}
const bucket = matches.get(id) ?? new Set<string>();
bucket.add(toolName);
if (toolName) {
bucket.add(toolName);
}
matches.set(id, bucket);
};
@@ -107,6 +109,22 @@ function collectTrustedToolResultMatches(message: AgentMessage): Map<string, Set
addMatch(extractToolResultMatchId(record), extractToolResultMatchName(record));
}
const content = (message as { content?: unknown }).content;
if (!Array.isArray(content)) {
return matches;
}
for (const block of content) {
if (!block || typeof block !== "object") {
continue;
}
const record = block as Record<string, unknown>;
if (record.type !== "toolResult" && record.type !== "tool") {
continue;
}
addMatch(extractToolResultMatchId(record), extractToolResultMatchName(record));
}
return matches;
}
@@ -198,9 +216,14 @@ function stripDanglingAnthropicToolUses(messages: AgentMessage[]): AgentMessage[
}
const blockId = normalizeOptionalString(block.id);
const blockName = normalizeOptionalString(block.name);
return blockId && blockName
? validToolResultMatches.get(blockId)?.has(blockName) === true
: false;
if (!blockId || !blockName) {
return false;
}
const matchingToolNames = validToolResultMatches.get(blockId);
if (!matchingToolNames) {
return false;
}
return matchingToolNames.size === 0 || matchingToolNames.has(blockName);
});
if (allToolCallsResolvable) {
result.push(msg);