Merge 5d04fdd94b into da71eaebd2
This commit is contained in:
commit
ad5bb988a9
@ -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