fix: downgrade Gemini tool history

This commit is contained in:
Peter Steinberger
2026-01-13 01:03:10 +00:00
parent 5dc187f00c
commit 0edbdb1948
3 changed files with 154 additions and 1 deletions

View File

@@ -8,6 +8,7 @@
- Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs.
- Update: run `clawdbot doctor --non-interactive` during updates to avoid TTY hangs. (#781 — thanks @ronyrus)
- Tools: allow Claude/Gemini tool param aliases (`file_path`, `old_string`, `new_string`) while enforcing required params at runtime. (#793 — thanks @hsrvc)
- Gemini: downgrade tool-call history missing `thought_signature` to avoid INVALID_ARGUMENT errors. (#793 — thanks @hsrvc)
## 2026.1.12-3

View File

@@ -704,3 +704,148 @@ export function isMessagingToolDuplicate(
sentTexts.map(normalizeTextForComparison),
);
}
/**
* Downgrades tool calls that are missing `thought_signature` (required by Gemini)
* into text representations, to prevent 400 INVALID_ARGUMENT errors.
* Also converts corresponding tool results into user messages.
*/
type GeminiToolCallBlock = {
type?: unknown;
thought_signature?: unknown;
id?: unknown;
toolCallId?: unknown;
name?: unknown;
toolName?: unknown;
arguments?: unknown;
input?: unknown;
};
export function downgradeGeminiHistory(
messages: AgentMessage[],
): AgentMessage[] {
const downgradedIds = new Set<string>();
const out: AgentMessage[] = [];
const resolveToolResultId = (
msg: Extract<AgentMessage, { role: "toolResult" }>,
): string | undefined => {
const toolCallId = (msg as { toolCallId?: unknown }).toolCallId;
if (typeof toolCallId === "string" && toolCallId) return toolCallId;
const toolUseId = (msg as { toolUseId?: unknown }).toolUseId;
if (typeof toolUseId === "string" && toolUseId) return toolUseId;
return undefined;
};
for (const msg of messages) {
if (!msg || typeof msg !== "object") {
out.push(msg);
continue;
}
const role = (msg as { role?: unknown }).role;
if (role === "assistant") {
const assistantMsg = msg as Extract<AgentMessage, { role: "assistant" }>;
if (!Array.isArray(assistantMsg.content)) {
out.push(msg);
continue;
}
let hasDowngraded = false;
const newContent = assistantMsg.content.map((block) => {
if (!block || typeof block !== "object") return block;
const blockRecord = block as GeminiToolCallBlock;
const type = blockRecord.type;
// Check for tool calls / function calls
if (
type === "toolCall" ||
type === "functionCall" ||
type === "toolUse"
) {
// Check if thought_signature is missing
// Note: TypeScript doesn't know about thought_signature on standard types
const hasSignature = Boolean(blockRecord.thought_signature);
if (!hasSignature) {
const id =
typeof blockRecord.id === "string"
? blockRecord.id
: typeof blockRecord.toolCallId === "string"
? blockRecord.toolCallId
: undefined;
const name =
typeof blockRecord.name === "string"
? blockRecord.name
: typeof blockRecord.toolName === "string"
? blockRecord.toolName
: undefined;
const args =
blockRecord.arguments !== undefined
? blockRecord.arguments
: blockRecord.input;
if (id) downgradedIds.add(id);
hasDowngraded = true;
const argsText =
typeof args === "string" ? args : JSON.stringify(args, null, 2);
return {
type: "text",
text: `[Tool Call: ${name ?? "unknown"}${
id ? ` (ID: ${id})` : ""
}]\nArguments: ${argsText}`,
};
}
}
return block;
});
if (hasDowngraded) {
out.push({ ...assistantMsg, content: newContent } as AgentMessage);
} else {
out.push(msg);
}
continue;
}
if (role === "toolResult") {
const toolMsg = msg as Extract<AgentMessage, { role: "toolResult" }>;
const toolResultId = resolveToolResultId(toolMsg);
if (toolResultId && downgradedIds.has(toolResultId)) {
// Convert to User message
let textContent = "";
if (Array.isArray(toolMsg.content)) {
textContent = toolMsg.content
.map((entry) => {
if (entry && typeof entry === "object") {
const text = (entry as { text?: unknown }).text;
if (typeof text === "string") return text;
}
return JSON.stringify(entry);
})
.join("\n");
} else {
textContent = JSON.stringify(toolMsg.content);
}
out.push({
role: "user",
content: [
{
type: "text",
text: `[Tool Result for ID ${toolResultId}]\n${textContent}`,
},
],
} as AgentMessage);
continue;
}
}
out.push(msg);
}
return out;
}

View File

@@ -85,6 +85,7 @@ export type { MessagingToolSend } from "./pi-embedded-messaging.js";
import {
buildBootstrapContextFiles,
classifyFailoverReason,
downgradeGeminiHistory,
type EmbeddedContextFile,
ensureSessionHeader,
formatAssistantErrorText,
@@ -493,8 +494,14 @@ async function sanitizeSessionHistory(params: {
},
);
const repairedTools = sanitizeToolUseResultPairing(sanitizedImages);
// Downgrade tool calls missing thought_signature if using Gemini
const downgraded = isGoogleModelApi(params.modelApi)
? downgradeGeminiHistory(repairedTools)
: repairedTools;
return applyGoogleTurnOrderingFix({
messages: repairedTools,
messages: downgraded,
modelApi: params.modelApi,
sessionManager: params.sessionManager,
sessionId: params.sessionId,