This commit is contained in:
Chester 2026-01-30 17:05:38 +05:30 committed by GitHub
commit bf074b8a1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 83 additions and 0 deletions

View File

@ -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" },

View File

@ -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);