baa-conductor

git clone 

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
A packages/codex-app-server/package.json
+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+}
A packages/codex-app-server/src/client.ts
+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+}
A packages/codex-app-server/src/contracts.ts
+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+}
A packages/codex-app-server/src/events.ts
+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+}
A packages/codex-app-server/src/index.test.js
+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+});
A packages/codex-app-server/src/index.ts
+4, -0
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";
A packages/codex-app-server/src/transport.ts
+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+}
A packages/codex-app-server/tsconfig.json
+9, -0
 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+}