Skip to content

Latest commit

 

History

History
321 lines (264 loc) · 19.6 KB

readme-dev.md

File metadata and controls

321 lines (264 loc) · 19.6 KB

This package uses the bundled @openai/codex dependency by default. Set CODEX_PATH to run a different Codex binary; versions other than the one specified in package.json may not be compatible.

Runtime environment

  • CODEX_API_KEY - API key used when the API-key auth method is selected. Takes precedence over OPENAI_API_KEY.
  • OPENAI_API_KEY - fallback API key used when the API-key auth method is selected.
  • CODEX_PATH - run a specific Codex executable instead of the bundled package dependency.
  • CODEX_CONFIG - JSON object merged into the Codex session config. hooks move to App Server startup (-c hooks=...) for AIR review; Codex rejects malformed hooks at startup and initialize reports its error.
  • MODEL_PROVIDER - model provider to pass to Codex for new sessions.
  • DEFAULT_AUTH_REQUEST - ACP auth request JSON used when Codex requires authentication.
  • INITIAL_AGENT_MODE - initial mode id: read-only, workspace-write, agent, or agent-full-access.
  • NO_BROWSER - hide browser-based ChatGPT auth when set.
  • APP_SERVER_LOGS - directory for adapter logs.
  • CODEX_ACP_APP_SERVER_CRASH_LIMIT - how many crashes of the Codex app-server in the crash window stop its automatic restart (default 5).
  • CODEX_ACP_APP_SERVER_CRASH_WINDOW_MS - the crash window in milliseconds (default 300000, 5 minutes).
  • CODEX_ACP_PROMPT_SUGGESTIONS - set to false to turn off the predicted follow-up prompt for AIR. It costs one extra model call per turn.

Quick start

Develop on Windows?

Adjust ACP client config

Run from sources

  1. Install dependencies npm install
  2. Adjust ACP client config
{
  "agent_servers": {
    "Codex (app-server)": {
      "command": "npm",
      "args": ["run", "start", "--prefix", "/path/to/project/"],
      "env": {
        "CODEX_PATH": "node_modules/.bin/codex",
        "APP_SERVER_LOGS": "optional/path/to/existing/log/directory"
      }
    }
  }
}

Run from binaries

  1. Download a codex-acp-<platform>.zip archive from https://github.com/agentclientprotocol/codex-acp/releases (<platform> is one of: linux, darwin, win32)
  2. Unzip the archive:
    unzip codex-acp-<platform>.zip
  3. Adjust ACP client config
{
  "agent_servers": {
    "Codex (app-server)": {
      "command": "/path/to/codex-acp",
      "env": {
        "CODEX_PATH": "/path/to/codex"
      }
    }
  }
}

Build binaries

Building standalone binaries requires bun.

Build single-file executables in dist/bin directory:

npm run bundle:all

Package binaries into zip archives:

npm run package:all

Update supported Codex version

  1. Update the @openai/codex version in package.json (under dependencies).
  2. Regenerate Codex types in src/app-server/: npm run generate-types
  3. Ensure there are no type errors or failed tests: npm run typecheck and npm run test

Test suites

  • npm test: unit tests with the Codex app-server mocked.
  • npm run test:integration: real codex-acp plus a real codex app-server. The model is a local mock of the OpenAI Responses API (src/__tests__/integration/mock-responses-server.ts) reached through the gateway auth method, so no API key or network is needed. Tests script model replies with textResponse, execCommandResponse and applyPatchResponse.
  • npm run test:e2e: real OpenAI model. Builds first and requires RUN_E2E_TESTS=true (set by the script) plus OPENAI_API_KEY or CODEX_API_KEY in the environment.

Session notices

The adapter implements Session Notices for Codex warnings, configuration warnings, deprecation notices, model rerouting, and the legacy thread/compacted advisory when the client advertises clientCapabilities.session.notices: {}. These are live session/update notifications with sessionUpdate: "notice", a severity, a plain-text title, and optional description. They are not replayed from session history and repeated notices remain independent events.

Without that capability (including absent or null capability objects), the adapter preserves the existing assistant/thought text or AIR sessionFailure advisory records. When notices are enabled, they take precedence over AIR advisory records. Clients control their presentation; the adapter does not rely on notices being displayed.

Command replies, review results, and terminal/retrying errors retain their existing response or failure channels. Clients advertising session compaction support continue to receive the dedicated compaction lifecycle instead of the legacy completion advisory.

Unsupported skills

The adapter disables Codex skills whose output ACP clients cannot show, and leaves them out of the skill commands and /skills. The list is in src/UnsupportedSkills.ts. Today it holds Codex's bundled Visualize skill: it ends a reply with a content reference that only the OpenAI apps render, so ACP clients show raw visualize{...} text.

The adapter matches skills by name, so it needs no skills/list request when a session starts. The cost: a skill from another plugin with the same qualified name, such as a visualize skill in some other plugin named visualize, is disabled too.

App-server recovery

The adapter restarts the Codex app-server when it dies (for example, killed for lack of memory). Its code is in src/app-server-recovery/.

  • A request that was waiting on the dead app-server fails with error 1001 and the cause, such as "was killed by SIGKILL, which usually means it ran out of memory". A running prompt ends at once: open tool calls become failed, open permission dialogs are cancelled, and an AIR client gets the transport_lost session failure.
  • An app-server that exits before its initialize handshake succeeded, such as one that rejects its startup config, fails the request with "Codex process has exited with code N:" and its stderr, and no promise of a restart.
  • The next request starts a new app-server with the same initialize handshake and provider routing. A session that was open is resumed on its next use, with the model, mode and settings that the adapter kept.
  • Codex writes a thread to disk only with its first message, so a session without messages cannot be resumed in a new app-server, after a crash or a provider restart. Its next use fails with "had no messages yet and was lost"; the client starts a new session. A provider restart still completes for the agent and the other sessions.
  • After CODEX_ACP_APP_SERVER_CRASH_LIMIT crashes in the crash window the adapter stops restarting it and says so, with the stderr of the last crash. A session that the app-server died opening twice in 30 minutes is not opened again for the rest of that time, so one session that is too large for the memory cannot take the other sessions down with it.

ACP v2 support

The agent speaks both ACP v1 and the unstable ACP v2 (Draft) protocol from the same process; the version is negotiated on the first initialize request and fixed for the connection's lifetime.

The v2 chain registers: initialize, session/new|list|close|delete|resume (v2 folds session/load into session/resume with replayFrom), session/set_config_option, session/cancel, auth/login|logout, session/prompt (plus $/cancel_request), session/fork, and providers/list|set|disable. Unstable extension methods on top of that: _session/steering, _session/goal (set/pause/resume/clear), _session/async_task/stop, and the AIR session index requests (_session/rename, _session/archive|unarchive, _session/list/subscribe|unsubscribe). session/fork drops the v1 modes field (v2 removed the modes API in favor of config options), and forked sessions do not get an available_commands_update, matching v1. There are no new environment/config knobs specific to v2; it reuses the runtime environment variables above.

Client system prompts

Both ACP versions advertise systemPrompt: {}: text-only append mode. Override and non-text content are rejected with -32602 before a session is created or a fork is made. Text blocks are joined with a blank line and appended to the existing developer-instruction baseline: AIR customInstructions when supplied, otherwise configured Codex developer instructions. Codex's base instructions and workspace context remain intact. Requests without systemPrompt keep the legacy AIR behavior.

New sessions use the instructions on their first turn. A changed prompt on an idle, persisted session detaches and reloads that thread under the same session ID. The retained model context changes at the next compaction, not immediately after resume; the adapter does not force a compaction. Forks with changed instructions have the same deferred behavior.

Changed prompts are rejected while session work or background terminals are running, when a live session has no saved rollout yet, or when Codex cannot confirm a reload. Existing work is not cancelled. Retry after work finishes, or create a new session for immediate instructions. New prompts, steering, and config changes cannot race an instruction reload.

