Merge 9353ce95d9 into da71eaebd2
This commit is contained in:
commit
bf074b8a1f
@ -89,6 +89,48 @@ describe("sanitizeToolUseResultPairing", () => {
|
||||
expect(results[0]?.toolCallId).toBe("call_1");
|
||||
});
|
||||
|
||||
it("drops errored assistant with tool calls and their matching tool results", () => {
|
||||
const input = [
|
||||
{ role: "user", content: "do something" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "call_err", name: "cron", arguments: {} }],
|
||||
stopReason: "error",
|
||||
errorMessage: "JSON parse error",
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call_err",
|
||||
toolName: "cron",
|
||||
content: [{ type: "text", text: "synthetic repair" }],
|
||||
isError: true,
|
||||
},
|
||||
{ role: "user", content: "fix this" },
|
||||
] satisfies AgentMessage[];
|
||||
|
||||
const out = sanitizeToolUseResultPairing(input);
|
||||
// The errored assistant and its tool_result should both be dropped
|
||||
expect(out.map((m) => m.role)).toEqual(["user", "user"]);
|
||||
expect(out).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps errored assistant without tool calls", () => {
|
||||
const input = [
|
||||
{ role: "user", content: "do something" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "partial response" }],
|
||||
stopReason: "error",
|
||||
errorMessage: "timeout",
|
||||
},
|
||||
{ role: "user", content: "try again" },
|
||||
] satisfies AgentMessage[];
|
||||
|
||||
const out = sanitizeToolUseResultPairing(input);
|
||||
expect(out).toHaveLength(3);
|
||||
expect(out[1]?.role).toBe("assistant");
|
||||
});
|
||||
|
||||
it("drops orphan tool results that do not match any tool call", () => {
|
||||
const input = [
|
||||
{ role: "user", content: "hello" },
|
||||
|
||||
@ -116,6 +116,47 @@ export function repairToolUseResultPairing(messages: AgentMessage[]): ToolUseRep
|
||||
}
|
||||
|
||||
const assistant = msg as Extract<AgentMessage, { role: "assistant" }>;
|
||||
|
||||
// Drop errored/aborted assistant messages that contain tool calls, along with
|
||||
// their matching tool_results. The provider-level transform (pi-ai) also drops
|
||||
// these assistants, but if the tool_results survive they become orphans that
|
||||
// cause "unexpected tool_use_id" API rejections. Defence-in-depth: strip them
|
||||
// here so downstream transforms never see the mismatch.
|
||||
if (
|
||||
(assistant as { stopReason?: unknown }).stopReason === "error" ||
|
||||
(assistant as { stopReason?: unknown }).stopReason === "aborted"
|
||||
) {
|
||||
const erroredToolCalls = extractToolCallsFromAssistant(assistant);
|
||||
if (erroredToolCalls.length > 0) {
|
||||
const erroredIds = new Set(erroredToolCalls.map((t) => t.id));
|
||||
// Skip ahead past any matching tool_results for this errored assistant
|
||||
let j = i + 1;
|
||||
for (; j < messages.length; j += 1) {
|
||||
const next = messages[j] as AgentMessage;
|
||||
if (!next || typeof next !== "object") continue;
|
||||
const nextRole = (next as { role?: unknown }).role;
|
||||
if (nextRole === "assistant") break;
|
||||
if (nextRole === "toolResult") {
|
||||
const id = extractToolResultId(
|
||||
next as Extract<AgentMessage, { role: "toolResult" }>,
|
||||
);
|
||||
if (id && erroredIds.has(id)) {
|
||||
// Drop the orphan tool_result that matched the errored assistant
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(next);
|
||||
}
|
||||
i = j - 1;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
// Errored assistant without tool calls - keep it
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const toolCalls = extractToolCallsFromAssistant(assistant);
|
||||
if (toolCalls.length === 0) {
|
||||
out.push(msg);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user