Fix Codex tool-use transcript completion

This commit is contained in:
dotta
2026-04-08 08:02:27 -05:00
parent 844b061267
commit 9eaf72ab31
3 changed files with 167 additions and 7 deletions
@@ -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") {
+37
View File
@@ -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([
{