Add some types and a basic test to the SDK (#4472)
This commit is contained in:
25
sdk/typescript/src/codex.ts
Normal file
25
sdk/typescript/src/codex.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { CodexOptions } from "./codexOptions";
|
||||
import { CodexExec } from "./exec";
|
||||
import { Thread } from "./thread";
|
||||
|
||||
export class Codex {
|
||||
private exec: CodexExec;
|
||||
private options: CodexOptions;
|
||||
|
||||
constructor(options: CodexOptions) {
|
||||
if (!options.executablePath) {
|
||||
throw new Error("executablePath is required");
|
||||
}
|
||||
|
||||
this.exec = new CodexExec(options.executablePath);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
startThread(): Thread {
|
||||
return new Thread(this.exec, this.options);
|
||||
}
|
||||
|
||||
resumeThread(id: string): Thread {
|
||||
return new Thread(this.exec, this.options, id);
|
||||
}
|
||||
}
|
||||
7
sdk/typescript/src/codexOptions.ts
Normal file
7
sdk/typescript/src/codexOptions.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type CodexOptions = {
|
||||
// TODO: remove
|
||||
executablePath: string;
|
||||
// TODO: remove
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
};
|
||||
52
sdk/typescript/src/events.ts
Normal file
52
sdk/typescript/src/events.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// based on event types from codex-rs/exec/src/exec_events.rs
|
||||
|
||||
import type { ConversationItem } from "./items";
|
||||
|
||||
export type SessionCreatedEvent = {
|
||||
type: "session.created";
|
||||
session_id: string;
|
||||
};
|
||||
|
||||
export type TurnStartedEvent = {
|
||||
type: "turn.started";
|
||||
};
|
||||
|
||||
export type Usage = {
|
||||
input_tokens: number;
|
||||
cached_input_tokens: number;
|
||||
output_tokens: number;
|
||||
};
|
||||
|
||||
export type TurnCompletedEvent = {
|
||||
type: "turn.completed";
|
||||
usage: Usage;
|
||||
};
|
||||
|
||||
export type ItemStartedEvent = {
|
||||
type: "item.started";
|
||||
item: ConversationItem;
|
||||
};
|
||||
|
||||
export type ItemUpdatedEvent = {
|
||||
type: "item.updated";
|
||||
item: ConversationItem;
|
||||
};
|
||||
|
||||
export type ItemCompletedEvent = {
|
||||
type: "item.completed";
|
||||
item: ConversationItem;
|
||||
};
|
||||
|
||||
export type ConversationErrorEvent = {
|
||||
type: "error";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type ConversationEvent =
|
||||
| SessionCreatedEvent
|
||||
| TurnStartedEvent
|
||||
| TurnCompletedEvent
|
||||
| ItemStartedEvent
|
||||
| ItemUpdatedEvent
|
||||
| ItemCompletedEvent
|
||||
| ConversationErrorEvent;
|
||||
81
sdk/typescript/src/exec.ts
Normal file
81
sdk/typescript/src/exec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { spawn } from "child_process";
|
||||
import readline from "node:readline";
|
||||
|
||||
export type CodexExecArgs = {
|
||||
input: string;
|
||||
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
sessionId?: string | null;
|
||||
};
|
||||
|
||||
export class CodexExec {
|
||||
private executablePath: string;
|
||||
constructor(executablePath: string) {
|
||||
this.executablePath = executablePath;
|
||||
}
|
||||
|
||||
async *run(args: CodexExecArgs): AsyncGenerator<string> {
|
||||
const commandArgs: string[] = ["exec", "--experimental-json"];
|
||||
if (args.sessionId) {
|
||||
commandArgs.push("resume", args.sessionId, args.input);
|
||||
} else {
|
||||
commandArgs.push(args.input);
|
||||
}
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
};
|
||||
if (args.baseUrl) {
|
||||
env.OPENAI_BASE_URL = args.baseUrl;
|
||||
}
|
||||
if (args.apiKey) {
|
||||
env.OPENAI_API_KEY = args.apiKey;
|
||||
}
|
||||
|
||||
const child = spawn(this.executablePath, commandArgs, {
|
||||
env,
|
||||
});
|
||||
|
||||
let spawnError: unknown | null = null;
|
||||
child.once("error", (err) => (spawnError = err));
|
||||
|
||||
if (!child.stdout) {
|
||||
child.kill();
|
||||
throw new Error("Child process has no stdout");
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: child.stdout,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
try {
|
||||
for await (const line of rl) {
|
||||
// `line` is a string (Node sets default encoding to utf8 for readline)
|
||||
yield line as string;
|
||||
}
|
||||
|
||||
const exitCode = new Promise((resolve) => {
|
||||
child.once("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(code);
|
||||
} else {
|
||||
throw new Error(`Codex Exec exited with code ${code}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (spawnError) throw spawnError;
|
||||
await exitCode;
|
||||
} finally {
|
||||
rl.close();
|
||||
child.removeAllListeners();
|
||||
try {
|
||||
if (!child.killed) child.kill();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,27 @@
|
||||
export class Codex {
|
||||
constructor() {
|
||||
}
|
||||
}
|
||||
export type {
|
||||
ConversationEvent,
|
||||
SessionCreatedEvent,
|
||||
TurnStartedEvent,
|
||||
TurnCompletedEvent,
|
||||
ItemStartedEvent,
|
||||
ItemUpdatedEvent,
|
||||
ItemCompletedEvent,
|
||||
ConversationErrorEvent,
|
||||
} from "./events";
|
||||
export type {
|
||||
ConversationItem,
|
||||
AssistantMessageItem,
|
||||
ReasoningItem,
|
||||
CommandExecutionItem,
|
||||
FileChangeItem,
|
||||
McpToolCallItem,
|
||||
WebSearchItem,
|
||||
TodoListItem,
|
||||
ErrorItem,
|
||||
} from "./items";
|
||||
|
||||
export type { Thread, RunResult, RunStreamedResult, Input } from "./thread";
|
||||
|
||||
export type { Codex } from "./codex";
|
||||
|
||||
export type { CodexOptions } from "./codexOptions";
|
||||
|
||||
99
sdk/typescript/src/items.ts
Normal file
99
sdk/typescript/src/items.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// based on item types from codex-rs/exec/src/exec_events.rs
|
||||
|
||||
export type CommandExecutionStatus = "in_progress" | "completed" | "failed";
|
||||
|
||||
export type CommandExecutionItem = {
|
||||
id: string;
|
||||
item_type: "command_execution";
|
||||
command: string;
|
||||
aggregated_output: string;
|
||||
exit_code?: number;
|
||||
status: CommandExecutionStatus;
|
||||
};
|
||||
|
||||
export type PatchChangeKind = "add" | "delete" | "update";
|
||||
|
||||
export type FileUpdateChange = {
|
||||
path: string;
|
||||
kind: PatchChangeKind;
|
||||
};
|
||||
|
||||
export type PatchApplyStatus = "completed" | "failed";
|
||||
|
||||
export type FileChangeItem = {
|
||||
id: string;
|
||||
item_type: "file_change";
|
||||
changes: FileUpdateChange[];
|
||||
status: PatchApplyStatus;
|
||||
};
|
||||
|
||||
export type McpToolCallStatus = "in_progress" | "completed" | "failed";
|
||||
|
||||
export type McpToolCallItem = {
|
||||
id: string;
|
||||
item_type: "mcp_tool_call";
|
||||
server: string;
|
||||
tool: string;
|
||||
status: McpToolCallStatus;
|
||||
};
|
||||
|
||||
export type AssistantMessageItem = {
|
||||
id: string;
|
||||
item_type: "assistant_message";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type ReasoningItem = {
|
||||
id: string;
|
||||
item_type: "reasoning";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type WebSearchItem = {
|
||||
id: string;
|
||||
item_type: "web_search";
|
||||
query: string;
|
||||
};
|
||||
|
||||
export type ErrorItem = {
|
||||
id: string;
|
||||
item_type: "error";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type TodoItem = {
|
||||
text: string;
|
||||
completed: boolean;
|
||||
};
|
||||
|
||||
export type TodoListItem = {
|
||||
id: string;
|
||||
item_type: "todo_list";
|
||||
items: TodoItem[];
|
||||
};
|
||||
|
||||
export type SessionItem = {
|
||||
id: string;
|
||||
item_type: "session";
|
||||
session_id: string;
|
||||
};
|
||||
|
||||
export type ConversationItem =
|
||||
| AssistantMessageItem
|
||||
| ReasoningItem
|
||||
| CommandExecutionItem
|
||||
| FileChangeItem
|
||||
| McpToolCallItem
|
||||
| WebSearchItem
|
||||
| TodoListItem
|
||||
| ErrorItem;
|
||||
|
||||
export type ConversationItemDetails =
|
||||
| AssistantMessageItem
|
||||
| ReasoningItem
|
||||
| CommandExecutionItem
|
||||
| FileChangeItem
|
||||
| McpToolCallItem
|
||||
| WebSearchItem
|
||||
| TodoListItem
|
||||
| ErrorItem;
|
||||
62
sdk/typescript/src/thread.ts
Normal file
62
sdk/typescript/src/thread.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { CodexOptions } from "./codexOptions";
|
||||
import { ConversationEvent } from "./events";
|
||||
import { CodexExec } from "./exec";
|
||||
import { ConversationItem } from "./items";
|
||||
|
||||
export type RunResult = {
|
||||
items: ConversationItem[];
|
||||
finalResponse: string;
|
||||
};
|
||||
|
||||
export type RunStreamedResult = {
|
||||
events: AsyncGenerator<ConversationEvent>;
|
||||
};
|
||||
|
||||
export type Input = string;
|
||||
|
||||
export class Thread {
|
||||
private exec: CodexExec;
|
||||
private options: CodexOptions;
|
||||
public id: string | null;
|
||||
|
||||
constructor(exec: CodexExec, options: CodexOptions, id: string | null = null) {
|
||||
this.exec = exec;
|
||||
this.options = options;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
async runStreamed(input: string): Promise<RunStreamedResult> {
|
||||
return { events: this.runStreamedInternal(input) };
|
||||
}
|
||||
|
||||
private async *runStreamedInternal(input: string): AsyncGenerator<ConversationEvent> {
|
||||
const generator = this.exec.run({
|
||||
input,
|
||||
baseUrl: this.options.baseUrl,
|
||||
apiKey: this.options.apiKey,
|
||||
sessionId: 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;
|
||||
}
|
||||
yield parsed;
|
||||
}
|
||||
}
|
||||
|
||||
async run(input: string): Promise<RunResult> {
|
||||
const generator = this.runStreamedInternal(input);
|
||||
const items: ConversationItem[] = [];
|
||||
let finalResponse: string = "";
|
||||
for await (const event of generator) {
|
||||
if (event.type === "item.completed") {
|
||||
if (event.item.item_type === "assistant_message") {
|
||||
finalResponse = event.item.text;
|
||||
}
|
||||
items.push(event.item);
|
||||
}
|
||||
}
|
||||
return { items, finalResponse };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user