fix(agents): skip tool extraction for aborted/errored assistant messages
When stopReason is 'error' or 'aborted', tool_use blocks may be incomplete (e.g., partialJson: true). Creating synthetic tool_results for these causes API 400 errors: 'unexpected tool_use_id found in tool_result blocks' This fix skips tool call extraction for failed messages, preventing session corruption after interruptions while preserving normal repair behavior. Adds tests for error, aborted, and normal (toolUse) stopReason cases.
This commit is contained in:
parent
bc432d8435
commit
5d04fdd94b
@ -1,6 +1,9 @@
|
|||||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { sanitizeToolUseResultPairing } from "./session-transcript-repair.js";
|
import {
|
||||||
|
sanitizeToolUseResultPairing,
|
||||||
|
repairToolUseResultPairing,
|
||||||
|
} from "./session-transcript-repair.js";
|
||||||
|
|
||||||
describe("sanitizeToolUseResultPairing", () => {
|
describe("sanitizeToolUseResultPairing", () => {
|
||||||
it("moves tool results directly after tool calls and inserts missing results", () => {
|
it("moves tool results directly after tool calls and inserts missing results", () => {
|
||||||
@ -109,4 +112,67 @@ describe("sanitizeToolUseResultPairing", () => {
|
|||||||
expect(out.some((m) => m.role === "toolResult")).toBe(false);
|
expect(out.some((m) => m.role === "toolResult")).toBe(false);
|
||||||
expect(out.map((m) => m.role)).toEqual(["user", "assistant"]);
|
expect(out.map((m) => m.role)).toEqual(["user", "assistant"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips tool call extraction for assistant messages with stopReason 'error'", () => {
|
||||||
|
// When an assistant message has stopReason: "error", its tool_use blocks may be
|
||||||
|
// incomplete/malformed. We should NOT create synthetic tool_results for them,
|
||||||
|
// as this causes API 400 errors: "unexpected tool_use_id found in tool_result blocks"
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "toolCall", id: "call_error", name: "exec", arguments: {} }],
|
||||||
|
stopReason: "error",
|
||||||
|
},
|
||||||
|
{ role: "user", content: "something went wrong" },
|
||||||
|
] as AgentMessage[];
|
||||||
|
|
||||||
|
const result = repairToolUseResultPairing(input);
|
||||||
|
|
||||||
|
// Should NOT add synthetic tool results for errored messages
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
// The assistant message should be passed through unchanged
|
||||||
|
expect(result.messages[0]?.role).toBe("assistant");
|
||||||
|
expect(result.messages[1]?.role).toBe("user");
|
||||||
|
expect(result.messages).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips tool call extraction for assistant messages with stopReason 'aborted'", () => {
|
||||||
|
// When a request is aborted mid-stream, the assistant message may have incomplete
|
||||||
|
// tool_use blocks (with partialJson). We should NOT create synthetic tool_results.
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "toolCall", id: "call_aborted", name: "Bash", arguments: {} }],
|
||||||
|
stopReason: "aborted",
|
||||||
|
},
|
||||||
|
{ role: "user", content: "retrying after abort" },
|
||||||
|
] as AgentMessage[];
|
||||||
|
|
||||||
|
const result = repairToolUseResultPairing(input);
|
||||||
|
|
||||||
|
// Should NOT add synthetic tool results for aborted messages
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
// Messages should be passed through without synthetic insertions
|
||||||
|
expect(result.messages).toHaveLength(2);
|
||||||
|
expect(result.messages[0]?.role).toBe("assistant");
|
||||||
|
expect(result.messages[1]?.role).toBe("user");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still repairs tool results for normal assistant messages with stopReason 'toolUse'", () => {
|
||||||
|
// Normal tool calls (stopReason: "toolUse" or "stop") should still be repaired
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "toolCall", id: "call_normal", name: "read", arguments: {} }],
|
||||||
|
stopReason: "toolUse",
|
||||||
|
},
|
||||||
|
{ role: "user", content: "user message" },
|
||||||
|
] as AgentMessage[];
|
||||||
|
|
||||||
|
const result = repairToolUseResultPairing(input);
|
||||||
|
|
||||||
|
// Should add a synthetic tool result for the missing result
|
||||||
|
expect(result.added).toHaveLength(1);
|
||||||
|
expect(result.added[0]?.toolCallId).toBe("call_normal");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -116,6 +116,19 @@ export function repairToolUseResultPairing(messages: AgentMessage[]): ToolUseRep
|
|||||||
}
|
}
|
||||||
|
|
||||||
const assistant = msg as Extract<AgentMessage, { role: "assistant" }>;
|
const assistant = msg as Extract<AgentMessage, { role: "assistant" }>;
|
||||||
|
|
||||||
|
// Skip tool call extraction for aborted or errored assistant messages.
|
||||||
|
// When stopReason is "error" or "aborted", the tool_use blocks may be incomplete
|
||||||
|
// (e.g., partialJson: true) and should not have synthetic tool_results created.
|
||||||
|
// Creating synthetic results for incomplete tool calls causes API 400 errors:
|
||||||
|
// "unexpected tool_use_id found in tool_result blocks"
|
||||||
|
// See: https://github.com/openclaw/openclaw/issues/4553
|
||||||
|
const stopReason = (assistant as { stopReason?: string }).stopReason;
|
||||||
|
if (stopReason === "error" || stopReason === "aborted") {
|
||||||
|
out.push(msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const toolCalls = extractToolCallsFromAssistant(assistant);
|
const toolCalls = extractToolCallsFromAssistant(assistant);
|
||||||
if (toolCalls.length === 0) {
|
if (toolCalls.length === 0) {
|
||||||
out.push(msg);
|
out.push(msg);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user