forked from farhoodlabs/paperclip
Fix Codex tool-use transcript completion
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseCodexStdoutLine } from "./parse-stdout.js";
|
||||
|
||||
describe("parseCodexStdoutLine", () => {
|
||||
it("marks completed tool_use items as resolved tool results", () => {
|
||||
const started = parseCodexStdoutLine(JSON.stringify({
|
||||
type: "item.started",
|
||||
item: {
|
||||
id: "tool-1",
|
||||
type: "tool_use",
|
||||
name: "search",
|
||||
input: { query: "paperclip" },
|
||||
},
|
||||
}), "2026-04-08T12:00:00.000Z");
|
||||
|
||||
const completed = parseCodexStdoutLine(JSON.stringify({
|
||||
type: "item.completed",
|
||||
item: {
|
||||
id: "tool-1",
|
||||
type: "tool_use",
|
||||
name: "search",
|
||||
status: "completed",
|
||||
},
|
||||
}), "2026-04-08T12:00:01.000Z");
|
||||
|
||||
expect(started).toEqual([{
|
||||
kind: "tool_call",
|
||||
ts: "2026-04-08T12:00:00.000Z",
|
||||
name: "search",
|
||||
toolUseId: "tool-1",
|
||||
input: { query: "paperclip" },
|
||||
}]);
|
||||
expect(completed).toEqual([{
|
||||
kind: "tool_result",
|
||||
ts: "2026-04-08T12:00:01.000Z",
|
||||
toolUseId: "tool-1",
|
||||
content: "search completed",
|
||||
isError: false,
|
||||
}]);
|
||||
});
|
||||
|
||||
it("keeps explicit tool_result payloads authoritative after tool_use completion", () => {
|
||||
const completed = parseCodexStdoutLine(JSON.stringify({
|
||||
type: "item.completed",
|
||||
item: {
|
||||
id: "tool-2",
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-1",
|
||||
content: "final payload",
|
||||
status: "completed",
|
||||
},
|
||||
}), "2026-04-08T12:00:02.000Z");
|
||||
|
||||
expect(completed).toEqual([{
|
||||
kind: "tool_result",
|
||||
ts: "2026-04-08T12:00:02.000Z",
|
||||
toolUseId: "tool-1",
|
||||
content: "final payload",
|
||||
isError: false,
|
||||
}]);
|
||||
});
|
||||
|
||||
it("marks failed completed tool_use items as error results", () => {
|
||||
const completed = parseCodexStdoutLine(JSON.stringify({
|
||||
type: "item.completed",
|
||||
item: {
|
||||
id: "tool-3",
|
||||
type: "tool_use",
|
||||
name: "write_file",
|
||||
status: "error",
|
||||
error: { message: "permission denied" },
|
||||
},
|
||||
}), "2026-04-08T12:00:03.000Z");
|
||||
|
||||
expect(completed).toEqual([{
|
||||
kind: "tool_result",
|
||||
ts: "2026-04-08T12:00:03.000Z",
|
||||
toolUseId: "tool-3",
|
||||
content: "permission denied",
|
||||
isError: true,
|
||||
}]);
|
||||
});
|
||||
});
|
||||
@@ -118,6 +118,52 @@ function parseFileChangeItem(item: Record<string, unknown>, ts: string): Transcr
|
||||
return [{ kind: "system", ts, text: `file changes: ${preview}${more}` }];
|
||||
}
|
||||
|
||||
function parseToolUseItem(
|
||||
item: Record<string, unknown>,
|
||||
ts: string,
|
||||
phase: "started" | "completed",
|
||||
): TranscriptEntry[] {
|
||||
const name = asString(item.name, "unknown");
|
||||
const toolUseId = asString(item.id, name || "tool_use");
|
||||
|
||||
if (phase === "started") {
|
||||
return [{
|
||||
kind: "tool_call",
|
||||
ts,
|
||||
name,
|
||||
toolUseId,
|
||||
input: item.input ?? {},
|
||||
}];
|
||||
}
|
||||
|
||||
const status = asString(item.status);
|
||||
const isError =
|
||||
item.is_error === true ||
|
||||
status === "failed" ||
|
||||
status === "errored" ||
|
||||
status === "error" ||
|
||||
status === "cancelled";
|
||||
const rawContent =
|
||||
item.content ??
|
||||
item.output ??
|
||||
item.result ??
|
||||
item.error ??
|
||||
item.message;
|
||||
const content =
|
||||
asString(rawContent) ||
|
||||
errorText(rawContent) ||
|
||||
stringifyUnknown(rawContent) ||
|
||||
`${name} ${isError ? "failed" : "completed"}`;
|
||||
|
||||
return [{
|
||||
kind: "tool_result",
|
||||
ts,
|
||||
toolUseId,
|
||||
content,
|
||||
isError,
|
||||
}];
|
||||
}
|
||||
|
||||
function parseCodexItem(
|
||||
item: Record<string, unknown>,
|
||||
ts: string,
|
||||
@@ -146,13 +192,7 @@ function parseCodexItem(
|
||||
}
|
||||
|
||||
if (itemType === "tool_use") {
|
||||
return [{
|
||||
kind: "tool_call",
|
||||
ts,
|
||||
name: asString(item.name, "unknown"),
|
||||
toolUseId: asString(item.id),
|
||||
input: item.input ?? {},
|
||||
}];
|
||||
return parseToolUseItem(item, ts, phase);
|
||||
}
|
||||
|
||||
if (itemType === "tool_result" && phase === "completed") {
|
||||
|
||||
@@ -130,6 +130,43 @@ describe("buildAssistantPartsFromTranscript", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats a completed tool-only segment as resolved once a tool_result arrives", () => {
|
||||
const result = buildAssistantPartsFromTranscript([
|
||||
{ kind: "thinking", ts: "2026-04-06T12:00:00.000Z", text: "Checking the task." },
|
||||
{
|
||||
kind: "tool_call",
|
||||
ts: "2026-04-06T12:00:01.000Z",
|
||||
name: "search",
|
||||
toolUseId: "tool-1",
|
||||
input: { query: "paperclip" },
|
||||
},
|
||||
{
|
||||
kind: "tool_result",
|
||||
ts: "2026-04-06T12:00:02.000Z",
|
||||
toolUseId: "tool-1",
|
||||
content: "search completed",
|
||||
isError: false,
|
||||
},
|
||||
{ kind: "assistant", ts: "2026-04-06T12:00:03.000Z", text: "Found the relevant code." },
|
||||
]);
|
||||
|
||||
expect(result.parts).toMatchObject([
|
||||
{ type: "reasoning", text: "Checking the task." },
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "tool-1",
|
||||
toolName: "search",
|
||||
result: "search completed",
|
||||
isError: false,
|
||||
},
|
||||
{ type: "text", text: "Found the relevant code." },
|
||||
]);
|
||||
expect(result.segments).toEqual([{
|
||||
startMs: new Date("2026-04-06T12:00:00.000Z").getTime(),
|
||||
endMs: new Date("2026-04-06T12:00:02.000Z").getTime(),
|
||||
}]);
|
||||
});
|
||||
|
||||
it("keeps run errors while suppressing init and system transcript noise", () => {
|
||||
const result = buildAssistantPartsFromTranscript([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user