fix: downgrade Gemini tool history
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
- Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs.
|
- 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)
|
- 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)
|
- 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
|
## 2026.1.12-3
|
||||||
|
|
||||||
|
|||||||
@@ -704,3 +704,148 @@ export function isMessagingToolDuplicate(
|
|||||||
sentTexts.map(normalizeTextForComparison),
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export type { MessagingToolSend } from "./pi-embedded-messaging.js";
|
|||||||
import {
|
import {
|
||||||
buildBootstrapContextFiles,
|
buildBootstrapContextFiles,
|
||||||
classifyFailoverReason,
|
classifyFailoverReason,
|
||||||
|
downgradeGeminiHistory,
|
||||||
type EmbeddedContextFile,
|
type EmbeddedContextFile,
|
||||||
ensureSessionHeader,
|
ensureSessionHeader,
|
||||||
formatAssistantErrorText,
|
formatAssistantErrorText,
|
||||||
@@ -493,8 +494,14 @@ async function sanitizeSessionHistory(params: {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const repairedTools = sanitizeToolUseResultPairing(sanitizedImages);
|
const repairedTools = sanitizeToolUseResultPairing(sanitizedImages);
|
||||||
|
|
||||||
|
// Downgrade tool calls missing thought_signature if using Gemini
|
||||||
|
const downgraded = isGoogleModelApi(params.modelApi)
|
||||||
|
? downgradeGeminiHistory(repairedTools)
|
||||||
|
: repairedTools;
|
||||||
|
|
||||||
return applyGoogleTurnOrderingFix({
|
return applyGoogleTurnOrderingFix({
|
||||||
messages: repairedTools,
|
messages: downgraded,
|
||||||
modelApi: params.modelApi,
|
modelApi: params.modelApi,
|
||||||
sessionManager: params.sessionManager,
|
sessionManager: params.sessionManager,
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
|
|||||||
Reference in New Issue
Block a user