Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions src/ResponseItemHistoryFallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export function parseResponseItemHistoryFallback(
case "reasoning":
pushUpdates(createReasoningUpdates(item));
break;
case "custom_tool_call":
case "function_call": {
const toolCallId = stringValue(item["call_id"]);
if (toolCallId && existingToolCallIds.has(toolCallId)) {
Expand All @@ -128,6 +129,7 @@ export function parseResponseItemHistoryFallback(
pushUpdates([result.update]);
break;
}
case "custom_tool_call_output":
case "function_call_output": {
const toolCallId = stringValue(item["call_id"]);
if (toolCallId && skippedToolCallIds.has(toolCallId)) {
Expand Down Expand Up @@ -368,8 +370,8 @@ function createFunctionCallUpdate(item: JsonRecord): LegacyFunctionCallUpdate |
return null;
}

const isExecCommand = name === "exec_command";
const args = parseFunctionArguments(item["arguments"]);
const isExecCommand = name === "exec_command" || name === "exec";
const args = parseFunctionArguments(item["input"] ?? item["arguments"]);
const command = isExecCommand ? commandFromFunctionArguments(args) : null;
const cwd = isExecCommand ? cwdFromFunctionArguments(args) : "";
const commandAction = command ? inferCommandAction(command, cwd) : null;
Expand All @@ -390,7 +392,7 @@ function createFunctionCallUpdate(item: JsonRecord): LegacyFunctionCallUpdate |
rawInput: rawInputForFunctionCall(name, args),
};

if (!functionCallUsesTerminal(item)) {
if (!isExecCommand) {
return { update, usesTerminal: false, isExecCommand };
}

Expand Down Expand Up @@ -500,6 +502,7 @@ function titleForFunctionCall(name: string, args: unknown): string {

function toolKindForFunctionCall(name: string): AcpToolKind {
switch (name) {
case "exec":
case "exec_command":
case "multi_tool_use.parallel":
return "execute";
Expand All @@ -512,10 +515,6 @@ function toolKindForFunctionCall(name: string): AcpToolKind {
}
}

function functionCallUsesTerminal(item: JsonRecord): boolean {
return item["name"] === "exec_command";
}

function commandFromFunctionArguments(args: unknown): string | null {
const record = asRecord(args);
if (!record) {
Expand Down
102 changes: 102 additions & 0 deletions src/__tests__/CodexACPAgent/response-item-history-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,90 @@ describe("ResponseItemHistoryFallback", () => {
expect(toolCallUpdateStatuses(updates)).toEqual([]);
});

it("recovers scripted tool calls recorded as custom_tool_call", () => {
const updates = parseResponseItemHistoryFallback(jsonl([
customToolCall(
"call-scripted",
'const r = await tools.exec_command({"cmd":"echo hello"});\ntext(r.output);\n',
),
customToolCallOutput("call-scripted", "Script completed\nOutput:\n", "hello\n"),
]), "terminal_output", new Set());

expect(toolCallIds(updates)).toEqual(["call-scripted"]);
expect(toolCallUpdateStatuses(updates)).toEqual([
{ toolCallId: "call-scripted", status: "completed" },
]);
});

it("surfaces the script itself, since what a script ran is not recorded", () => {
const script = 'for (const cmd of ["echo alpha", "echo beta"]) { await tools.exec_command({cmd}); }';
const updates = parseResponseItemHistoryFallback(jsonl([
customToolCall("call-loop", script),
]), "terminal_output", new Set());

const serialized = JSON.stringify(updates);
expect(serialized).toContain("echo alpha");
expect(serialized).toContain("echo beta");
});

it("maps a scripted call to the execute kind", () => {
const updates = parseResponseItemHistoryFallback(jsonl([
customToolCall("call-kind", 'await tools.exec_command({"cmd":"ls"});'),
]), "terminal_output", new Set());

const toolCall = (updates ?? []).find((update) => update.sessionUpdate === "tool_call");
expect(toolCall && "kind" in toolCall ? toolCall.kind : null).toBe("execute");
});

it("carries the combined script output through", () => {
const updates = parseResponseItemHistoryFallback(jsonl([
customToolCall("call-out", 'await tools.exec_command({"cmd":"ls"});'),
customToolCallOutput("call-out", "Output:\n", "1: alpha\n", "2: beta\n"),
]), "terminal_output", new Set());

const serialized = JSON.stringify(updates);
expect(serialized).toContain("1: alpha");
expect(serialized).toContain("2: beta");
});

it("skips scripted calls already present in the parsed thread", () => {
const updates = parseResponseItemHistoryFallback(jsonl([
customToolCall("call-existing", 'await tools.exec_command({"cmd":"ls"});'),
customToolCallOutput("call-existing", "Output:\n", "ok\n"),
]), "terminal_output", new Set(["call-existing"]));

expect(toolCallIds(updates)).toEqual([]);
expect(toolCallUpdateStatuses(updates)).toEqual([]);
});

it("reports a failing script as failed", () => {
const updates = parseResponseItemHistoryFallback(jsonl([
customToolCall("call-fail", 'await tools.exec_command({"cmd":"false"});'),
customToolCallOutput("call-fail", "Permission denied\n"),
]), "terminal_output", new Set());

expect(toolCallUpdateStatuses(updates)).toEqual([
{ toolCallId: "call-fail", status: "failed" },
]);
});

it("still recovers a scripted call whose tool name is unfamiliar", () => {
const updates = parseResponseItemHistoryFallback(jsonl([
{
type: "response_item",
payload: {
type: "custom_tool_call",
call_id: "call-other",
name: "some_other_sandbox",
input: 'await tools.exec_command({"cmd":"ls"});',
},
},
]), "terminal_output", new Set());

expect(toolCallIds(updates)).toEqual(["call-other"]);
expect(JSON.stringify(updates)).toContain("tools.exec_command");
});

it("does not duplicate adjacent reasoning from event and response item records", () => {
const updates = parseResponseItemHistoryFallback(jsonl([
{
Expand Down Expand Up @@ -129,6 +213,24 @@ function functionCallOutput(callId: string, output: string): unknown {
};
}

function customToolCall(callId: string, input: string): unknown {
return {
type: "response_item",
payload: { type: "custom_tool_call", call_id: callId, name: "exec", input },
};
}

function customToolCallOutput(callId: string, ...texts: string[]): unknown {
return {
type: "response_item",
payload: {
type: "custom_tool_call_output",
call_id: callId,
output: texts.map((text) => ({ type: "input_text", text })),
},
};
}

function toolCallIds(updates: UpdateSessionEvent[] | null): string[] {
return (updates ?? [])
.filter((update): update is Extract<UpdateSessionEvent, { sessionUpdate: "tool_call" }> => (
Expand Down