- commit
- d9df207
- parent
- b2bc870
- author
- im_wower
- date
- 2026-03-22 20:51:21 +0800 CST
feat(codex-app-server): add app-server adapter package
8 files changed,
+1436,
-0
+16,
-0
1@@ -0,0 +1,16 @@
2+{
3+ "name": "@baa-conductor/codex-app-server",
4+ "private": true,
5+ "type": "module",
6+ "main": "dist/index.js",
7+ "exports": {
8+ ".": "./dist/index.js"
9+ },
10+ "types": "dist/index.d.ts",
11+ "scripts": {
12+ "build": "pnpm exec tsc -p tsconfig.json",
13+ "typecheck": "pnpm exec tsc --noEmit -p tsconfig.json",
14+ "test": "pnpm run build && node --test src/index.test.js",
15+ "smoke": "pnpm run test"
16+ }
17+}
+440,
-0
1@@ -0,0 +1,440 @@
2+declare function clearTimeout(handle: unknown): void;
3+declare function setTimeout(callback: () => void, delay?: number): unknown;
4+
5+import {
6+ type CodexAppServerEvent,
7+ type CodexAppServerInitializeCapabilities,
8+ type CodexAppServerInitializeResult,
9+ type CodexAppServerNotificationEnvelope,
10+ type CodexAppServerPlanStep,
11+ type CodexAppServerRequestId,
12+ type CodexAppServerRpcFailure,
13+ type CodexAppServerRpcMessage,
14+ type CodexAppServerRpcSuccess,
15+ type CodexAppServerThreadSession,
16+ type CodexAppServerThreadStartParams,
17+ type CodexAppServerThreadResumeParams,
18+ type CodexAppServerThreadStatus,
19+ type CodexAppServerTurn,
20+ type CodexAppServerTurnError,
21+ type CodexAppServerTurnInterruptParams,
22+ type CodexAppServerTurnStartParams,
23+ type CodexAppServerTurnStartResult,
24+ type CodexAppServerTurnSteerParams,
25+ type CodexAppServerTurnSteerResult,
26+ type CodexAppServerClientInfo,
27+ type JsonValue
28+} from "./contracts.js";
29+import { CodexAppServerEventStream } from "./events.js";
30+import type { CodexAppServerTransport, CodexAppServerTransportHandlers } from "./transport.js";
31+
32+const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
33+
34+interface PendingRequest {
35+ requestMethod: string;
36+ reject(error: Error): void;
37+ resolve(result: unknown): void;
38+ timeoutHandle: unknown;
39+}
40+
41+function isRecord(value: unknown): value is Record<string, unknown> {
42+ return typeof value === "object" && value !== null;
43+}
44+
45+function isRpcSuccess(value: unknown): value is CodexAppServerRpcSuccess {
46+ return isRecord(value) && "id" in value && "result" in value;
47+}
48+
49+function isRpcFailure(value: unknown): value is CodexAppServerRpcFailure {
50+ return isRecord(value) && "id" in value && "error" in value;
51+}
52+
53+function isNotification(value: unknown): value is CodexAppServerNotificationEnvelope {
54+ return isRecord(value) && typeof value.method === "string" && !("id" in value);
55+}
56+
57+function toError(cause: unknown, fallback: string): Error {
58+ if (cause instanceof Error) {
59+ return cause;
60+ }
61+
62+ if (typeof cause === "string" && cause !== "") {
63+ return new Error(cause);
64+ }
65+
66+ return new Error(fallback);
67+}
68+
69+function normalizeThreadStartParams(
70+ params: CodexAppServerThreadStartParams
71+): Record<string, unknown> {
72+ return {
73+ ...params,
74+ experimentalRawEvents: params.experimentalRawEvents ?? false,
75+ persistExtendedHistory: params.persistExtendedHistory ?? false
76+ };
77+}
78+
79+function normalizeThreadResumeParams(
80+ params: CodexAppServerThreadResumeParams
81+): Record<string, unknown> {
82+ return {
83+ ...params,
84+ persistExtendedHistory: params.persistExtendedHistory ?? false
85+ };
86+}
87+
88+function mapNotificationToEvent(notification: CodexAppServerNotificationEnvelope): CodexAppServerEvent {
89+ const params = notification.params;
90+
91+ switch (notification.method) {
92+ case "thread/started":
93+ return {
94+ type: "thread.started",
95+ notificationMethod: "thread/started",
96+ thread: (params as { thread: CodexAppServerThreadSession["thread"] }).thread
97+ };
98+
99+ case "thread/status/changed":
100+ return {
101+ type: "thread.status.changed",
102+ notificationMethod: "thread/status/changed",
103+ threadId: (params as { threadId: string }).threadId,
104+ status: (params as { status: CodexAppServerThreadStatus }).status
105+ };
106+
107+ case "turn/started":
108+ return {
109+ type: "turn.started",
110+ notificationMethod: "turn/started",
111+ threadId: (params as { threadId: string }).threadId,
112+ turn: (params as { turn: CodexAppServerTurn }).turn
113+ };
114+
115+ case "turn/completed":
116+ return {
117+ type: "turn.completed",
118+ notificationMethod: "turn/completed",
119+ threadId: (params as { threadId: string }).threadId,
120+ turn: (params as { turn: CodexAppServerTurn }).turn
121+ };
122+
123+ case "turn/diff/updated":
124+ return {
125+ type: "turn.diff.updated",
126+ notificationMethod: "turn/diff/updated",
127+ threadId: (params as { threadId: string }).threadId,
128+ turnId: (params as { turnId: string }).turnId,
129+ diff: (params as { diff: string }).diff
130+ };
131+
132+ case "turn/plan/updated":
133+ return {
134+ type: "turn.plan.updated",
135+ notificationMethod: "turn/plan/updated",
136+ threadId: (params as { threadId: string }).threadId,
137+ turnId: (params as { turnId: string }).turnId,
138+ explanation: (params as { explanation: string | null }).explanation,
139+ plan: (params as { plan: CodexAppServerPlanStep[] }).plan
140+ };
141+
142+ case "item/agentMessage/delta":
143+ return {
144+ type: "turn.message.delta",
145+ notificationMethod: "item/agentMessage/delta",
146+ threadId: (params as { threadId: string }).threadId,
147+ turnId: (params as { turnId: string }).turnId,
148+ itemId: (params as { itemId: string }).itemId,
149+ delta: (params as { delta: string }).delta
150+ };
151+
152+ case "item/plan/delta":
153+ return {
154+ type: "turn.plan.delta",
155+ notificationMethod: "item/plan/delta",
156+ threadId: (params as { threadId: string }).threadId,
157+ turnId: (params as { turnId: string }).turnId,
158+ itemId: (params as { itemId: string }).itemId,
159+ delta: (params as { delta: string }).delta
160+ };
161+
162+ case "error":
163+ return {
164+ type: "turn.error",
165+ notificationMethod: "error",
166+ threadId: (params as { threadId: string }).threadId,
167+ turnId: (params as { turnId: string }).turnId,
168+ error: (params as { error: CodexAppServerTurnError }).error,
169+ willRetry: (params as { willRetry: boolean }).willRetry
170+ };
171+
172+ case "command/exec/outputDelta":
173+ return {
174+ type: "command.output.delta",
175+ notificationMethod: "command/exec/outputDelta",
176+ processId: (params as { processId: string }).processId,
177+ stream: (params as { stream: "stdout" | "stderr" }).stream,
178+ deltaBase64: (params as { deltaBase64: string }).deltaBase64,
179+ capReached: (params as { capReached: boolean }).capReached
180+ };
181+
182+ default:
183+ return {
184+ type: "notification",
185+ notificationMethod: notification.method,
186+ params
187+ };
188+ }
189+}
190+
191+export interface CodexAppServerInitializeOptions {
192+ capabilities?: CodexAppServerInitializeCapabilities | null;
193+ clientInfo?: CodexAppServerClientInfo;
194+}
195+
196+export interface CodexAppServerClientConfig {
197+ clientInfo: CodexAppServerClientInfo;
198+ transport: CodexAppServerTransport;
199+ capabilities?: CodexAppServerInitializeCapabilities | null;
200+ createRequestId?: () => CodexAppServerRequestId;
201+ requestTimeoutMs?: number;
202+}
203+
204+export interface CodexAppServerAdapter {
205+ readonly events: CodexAppServerEventStream;
206+ close(): Promise<void>;
207+ initialize(options?: CodexAppServerInitializeOptions): Promise<CodexAppServerInitializeResult>;
208+ threadResume(params: CodexAppServerThreadResumeParams): Promise<CodexAppServerThreadSession>;
209+ threadStart(params?: CodexAppServerThreadStartParams): Promise<CodexAppServerThreadSession>;
210+ turnInterrupt(params: CodexAppServerTurnInterruptParams): Promise<void>;
211+ turnStart(params: CodexAppServerTurnStartParams): Promise<CodexAppServerTurnStartResult>;
212+ turnSteer(params: CodexAppServerTurnSteerParams): Promise<CodexAppServerTurnSteerResult>;
213+}
214+
215+export class CodexAppServerRpcError extends Error {
216+ readonly code: number;
217+ readonly data?: JsonValue;
218+ readonly requestMethod: string;
219+
220+ constructor(requestMethod: string, payload: CodexAppServerRpcFailure["error"]) {
221+ super(`Codex app-server request failed for ${requestMethod}: ${payload.message}`);
222+ this.name = "CodexAppServerRpcError";
223+ this.code = payload.code;
224+ this.data = payload.data;
225+ this.requestMethod = requestMethod;
226+ }
227+}
228+
229+export class CodexAppServerClientClosedError extends Error {
230+ constructor(message = "Codex app-server client is closed.") {
231+ super(message);
232+ this.name = "CodexAppServerClientClosedError";
233+ }
234+}
235+
236+export class CodexAppServerRequestTimeoutError extends Error {
237+ readonly requestMethod: string;
238+ readonly timeoutMs: number;
239+
240+ constructor(requestMethod: string, timeoutMs: number) {
241+ super(`Codex app-server request timed out after ${timeoutMs}ms: ${requestMethod}`);
242+ this.name = "CodexAppServerRequestTimeoutError";
243+ this.requestMethod = requestMethod;
244+ this.timeoutMs = timeoutMs;
245+ }
246+}
247+
248+export class CodexAppServerClient implements CodexAppServerAdapter {
249+ readonly events = new CodexAppServerEventStream();
250+
251+ private closed = false;
252+ private connectPromise: Promise<void> | null = null;
253+ private readonly pending = new Map<CodexAppServerRequestId, PendingRequest>();
254+ private nextRequestId = 1;
255+ private readonly requestTimeoutMs: number;
256+ private readonly transportHandlers: CodexAppServerTransportHandlers;
257+
258+ constructor(private readonly config: CodexAppServerClientConfig) {
259+ this.requestTimeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
260+ this.transportHandlers = {
261+ onClose: (error?: Error) => {
262+ this.handleTransportClosed(error);
263+ },
264+ onMessage: (message: string) => {
265+ this.handleTransportMessage(message);
266+ }
267+ };
268+ }
269+
270+ async initialize(
271+ options?: CodexAppServerInitializeOptions
272+ ): Promise<CodexAppServerInitializeResult> {
273+ return await this.request("initialize", {
274+ capabilities: options?.capabilities ?? this.config.capabilities ?? null,
275+ clientInfo: options?.clientInfo ?? this.config.clientInfo
276+ });
277+ }
278+
279+ async threadStart(
280+ params: CodexAppServerThreadStartParams = {}
281+ ): Promise<CodexAppServerThreadSession> {
282+ return await this.request("thread/start", normalizeThreadStartParams(params));
283+ }
284+
285+ async threadResume(
286+ params: CodexAppServerThreadResumeParams
287+ ): Promise<CodexAppServerThreadSession> {
288+ return await this.request("thread/resume", normalizeThreadResumeParams(params));
289+ }
290+
291+ async turnStart(params: CodexAppServerTurnStartParams): Promise<CodexAppServerTurnStartResult> {
292+ return await this.request("turn/start", params);
293+ }
294+
295+ async turnSteer(
296+ params: CodexAppServerTurnSteerParams
297+ ): Promise<CodexAppServerTurnSteerResult> {
298+ return await this.request("turn/steer", params);
299+ }
300+
301+ async turnInterrupt(params: CodexAppServerTurnInterruptParams): Promise<void> {
302+ await this.request("turn/interrupt", params);
303+ }
304+
305+ async close(): Promise<void> {
306+ if (this.closed) {
307+ return;
308+ }
309+
310+ this.closed = true;
311+ this.rejectPending(new CodexAppServerClientClosedError());
312+ this.events.close();
313+ await this.config.transport.close();
314+ }
315+
316+ private async ensureConnected(): Promise<void> {
317+ if (this.closed) {
318+ throw new CodexAppServerClientClosedError();
319+ }
320+
321+ if (this.connectPromise === null) {
322+ this.connectPromise = this.config.transport.connect(this.transportHandlers).catch((error) => {
323+ this.connectPromise = null;
324+ throw error;
325+ });
326+ }
327+
328+ await this.connectPromise;
329+ }
330+
331+ private createRequestId(): CodexAppServerRequestId {
332+ if (typeof this.config.createRequestId === "function") {
333+ return this.config.createRequestId();
334+ }
335+
336+ const nextId = this.nextRequestId;
337+ this.nextRequestId += 1;
338+ return nextId;
339+ }
340+
341+ private async request<TResult>(
342+ method: string,
343+ params: unknown
344+ ): Promise<TResult> {
345+ await this.ensureConnected();
346+
347+ const id = this.createRequestId();
348+
349+ return await new Promise<TResult>((resolve, reject) => {
350+ const timeoutHandle = setTimeout(() => {
351+ this.pending.delete(id);
352+ reject(new CodexAppServerRequestTimeoutError(method, this.requestTimeoutMs));
353+ }, this.requestTimeoutMs);
354+
355+ this.pending.set(id, {
356+ requestMethod: method,
357+ timeoutHandle,
358+ resolve,
359+ reject
360+ });
361+
362+ const payload = JSON.stringify({
363+ id,
364+ method,
365+ params
366+ });
367+
368+ this.config.transport.send(payload).catch((error) => {
369+ clearTimeout(timeoutHandle);
370+ this.pending.delete(id);
371+ reject(toError(error, `Failed to send Codex app-server request: ${method}`));
372+ });
373+ });
374+ }
375+
376+ private handleTransportMessage(message: string): void {
377+ let parsed: CodexAppServerRpcMessage;
378+
379+ try {
380+ parsed = JSON.parse(message) as CodexAppServerRpcMessage;
381+ } catch (error) {
382+ this.rejectPending(toError(error, "Failed to parse Codex app-server message."));
383+ return;
384+ }
385+
386+ if (isRpcSuccess(parsed)) {
387+ const pending = this.pending.get(parsed.id);
388+
389+ if (pending === undefined) {
390+ return;
391+ }
392+
393+ clearTimeout(pending.timeoutHandle);
394+ this.pending.delete(parsed.id);
395+ pending.resolve(parsed.result);
396+ return;
397+ }
398+
399+ if (isRpcFailure(parsed)) {
400+ const requestId = parsed.id;
401+
402+ if (requestId === null) {
403+ return;
404+ }
405+
406+ const pending = this.pending.get(requestId);
407+
408+ if (pending === undefined) {
409+ return;
410+ }
411+
412+ clearTimeout(pending.timeoutHandle);
413+ this.pending.delete(requestId);
414+ pending.reject(new CodexAppServerRpcError(pending.requestMethod, parsed.error));
415+ return;
416+ }
417+
418+ if (isNotification(parsed)) {
419+ this.events.emit(mapNotificationToEvent(parsed));
420+ }
421+ }
422+
423+ private handleTransportClosed(error?: Error): void {
424+ if (this.closed) {
425+ return;
426+ }
427+
428+ this.closed = true;
429+ this.rejectPending(error ?? new CodexAppServerClientClosedError("Codex app-server transport closed."));
430+ this.events.close();
431+ }
432+
433+ private rejectPending(error: Error): void {
434+ for (const pending of this.pending.values()) {
435+ clearTimeout(pending.timeoutHandle);
436+ pending.reject(error);
437+ }
438+
439+ this.pending.clear();
440+ }
441+}
+424,
-0
1@@ -0,0 +1,424 @@
2+export type JsonPrimitive = boolean | number | string | null;
3+export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue | undefined };
4+
5+export type CodexAppServerRequestId = number | string;
6+export type CodexAppServerThreadId = string;
7+export type CodexAppServerTurnId = string;
8+export type CodexAppServerItemId = string;
9+export type CodexAppServerProcessId = string;
10+
11+export const CODEX_APP_SERVER_REQUEST_METHODS = [
12+ "initialize",
13+ "thread/start",
14+ "thread/resume",
15+ "turn/start",
16+ "turn/steer",
17+ "turn/interrupt"
18+] as const;
19+
20+export const CODEX_APP_SERVER_NOTIFICATION_METHODS = [
21+ "error",
22+ "thread/started",
23+ "thread/status/changed",
24+ "turn/started",
25+ "turn/completed",
26+ "turn/diff/updated",
27+ "turn/plan/updated",
28+ "item/agentMessage/delta",
29+ "item/plan/delta",
30+ "command/exec/outputDelta"
31+] as const;
32+
33+export type CodexAppServerRequestMethod = (typeof CODEX_APP_SERVER_REQUEST_METHODS)[number];
34+export type CodexAppServerNotificationMethod =
35+ (typeof CODEX_APP_SERVER_NOTIFICATION_METHODS)[number];
36+
37+export interface CodexAppServerRpcErrorPayload {
38+ code: number;
39+ message: string;
40+ data?: JsonValue;
41+}
42+
43+export interface CodexAppServerRpcRequest<TParams = unknown> {
44+ id: CodexAppServerRequestId;
45+ method: string;
46+ params?: TParams;
47+}
48+
49+export interface CodexAppServerRpcSuccess<TResult = unknown> {
50+ id: CodexAppServerRequestId;
51+ result: TResult;
52+}
53+
54+export interface CodexAppServerRpcFailure {
55+ id: CodexAppServerRequestId | null;
56+ error: CodexAppServerRpcErrorPayload;
57+}
58+
59+export interface CodexAppServerRpcNotification<TParams = unknown> {
60+ method: string;
61+ params?: TParams;
62+}
63+
64+export type CodexAppServerRpcMessage =
65+ | CodexAppServerRpcRequest
66+ | CodexAppServerRpcSuccess
67+ | CodexAppServerRpcFailure
68+ | CodexAppServerRpcNotification;
69+
70+export interface CodexAppServerClientInfo {
71+ name: string;
72+ title: string | null;
73+ version: string;
74+}
75+
76+export interface CodexAppServerInitializeCapabilities {
77+ experimentalApi: boolean;
78+ optOutNotificationMethods?: string[] | null;
79+}
80+
81+export interface CodexAppServerInitializeParams {
82+ clientInfo: CodexAppServerClientInfo;
83+ capabilities: CodexAppServerInitializeCapabilities | null;
84+}
85+
86+export interface CodexAppServerInitializeResult {
87+ userAgent: string;
88+ platformFamily: string;
89+ platformOs: string;
90+}
91+
92+export interface CodexAppServerApprovalPolicyGranular {
93+ sandboxApproval: boolean;
94+ rules: boolean;
95+ skillApproval: boolean;
96+ requestPermissions: boolean;
97+ mcpElicitations: boolean;
98+}
99+
100+export type CodexAppServerApprovalPolicy =
101+ | "untrusted"
102+ | "on-failure"
103+ | "on-request"
104+ | "never"
105+ | { granular: CodexAppServerApprovalPolicyGranular };
106+
107+export type CodexAppServerSandboxMode =
108+ | "read-only"
109+ | "workspace-write"
110+ | "danger-full-access";
111+
112+export type CodexAppServerThreadActiveFlag = "waitingOnApproval" | "waitingOnUserInput";
113+export type CodexAppServerTurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
114+export type CodexAppServerPlanStepStatus = "pending" | "inProgress" | "completed";
115+export type CodexAppServerCommandOutputStream = "stdout" | "stderr";
116+
117+export type CodexAppServerReadOnlyAccess =
118+ | {
119+ type: "restricted";
120+ includePlatformDefaults: boolean;
121+ readableRoots: string[];
122+ }
123+ | { type: "fullAccess" };
124+
125+export type CodexAppServerSandboxPolicy =
126+ | {
127+ type: "dangerFullAccess";
128+ }
129+ | {
130+ type: "readOnly";
131+ access: CodexAppServerReadOnlyAccess;
132+ networkAccess: boolean;
133+ }
134+ | {
135+ type: "externalSandbox";
136+ networkAccess: "restricted" | "enabled";
137+ }
138+ | {
139+ type: "workspaceWrite";
140+ writableRoots: string[];
141+ readOnlyAccess: CodexAppServerReadOnlyAccess;
142+ networkAccess: boolean;
143+ excludeTmpdirEnvVar: boolean;
144+ excludeSlashTmp: boolean;
145+ };
146+
147+export type CodexAppServerThreadStatus =
148+ | { type: "notLoaded" }
149+ | { type: "idle" }
150+ | { type: "systemError" }
151+ | { type: "active"; activeFlags: CodexAppServerThreadActiveFlag[] };
152+
153+export interface CodexAppServerTurnError {
154+ message: string;
155+ codexErrorInfo: JsonValue | null;
156+ additionalDetails: string | null;
157+}
158+
159+export interface CodexAppServerPlanStep {
160+ step: string;
161+ status: CodexAppServerPlanStepStatus;
162+}
163+
164+export interface CodexAppServerTurn {
165+ id: CodexAppServerTurnId;
166+ status: CodexAppServerTurnStatus;
167+ error: CodexAppServerTurnError | null;
168+}
169+
170+export type CodexAppServerThreadSource =
171+ | "cli"
172+ | "vscode"
173+ | "exec"
174+ | "mcp"
175+ | "unknown"
176+ | { custom: string }
177+ | { subagent: string };
178+
179+export interface CodexAppServerThread {
180+ id: CodexAppServerThreadId;
181+ preview: string;
182+ ephemeral: boolean;
183+ modelProvider: string;
184+ createdAt: number;
185+ updatedAt: number;
186+ status: CodexAppServerThreadStatus;
187+ cwd: string;
188+ cliVersion: string;
189+ source: CodexAppServerThreadSource;
190+ agentNickname?: string | null;
191+ agentRole?: string | null;
192+ name: string | null;
193+ turns?: CodexAppServerTurn[];
194+}
195+
196+export interface CodexAppServerThreadSession {
197+ thread: CodexAppServerThread;
198+ model: string;
199+ modelProvider: string;
200+ serviceTier: string | null;
201+ cwd: string;
202+ approvalPolicy: CodexAppServerApprovalPolicy;
203+ sandbox: CodexAppServerSandboxPolicy;
204+ reasoningEffort: string | null;
205+}
206+
207+export interface CodexAppServerByteRange {
208+ start: number;
209+ end: number;
210+}
211+
212+export interface CodexAppServerTextElement {
213+ byteRange: CodexAppServerByteRange;
214+ placeholder: string | null;
215+}
216+
217+export interface CodexAppServerTextUserInput {
218+ type: "text";
219+ text: string;
220+ text_elements: CodexAppServerTextElement[];
221+}
222+
223+export interface CodexAppServerImageUserInput {
224+ type: "image";
225+ url: string;
226+}
227+
228+export interface CodexAppServerLocalImageUserInput {
229+ type: "localImage";
230+ path: string;
231+}
232+
233+export interface CodexAppServerSkillUserInput {
234+ type: "skill";
235+ name: string;
236+ path: string;
237+}
238+
239+export interface CodexAppServerMentionUserInput {
240+ type: "mention";
241+ name: string;
242+ path: string;
243+}
244+
245+export type CodexAppServerUserInput =
246+ | CodexAppServerTextUserInput
247+ | CodexAppServerImageUserInput
248+ | CodexAppServerLocalImageUserInput
249+ | CodexAppServerSkillUserInput
250+ | CodexAppServerMentionUserInput;
251+
252+export interface CodexAppServerThreadStartParams {
253+ model?: string | null;
254+ modelProvider?: string | null;
255+ serviceTier?: string | null;
256+ cwd?: string | null;
257+ approvalPolicy?: CodexAppServerApprovalPolicy | null;
258+ sandbox?: CodexAppServerSandboxMode | null;
259+ config?: Record<string, JsonValue | undefined> | null;
260+ serviceName?: string | null;
261+ baseInstructions?: string | null;
262+ developerInstructions?: string | null;
263+ personality?: string | null;
264+ ephemeral?: boolean | null;
265+ experimentalRawEvents?: boolean;
266+ persistExtendedHistory?: boolean;
267+}
268+
269+export interface CodexAppServerThreadResumeParams {
270+ threadId: CodexAppServerThreadId;
271+ model?: string | null;
272+ modelProvider?: string | null;
273+ serviceTier?: string | null;
274+ cwd?: string | null;
275+ approvalPolicy?: CodexAppServerApprovalPolicy | null;
276+ sandbox?: CodexAppServerSandboxMode | null;
277+ config?: Record<string, JsonValue | undefined> | null;
278+ baseInstructions?: string | null;
279+ developerInstructions?: string | null;
280+ personality?: string | null;
281+ persistExtendedHistory?: boolean;
282+}
283+
284+export interface CodexAppServerTurnStartParams {
285+ threadId: CodexAppServerThreadId;
286+ input: CodexAppServerUserInput[];
287+ cwd?: string | null;
288+ approvalPolicy?: CodexAppServerApprovalPolicy | null;
289+ sandboxPolicy?: CodexAppServerSandboxPolicy | null;
290+ model?: string | null;
291+ serviceTier?: string | null;
292+ effort?: string | null;
293+ summary?: JsonValue | null;
294+ personality?: string | null;
295+ outputSchema?: JsonValue | null;
296+ collaborationMode?: JsonValue | null;
297+}
298+
299+export interface CodexAppServerTurnStartResult {
300+ turn: CodexAppServerTurn;
301+}
302+
303+export interface CodexAppServerTurnSteerParams {
304+ threadId: CodexAppServerThreadId;
305+ input: CodexAppServerUserInput[];
306+ expectedTurnId: CodexAppServerTurnId;
307+}
308+
309+export interface CodexAppServerTurnSteerResult {
310+ turnId: CodexAppServerTurnId;
311+}
312+
313+export interface CodexAppServerTurnInterruptParams {
314+ threadId: CodexAppServerThreadId;
315+ turnId: CodexAppServerTurnId;
316+}
317+
318+export interface CodexAppServerNotificationEnvelope {
319+ method: string;
320+ params?: unknown;
321+}
322+
323+export type CodexAppServerEvent =
324+ | {
325+ type: "thread.started";
326+ notificationMethod: "thread/started";
327+ thread: CodexAppServerThread;
328+ }
329+ | {
330+ type: "thread.status.changed";
331+ notificationMethod: "thread/status/changed";
332+ threadId: CodexAppServerThreadId;
333+ status: CodexAppServerThreadStatus;
334+ }
335+ | {
336+ type: "turn.started";
337+ notificationMethod: "turn/started";
338+ threadId: CodexAppServerThreadId;
339+ turn: CodexAppServerTurn;
340+ }
341+ | {
342+ type: "turn.completed";
343+ notificationMethod: "turn/completed";
344+ threadId: CodexAppServerThreadId;
345+ turn: CodexAppServerTurn;
346+ }
347+ | {
348+ type: "turn.diff.updated";
349+ notificationMethod: "turn/diff/updated";
350+ threadId: CodexAppServerThreadId;
351+ turnId: CodexAppServerTurnId;
352+ diff: string;
353+ }
354+ | {
355+ type: "turn.plan.updated";
356+ notificationMethod: "turn/plan/updated";
357+ threadId: CodexAppServerThreadId;
358+ turnId: CodexAppServerTurnId;
359+ explanation: string | null;
360+ plan: CodexAppServerPlanStep[];
361+ }
362+ | {
363+ type: "turn.message.delta";
364+ notificationMethod: "item/agentMessage/delta";
365+ threadId: CodexAppServerThreadId;
366+ turnId: CodexAppServerTurnId;
367+ itemId: CodexAppServerItemId;
368+ delta: string;
369+ }
370+ | {
371+ type: "turn.plan.delta";
372+ notificationMethod: "item/plan/delta";
373+ threadId: CodexAppServerThreadId;
374+ turnId: CodexAppServerTurnId;
375+ itemId: CodexAppServerItemId;
376+ delta: string;
377+ }
378+ | {
379+ type: "turn.error";
380+ notificationMethod: "error";
381+ threadId: CodexAppServerThreadId;
382+ turnId: CodexAppServerTurnId;
383+ error: CodexAppServerTurnError;
384+ willRetry: boolean;
385+ }
386+ | {
387+ type: "command.output.delta";
388+ notificationMethod: "command/exec/outputDelta";
389+ processId: CodexAppServerProcessId;
390+ stream: CodexAppServerCommandOutputStream;
391+ deltaBase64: string;
392+ capReached: boolean;
393+ }
394+ | {
395+ type: "notification";
396+ notificationMethod: string;
397+ params?: unknown;
398+ };
399+
400+export function createCodexAppServerTextInput(
401+ text: string,
402+ textElements: CodexAppServerTextElement[] = []
403+): CodexAppServerTextUserInput {
404+ return {
405+ type: "text",
406+ text,
407+ text_elements: textElements
408+ };
409+}
410+
411+export function createCodexAppServerReadOnlySandboxPolicy(options?: {
412+ includePlatformDefaults?: boolean;
413+ networkAccess?: boolean;
414+ readableRoots?: string[];
415+}): CodexAppServerSandboxPolicy {
416+ return {
417+ type: "readOnly",
418+ access: {
419+ type: "restricted",
420+ includePlatformDefaults: options?.includePlatformDefaults ?? true,
421+ readableRoots: options?.readableRoots ?? []
422+ },
423+ networkAccess: options?.networkAccess ?? false
424+ };
425+}
+92,
-0
1@@ -0,0 +1,92 @@
2+import type { CodexAppServerEvent } from "./contracts.js";
3+
4+export interface CodexAppServerEventSubscription {
5+ unsubscribe(): void;
6+}
7+
8+export type CodexAppServerEventListener = (event: CodexAppServerEvent) => void;
9+
10+interface PendingIterator {
11+ resolve(result: IteratorResult<CodexAppServerEvent>): void;
12+}
13+
14+export class CodexAppServerEventStream implements AsyncIterable<CodexAppServerEvent> {
15+ private readonly listeners = new Set<CodexAppServerEventListener>();
16+ private readonly queue: CodexAppServerEvent[] = [];
17+ private readonly waiters: PendingIterator[] = [];
18+ private closed = false;
19+
20+ emit(event: CodexAppServerEvent): void {
21+ if (this.closed) {
22+ return;
23+ }
24+
25+ const waiter = this.waiters.shift();
26+
27+ if (waiter !== undefined) {
28+ waiter.resolve({
29+ done: false,
30+ value: event
31+ });
32+ } else {
33+ this.queue.push(event);
34+ }
35+
36+ for (const listener of this.listeners) {
37+ listener(event);
38+ }
39+ }
40+
41+ close(): void {
42+ if (this.closed) {
43+ return;
44+ }
45+
46+ this.closed = true;
47+
48+ while (this.waiters.length > 0) {
49+ const waiter = this.waiters.shift();
50+
51+ waiter?.resolve({
52+ done: true,
53+ value: undefined
54+ });
55+ }
56+ }
57+
58+ subscribe(listener: CodexAppServerEventListener): CodexAppServerEventSubscription {
59+ this.listeners.add(listener);
60+
61+ return {
62+ unsubscribe: () => {
63+ this.listeners.delete(listener);
64+ }
65+ };
66+ }
67+
68+ [Symbol.asyncIterator](): AsyncIterator<CodexAppServerEvent> {
69+ return {
70+ next: async (): Promise<IteratorResult<CodexAppServerEvent>> => {
71+ const buffered = this.queue.shift();
72+
73+ if (buffered !== undefined) {
74+ return {
75+ done: false,
76+ value: buffered
77+ };
78+ }
79+
80+ if (this.closed) {
81+ return {
82+ done: true,
83+ value: undefined
84+ };
85+ }
86+
87+ return await new Promise<IteratorResult<CodexAppServerEvent>>((resolve) => {
88+ this.waiters.push({ resolve });
89+ });
90+ }
91+ };
92+ }
93+}
+275,
-0
1@@ -0,0 +1,275 @@
2+import assert from "node:assert/strict";
3+import test from "node:test";
4+
5+import {
6+ CodexAppServerClient,
7+ CodexAppServerEventStream,
8+ createCodexAppServerReadOnlySandboxPolicy,
9+ createCodexAppServerTextInput
10+} from "../dist/index.js";
11+
12+class FakeTransport {
13+ constructor(handlersByMethod) {
14+ this.handlersByMethod = handlersByMethod;
15+ this.requests = [];
16+ this.handlers = null;
17+ this.closed = false;
18+ }
19+
20+ async connect(handlers) {
21+ this.handlers = handlers;
22+ }
23+
24+ async send(message) {
25+ const request = JSON.parse(message);
26+ this.requests.push(request);
27+
28+ const handler = this.handlersByMethod[request.method];
29+
30+ if (typeof handler !== "function") {
31+ throw new Error(`Unexpected request method in test transport: ${request.method}`);
32+ }
33+
34+ const plan = await handler(request);
35+
36+ for (const notification of plan.notifications ?? []) {
37+ this.handlers.onMessage(JSON.stringify(notification));
38+ }
39+
40+ if (plan.error) {
41+ this.handlers.onMessage(
42+ JSON.stringify({
43+ id: request.id,
44+ error: plan.error
45+ })
46+ );
47+ return;
48+ }
49+
50+ this.handlers.onMessage(
51+ JSON.stringify({
52+ id: request.id,
53+ result: plan.result ?? {}
54+ })
55+ );
56+ }
57+
58+ async close() {
59+ if (this.closed) {
60+ return;
61+ }
62+
63+ this.closed = true;
64+ this.handlers?.onClose(new Error("closed by test"));
65+ }
66+}
67+
68+test("CodexAppServerClient maps app-server methods and notifications into a reusable adapter", async () => {
69+ const thread = {
70+ id: "thread-1",
71+ preview: "hello",
72+ ephemeral: true,
73+ modelProvider: "openai",
74+ createdAt: 1,
75+ updatedAt: 2,
76+ status: { type: "idle" },
77+ cwd: "/tmp/codexd-smoke",
78+ cliVersion: "0.116.0",
79+ source: { custom: "codexd-test" },
80+ name: "smoke",
81+ turns: []
82+ };
83+ const turn = {
84+ id: "turn-1",
85+ status: "inProgress",
86+ error: null
87+ };
88+ const completedTurn = {
89+ ...turn,
90+ status: "completed"
91+ };
92+ const session = {
93+ thread,
94+ model: "gpt-5.4",
95+ modelProvider: "openai",
96+ serviceTier: null,
97+ cwd: thread.cwd,
98+ approvalPolicy: "never",
99+ sandbox: createCodexAppServerReadOnlySandboxPolicy(),
100+ reasoningEffort: "medium"
101+ };
102+
103+ const transport = new FakeTransport({
104+ initialize: async () => ({
105+ result: {
106+ userAgent: "codex-cli 0.116.0",
107+ platformFamily: "unix",
108+ platformOs: "macos"
109+ }
110+ }),
111+ "thread/start": async () => ({
112+ notifications: [
113+ {
114+ method: "thread/started",
115+ params: { thread }
116+ }
117+ ],
118+ result: session
119+ }),
120+ "thread/resume": async () => ({
121+ result: session
122+ }),
123+ "turn/start": async () => ({
124+ notifications: [
125+ {
126+ method: "turn/started",
127+ params: {
128+ threadId: thread.id,
129+ turn
130+ }
131+ },
132+ {
133+ method: "item/agentMessage/delta",
134+ params: {
135+ threadId: thread.id,
136+ turnId: turn.id,
137+ itemId: "item-1",
138+ delta: "hel"
139+ }
140+ },
141+ {
142+ method: "item/agentMessage/delta",
143+ params: {
144+ threadId: thread.id,
145+ turnId: turn.id,
146+ itemId: "item-1",
147+ delta: "lo"
148+ }
149+ },
150+ {
151+ method: "turn/completed",
152+ params: {
153+ threadId: thread.id,
154+ turn: completedTurn
155+ }
156+ }
157+ ],
158+ result: { turn }
159+ }),
160+ "turn/steer": async () => ({
161+ result: {
162+ turnId: turn.id
163+ }
164+ }),
165+ "turn/interrupt": async () => ({
166+ result: {}
167+ })
168+ });
169+
170+ const client = new CodexAppServerClient({
171+ clientInfo: {
172+ name: "codexd-smoke",
173+ title: "smoke",
174+ version: "0.1.0"
175+ },
176+ transport
177+ });
178+ const receivedEvents = [];
179+ const subscription = client.events.subscribe((event) => {
180+ receivedEvents.push(event);
181+ });
182+
183+ const initialize = await client.initialize();
184+ const startedSession = await client.threadStart({
185+ cwd: thread.cwd,
186+ baseInstructions: "Be concise."
187+ });
188+ const resumedSession = await client.threadResume({
189+ threadId: thread.id
190+ });
191+ const startedTurn = await client.turnStart({
192+ threadId: thread.id,
193+ input: [createCodexAppServerTextInput("Reply with hello.")]
194+ });
195+ const steeredTurn = await client.turnSteer({
196+ threadId: thread.id,
197+ expectedTurnId: turn.id,
198+ input: [createCodexAppServerTextInput("Reply with hello again.")]
199+ });
200+
201+ await client.turnInterrupt({
202+ threadId: thread.id,
203+ turnId: turn.id
204+ });
205+
206+ subscription.unsubscribe();
207+ await client.close();
208+
209+ assert.equal(initialize.userAgent, "codex-cli 0.116.0");
210+ assert.equal(startedSession.thread.id, thread.id);
211+ assert.equal(resumedSession.thread.id, thread.id);
212+ assert.equal(startedTurn.turn.id, turn.id);
213+ assert.equal(steeredTurn.turnId, turn.id);
214+ assert.deepEqual(
215+ transport.requests.map((request) => request.method),
216+ [
217+ "initialize",
218+ "thread/start",
219+ "thread/resume",
220+ "turn/start",
221+ "turn/steer",
222+ "turn/interrupt"
223+ ]
224+ );
225+ assert.deepEqual(
226+ receivedEvents.map((event) => event.type),
227+ ["thread.started", "turn.started", "turn.message.delta", "turn.message.delta", "turn.completed"]
228+ );
229+ assert.equal(receivedEvents[2].delta, "hel");
230+ assert.equal(receivedEvents[3].delta, "lo");
231+});
232+
233+test("CodexAppServerEventStream supports async iteration for downstream codexd consumers", async () => {
234+ const stream = new CodexAppServerEventStream();
235+
236+ const iteratorTask = (async () => {
237+ const collected = [];
238+
239+ for await (const event of stream) {
240+ collected.push(event);
241+
242+ if (collected.length === 2) {
243+ break;
244+ }
245+ }
246+
247+ return collected;
248+ })();
249+
250+ stream.emit({
251+ type: "turn.message.delta",
252+ notificationMethod: "item/agentMessage/delta",
253+ threadId: "thread-1",
254+ turnId: "turn-1",
255+ itemId: "item-1",
256+ delta: "A"
257+ });
258+ stream.emit({
259+ type: "turn.completed",
260+ notificationMethod: "turn/completed",
261+ threadId: "thread-1",
262+ turn: {
263+ id: "turn-1",
264+ status: "completed",
265+ error: null
266+ }
267+ });
268+ stream.close();
269+
270+ const collected = await iteratorTask;
271+
272+ assert.deepEqual(
273+ collected.map((event) => event.type),
274+ ["turn.message.delta", "turn.completed"]
275+ );
276+});
1@@ -0,0 +1,4 @@
2+export * from "./client.js";
3+export * from "./contracts.js";
4+export * from "./events.js";
5+export * from "./transport.js";
+176,
-0
1@@ -0,0 +1,176 @@
2+declare const WebSocket:
3+ | undefined
4+ | (new (url: string) => CodexAppServerWebSocket);
5+
6+export interface CodexAppServerTransportHandlers {
7+ onClose(error?: Error): void;
8+ onMessage(message: string): void;
9+}
10+
11+export interface CodexAppServerTransport {
12+ close(): Promise<void>;
13+ connect(handlers: CodexAppServerTransportHandlers): Promise<void>;
14+ send(message: string): Promise<void>;
15+}
16+
17+export interface CodexAppServerWebSocketMessageEvent {
18+ data: unknown;
19+}
20+
21+export interface CodexAppServerWebSocketCloseEvent {
22+ code?: number;
23+ reason?: string;
24+}
25+
26+export interface CodexAppServerWebSocket {
27+ onclose: ((event: CodexAppServerWebSocketCloseEvent) => void) | null;
28+ onerror: ((event: unknown) => void) | null;
29+ onmessage: ((event: CodexAppServerWebSocketMessageEvent) => void) | null;
30+ onopen: (() => void) | null;
31+ close(code?: number, reason?: string): void;
32+ send(data: string): void;
33+}
34+
35+export type CodexAppServerWebSocketFactory = (url: string) => CodexAppServerWebSocket;
36+
37+export interface CodexAppServerWebSocketTransportConfig {
38+ url: string;
39+ closeCode?: number;
40+ closeReason?: string;
41+ createSocket?: CodexAppServerWebSocketFactory;
42+}
43+
44+function toError(cause: unknown, fallback: string): Error {
45+ if (cause instanceof Error) {
46+ return cause;
47+ }
48+
49+ if (typeof cause === "string" && cause !== "") {
50+ return new Error(cause);
51+ }
52+
53+ return new Error(fallback);
54+}
55+
56+function resolveDefaultWebSocketFactory(): CodexAppServerWebSocketFactory {
57+ if (typeof WebSocket !== "function") {
58+ throw new Error(
59+ "Global WebSocket is unavailable. Pass createSocket to createCodexAppServerWebSocketTransport."
60+ );
61+ }
62+
63+ return (url: string) => new WebSocket(url);
64+}
65+
66+export function createCodexAppServerWebSocketTransport(
67+ config: CodexAppServerWebSocketTransportConfig
68+): CodexAppServerTransport {
69+ let handlers: CodexAppServerTransportHandlers | null = null;
70+ let socket: CodexAppServerWebSocket | null = null;
71+ let state: "idle" | "connecting" | "open" | "closed" = "idle";
72+
73+ return {
74+ async connect(nextHandlers: CodexAppServerTransportHandlers): Promise<void> {
75+ if (state === "open") {
76+ handlers = nextHandlers;
77+ return;
78+ }
79+
80+ if (state === "connecting") {
81+ throw new Error("Codex app-server transport is already connecting.");
82+ }
83+
84+ if (state === "closed") {
85+ throw new Error("Codex app-server transport is already closed.");
86+ }
87+
88+ handlers = nextHandlers;
89+ state = "connecting";
90+
91+ const createSocket = config.createSocket ?? resolveDefaultWebSocketFactory();
92+ const activeSocket = createSocket(config.url);
93+ socket = activeSocket;
94+
95+ await new Promise<void>((resolve, reject) => {
96+ let settled = false;
97+
98+ activeSocket.onopen = () => {
99+ if (settled) {
100+ return;
101+ }
102+
103+ settled = true;
104+ state = "open";
105+ resolve();
106+ };
107+
108+ activeSocket.onmessage = (event) => {
109+ const message =
110+ typeof event.data === "string"
111+ ? event.data
112+ : event.data === undefined || event.data === null
113+ ? ""
114+ : String(event.data);
115+
116+ handlers?.onMessage(message);
117+ };
118+
119+ activeSocket.onerror = (event) => {
120+ const error = toError(event, `Failed to connect to Codex app-server at ${config.url}.`);
121+
122+ if (!settled) {
123+ settled = true;
124+ state = "closed";
125+ socket = null;
126+ reject(error);
127+ return;
128+ }
129+
130+ state = "closed";
131+ socket = null;
132+ handlers?.onClose(error);
133+ };
134+
135+ activeSocket.onclose = (event) => {
136+ const code = event.code ?? 1000;
137+ const reason = event.reason ? `: ${event.reason}` : "";
138+ const error = new Error(`Codex app-server WebSocket closed (${code})${reason}`);
139+
140+ if (!settled) {
141+ settled = true;
142+ state = "closed";
143+ socket = null;
144+ reject(error);
145+ return;
146+ }
147+
148+ state = "closed";
149+ socket = null;
150+ handlers?.onClose(error);
151+ };
152+ });
153+ },
154+
155+ async send(message: string): Promise<void> {
156+ if (state !== "open" || socket === null) {
157+ throw new Error("Codex app-server transport is not connected.");
158+ }
159+
160+ socket.send(message);
161+ },
162+
163+ async close(): Promise<void> {
164+ if (socket === null) {
165+ state = "closed";
166+ handlers = null;
167+ return;
168+ }
169+
170+ const activeSocket = socket;
171+ socket = null;
172+ state = "closed";
173+ handlers = null;
174+ activeSocket.close(config.closeCode ?? 1000, config.closeReason ?? "client closing");
175+ }
176+ };
177+}
1@@ -0,0 +1,9 @@
2+{
3+ "extends": "../../tsconfig.base.json",
4+ "compilerOptions": {
5+ "declaration": true,
6+ "rootDir": "src",
7+ "outDir": "dist"
8+ },
9+ "include": ["src/**/*.ts", "src/**/*.d.ts"]
10+}