Rename conversation to thread in codex exec (#4482)

This commit is contained in:
pakrym-oai
2025-09-29 20:18:30 -07:00
committed by GitHub
parent a8edc57740
commit ea82f86662
11 changed files with 211 additions and 229 deletions

View File

@@ -4,7 +4,7 @@ import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { Codex } from "@openai/codex-sdk";
import type { ConversationEvent, ConversationItem } from "@openai/codex-sdk";
import type { ThreadEvent, ThreadItem } from "@openai/codex-sdk";
import path from "node:path";
const executablePath =
@@ -15,7 +15,7 @@ const codex = new Codex({ executablePath });
const thread = codex.startThread();
const rl = createInterface({ input, output });
const handleItemCompleted = (item: ConversationItem): void => {
const handleItemCompleted = (item: ThreadItem): void => {
switch (item.item_type) {
case "assistant_message":
console.log(`Assistant: ${item.text}`);
@@ -37,7 +37,7 @@ const handleItemCompleted = (item: ConversationItem): void => {
}
};
const handleItemUpdated = (item: ConversationItem): void => {
const handleItemUpdated = (item: ThreadItem): void => {
switch (item.item_type) {
case "todo_list": {
console.log(`Todo:`);
@@ -49,7 +49,7 @@ const handleItemUpdated = (item: ConversationItem): void => {
}
};
const handleEvent = (event: ConversationEvent): void => {
const handleEvent = (event: ThreadEvent): void => {
switch (event.type) {
case "item.completed":
handleItemCompleted(event.item);
@@ -63,6 +63,9 @@ const handleEvent = (event: ConversationEvent): void => {
`Used ${event.usage.input_tokens} input tokens, ${event.usage.cached_input_tokens} cached input tokens, ${event.usage.output_tokens} output tokens.`,
);
break;
case "turn.failed":
console.error(`Turn failed: ${event.error.message}`);
break;
}
};

View File

@@ -1,10 +1,10 @@
// based on event types from codex-rs/exec/src/exec_events.rs
import type { ConversationItem } from "./items";
import type { ThreadItem } from "./items";
export type SessionCreatedEvent = {
type: "session.created";
session_id: string;
export type ThreadStartedEvent = {
type: "thread.started";
thread_id: string;
};
export type TurnStartedEvent = {
@@ -22,31 +22,41 @@ export type TurnCompletedEvent = {
usage: Usage;
};
export type TurnFailedEvent = {
type: "turn.failed";
error: ThreadError;
};
export type ItemStartedEvent = {
type: "item.started";
item: ConversationItem;
item: ThreadItem;
};
export type ItemUpdatedEvent = {
type: "item.updated";
item: ConversationItem;
item: ThreadItem;
};
export type ItemCompletedEvent = {
type: "item.completed";
item: ConversationItem;
item: ThreadItem;
};
export type ConversationErrorEvent = {
export type ThreadError = {
message: string;
};
export type ThreadErrorEvent = {
type: "error";
message: string;
};
export type ConversationEvent =
| SessionCreatedEvent
export type ThreadEvent =
| ThreadStartedEvent
| TurnStartedEvent
| TurnCompletedEvent
| TurnFailedEvent
| ItemStartedEvent
| ItemUpdatedEvent
| ItemCompletedEvent
| ConversationErrorEvent;
| ThreadErrorEvent;

View File

@@ -6,7 +6,7 @@ export type CodexExecArgs = {
baseUrl?: string;
apiKey?: string;
sessionId?: string | null;
threadId?: string | null;
};
export class CodexExec {
@@ -17,8 +17,8 @@ export class CodexExec {
async *run(args: CodexExecArgs): AsyncGenerator<string> {
const commandArgs: string[] = ["exec", "--experimental-json"];
if (args.sessionId) {
commandArgs.push("resume", args.sessionId, args.input);
if (args.threadId) {
commandArgs.push("resume", args.threadId, args.input);
} else {
commandArgs.push(args.input);
}

View File

@@ -1,15 +1,17 @@
export type {
ConversationEvent,
SessionCreatedEvent,
ThreadEvent,
ThreadStartedEvent,
TurnStartedEvent,
TurnCompletedEvent,
TurnFailedEvent,
ItemStartedEvent,
ItemUpdatedEvent,
ItemCompletedEvent,
ConversationErrorEvent,
ThreadError,
ThreadErrorEvent,
} from "./events";
export type {
ConversationItem,
ThreadItem,
AssistantMessageItem,
ReasoningItem,
CommandExecutionItem,

View File

@@ -78,17 +78,7 @@ export type SessionItem = {
session_id: string;
};
export type ConversationItem =
| AssistantMessageItem
| ReasoningItem
| CommandExecutionItem
| FileChangeItem
| McpToolCallItem
| WebSearchItem
| TodoListItem
| ErrorItem;
export type ConversationItemDetails =
export type ThreadItem =
| AssistantMessageItem
| ReasoningItem
| CommandExecutionItem

View File

@@ -1,15 +1,15 @@
import { CodexOptions } from "./codexOptions";
import { ConversationEvent } from "./events";
import { ThreadEvent } from "./events";
import { CodexExec } from "./exec";
import { ConversationItem } from "./items";
import { ThreadItem } from "./items";
export type RunResult = {
items: ConversationItem[];
items: ThreadItem[];
finalResponse: string;
};
export type RunStreamedResult = {
events: AsyncGenerator<ConversationEvent>;
events: AsyncGenerator<ThreadEvent>;
};
export type Input = string;
@@ -29,17 +29,17 @@ export class Thread {
return { events: this.runStreamedInternal(input) };
}
private async *runStreamedInternal(input: string): AsyncGenerator<ConversationEvent> {
private async *runStreamedInternal(input: string): AsyncGenerator<ThreadEvent> {
const generator = this.exec.run({
input,
baseUrl: this.options.baseUrl,
apiKey: this.options.apiKey,
sessionId: this.id,
threadId: this.id,
});
for await (const item of generator) {
const parsed = JSON.parse(item) as ConversationEvent;
if (parsed.type === "session.created") {
this.id = parsed.session_id;
const parsed = JSON.parse(item) as ThreadEvent;
if (parsed.type === "thread.started") {
this.id = parsed.thread_id;
}
yield parsed;
}
@@ -47,7 +47,7 @@ export class Thread {
async run(input: string): Promise<RunResult> {
const generator = this.runStreamedInternal(input);
const items: ConversationItem[] = [];
const items: ThreadItem[] = [];
let finalResponse: string = "";
for await (const event of generator) {
if (event.type === "item.completed") {

View File

@@ -15,7 +15,7 @@ import {
const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
describe("Codex", () => {
it("returns session events", async () => {
it("returns thread events", async () => {
const { url, close } = await startResponsesTestProxy({
statusCode: 200,
responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())],
@@ -65,7 +65,7 @@ describe("Codex", () => {
await thread.run("first input");
await thread.run("second input");
// Check second request continues conversation
// Check second request continues the same thread
expect(requests.length).toBeGreaterThanOrEqual(2);
const secondRequest = requests[1];
expect(secondRequest).toBeDefined();

View File

@@ -3,7 +3,7 @@ import path from "path";
import { describe, expect, it } from "@jest/globals";
import { Codex } from "../src/codex";
import { ConversationEvent } from "../src/index";
import { ThreadEvent } from "../src/index";
import {
assistantMessage,
@@ -16,7 +16,7 @@ import {
const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
describe("Codex", () => {
it("returns session events", async () => {
it("returns thread events", async () => {
const { url, close } = await startResponsesTestProxy({
statusCode: 200,
responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())],
@@ -28,15 +28,15 @@ describe("Codex", () => {
const thread = client.startThread();
const result = await thread.runStreamed("Hello, world!");
const events: ConversationEvent[] = [];
const events: ThreadEvent[] = [];
for await (const event of result.events) {
events.push(event);
}
expect(events).toEqual([
{
type: "session.created",
session_id: expect.any(String),
type: "thread.started",
thread_id: expect.any(String),
},
{
type: "turn.started",
@@ -91,7 +91,7 @@ describe("Codex", () => {
const second = await thread.runStreamed("second input");
await drainEvents(second.events);
// Check second request continues conversation
// Check second request continues the same thread
expect(requests.length).toBeGreaterThanOrEqual(2);
const secondRequest = requests[1];
expect(secondRequest).toBeDefined();
@@ -159,7 +159,7 @@ describe("Codex", () => {
});
});
async function drainEvents(events: AsyncGenerator<ConversationEvent>): Promise<void> {
async function drainEvents(events: AsyncGenerator<ThreadEvent>): Promise<void> {
let done = false;
do {
done = (await events.next()).done ?? false;