151 lines
4.6 KiB
TypeScript
151 lines
4.6 KiB
TypeScript
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
sanitizeToolCallInputs,
|
|
sanitizeToolUseResultPairing,
|
|
} from "./session-transcript-repair.js";
|
|
|
|
describe("sanitizeToolUseResultPairing", () => {
|
|
it("moves tool results directly after tool calls and inserts missing results", () => {
|
|
const input = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "toolCall", id: "call_1", name: "read", arguments: {} },
|
|
{ type: "toolCall", id: "call_2", name: "exec", arguments: {} },
|
|
],
|
|
},
|
|
{ role: "user", content: "user message that should come after tool use" },
|
|
{
|
|
role: "toolResult",
|
|
toolCallId: "call_2",
|
|
toolName: "exec",
|
|
content: [{ type: "text", text: "ok" }],
|
|
isError: false,
|
|
},
|
|
] satisfies AgentMessage[];
|
|
|
|
const out = sanitizeToolUseResultPairing(input);
|
|
expect(out[0]?.role).toBe("assistant");
|
|
expect(out[1]?.role).toBe("toolResult");
|
|
expect((out[1] as { toolCallId?: string }).toolCallId).toBe("call_1");
|
|
expect(out[2]?.role).toBe("toolResult");
|
|
expect((out[2] as { toolCallId?: string }).toolCallId).toBe("call_2");
|
|
expect(out[3]?.role).toBe("user");
|
|
});
|
|
|
|
it("drops duplicate tool results for the same id within a span", () => {
|
|
const input = [
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
|
|
},
|
|
{
|
|
role: "toolResult",
|
|
toolCallId: "call_1",
|
|
toolName: "read",
|
|
content: [{ type: "text", text: "first" }],
|
|
isError: false,
|
|
},
|
|
{
|
|
role: "toolResult",
|
|
toolCallId: "call_1",
|
|
toolName: "read",
|
|
content: [{ type: "text", text: "second" }],
|
|
isError: false,
|
|
},
|
|
{ role: "user", content: "ok" },
|
|
] satisfies AgentMessage[];
|
|
|
|
const out = sanitizeToolUseResultPairing(input);
|
|
expect(out.filter((m) => m.role === "toolResult")).toHaveLength(1);
|
|
});
|
|
|
|
it("drops duplicate tool results for the same id across the transcript", () => {
|
|
const input = [
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: {} }],
|
|
},
|
|
{
|
|
role: "toolResult",
|
|
toolCallId: "call_1",
|
|
toolName: "read",
|
|
content: [{ type: "text", text: "first" }],
|
|
isError: false,
|
|
},
|
|
{ role: "assistant", content: [{ type: "text", text: "ok" }] },
|
|
{
|
|
role: "toolResult",
|
|
toolCallId: "call_1",
|
|
toolName: "read",
|
|
content: [{ type: "text", text: "second (duplicate)" }],
|
|
isError: false,
|
|
},
|
|
] satisfies AgentMessage[];
|
|
|
|
const out = sanitizeToolUseResultPairing(input);
|
|
const results = out.filter((m) => m.role === "toolResult") as Array<{
|
|
toolCallId?: string;
|
|
}>;
|
|
expect(results).toHaveLength(1);
|
|
expect(results[0]?.toolCallId).toBe("call_1");
|
|
});
|
|
|
|
it("drops orphan tool results that do not match any tool call", () => {
|
|
const input = [
|
|
{ role: "user", content: "hello" },
|
|
{
|
|
role: "toolResult",
|
|
toolCallId: "call_orphan",
|
|
toolName: "read",
|
|
content: [{ type: "text", text: "orphan" }],
|
|
isError: false,
|
|
},
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "ok" }],
|
|
},
|
|
] satisfies AgentMessage[];
|
|
|
|
const out = sanitizeToolUseResultPairing(input);
|
|
expect(out.some((m) => m.role === "toolResult")).toBe(false);
|
|
expect(out.map((m) => m.role)).toEqual(["user", "assistant"]);
|
|
});
|
|
});
|
|
|
|
describe("sanitizeToolCallInputs", () => {
|
|
it("drops tool calls missing input or arguments", () => {
|
|
const input: AgentMessage[] = [
|
|
{
|
|
role: "assistant",
|
|
content: [{ type: "toolCall", id: "call_1", name: "read" }],
|
|
},
|
|
{ role: "user", content: "hello" },
|
|
];
|
|
|
|
const out = sanitizeToolCallInputs(input);
|
|
expect(out.map((m) => m.role)).toEqual(["user"]);
|
|
});
|
|
|
|
it("keeps valid tool calls and preserves text blocks", () => {
|
|
const input: AgentMessage[] = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "text", text: "before" },
|
|
{ type: "toolUse", id: "call_ok", name: "read", input: { path: "a" } },
|
|
{ type: "toolCall", id: "call_drop", name: "read" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const out = sanitizeToolCallInputs(input);
|
|
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
|
|
const types = Array.isArray(assistant.content)
|
|
? assistant.content.map((block) => (block as { type?: unknown }).type)
|
|
: [];
|
|
expect(types).toEqual(["text", "toolUse"]);
|
|
});
|
|
});
|