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: show a reading indicator bubble while the assistant is responding.
|
||||||
- Control UI: animate reading indicator dots (honors reduced-motion).
|
- Control UI: animate reading indicator dots (honors reduced-motion).
|
||||||
- Control UI: stabilize chat streaming during tool runs (no flicker/vanishing text; correct run scoping).
|
- 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: 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: scroll chat to bottom on initial load. Thanks @kiranjd for PR #274.
|
||||||
- Control UI: add Chat focus mode toggle to collapse header + sidebar.
|
- Control UI: add Chat focus mode toggle to collapse header + sidebar.
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||||
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildBootstrapContextFiles,
|
buildBootstrapContextFiles,
|
||||||
formatAssistantErrorText,
|
formatAssistantErrorText,
|
||||||
isContextOverflowError,
|
isContextOverflowError,
|
||||||
|
sanitizeGoogleTurnOrdering,
|
||||||
} from "./pi-embedded-helpers.js";
|
} from "./pi-embedded-helpers.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_AGENTS_FILENAME,
|
DEFAULT_AGENTS_FILENAME,
|
||||||
@@ -83,3 +85,26 @@ describe("formatAssistantErrorText", () => {
|
|||||||
expect(formatAssistantErrorText(msg)).toContain("Context overflow");
|
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;
|
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(
|
export function buildBootstrapContextFiles(
|
||||||
files: WorkspaceBootstrapFile[],
|
files: WorkspaceBootstrapFile[],
|
||||||
): EmbeddedContextFile[] {
|
): EmbeddedContextFile[] {
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import {
|
|||||||
isRateLimitAssistantError,
|
isRateLimitAssistantError,
|
||||||
isRateLimitErrorMessage,
|
isRateLimitErrorMessage,
|
||||||
pickFallbackThinkingLevel,
|
pickFallbackThinkingLevel,
|
||||||
|
sanitizeGoogleTurnOrdering,
|
||||||
sanitizeSessionMessagesImages,
|
sanitizeSessionMessagesImages,
|
||||||
} from "./pi-embedded-helpers.js";
|
} from "./pi-embedded-helpers.js";
|
||||||
import {
|
import {
|
||||||
@@ -699,10 +700,27 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prior = await sanitizeSessionMessagesImages(
|
const sanitizedImages = await sanitizeSessionMessagesImages(
|
||||||
session.messages,
|
session.messages,
|
||||||
"session:history",
|
"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) {
|
if (prior.length > 0) {
|
||||||
session.agent.replaceMessages(prior);
|
session.agent.replaceMessages(prior);
|
||||||
}
|
}
|
||||||
@@ -1026,8 +1044,25 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
session.messages,
|
session.messages,
|
||||||
"session:history",
|
"session:history",
|
||||||
);
|
);
|
||||||
if (prior.length > 0) {
|
const needsGoogleBootstrap =
|
||||||
session.agent.replaceMessages(prior);
|
(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) {
|
} catch (err) {
|
||||||
session.dispose();
|
session.dispose();
|
||||||
|
|||||||
Reference in New Issue
Block a user