mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Fix Codex tool-use transcript completion
This commit is contained in:
parent
844b061267
commit
9eaf72ab31
3 changed files with 167 additions and 7 deletions
83
packages/adapters/codex-local/src/ui/parse-stdout.test.ts
Normal file
83
packages/adapters/codex-local/src/ui/parse-stdout.test.ts
Normal file
|
|
@ -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}` }];
|
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(
|
function parseCodexItem(
|
||||||
item: Record<string, unknown>,
|
item: Record<string, unknown>,
|
||||||
ts: string,
|
ts: string,
|
||||||
|
|
@ -146,13 +192,7 @@ function parseCodexItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemType === "tool_use") {
|
if (itemType === "tool_use") {
|
||||||
return [{
|
return parseToolUseItem(item, ts, phase);
|
||||||
kind: "tool_call",
|
|
||||||
ts,
|
|
||||||
name: asString(item.name, "unknown"),
|
|
||||||
toolUseId: asString(item.id),
|
|
||||||
input: item.input ?? {},
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemType === "tool_result" && phase === "completed") {
|
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", () => {
|
it("keeps run errors while suppressing init and system transcript noise", () => {
|
||||||
const result = buildAssistantPartsFromTranscript([
|
const result = buildAssistantPartsFromTranscript([
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue