Skip to content

Commit 31675d6

Browse files
authored
fix(agents): preserve anthropic thinking block order (openclaw#52961)
1 parent 6872e07 commit 31675d6

File tree

3 files changed

+70
-11
lines changed

3 files changed

+70
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Docs: https://docs.openclaw.ai
114114

115115
### Fixes
116116

117+
- Agents/Anthropic: preserve latest assistant thinking and redacted-thinking block ordering during transcript image sanitization so follow-up turns do not trip Anthropic's unmodified-thinking validation. Thanks @vincentkoc.
117118
- Models/OpenAI Codex OAuth and Plugins/MiniMax OAuth: ensure env-configured HTTP/HTTPS proxy dispatchers are initialized before OAuth preflight and token exchange requests so proxy-required environments can complete MiniMax and OpenAI Codex sign-in flows again. (#52228; fixes #51619, #51569) Thanks @openperf.
118119
- Plugins/DeepSeek: refactor the bundled DeepSeek provider onto the shared single-provider plugin entry, move its coverage into the extension test lane, and keep bundled auth env-var metadata on the generated manifest path. (#48762) Thanks @07akioni.
119120
- Web tools/search provider lists: keep onboarding, configure, and docs provider lists alphabetical while preserving the separate runtime auto-detect precedence used for credential-based provider selection.

src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ describe("sanitizeSessionMessagesImages", () => {
118118
const toolCall = assistant.content?.find((b) => b.type === "toolCall");
119119
expect(toolCall).toBeTruthy();
120120
expect("input" in (toolCall ?? {})).toBe(false);
121-
expect("arguments" in (toolCall ?? {})).toBe(false);
122121
});
123122

124123
it("removes empty assistant text blocks but preserves tool calls", async () => {
@@ -312,6 +311,53 @@ describe("sanitizeSessionMessagesImages", () => {
312311
expect("thought_signature" in ((content?.[0] ?? {}) as object)).toBe(false);
313312
expect((content?.[1] as { thought_signature?: unknown })?.thought_signature).toBe("AQID");
314313
});
314+
315+
it("preserves interleaved thinking block order when signatures are preserved", async () => {
316+
const input = castAgentMessages([
317+
{
318+
role: "assistant",
319+
content: [
320+
{
321+
type: "thinking",
322+
thinking: "first",
323+
thought_signature: "sig-1",
324+
},
325+
{ type: "text", text: "" },
326+
{ type: "text", text: "visible" },
327+
{
328+
type: "redacted_thinking",
329+
data: "opaque",
330+
thought_signature: "sig-2",
331+
},
332+
{ type: "text", text: "tail" },
333+
],
334+
},
335+
]);
336+
337+
const out = await sanitizeSessionMessagesImages(input, "test", {
338+
preserveSignatures: true,
339+
});
340+
341+
expect(out).toHaveLength(1);
342+
const content = (out[0] as { content?: Array<{ type?: string; text?: string }> }).content;
343+
expect(content?.map((block) => block.type)).toEqual([
344+
"thinking",
345+
"text",
346+
"text",
347+
"redacted_thinking",
348+
"text",
349+
]);
350+
expect(content?.[0]).toMatchObject({
351+
type: "thinking",
352+
thinking: "first",
353+
thought_signature: "sig-1",
354+
});
355+
expect(content?.[1]).toMatchObject({ type: "text", text: "" });
356+
expect(content?.[3]).toMatchObject({
357+
type: "redacted_thinking",
358+
thought_signature: "sig-2",
359+
});
360+
});
315361
});
316362
});
317363

src/agents/pi-embedded-helpers/images.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import { stripThoughtSignatures } from "./bootstrap.js";
77

88
type ContentBlock = AgentToolResult<unknown>["content"][number];
99

10+
function isThinkingOrRedactedBlock(block: unknown): boolean {
11+
if (!block || typeof block !== "object") {
12+
return false;
13+
}
14+
const rec = block as { type?: unknown };
15+
return rec.type === "thinking" || rec.type === "redacted_thinking";
16+
}
17+
1018
export function isEmptyAssistantMessageContent(
1119
message: Extract<AgentMessage, { role: "assistant" }>,
1220
): boolean {
@@ -125,16 +133,20 @@ export async function sanitizeSessionMessagesImages(
125133
? content // Keep signatures for Antigravity Claude
126134
: stripThoughtSignatures(content, options?.sanitizeThoughtSignatures); // Strip for Gemini
127135

128-
const filteredContent = strippedContent.filter((block) => {
129-
if (!block || typeof block !== "object") {
130-
return true;
131-
}
132-
const rec = block as { type?: unknown; text?: unknown };
133-
if (rec.type !== "text" || typeof rec.text !== "string") {
134-
return true;
135-
}
136-
return rec.text.trim().length > 0;
137-
});
136+
const filteredContent =
137+
options?.preserveSignatures &&
138+
strippedContent.some((block) => isThinkingOrRedactedBlock(block))
139+
? strippedContent
140+
: strippedContent.filter((block) => {
141+
if (!block || typeof block !== "object") {
142+
return true;
143+
}
144+
const rec = block as { type?: unknown; text?: unknown };
145+
if (rec.type !== "text" || typeof rec.text !== "string") {
146+
return true;
147+
}
148+
return rec.text.trim().length > 0;
149+
});
138150
const finalContent = (await sanitizeContentBlocksImages(
139151
filteredContent as unknown as ContentBlock[],
140152
label,

0 commit comments

Comments
 (0)