fix(google): repair Cloud Code Assist tool-call ordering (#406)

This commit is contained in:
Jonáš Jančařík
2026-01-07 18:30:35 +01:00
committed by Peter Steinberger
parent d4198bbce4
commit 974619d285
4 changed files with 94 additions and 3 deletions

View File

@@ -136,6 +136,7 @@
- Control UI: show a reading indicator bubble while the assistant is responding.
- Control UI: animate reading indicator dots (honors reduced-motion).
- Control UI: stabilize chat streaming during tool runs (no flicker/vanishing text; correct run scoping).
- Google: recover from corrupted transcripts that start with an assistant tool call to avoid Cloud Code Assist 400 ordering errors. Thanks @jonasjancarik for PR #421. (#406)
- Control UI: let config-form enums select empty-string values. Thanks @sreekaransrinath for PR #268.
- Control UI: scroll chat to bottom on initial load. Thanks @kiranjd for PR #274.
- Control UI: add Chat focus mode toggle to collapse header + sidebar.

View File

@@ -1,10 +1,12 @@
import type { AssistantMessage } from "@mariozechner/pi-ai";
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { describe, expect, it } from "vitest";
import {
buildBootstrapContextFiles,
formatAssistantErrorText,
isContextOverflowError,
sanitizeGoogleTurnOrdering,
} from "./pi-embedded-helpers.js";
import {
DEFAULT_AGENTS_FILENAME,
@@ -83,3 +85,26 @@ describe("formatAssistantErrorText", () => {
expect(formatAssistantErrorText(msg)).toContain("Context overflow");
});
});
describe("sanitizeGoogleTurnOrdering", () => {
it("prepends a synthetic user turn when history starts with assistant", () => {
const input = [
{
role: "assistant",
content: [
{ type: "toolCall", id: "call_1", name: "bash", arguments: {} },
],
},
] satisfies AgentMessage[];
const out = sanitizeGoogleTurnOrdering(input);
expect(out[0]?.role).toBe("user");
expect(out[1]?.role).toBe("assistant");
});
it("is a no-op when history starts with user", () => {
const input = [{ role: "user", content: "hi" }] satisfies AgentMessage[];
const out = sanitizeGoogleTurnOrdering(input);
expect(out).toBe(input);
});
});

View File

@@ -104,6 +104,36 @@ export async function sanitizeSessionMessagesImages(
return out;
}
const GOOGLE_TURN_ORDER_BOOTSTRAP_TEXT = "(session bootstrap)";
export function sanitizeGoogleTurnOrdering(
messages: AgentMessage[],
): AgentMessage[] {
const first = messages[0] as
| { role?: unknown; content?: unknown }
| undefined;
const role = first?.role;
const content = first?.content;
if (
role === "user" &&
typeof content === "string" &&
content.trim() === GOOGLE_TURN_ORDER_BOOTSTRAP_TEXT
) {
return messages;
}
if (role !== "assistant") return messages;
// Cloud Code Assist rejects histories that begin with a model turn (tool call or text).
// Prepend a tiny synthetic user turn so the rest of the transcript can be used.
const bootstrap: AgentMessage = {
role: "user",
content: GOOGLE_TURN_ORDER_BOOTSTRAP_TEXT,
timestamp: Date.now(),
} as AgentMessage;
return [bootstrap, ...messages];
}
export function buildBootstrapContextFiles(
files: WorkspaceBootstrapFile[],
): EmbeddedContextFile[] {

View File

@@ -63,6 +63,7 @@ import {
isRateLimitAssistantError,
isRateLimitErrorMessage,
pickFallbackThinkingLevel,
sanitizeGoogleTurnOrdering,
sanitizeSessionMessagesImages,
} from "./pi-embedded-helpers.js";
import {
@@ -699,10 +700,27 @@ export async function compactEmbeddedPiSession(params: {
}));
try {
const prior = await sanitizeSessionMessagesImages(
const sanitizedImages = await sanitizeSessionMessagesImages(
session.messages,
"session:history",
);
const needsGoogleBootstrap =
(model.api === "google-gemini-cli" ||
model.api === "google-generative-ai") &&
sanitizedImages[0] &&
typeof sanitizedImages[0] === "object" &&
"role" in sanitizedImages[0] &&
sanitizedImages[0].role === "assistant";
const prior =
model.api === "google-gemini-cli" ||
model.api === "google-generative-ai"
? sanitizeGoogleTurnOrdering(sanitizedImages)
: sanitizedImages;
if (needsGoogleBootstrap) {
log.warn(
`google turn ordering fixup: prepended user bootstrap (sessionId=${params.sessionId})`,
);
}
if (prior.length > 0) {
session.agent.replaceMessages(prior);
}
@@ -1026,8 +1044,25 @@ export async function runEmbeddedPiAgent(params: {
session.messages,
"session:history",
);
if (prior.length > 0) {
session.agent.replaceMessages(prior);
const needsGoogleBootstrap =
(model.api === "google-gemini-cli" ||
model.api === "google-generative-ai") &&
prior[0] &&
typeof prior[0] === "object" &&
"role" in prior[0] &&
prior[0].role === "assistant";
const sanitizedPrior =
model.api === "google-gemini-cli" ||
model.api === "google-generative-ai"
? sanitizeGoogleTurnOrdering(prior)
: prior;
if (needsGoogleBootstrap) {
log.warn(
`google turn ordering fixup: prepended user bootstrap (sessionId=${params.sessionId})`,
);
}
if (sanitizedPrior.length > 0) {
session.agent.replaceMessages(sanitizedPrior);
}
} catch (err) {
session.dispose();