From 981cb89ea3f75def1f280bf6dd27c0d6fc3256d2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 09:41:29 +0100 Subject: [PATCH] fix(agents): strip stale gemini assistant prefill --- CHANGELOG.md | 1 + .../pi-embedded-runner/run/attempt.test.ts | 38 +++++++++++++++++++ .../run/attempt.tool-call-normalization.ts | 11 +++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eed59bb357..39a6fd760d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai - 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. +- Agents/Google: strip stale trailing assistant/model prefill turns from Gemini outbound replay so Google Generative AI requests end with a user turn or function response. Follow-up to #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. diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index 18760aea300..d37fb381227 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -1671,6 +1671,44 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => { expect(seenContext.messages).not.toBe(messages); }); + it("strips trailing assistant prefill turns for Gemini outbound replay", async () => { + const messages = [ + { + role: "user", + content: [{ type: "text", text: "earlier question" }], + }, + { + role: "assistant", + content: [{ type: "text", text: "stale model answer" }], + }, + ]; + const baseFn = vi.fn((_model, _context) => + createFakeStream({ events: [], resultMessage: { role: "assistant", content: [] } }), + ); + + const wrapped = wrapStreamFnSanitizeMalformedToolCalls(baseFn as never, new Set(["read"]), { + validateGeminiTurns: true, + preserveSignatures: true, + dropThinkingBlocks: false, + } as never); + const stream = wrapped( + { api: "google-generative-ai" } as never, + { messages } as never, + {} as never, + ) as FakeWrappedStream | Promise; + 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 = [ { diff --git a/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts b/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts index b3aec34d335..d4d3bdfed6e 100644 --- a/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts +++ b/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts @@ -895,16 +895,25 @@ export function wrapStreamFnSanitizeMalformedToolCalls( let nextMessages = replayInputsChanged ? sanitizeToolUseResultPairing(sanitized.messages) : sanitized.messages; + let strippedTrailingAssistantPrefill = false; if (transcriptPolicy?.validateAnthropicTurns) { nextMessages = sanitizeAnthropicReplayToolResults(nextMessages, { disallowEmbeddedUserToolResultsForSignedThinkingReplay: allowProviderOwnedThinkingReplay, }); + } + if (transcriptPolicy?.validateAnthropicTurns || transcriptPolicy?.validateGeminiTurns) { + const beforeStrip = nextMessages; nextMessages = stripTrailingAssistantPrefillTurns(nextMessages); + strippedTrailingAssistantPrefill ||= nextMessages !== beforeStrip; } if (nextMessages === messages) { return baseFn(model, context, options); } - if (sanitized.droppedAssistantMessages > 0 || transcriptPolicy?.validateAnthropicTurns) { + if ( + sanitized.droppedAssistantMessages > 0 || + transcriptPolicy?.validateAnthropicTurns || + strippedTrailingAssistantPrefill + ) { if (transcriptPolicy?.validateGeminiTurns) { nextMessages = validateGeminiTurns(nextMessages); }