Show every Codex terminal command in agent timelines (#2037)
* fix(codex): surface silent terminal activity Successful no-output commands use a null output field, while terminal interactions carry their stdin separately. Preserve both so the agent timeline remains auditable. * refactor(tests): share Codex event waiter * fix(codex): preserve bounded terminal audit details Keep terminal stdin through late command relabeling, cap it with other timeline content, and accept nullable output from legacy live completion events. * fix(codex): preserve repeated terminal writes
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { limitAgentTimelineItemContent } from "./agent-timeline-content.js";
|
||||||
|
|
||||||
|
describe("agent timeline content", () => {
|
||||||
|
test("limits terminal input to the tool-call content budget", () => {
|
||||||
|
const oversizedInput = "x".repeat(64 * 1024 + 1);
|
||||||
|
|
||||||
|
const item = limitAgentTimelineItemContent({
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "terminal-session-4242",
|
||||||
|
name: "terminal",
|
||||||
|
status: "completed",
|
||||||
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: oversizedInput,
|
||||||
|
icon: "square_terminal",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(item).toEqual({
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "terminal-session-4242",
|
||||||
|
name: "terminal",
|
||||||
|
status: "completed",
|
||||||
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: "x".repeat(64 * 1024),
|
||||||
|
icon: "square_terminal",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,8 +24,27 @@ function limitFailedShellError(item: AgentTimelineItem): AgentTimelineItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function limitPlainText(item: AgentTimelineItem): AgentTimelineItem {
|
||||||
|
if (
|
||||||
|
item.type !== "tool_call" ||
|
||||||
|
item.detail.type !== "plain_text" ||
|
||||||
|
typeof item.detail.text !== "string" ||
|
||||||
|
item.detail.text.length <= TOOL_CALL_CONTENT_MAX_LENGTH
|
||||||
|
) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
detail: {
|
||||||
|
...item.detail,
|
||||||
|
text: item.detail.text.slice(0, TOOL_CALL_CONTENT_MAX_LENGTH),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function limitAgentTimelineItemContent(item: AgentTimelineItem): AgentTimelineItem {
|
export function limitAgentTimelineItemContent(item: AgentTimelineItem): AgentTimelineItem {
|
||||||
item = limitFailedShellError(item);
|
item = limitFailedShellError(item);
|
||||||
|
item = limitPlainText(item);
|
||||||
if (
|
if (
|
||||||
item.type !== "tool_call" ||
|
item.type !== "tool_call" ||
|
||||||
item.detail.type !== "shell" ||
|
item.detail.type !== "shell" ||
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import {
|
|||||||
createFakeCodexAppServer,
|
createFakeCodexAppServer,
|
||||||
type FakeCodexAppServer,
|
type FakeCodexAppServer,
|
||||||
waitForNextPermission,
|
waitForNextPermission,
|
||||||
|
waitForNextTimelineItem,
|
||||||
|
waitForTimelineToolCall,
|
||||||
} from "./codex/test-utils/fake-app-server.js";
|
} from "./codex/test-utils/fake-app-server.js";
|
||||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
import { asInternals as castInternals, createStub } from "../../test-utils/class-mocks.js";
|
import { asInternals as castInternals, createStub } from "../../test-utils/class-mocks.js";
|
||||||
@@ -554,6 +556,215 @@ describe("Codex app-server provider", () => {
|
|||||||
await session.close();
|
await session.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("shows a successful shell command that produces no output", async () => {
|
||||||
|
const appServer = createFakeCodexAppServer();
|
||||||
|
const session = new CodexAppServerAgentSession(
|
||||||
|
createConfig({ cwd: "/workspace/project" }),
|
||||||
|
null,
|
||||||
|
createTestLogger(),
|
||||||
|
async () => appServer.child,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await session.connect();
|
||||||
|
const nextTimelineItem = waitForNextTimelineItem(session);
|
||||||
|
|
||||||
|
appServer.completesSilentCommand({
|
||||||
|
threadId: "thread-1",
|
||||||
|
callId: "silent-merge",
|
||||||
|
command: "gh pr merge 2030 --squash",
|
||||||
|
cwd: "/workspace/project",
|
||||||
|
});
|
||||||
|
appServer.says({ threadId: "thread-1", text: "Merged." });
|
||||||
|
|
||||||
|
await expect(nextTimelineItem).resolves.toEqual({
|
||||||
|
type: "timeline",
|
||||||
|
provider: "codex",
|
||||||
|
item: {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "silent-merge",
|
||||||
|
name: "shell",
|
||||||
|
status: "completed",
|
||||||
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "shell",
|
||||||
|
command: "gh pr merge 2030 --squash",
|
||||||
|
cwd: "/workspace/project",
|
||||||
|
exitCode: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
appServer.assertNoErrors();
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows a silent shell command from legacy live notifications", async () => {
|
||||||
|
const appServer = createFakeCodexAppServer();
|
||||||
|
const session = new CodexAppServerAgentSession(
|
||||||
|
createConfig({ cwd: "/workspace/project" }),
|
||||||
|
null,
|
||||||
|
createTestLogger(),
|
||||||
|
async () => appServer.child,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await session.connect();
|
||||||
|
const nextTimelineItem = waitForNextTimelineItem(session);
|
||||||
|
|
||||||
|
appServer.completesSilentLegacyCommand({
|
||||||
|
threadId: "thread-1",
|
||||||
|
callId: "legacy-silent-merge",
|
||||||
|
command: "gh pr merge 2030 --squash",
|
||||||
|
cwd: "/workspace/project",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(nextTimelineItem).resolves.toEqual({
|
||||||
|
type: "timeline",
|
||||||
|
provider: "codex",
|
||||||
|
item: {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "legacy-silent-merge",
|
||||||
|
name: "shell",
|
||||||
|
status: "completed",
|
||||||
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "shell",
|
||||||
|
command: "gh pr merge 2030 --squash",
|
||||||
|
cwd: "/workspace/project",
|
||||||
|
exitCode: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
appServer.assertNoErrors();
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows the exact bytes Codex writes into an existing terminal", async () => {
|
||||||
|
const appServer = createFakeCodexAppServer();
|
||||||
|
const session = new CodexAppServerAgentSession(
|
||||||
|
createConfig({ cwd: "/workspace/project" }),
|
||||||
|
null,
|
||||||
|
createTestLogger(),
|
||||||
|
async () => appServer.child,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await session.connect();
|
||||||
|
const nextTimelineItem = waitForNextTimelineItem(session);
|
||||||
|
|
||||||
|
appServer.typesIntoTerminal({
|
||||||
|
threadId: "thread-1",
|
||||||
|
turnId: "turn-1",
|
||||||
|
itemId: "interactive-shell",
|
||||||
|
processId: "4242",
|
||||||
|
text: "gh pr merge 2030 --squash\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(nextTimelineItem).resolves.toEqual({
|
||||||
|
type: "timeline",
|
||||||
|
provider: "codex",
|
||||||
|
item: {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "terminal-session-4242-1",
|
||||||
|
name: "terminal",
|
||||||
|
status: "completed",
|
||||||
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "plain_text",
|
||||||
|
text: "gh pr merge 2030 --squash\n",
|
||||||
|
icon: "square_terminal",
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
processId: "4242",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const relabeledTerminal = waitForTimelineToolCall(session, "terminal-session-4242-1");
|
||||||
|
appServer.runsLegacyCommand({
|
||||||
|
threadId: "thread-1",
|
||||||
|
callId: "interactive-shell",
|
||||||
|
command: "sleep 30",
|
||||||
|
output: "Process running with session id 4242",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(relabeledTerminal).resolves.toEqual({
|
||||||
|
type: "timeline",
|
||||||
|
provider: "codex",
|
||||||
|
item: {
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "terminal-session-4242-1",
|
||||||
|
name: "terminal",
|
||||||
|
status: "completed",
|
||||||
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "plain_text",
|
||||||
|
label: "sleep 30",
|
||||||
|
text: "gh pr merge 2030 --squash\n",
|
||||||
|
icon: "square_terminal",
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
processId: "4242",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
appServer.assertNoErrors();
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps repeated writes to one terminal as separate timeline rows", async () => {
|
||||||
|
const appServer = createFakeCodexAppServer();
|
||||||
|
const session = new CodexAppServerAgentSession(
|
||||||
|
createConfig({ cwd: "/workspace/project" }),
|
||||||
|
null,
|
||||||
|
createTestLogger(),
|
||||||
|
async () => appServer.child,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await session.connect();
|
||||||
|
|
||||||
|
const firstTimelineItem = waitForNextTimelineItem(session);
|
||||||
|
appServer.typesIntoTerminal({
|
||||||
|
threadId: "thread-1",
|
||||||
|
turnId: "turn-1",
|
||||||
|
itemId: "interactive-shell",
|
||||||
|
processId: "4242",
|
||||||
|
text: "git status\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondTimelineItem = waitForNextTimelineItem(session);
|
||||||
|
appServer.typesIntoTerminal({
|
||||||
|
threadId: "thread-1",
|
||||||
|
turnId: "turn-1",
|
||||||
|
itemId: "interactive-shell",
|
||||||
|
processId: "4242",
|
||||||
|
text: "git push\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [first, second] = await Promise.all([firstTimelineItem, secondTimelineItem]);
|
||||||
|
expect(first.item).toMatchObject({
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "terminal-session-4242-1",
|
||||||
|
detail: { type: "plain_text", text: "git status\n" },
|
||||||
|
});
|
||||||
|
expect(second.item).toMatchObject({
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "terminal-session-4242-2",
|
||||||
|
detail: { type: "plain_text", text: "git push\n" },
|
||||||
|
});
|
||||||
|
appServer.assertNoErrors();
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("surfaces an MCP elicitation and returns Codex's required approval action", async () => {
|
test("surfaces an MCP elicitation and returns Codex's required approval action", async () => {
|
||||||
const appServer = createFakeCodexAppServer();
|
const appServer = createFakeCodexAppServer();
|
||||||
const session = new CodexAppServerAgentSession(
|
const session = new CodexAppServerAgentSession(
|
||||||
|
|||||||
@@ -1514,24 +1514,23 @@ export function mapCodexPatchNotificationToToolCall(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapCodexTerminalInteractionToToolCall(params: {
|
function mapCodexTerminalInteractionToToolCall(params: {
|
||||||
|
callId: string;
|
||||||
processId?: string | null;
|
processId?: string | null;
|
||||||
fallbackCallId?: string | null;
|
|
||||||
command?: string | null;
|
command?: string | null;
|
||||||
|
stdin?: string | null;
|
||||||
}): ToolCallTimelineItem {
|
}): ToolCallTimelineItem {
|
||||||
const processId = nonEmptyString(params.processId ?? undefined);
|
const processId = nonEmptyString(params.processId ?? undefined);
|
||||||
const callId = processId
|
|
||||||
? `terminal-session-${processId}`
|
|
||||||
: (nonEmptyString(params.fallbackCallId ?? undefined) ?? "terminal-interaction");
|
|
||||||
const label = nonEmptyString(params.command ?? undefined);
|
const label = nonEmptyString(params.command ?? undefined);
|
||||||
return {
|
return {
|
||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
callId,
|
callId: params.callId,
|
||||||
name: "terminal",
|
name: "terminal",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
error: null,
|
error: null,
|
||||||
detail: {
|
detail: {
|
||||||
type: "plain_text",
|
type: "plain_text",
|
||||||
...(label ? { label } : {}),
|
...(label ? { label } : {}),
|
||||||
|
...(params.stdin !== null && params.stdin !== undefined ? { text: params.stdin } : {}),
|
||||||
icon: "square_terminal",
|
icon: "square_terminal",
|
||||||
},
|
},
|
||||||
...(processId ? { metadata: { processId } } : {}),
|
...(processId ? { metadata: { processId } } : {}),
|
||||||
@@ -2111,8 +2110,8 @@ const CodexEventExecCommandEndNotificationSchema = z
|
|||||||
cwd: z.string().optional(),
|
cwd: z.string().optional(),
|
||||||
stdout: z.string().optional(),
|
stdout: z.string().optional(),
|
||||||
stderr: z.string().optional(),
|
stderr: z.string().optional(),
|
||||||
aggregated_output: z.string().optional(),
|
aggregated_output: z.string().nullable().optional(),
|
||||||
aggregatedOutput: z.string().optional(),
|
aggregatedOutput: z.string().nullable().optional(),
|
||||||
formatted_output: z.string().optional(),
|
formatted_output: z.string().optional(),
|
||||||
exit_code: z.number().nullable().optional(),
|
exit_code: z.number().nullable().optional(),
|
||||||
exitCode: z.number().nullable().optional(),
|
exitCode: z.number().nullable().optional(),
|
||||||
@@ -3102,7 +3101,11 @@ export class CodexAppServerAgentSession implements AgentSession {
|
|||||||
private pendingFileChangeOutputDeltas = new Map<string, string[]>();
|
private pendingFileChangeOutputDeltas = new Map<string, string[]>();
|
||||||
private pendingAssistantMessageBoundary = false;
|
private pendingAssistantMessageBoundary = false;
|
||||||
private terminalCommandByProcessId = new Map<string, string>();
|
private terminalCommandByProcessId = new Map<string, string>();
|
||||||
private pendingUnlabeledTerminalInteractions = new Set<string>();
|
private pendingUnlabeledTerminalInteractions = new Map<
|
||||||
|
string,
|
||||||
|
Array<{ callId: string; stdin: string | null }>
|
||||||
|
>();
|
||||||
|
private nextTerminalInteractionOrdinal = 0;
|
||||||
private emittedTerminalInteractionKeys = new Set<string>();
|
private emittedTerminalInteractionKeys = new Set<string>();
|
||||||
private emittedExecCommandStartedCallIds = new Set<string>();
|
private emittedExecCommandStartedCallIds = new Set<string>();
|
||||||
private emittedExecCommandCompletedCallIds = new Set<string>();
|
private emittedExecCommandCompletedCallIds = new Set<string>();
|
||||||
@@ -5447,13 +5450,18 @@ export class CodexAppServerAgentSession implements AgentSession {
|
|||||||
const command =
|
const command =
|
||||||
(parsed.processId ? this.terminalCommandByProcessId.get(parsed.processId) : undefined) ??
|
(parsed.processId ? this.terminalCommandByProcessId.get(parsed.processId) : undefined) ??
|
||||||
null;
|
null;
|
||||||
|
const callId = this.createTerminalInteractionCallId(parsed.processId, parsed.callId);
|
||||||
if (!command && parsed.processId) {
|
if (!command && parsed.processId) {
|
||||||
this.pendingUnlabeledTerminalInteractions.add(parsed.processId);
|
const pendingInteractions =
|
||||||
|
this.pendingUnlabeledTerminalInteractions.get(parsed.processId) ?? [];
|
||||||
|
pendingInteractions.push({ callId, stdin: parsed.stdin });
|
||||||
|
this.pendingUnlabeledTerminalInteractions.set(parsed.processId, pendingInteractions);
|
||||||
}
|
}
|
||||||
const timelineItem = mapCodexTerminalInteractionToToolCall({
|
const timelineItem = mapCodexTerminalInteractionToToolCall({
|
||||||
|
callId,
|
||||||
processId: parsed.processId,
|
processId: parsed.processId,
|
||||||
fallbackCallId: parsed.callId,
|
|
||||||
command,
|
command,
|
||||||
|
stdin: parsed.stdin,
|
||||||
});
|
});
|
||||||
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
|
||||||
}
|
}
|
||||||
@@ -5862,15 +5870,31 @@ export class CodexAppServerAgentSession implements AgentSession {
|
|||||||
if (!this.pendingUnlabeledTerminalInteractions.has(processId)) {
|
if (!this.pendingUnlabeledTerminalInteractions.has(processId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const pendingInteractions = this.pendingUnlabeledTerminalInteractions.get(processId) ?? [];
|
||||||
this.pendingUnlabeledTerminalInteractions.delete(processId);
|
this.pendingUnlabeledTerminalInteractions.delete(processId);
|
||||||
this.emitEvent({
|
for (const pendingInteraction of pendingInteractions) {
|
||||||
type: "timeline",
|
this.emitEvent({
|
||||||
provider: CODEX_PROVIDER,
|
type: "timeline",
|
||||||
item: mapCodexTerminalInteractionToToolCall({
|
provider: CODEX_PROVIDER,
|
||||||
processId,
|
item: mapCodexTerminalInteractionToToolCall({
|
||||||
command: displayCommand,
|
callId: pendingInteraction.callId,
|
||||||
}),
|
processId,
|
||||||
});
|
command: displayCommand,
|
||||||
|
stdin: pendingInteraction.stdin,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createTerminalInteractionCallId(
|
||||||
|
processId: string | null,
|
||||||
|
fallbackCallId: string | null,
|
||||||
|
): string {
|
||||||
|
const baseCallId = processId
|
||||||
|
? `terminal-session-${processId}`
|
||||||
|
: (nonEmptyString(fallbackCallId ?? undefined) ?? "terminal-interaction");
|
||||||
|
this.nextTerminalInteractionOrdinal += 1;
|
||||||
|
return `${baseCallId}-${this.nextTerminalInteractionOrdinal}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldEmitTerminalInteractionKey(key: string): boolean {
|
private shouldEmitTerminalInteractionKey(key: string): boolean {
|
||||||
|
|||||||
@@ -19,6 +19,19 @@ interface FakeLegacyCommand {
|
|||||||
command: string;
|
command: string;
|
||||||
output: string;
|
output: string;
|
||||||
}
|
}
|
||||||
|
interface FakeSilentCommand {
|
||||||
|
threadId: string;
|
||||||
|
callId: string;
|
||||||
|
command: string;
|
||||||
|
cwd: string;
|
||||||
|
}
|
||||||
|
interface FakeTerminalInput {
|
||||||
|
threadId: string;
|
||||||
|
turnId: string;
|
||||||
|
itemId: string;
|
||||||
|
processId: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
interface FakeLegacyPatch {
|
interface FakeLegacyPatch {
|
||||||
threadId: string;
|
threadId: string;
|
||||||
callId: string;
|
callId: string;
|
||||||
@@ -51,6 +64,9 @@ export interface FakeCodexAppServer {
|
|||||||
runsLegacyCommand(params: FakeLegacyCommand): void;
|
runsLegacyCommand(params: FakeLegacyCommand): void;
|
||||||
appliesLegacyPatch(params: FakeLegacyPatch): void;
|
appliesLegacyPatch(params: FakeLegacyPatch): void;
|
||||||
completesCommand(params: FakeLegacyCommand): void;
|
completesCommand(params: FakeLegacyCommand): void;
|
||||||
|
completesSilentCommand(params: FakeSilentCommand): void;
|
||||||
|
completesSilentLegacyCommand(params: FakeSilentCommand): void;
|
||||||
|
typesIntoTerminal(params: FakeTerminalInput): void;
|
||||||
says(params: { threadId: string; itemId?: string; text: string; chunks?: string[] }): void;
|
says(params: { threadId: string; itemId?: string; text: string; chunks?: string[] }): void;
|
||||||
requestCommandApproval(params: {
|
requestCommandApproval(params: {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
@@ -366,6 +382,37 @@ export function createFakeCodexAppServer(
|
|||||||
exitCode: 0,
|
exitCode: 0,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
completesSilentCommand(params) {
|
||||||
|
completeItem(params.threadId, {
|
||||||
|
type: "commandExecution",
|
||||||
|
id: params.callId,
|
||||||
|
status: "completed",
|
||||||
|
command: params.command,
|
||||||
|
cwd: params.cwd,
|
||||||
|
aggregatedOutput: null,
|
||||||
|
exitCode: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
completesSilentLegacyCommand(params) {
|
||||||
|
writeLegacyEvent(params.threadId, "codex/event/exec_command_end", {
|
||||||
|
type: "exec_command_end",
|
||||||
|
call_id: params.callId,
|
||||||
|
command: params.command,
|
||||||
|
cwd: params.cwd,
|
||||||
|
aggregatedOutput: null,
|
||||||
|
exit_code: 0,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
typesIntoTerminal(params) {
|
||||||
|
writeNotification("item/commandExecution/terminalInteraction", {
|
||||||
|
threadId: params.threadId,
|
||||||
|
turnId: params.turnId,
|
||||||
|
itemId: params.itemId,
|
||||||
|
processId: params.processId,
|
||||||
|
stdin: params.text,
|
||||||
|
});
|
||||||
|
},
|
||||||
says(params) {
|
says(params) {
|
||||||
if (params.itemId) {
|
if (params.itemId) {
|
||||||
for (const chunk of params.chunks ?? [params.text]) {
|
for (const chunk of params.chunks ?? [params.text]) {
|
||||||
@@ -458,21 +505,53 @@ function toJsonObject(value: unknown): JsonObject {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function waitForNextPermission(
|
type StreamEventType = AgentStreamEvent["type"];
|
||||||
|
type StreamEventOfType<TType extends StreamEventType> = Extract<AgentStreamEvent, { type: TType }>;
|
||||||
|
|
||||||
|
function waitForNextEvent<TType extends StreamEventType>(
|
||||||
session: AgentSession,
|
session: AgentSession,
|
||||||
): Promise<Extract<AgentStreamEvent, { type: "permission_requested" }>> {
|
type: TType,
|
||||||
|
accepts?: (event: StreamEventOfType<TType>) => boolean,
|
||||||
|
): Promise<StreamEventOfType<TType>> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
reject(new Error("Timed out waiting for permission_requested"));
|
reject(new Error(`Timed out waiting for ${type}`));
|
||||||
}, 1000);
|
}, 1000);
|
||||||
const unsubscribe = session.subscribe((event) => {
|
const unsubscribe = session.subscribe((event) => {
|
||||||
if (event.type !== "permission_requested") {
|
if (event.type !== type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const typedEvent = event as StreamEventOfType<TType>;
|
||||||
|
if (accepts && !accepts(typedEvent)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
resolve(event);
|
resolve(typedEvent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TimelineEvent = StreamEventOfType<"timeline">;
|
||||||
|
|
||||||
|
export function waitForNextPermission(
|
||||||
|
session: AgentSession,
|
||||||
|
): Promise<StreamEventOfType<"permission_requested">> {
|
||||||
|
return waitForNextEvent(session, "permission_requested");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitForNextTimelineItem(session: AgentSession): Promise<TimelineEvent> {
|
||||||
|
return waitForNextEvent(session, "timeline");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitForTimelineToolCall(
|
||||||
|
session: AgentSession,
|
||||||
|
callId: string,
|
||||||
|
): Promise<TimelineEvent> {
|
||||||
|
return waitForNextEvent(
|
||||||
|
session,
|
||||||
|
"timeline",
|
||||||
|
(event) => event.item.type === "tool_call" && event.item.callId === callId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const CodexCommandExecutionItemSchema = z
|
|||||||
error: z.unknown().optional(),
|
error: z.unknown().optional(),
|
||||||
command: CodexCommandValueSchema.optional(),
|
command: CodexCommandValueSchema.optional(),
|
||||||
cwd: z.string().optional(),
|
cwd: z.string().optional(),
|
||||||
aggregatedOutput: z.string().optional(),
|
aggregatedOutput: z.string().nullable().optional(),
|
||||||
exitCode: z.number().nullable().optional(),
|
exitCode: z.number().nullable().optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
@@ -694,7 +694,7 @@ function mapCommandExecutionItem(
|
|||||||
item: z.infer<typeof CodexCommandExecutionItemSchema>,
|
item: z.infer<typeof CodexCommandExecutionItemSchema>,
|
||||||
): CodexNormalizedToolCallEnvelope {
|
): CodexNormalizedToolCallEnvelope {
|
||||||
const command = normalizeCommandExecutionCommand(item.command);
|
const command = normalizeCommandExecutionCommand(item.command);
|
||||||
const parsedOutput = extractCodexShellOutput(item.aggregatedOutput);
|
const parsedOutput = extractCodexShellOutput(item.aggregatedOutput ?? undefined);
|
||||||
const input = toNullableObject({
|
const input = toNullableObject({
|
||||||
...(command !== undefined ? { command } : {}),
|
...(command !== undefined ? { command } : {}),
|
||||||
...(item.cwd !== undefined ? { cwd: item.cwd } : {}),
|
...(item.cwd !== undefined ? { cwd: item.cwd } : {}),
|
||||||
|
|||||||
Reference in New Issue
Block a user