fix(agents): strip stale gemini assistant prefill

This commit is contained in:
Peter Steinberger
2026-04-27 09:41:29 +01:00
parent a35ad200d1
commit 981cb89ea3
3 changed files with 49 additions and 1 deletions

View File

@@ -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.

View File

@@ -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<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 = [
{

View File

@@ -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);
}