Omitted/null prompts keep the instructions known to the currently open adapter session, including through app-server recovery and provider restarts. After closing a session or restarting the adapter, clients should resend their prompt on load/resume/fork; without it, Codex's retained context and configured defaults determine the instructions.

Manual compaction

/compact finishes foreground work only after Codex reports turn/completed for the compact turn. Item completion still reports compaction progress, but does not release the next prompt. The v1 prompt response and v2 idle state wait for the turn to finish; v2's early prompt-acceptance response is unchanged.

AIR v2 client contract

These are behaviors the AIR (JetBrains) ACP client relies on that aren't obvious from the wire schema alone:

  • Initial commands come from availableCommands in v2 session/new, session/resume, and session/fork responses, not an initial available_commands_update. Later command changes still use notifications. Text inputs use type: "text", and AIR command actions and skill paths are preserved. Skill discovery is best-effort: setup succeeds without initial commands if it fails.
  • Failed foreground work ends with state: "idle", stopReason: "error" and a standard error object containing code and a user-facing message. Before insertion, the prompt also fails with a JSON-RPC error; after insertion, its successful messageId response is not revoked. Cancellation remains cancelled. Retries, individual failed tools, and idle background errors do not become terminal session failures. Raw process stderr and opaque error data are not sent in the standard error object.
  • On a failed prompt, read sessionFailure (_meta.jetbrains.air.sessionFailure) and quota (_meta.quota) from the idle state_update that ends the turn, not from a separate error channel. The same _meta v1 attaches to PromptResponse is copied onto the v2 idle update.
  • Custom session updates are renamed on v2: subagent_spawned/subagent_state_update become a single _subagent_update (gated on the AIR nativeSubagentSessions capability; this contract is retained independently of standard subagent sessions), and async_task_spawned/async_task_state_update become _async_task_spawned/_async_task_state_update (gated on the AIR asyncTasks capability). Payload shapes are unchanged.
  • Foreground state is independent of prompt acceptance. running can precede the user-message echo, including while Codex runs hooks. A successful prompt response still waits for insertion.
  • Every successful session/resume sends a current-state snapshot after any replay and before its response. An idle snapshot is not another completed turn and repeats no usage or failure metadata.
  • Permission and elicitation requests affect aggregate foreground state: another runnable item keeps the session running, and answering one wait does not resume work that remains blocked. Withdrawing a request on cancellation does not resume the workflow; late answers cannot revive an ended turn.
  • A steer (_session/steering) shows up as a user_message once it actually lands in the turn.
  • On cancel, any permission or elicitation request still waiting on a client response is withdrawn with $/cancel_request, not just the underlying Codex turn.
  • Turns Codex starts on its own (goal auto-turns, e.g. after session/resume or session/new) are rendered the same as prompted turns (running then one idle) but with no corresponding session/prompt call — clients should not assume every running/idle pair has a matching outbound prompt.
  • After a tool call streams terminal output as terminal_output_chunks, completion does not resend a full output snapshot; the streamed chunks are the only copy of the output.
  • The AIR tool call contract (docs/air-extensions.md) does not apply on v2: every v2 client gets the standard tool call reports, so rawInputRendering, planContentDelta and diffPatch are not offered there. A read, search or list command has no terminal; its output arrives as content text when it completes. A file change sends one diff per file whose Git patch is built from the Codex diff.
  • The AIR sessionIndex extension (docs/air-extensions.md, "Session index") works as on v1: a client that declares sessionIndex gets it, session/list reads _meta.jetbrains.air.list, and rows, session_info_updates and _session/list/changes carry the same _meta.
  • Other AIR _meta keys (goal, message phase, mode kind, permission presentation) go only to AIR clients, as on v1. Every v2 permission request still gets its prompt title and description.

Live standard subagent sessions

Non-AIR v1 clients opt in with clientCapabilities.subagents: {}. Non-AIR v2 clients use standard subagent_update associations automatically. AIR keeps its existing opt-in, updates, and generation-based identities; clients do not receive both contracts.

