fix: repair tool-use history for anthropic
This commit is contained in:
@@ -85,7 +85,7 @@ function isEmptyAssistantErrorMessage(
|
||||
export async function sanitizeSessionMessagesImages(
|
||||
messages: AgentMessage[],
|
||||
label: string,
|
||||
options?: { sanitizeToolCallIds?: boolean },
|
||||
options?: { sanitizeToolCallIds?: boolean; enforceToolCallLast?: boolean },
|
||||
): Promise<AgentMessage[]> {
|
||||
// We sanitize historical session messages because Anthropic can reject a request
|
||||
// if the transcript contains oversized base64 images (see MAX_IMAGE_DIMENSION_PX).
|
||||
@@ -155,9 +155,29 @@ export async function sanitizeSessionMessagesImages(
|
||||
if (rec.type !== "text" || typeof rec.text !== "string") return true;
|
||||
return rec.text.trim().length > 0;
|
||||
});
|
||||
const normalizedContent = options?.enforceToolCallLast
|
||||
? (() => {
|
||||
let lastToolIndex = -1;
|
||||
for (let i = filteredContent.length - 1; i >= 0; i -= 1) {
|
||||
const block = filteredContent[i];
|
||||
if (!block || typeof block !== "object") continue;
|
||||
const type = (block as { type?: unknown }).type;
|
||||
if (
|
||||
type === "functionCall" ||
|
||||
type === "toolUse" ||
|
||||
type === "toolCall"
|
||||
) {
|
||||
lastToolIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastToolIndex === -1) return filteredContent;
|
||||
return filteredContent.slice(0, lastToolIndex + 1);
|
||||
})()
|
||||
: filteredContent;
|
||||
const sanitizedContent = options?.sanitizeToolCallIds
|
||||
? await Promise.all(
|
||||
filteredContent.map(async (block) => {
|
||||
normalizedContent.map(async (block) => {
|
||||
if (!block || typeof block !== "object") return block;
|
||||
|
||||
const type = (block as { type?: unknown }).type;
|
||||
@@ -179,7 +199,7 @@ export async function sanitizeSessionMessagesImages(
|
||||
return block;
|
||||
}),
|
||||
)
|
||||
: filteredContent;
|
||||
: normalizedContent;
|
||||
const finalContent = (await sanitizeContentBlocksImages(
|
||||
sanitizedContent as unknown as ContentBlock[],
|
||||
label,
|
||||
@@ -197,6 +217,150 @@ export async function sanitizeSessionMessagesImages(
|
||||
return out;
|
||||
}
|
||||
|
||||
type ToolCallLike = {
|
||||
id: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
function extractToolCallsFromAssistant(
|
||||
msg: Extract<AgentMessage, { role: "assistant" }>,
|
||||
): ToolCallLike[] {
|
||||
const content = msg.content;
|
||||
if (!Array.isArray(content)) return [];
|
||||
|
||||
const toolCalls: ToolCallLike[] = [];
|
||||
for (const block of content) {
|
||||
if (!block || typeof block !== "object") continue;
|
||||
const rec = block as { type?: unknown; id?: unknown; name?: unknown };
|
||||
if (typeof rec.id !== "string" || !rec.id) continue;
|
||||
|
||||
if (
|
||||
rec.type === "toolCall" ||
|
||||
rec.type === "toolUse" ||
|
||||
rec.type === "functionCall"
|
||||
) {
|
||||
toolCalls.push({
|
||||
id: rec.id,
|
||||
name: typeof rec.name === "string" ? rec.name : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
return toolCalls;
|
||||
}
|
||||
|
||||
function extractToolResultId(
|
||||
msg: Extract<AgentMessage, { role: "toolResult" }>,
|
||||
): string | null {
|
||||
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 null;
|
||||
}
|
||||
|
||||
function makeMissingToolResult(params: {
|
||||
toolCallId: string;
|
||||
toolName?: string;
|
||||
}): Extract<AgentMessage, { role: "toolResult" }> {
|
||||
return {
|
||||
role: "toolResult",
|
||||
toolCallId: params.toolCallId,
|
||||
toolName: params.toolName ?? "unknown",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "[clawdbot] missing tool result in session history; inserted synthetic error result for transcript repair.",
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
timestamp: Date.now(),
|
||||
} as Extract<AgentMessage, { role: "toolResult" }>;
|
||||
}
|
||||
|
||||
export function sanitizeToolUseResultPairing(
|
||||
messages: AgentMessage[],
|
||||
): AgentMessage[] {
|
||||
// Anthropic (and Cloud Code Assist) reject transcripts where assistant tool calls are not
|
||||
// immediately followed by matching tool results. Session files can end up with results
|
||||
// displaced (e.g. after user turns) or duplicated. Repair by:
|
||||
// - moving matching toolResult messages directly after their assistant toolCall turn
|
||||
// - inserting synthetic error toolResults for missing ids
|
||||
// - dropping duplicate toolResults for the same id within the span
|
||||
const out: AgentMessage[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const msg = messages[i] as AgentMessage;
|
||||
if (!msg || typeof msg !== "object") {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const role = (msg as { role?: unknown }).role;
|
||||
if (role !== "assistant") {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const assistant = msg as Extract<AgentMessage, { role: "assistant" }>;
|
||||
const toolCalls = extractToolCallsFromAssistant(assistant);
|
||||
if (toolCalls.length === 0) {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const toolCallIds = new Set(toolCalls.map((t) => t.id));
|
||||
|
||||
const spanResultsById = new Map<
|
||||
string,
|
||||
Extract<AgentMessage, { role: "toolResult" }>
|
||||
>();
|
||||
const remainder: AgentMessage[] = [];
|
||||
|
||||
let j = i + 1;
|
||||
for (; j < messages.length; j += 1) {
|
||||
const next = messages[j] as AgentMessage;
|
||||
if (!next || typeof next !== "object") {
|
||||
remainder.push(next);
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextRole = (next as { role?: unknown }).role;
|
||||
if (nextRole === "assistant") break;
|
||||
|
||||
if (nextRole === "toolResult") {
|
||||
const toolResult = next as Extract<
|
||||
AgentMessage,
|
||||
{ role: "toolResult" }
|
||||
>;
|
||||
const id = extractToolResultId(toolResult);
|
||||
if (id && toolCallIds.has(id)) {
|
||||
if (!spanResultsById.has(id)) {
|
||||
spanResultsById.set(id, toolResult);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
remainder.push(next);
|
||||
}
|
||||
|
||||
out.push(msg);
|
||||
|
||||
for (const call of toolCalls) {
|
||||
const existing = spanResultsById.get(call.id);
|
||||
out.push(
|
||||
existing ??
|
||||
makeMissingToolResult({ toolCallId: call.id, toolName: call.name }),
|
||||
);
|
||||
}
|
||||
|
||||
out.push(...remainder);
|
||||
i = j - 1;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
const GOOGLE_TURN_ORDER_BOOTSTRAP_TEXT = "(session bootstrap)";
|
||||
|
||||
export function isGoogleModelApi(api?: string | null): boolean {
|
||||
|
||||
Reference in New Issue
Block a user