fix(google): repair Cloud Code Assist tool-call ordering (#406)
This commit is contained in:
committed by
Peter Steinberger
parent
d4198bbce4
commit
974619d285
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user