A standard child keeps one opaque conversation ID across assignments and parent handler changes. Its ancestors are announced before its output, tool updates, or interactions. Permissions and elicitation use the child's ID and survive parent replay; actual work endings withdraw obsolete requests locally, even if the client returns a late answer. Child tools, terminals, plans, titles, and observed configuration stay separate from the parent.

Only cancellation is advertised, and only while an owned, observable child's current turn can be identified. Send session/cancel with its opaque ACP child ID to interrupt that work and observed owned descendant work, not its parent or siblings. Raw provider IDs cannot bypass the restriction. Prompting, loading, configuration changes, closing, and other child mutations remain disabled. Parent cancellation still interrupts observed live descendants. An observed work ending reports idle; loss of observability reports unknown, not a fabricated failure or a permanently closed conversation.

Cancellation withdraws affected permission/elicitation requests, but an interrupt acknowledgement does not fabricate a cancelled outcome. Actual provider completion reports the ending; the same conversation may receive later work. Idle or unobservable children have no cancel capability. Reattachment suspends child controls until the latest successful response is actually written, including complete JSON-RPC batches; historical or copied child records never grant control.

Child config_option_update notifications contain complete reported snapshots. Observed model and reasoning effort use singleton selects, including changes between assignments. Unknown or unset values are omitted; no values are inferred from the parent or requested spawn arguments. These reports do not enable configuration changes, even to the current value. Replayed configuration is historical, not confirmed current. Reattachment drops cached confirmation and refreshes loaded children after the response. Optional reads do not block child delivery; newer observations or attachment changes discard stale results.

Replay follows both recorded spawn formats and replays available child transcripts once per conversation, including nested children, using legacy or paginated history. Announcements and all selected history precede the response. Completed snapshots reconcile tool and rendering state before duplicate live payloads are suppressed; live bookkeeping still runs. In-progress commands retain classification without resetting streamed output. Incomplete text has no safe delta cursor; it and other records without a safe partial representation are omitted rather than replayed over newer content. Agent-owned input is not rendered as human input. Missing, omitted, or incomplete child history produces a parent advisory; an empty transcript alone does not invent a gap.

Replay reports the latest recorded child turn's known success, cancellation, or failure as historical idle before the response. A missing or still-running turn invents no outcome. Legacy history reuses its turn data; paginated history reads the latest turn summary separately, retaining available text and reporting a gap if that summary is unavailable. Recorded outcomes do not enter the live foreground cache, withdraw current interactions, confirm runtime ownership, or grant controls. Rejoining a loaded child still requires current provider parentage; copied fork history can be displayed but cannot acquire the original runtime's subscriptions. After replay, current child work/configuration snapshots follow the actual response write, including whole JSON-RPC batches. Parent setup operations serialize without waiting on each other's batch delivery.

Models of a providers/set gateway

providers/set lists the models of the gateway (GET {baseUrl}/models, with the headers Codex sends to it) before it responds. The models reported to the client are then only the Codex models whose model or id the gateway lists, matched without case and date suffix. A session whose thread model the gateway does not list uses the default of these models; a thread model that the gateway lists but Codex does not know is kept. A gateway that lists none of the Codex models leaves no model option at all, and the session keeps the thread's model as current. Sessions already open get the models of the new routing when the provider restart resumes them, after providers/disable too. A 4xx answer means the gateway has no listing: providers/set succeeds and nothing is narrowed. A failed listing (network error, a status other than 2xx or 4xx, malformed body, or the timeout) fails providers/set with internal_error and keeps the current routing; a base URL that is not http(s) fails with invalid_params. A listing that finishes after a newer providers/set or providers/disable was applied is dropped. The gateway auth method and providers from the Codex config are not listed.

Known gaps

  • After providers/disable("openai"), providers/list still reports native OpenAI routing as the current provider. The providers RFD says a disabled provider's current should be null; this is left as-is and documented here rather than fixed.
  • Open question for AIR: AIR's fork points match Codex item ids, but on v2 a user message's rendered id is the client-minted clientId (when the client supplied one), not the Codex item id. A fork at a user message would not resolve. This is flagged for AIR, not fixed here.