mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 07:51:26 +00:00
fix: sanitize thinking blocks for GitHub Copilot Claude models (openclaw#19459) thanks @jackheuberger
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: jackheuberger <12731288+jackheuberger@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
import type { TranscriptPolicy } from "../transcript-policy.js";
|
||||
import { resolveTranscriptPolicy } from "../transcript-policy.js";
|
||||
import { log } from "./logger.js";
|
||||
import { dropThinkingBlocks } from "./thinking.js";
|
||||
import { describeUnknownError } from "./utils.js";
|
||||
|
||||
const GOOGLE_TURN_ORDERING_CUSTOM_TYPE = "google-turn-ordering-bootstrap";
|
||||
@@ -50,6 +51,7 @@ const GOOGLE_SCHEMA_UNSUPPORTED_KEYWORDS = new Set([
|
||||
"minProperties",
|
||||
"maxProperties",
|
||||
]);
|
||||
|
||||
const ANTIGRAVITY_SIGNATURE_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
const INTER_SESSION_PREFIX_BASE = "[Inter-session message]";
|
||||
|
||||
@@ -450,9 +452,12 @@ export async function sanitizeSessionHistory(params: {
|
||||
...resolveImageSanitizationLimits(params.config),
|
||||
},
|
||||
);
|
||||
const sanitizedThinking = policy.normalizeAntigravityThinkingBlocks
|
||||
? sanitizeAntigravityThinkingBlocks(sanitizedImages)
|
||||
const droppedThinking = policy.dropThinkingBlocks
|
||||
? dropThinkingBlocks(sanitizedImages)
|
||||
: sanitizedImages;
|
||||
const sanitizedThinking = policy.sanitizeThinkingSignatures
|
||||
? sanitizeAntigravityThinkingBlocks(droppedThinking)
|
||||
: droppedThinking;
|
||||
const sanitizedToolCalls = sanitizeToolCallInputs(sanitizedThinking);
|
||||
const repairedTools = policy.repairToolUseResultPairing
|
||||
? sanitizeToolUseResultPairing(sanitizedToolCalls)
|
||||
|
||||
@@ -94,6 +94,7 @@ import {
|
||||
buildEmbeddedSystemPrompt,
|
||||
createSystemPromptOverride,
|
||||
} from "../system-prompt.js";
|
||||
import { dropThinkingBlocks } from "../thinking.js";
|
||||
import { installToolResultContextGuard } from "../tool-result-context-guard.js";
|
||||
import { splitSdkTools } from "../tool-split.js";
|
||||
import { describeUnknownError, mapThinkingLevel } from "../utils.js";
|
||||
@@ -652,6 +653,30 @@ export async function runEmbeddedAttempt(
|
||||
});
|
||||
activeSession.agent.streamFn = cacheTrace.wrapStreamFn(activeSession.agent.streamFn);
|
||||
}
|
||||
|
||||
// Copilot/Claude can reject persisted `thinking` blocks (e.g. thinkingSignature:"reasoning_text")
|
||||
// on *any* follow-up provider call (including tool continuations). Wrap the stream function
|
||||
// so every outbound request sees sanitized messages.
|
||||
if (transcriptPolicy.dropThinkingBlocks) {
|
||||
const inner = activeSession.agent.streamFn;
|
||||
activeSession.agent.streamFn = (model, context, options) => {
|
||||
const ctx = context as unknown as { messages?: unknown };
|
||||
const messages = ctx?.messages;
|
||||
if (!Array.isArray(messages)) {
|
||||
return inner(model, context, options);
|
||||
}
|
||||
const sanitized = dropThinkingBlocks(messages as unknown as AgentMessage[]) as unknown;
|
||||
if (sanitized === messages) {
|
||||
return inner(model, context, options);
|
||||
}
|
||||
const nextContext = {
|
||||
...(context as unknown as Record<string, unknown>),
|
||||
messages: sanitized,
|
||||
} as unknown;
|
||||
return inner(model, nextContext as typeof context, options);
|
||||
};
|
||||
}
|
||||
|
||||
if (anthropicPayloadLogger) {
|
||||
activeSession.agent.streamFn = anthropicPayloadLogger.wrapStreamFn(
|
||||
activeSession.agent.streamFn,
|
||||
@@ -945,7 +970,7 @@ export async function runEmbeddedAttempt(
|
||||
sessionManager.resetLeaf();
|
||||
}
|
||||
const sessionContext = sessionManager.buildSessionContext();
|
||||
const sanitizedOrphan = transcriptPolicy.normalizeAntigravityThinkingBlocks
|
||||
const sanitizedOrphan = transcriptPolicy.sanitizeThinkingSignatures
|
||||
? sanitizeAntigravityThinkingBlocks(sessionContext.messages)
|
||||
: sessionContext.messages;
|
||||
activeSession.agent.replaceMessages(sanitizedOrphan);
|
||||
|
||||
47
src/agents/pi-embedded-runner/thinking.ts
Normal file
47
src/agents/pi-embedded-runner/thinking.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
|
||||
type AssistantContentBlock = Extract<AgentMessage, { role: "assistant" }>["content"][number];
|
||||
|
||||
/**
|
||||
* Strip all `type: "thinking"` content blocks from assistant messages.
|
||||
*
|
||||
* If an assistant message becomes empty after stripping, it is replaced with
|
||||
* a synthetic `{ type: "text", text: "" }` block to preserve turn structure
|
||||
* (some providers require strict user/assistant alternation).
|
||||
*
|
||||
* Returns the original array reference when nothing was changed (callers can
|
||||
* use reference equality to skip downstream work).
|
||||
*/
|
||||
export function dropThinkingBlocks(messages: AgentMessage[]): AgentMessage[] {
|
||||
let touched = false;
|
||||
const out: AgentMessage[] = [];
|
||||
for (const msg of messages) {
|
||||
if (!msg || typeof msg !== "object" || msg.role !== "assistant") {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(msg.content)) {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
const nextContent: AssistantContentBlock[] = [];
|
||||
let changed = false;
|
||||
for (const block of msg.content) {
|
||||
if (block && typeof block === "object" && (block as { type?: unknown }).type === "thinking") {
|
||||
touched = true;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
nextContent.push(block);
|
||||
}
|
||||
if (!changed) {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
// Preserve the assistant turn even if all blocks were thinking-only.
|
||||
const content =
|
||||
nextContent.length > 0 ? nextContent : [{ type: "text", text: "" } as AssistantContentBlock];
|
||||
out.push({ ...msg, content });
|
||||
}
|
||||
return touched ? out : messages;
|
||||
}
|
||||
Reference in New Issue
Block a user