mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:50:43 +00:00
fix(agents): strip stale anthropic assistant prefill
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/sessions: acquire the session write lock only after cold bootstrap, plugin, and tool setup so fallback runs are not blocked by stalled pre-model startup work. Thanks @codex.
|
||||
- Gateway/models: skip external OpenRouter and LiteLLM pricing refreshes for local/self-hosted model endpoints so startup does not wait on remote pricing catalogs for local-only Ollama, vLLM, and compatible providers. Thanks @codex.
|
||||
- CLI/plugins: stop security-blocked plugin installs from retrying as hook packs, so normal plugin packages report the scanner failure without a misleading "not a valid hook pack" follow-up. Fixes #61175; supersedes #64102. Thanks @KonsultDigital and @ziyincody.
|
||||
- Agents/Anthropic: strip stale trailing assistant prefill turns from outbound replay so context-engine short circuits cannot send unsupported assistant-prefill payloads to provider APIs. Fixes #72556. Thanks @Veda-openclaw.
|
||||
- Control UI/Dreaming: require explicit confirmation before applying restart-impacting Dreaming mode changes, with restart warning copy and loading feedback. Fixes #63804. (#63807) Thanks @bbddbb1.
|
||||
- CLI/update: keep the automatic post-update completion refresh on the core-command tree so it no longer stages bundled plugin runtime deps before the Gateway restart path, avoiding `.24` update hangs and 1006 disconnect cascades. Fixes #72665. Thanks @sakalaboator and @He-Pin.
|
||||
- Agents/Bedrock: stop heartbeat runs from persisting blank user transcript turns and repair existing blank user text messages before replay, preventing AWS Bedrock `ContentBlock` blank-text validation failures. Fixes #72640 and #72622. Thanks @goldzulu.
|
||||
|
||||
@@ -1633,6 +1633,44 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => {
|
||||
expect(seenContext.messages).toBe(messages);
|
||||
});
|
||||
|
||||
it("strips trailing assistant prefill turns for Anthropic outbound replay", async () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "earlier question" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "stale assistant answer" }],
|
||||
},
|
||||
];
|
||||
const baseFn = vi.fn((_model, _context) =>
|
||||
createFakeStream({ events: [], resultMessage: { role: "assistant", content: [] } }),
|
||||
);
|
||||
|
||||
const wrapped = wrapStreamFnSanitizeMalformedToolCalls(baseFn as never, new Set(["read"]), {
|
||||
validateAnthropicTurns: true,
|
||||
preserveSignatures: true,
|
||||
dropThinkingBlocks: false,
|
||||
} as never);
|
||||
const stream = wrapped(
|
||||
{ api: "anthropic-messages" } as never,
|
||||
{ messages } as never,
|
||||
{} as never,
|
||||
) as FakeWrappedStream | Promise<FakeWrappedStream>;
|
||||
await Promise.resolve(stream);
|
||||
|
||||
expect(baseFn).toHaveBeenCalledTimes(1);
|
||||
const seenContext = baseFn.mock.calls[0]?.[1] as { messages: unknown[] };
|
||||
expect(seenContext.messages).toEqual([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "earlier question" }],
|
||||
},
|
||||
]);
|
||||
expect(seenContext.messages).not.toBe(messages);
|
||||
});
|
||||
|
||||
it("drops signed thinking turns when sibling replay tool calls are not allowlisted", async () => {
|
||||
const messages = [
|
||||
{
|
||||
|
||||
@@ -525,6 +525,32 @@ function sanitizeAnthropicReplayToolResults(
|
||||
return changed ? out : messages;
|
||||
}
|
||||
|
||||
function assistantTurnHasReplayToolCall(message: AgentMessage): boolean {
|
||||
if (!message || typeof message !== "object" || message.role !== "assistant") {
|
||||
return false;
|
||||
}
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (!Array.isArray(content)) {
|
||||
return false;
|
||||
}
|
||||
return content.some((block) => isReplayToolCallBlock(block));
|
||||
}
|
||||
|
||||
function stripTrailingAssistantPrefillTurns(messages: AgentMessage[]): AgentMessage[] {
|
||||
let end = messages.length;
|
||||
while (end > 0) {
|
||||
const message = messages[end - 1];
|
||||
if (!message || typeof message !== "object" || message.role !== "assistant") {
|
||||
break;
|
||||
}
|
||||
if (assistantTurnHasReplayToolCall(message)) {
|
||||
break;
|
||||
}
|
||||
end -= 1;
|
||||
}
|
||||
return end === messages.length ? messages : messages.slice(0, end);
|
||||
}
|
||||
|
||||
function normalizeToolCallIdsInMessage(message: unknown): void {
|
||||
if (!message || typeof message !== "object") {
|
||||
return;
|
||||
@@ -873,6 +899,7 @@ export function wrapStreamFnSanitizeMalformedToolCalls(
|
||||
nextMessages = sanitizeAnthropicReplayToolResults(nextMessages, {
|
||||
disallowEmbeddedUserToolResultsForSignedThinkingReplay: allowProviderOwnedThinkingReplay,
|
||||
});
|
||||
nextMessages = stripTrailingAssistantPrefillTurns(nextMessages);
|
||||
}
|
||||
if (nextMessages === messages) {
|
||||
return baseFn(model, context, options);
|
||||
|
||||
Reference in New Issue
Block a user