361 lines
17 KiB
Markdown
361 lines
17 KiB
Markdown
# codex-app-server
|
||
|
||
`codex app-server` is the interface Codex uses to power rich interfaces such as the [Codex VS Code extension](https://marketplace.visualstudio.com/items?itemName=openai.chatgpt). The message schema is currently unstable, but those who wish to build experimental UIs on top of Codex may find it valuable.
|
||
|
||
## Table of Contents
|
||
- [Protocol](#protocol)
|
||
- [Message Schema](#message-schema)
|
||
- [Lifecycle Overview](#lifecycle-overview)
|
||
- [Initialization](#initialization)
|
||
- [Core primitives](#core-primitives)
|
||
- [Thread & turn endpoints](#thread--turn-endpoints)
|
||
- [Auth endpoints](#auth-endpoints)
|
||
- [Events (work-in-progress)](#v2-streaming-events-work-in-progress)
|
||
|
||
## Protocol
|
||
|
||
Similar to [MCP](https://modelcontextprotocol.io/), `codex app-server` supports bidirectional communication, streaming JSONL over stdio. The protocol is JSON-RPC 2.0, though the `"jsonrpc":"2.0"` header is omitted.
|
||
|
||
## Message Schema
|
||
|
||
Currently, you can dump a TypeScript version of the schema using `codex app-server generate-ts`, or a JSON Schema bundle via `codex app-server generate-json-schema`. Each output is specific to the version of Codex you used to run the command, so the generated artifacts are guaranteed to match that version.
|
||
|
||
```
|
||
codex app-server generate-ts --out DIR
|
||
codex app-server generate-json-schema --out DIR
|
||
```
|
||
|
||
## Lifecycle Overview
|
||
|
||
- Initialize once: Immediately after launching the codex app-server process, send an `initialize` request with your client metadata, then emit an `initialized` notification. Any other request before this handshake gets rejected.
|
||
- Start (or resume) a thread: Call `thread/start` to open a fresh conversation. The response returns the thread object and you’ll also get a `thread/started` notification. If you’re continuing an existing conversation, call `thread/resume` with its ID instead.
|
||
- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy, etc. This immediately returns the new turn object and triggers a `turn/started` notification.
|
||
- Stream events: After `turn/start`, keep reading JSON-RPC notifications on stdout. You’ll see `item/started`, `item/completed`, deltas like `item/agentMessage/delta`, tool progress, etc. These represent streaming model output plus any side effects (commands, tool calls, reasoning notes).
|
||
- Finish the turn: When the model is done (or the turn is interrupted via making the `turn/interrupt` call), the server sends `turn/completed` with the final turn state and token usage.
|
||
|
||
## Initialization
|
||
|
||
Clients must send a single `initialize` request before invoking any other method, then acknowledge with an `initialized` notification. The server returns the user agent string it will present to upstream services; subsequent requests issued before initialization receive a `"Not initialized"` error, and repeated `initialize` calls receive an `"Already initialized"` error.
|
||
|
||
Example:
|
||
|
||
```json
|
||
{ "method": "initialize", "id": 0, "params": {
|
||
"clientInfo": { "name": "codex-vscode", "title": "Codex VS Code Extension", "version": "0.1.0" }
|
||
} }
|
||
{ "id": 0, "result": { "userAgent": "codex-app-server/0.1.0 codex-vscode/0.1.0" } }
|
||
{ "method": "initialized" }
|
||
```
|
||
|
||
## Core primitives
|
||
|
||
We have 3 top level primitives:
|
||
- Thread - a conversation between the Codex agent and a user. Each thread contains multiple turns.
|
||
- Turn - one turn of the conversation, typically starting with a user message and finishing with an agent message. Each turn contains multiple items.
|
||
- Item - represents user inputs and agent outputs as part of the turn, persisted and used as the context for future conversations.
|
||
|
||
## Thread & turn endpoints
|
||
|
||
The JSON-RPC API exposes dedicated methods for managing Codex conversations. Threads store long-lived conversation metadata, and turns store the per-message exchange (input → Codex output, including streamed items). Use the thread APIs to create, list, or archive sessions, then drive the conversation with turn APIs and notifications.
|
||
|
||
### Quick reference
|
||
- `thread/start` — create a new thread; emits `thread/started` and auto-subscribes you to turn/item events for that thread.
|
||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it.
|
||
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders` filtering.
|
||
- `thread/archive` — move a thread’s rollout file into the archived directory; returns `{}` on success.
|
||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications.
|
||
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
|
||
- `review/start` — kick off Codex’s automated reviewer for a thread; responds like `turn/start` and emits a `item/completed` notification with a `codeReview` item when results are ready.
|
||
|
||
### 1) Start or resume a thread
|
||
|
||
Start a fresh thread when you need a new Codex conversation.
|
||
|
||
```json
|
||
{ "method": "thread/start", "id": 10, "params": {
|
||
// Optionally set config settings. If not specified, will use the user's
|
||
// current config settings.
|
||
"model": "gpt-5.1-codex",
|
||
"cwd": "/Users/me/project",
|
||
"approvalPolicy": "never",
|
||
"sandbox": "workspaceWrite",
|
||
} }
|
||
{ "id": 10, "result": {
|
||
"thread": {
|
||
"id": "thr_123",
|
||
"preview": "",
|
||
"modelProvider": "openai",
|
||
"createdAt": 1730910000
|
||
}
|
||
} }
|
||
{ "method": "thread/started", "params": { "thread": { … } } }
|
||
```
|
||
|
||
To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`, and no additional notifications are emitted:
|
||
|
||
```json
|
||
{ "method": "thread/resume", "id": 11, "params": { "threadId": "thr_123" } }
|
||
{ "id": 11, "result": { "thread": { "id": "thr_123", … } } }
|
||
```
|
||
|
||
### 2) List threads (pagination & filters)
|
||
|
||
`thread/list` lets you render a history UI. Pass any combination of:
|
||
- `cursor` — opaque string from a prior response; omit for the first page.
|
||
- `limit` — server defaults to a reasonable page size if unset.
|
||
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
|
||
|
||
Example:
|
||
|
||
```json
|
||
{ "method": "thread/list", "id": 20, "params": {
|
||
"cursor": null,
|
||
"limit": 25,
|
||
} }
|
||
{ "id": 20, "result": {
|
||
"data": [
|
||
{ "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111 },
|
||
{ "id": "thr_b", "preview": "Fix tests", "modelProvider": "openai", "createdAt": 1730750000 }
|
||
],
|
||
"nextCursor": "opaque-token-or-null"
|
||
} }
|
||
```
|
||
|
||
When `nextCursor` is `null`, you’ve reached the final page.
|
||
|
||
### 3) Archive a thread
|
||
|
||
Use `thread/archive` to move the persisted rollout (stored as a JSONL file on disk) into the archived sessions directory.
|
||
|
||
```json
|
||
{ "method": "thread/archive", "id": 21, "params": { "threadId": "thr_b" } }
|
||
{ "id": 21, "result": {} }
|
||
```
|
||
|
||
An archived thread will not appear in future calls to `thread/list`.
|
||
|
||
### 4) Start a turn (send user input)
|
||
|
||
Turns attach user input (text or images) to a thread and trigger Codex generation. The `input` field is a list of discriminated unions:
|
||
|
||
- `{"type":"text","text":"Explain this diff"}`
|
||
- `{"type":"image","url":"https://…png"}`
|
||
- `{"type":"localImage","path":"/tmp/screenshot.png"}`
|
||
|
||
You can optionally specify config overrides on the new turn. If specified, these settings become the default for subsequent turns on the same thread.
|
||
|
||
```json
|
||
{ "method": "turn/start", "id": 30, "params": {
|
||
"threadId": "thr_123",
|
||
"input": [ { "type": "text", "text": "Run tests" } ],
|
||
// Below are optional config overrides
|
||
"cwd": "/Users/me/project",
|
||
"approvalPolicy": "unlessTrusted",
|
||
"sandboxPolicy": {
|
||
"mode": "workspaceWrite",
|
||
"writableRoots": ["/Users/me/project"],
|
||
"networkAccess": true
|
||
},
|
||
"model": "gpt-5.1-codex",
|
||
"effort": "medium",
|
||
"summary": "concise"
|
||
} }
|
||
{ "id": 30, "result": { "turn": {
|
||
"id": "turn_456",
|
||
"status": "inProgress",
|
||
"items": [],
|
||
"error": null
|
||
} } }
|
||
```
|
||
|
||
### 5) Interrupt an active turn
|
||
|
||
You can cancel a running Turn with `turn/interrupt`.
|
||
|
||
```json
|
||
{ "method": "turn/interrupt", "id": 31, "params": {
|
||
"threadId": "thr_123",
|
||
"turnId": "turn_456"
|
||
} }
|
||
{ "id": 31, "result": {} }
|
||
```
|
||
|
||
The server requests cancellations for running subprocesses, then emits a `turn/completed` event with `status: "interrupted"`. Rely on the `turn/completed` to know when Codex-side cleanup is done.
|
||
|
||
### 6) Request a code review
|
||
|
||
Use `review/start` to run Codex’s reviewer on the currently checked-out project. The request takes the thread id plus a `target` describing what should be reviewed:
|
||
|
||
- `{"type":"uncommittedChanges"}` — staged, unstaged, and untracked files.
|
||
- `{"type":"baseBranch","branch":"main"}` — diff against the provided branch’s upstream (see prompt for the exact `git merge-base`/`git diff` instructions Codex will run).
|
||
- `{"type":"commit","sha":"abc1234","title":"Optional subject"}` — review a specific commit.
|
||
- `{"type":"custom","instructions":"Free-form reviewer instructions"}` — fallback prompt equivalent to the legacy manual review request.
|
||
- `appendToOriginalThread` (bool, default `false`) — when `true`, Codex also records a final assistant-style message with the review summary in the original thread. When `false`, only the `codeReview` item is emitted for the review run and no extra message is added to the original thread.
|
||
|
||
Example request/response:
|
||
|
||
```json
|
||
{ "method": "review/start", "id": 40, "params": {
|
||
"threadId": "thr_123",
|
||
"appendToOriginalThread": true,
|
||
"target": { "type": "commit", "sha": "1234567deadbeef", "title": "Polish tui colors" }
|
||
} }
|
||
{ "id": 40, "result": { "turn": {
|
||
"id": "turn_900",
|
||
"status": "inProgress",
|
||
"items": [
|
||
{ "type": "userMessage", "id": "turn_900", "content": [ { "type": "text", "text": "Review commit 1234567: Polish tui colors" } ] }
|
||
],
|
||
"error": null
|
||
} } }
|
||
```
|
||
|
||
Codex streams the usual `turn/started` notification followed by an `item/started`
|
||
with the same `codeReview` item id so clients can show progress:
|
||
|
||
```json
|
||
{ "method": "item/started", "params": { "item": {
|
||
"type": "codeReview",
|
||
"id": "turn_900",
|
||
"review": "current changes"
|
||
} } }
|
||
```
|
||
|
||
When the reviewer finishes, the server emits `item/completed` containing the same
|
||
`codeReview` item with the final review text:
|
||
|
||
```json
|
||
{ "method": "item/completed", "params": { "item": {
|
||
"type": "codeReview",
|
||
"id": "turn_900",
|
||
"review": "Looks solid overall...\n\n- Prefer Stylize helpers — app.rs:10-20\n ..."
|
||
} } }
|
||
```
|
||
|
||
The `review` string is plain text that already bundles the overall explanation plus a bullet list for each structured finding (matching `ThreadItem::CodeReview` in the generated schema). Use this notification to render the reviewer output in your client.
|
||
|
||
## Auth endpoints
|
||
|
||
The JSON-RPC auth/account surface exposes request/response methods plus server-initiated notifications (no `id`). Use these to determine auth state, start or cancel logins, logout, and inspect ChatGPT rate limits.
|
||
|
||
### Quick reference
|
||
- `account/read` — fetch current account info; optionally refresh tokens.
|
||
- `account/login/start` — begin login (`apiKey` or `chatgpt`).
|
||
- `account/login/completed` (notify) — emitted when a login attempt finishes (success or error).
|
||
- `account/login/cancel` — cancel a pending ChatGPT login by `loginId`.
|
||
- `account/logout` — sign out; triggers `account/updated`.
|
||
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`).
|
||
- `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify).
|
||
|
||
### 1) Check auth state
|
||
|
||
Request:
|
||
```json
|
||
{ "method": "account/read", "id": 1, "params": { "refreshToken": false } }
|
||
```
|
||
|
||
Response examples:
|
||
```json
|
||
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": false } } // No OpenAI auth needed (e.g., OSS/local models)
|
||
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": true } } // OpenAI auth required (typical for OpenAI-hosted models)
|
||
{ "id": 1, "result": { "account": { "type": "apiKey" }, "requiresOpenaiAuth": true } }
|
||
{ "id": 1, "result": { "account": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "requiresOpenaiAuth": true } }
|
||
```
|
||
|
||
Field notes:
|
||
- `refreshToken` (bool): set `true` to force a token refresh.
|
||
- `requiresOpenaiAuth` reflects the active provider; when `false`, Codex can run without OpenAI credentials.
|
||
|
||
### 2) Log in with an API key
|
||
|
||
1. Send:
|
||
```json
|
||
{ "method": "account/login/start", "id": 2, "params": { "type": "apiKey", "apiKey": "sk-…" } }
|
||
```
|
||
2. Expect:
|
||
```json
|
||
{ "id": 2, "result": { "type": "apiKey" } }
|
||
```
|
||
3. Notifications:
|
||
```json
|
||
{ "method": "account/login/completed", "params": { "loginId": null, "success": true, "error": null } }
|
||
{ "method": "account/updated", "params": { "authMode": "apikey" } }
|
||
```
|
||
|
||
### 3) Log in with ChatGPT (browser flow)
|
||
|
||
1. Start:
|
||
```json
|
||
{ "method": "account/login/start", "id": 3, "params": { "type": "chatgpt" } }
|
||
{ "id": 3, "result": { "type": "chatgpt", "loginId": "<uuid>", "authUrl": "https://chatgpt.com/…&redirect_uri=http%3A%2F%2Flocalhost%3A<port>%2Fauth%2Fcallback" } }
|
||
```
|
||
2. Open `authUrl` in a browser; the app-server hosts the local callback.
|
||
3. Wait for notifications:
|
||
```json
|
||
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": true, "error": null } }
|
||
{ "method": "account/updated", "params": { "authMode": "chatgpt" } }
|
||
```
|
||
|
||
### 4) Cancel a ChatGPT login
|
||
|
||
```json
|
||
{ "method": "account/login/cancel", "id": 4, "params": { "loginId": "<uuid>" } }
|
||
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": false, "error": "…" } }
|
||
```
|
||
|
||
### 5) Logout
|
||
|
||
```json
|
||
{ "method": "account/logout", "id": 5 }
|
||
{ "id": 5, "result": {} }
|
||
{ "method": "account/updated", "params": { "authMode": null } }
|
||
```
|
||
|
||
### 6) Rate limits (ChatGPT)
|
||
|
||
```json
|
||
{ "method": "account/rateLimits/read", "id": 6 }
|
||
{ "id": 6, "result": { "rateLimits": { "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 }, "secondary": null } } }
|
||
{ "method": "account/rateLimits/updated", "params": { "rateLimits": { … } } }
|
||
```
|
||
|
||
Field notes:
|
||
- `usedPercent` is current usage within the OpenAI quota window.
|
||
- `windowDurationMins` is the quota window length.
|
||
- `resetsAt` is a Unix timestamp (seconds) for the next reset.
|
||
|
||
### Dev notes
|
||
|
||
- `codex app-server generate-ts --out <dir>` emits v2 types under `v2/`.
|
||
- `codex app-server generate-json-schema --out <dir>` outputs `codex_app_server_protocol.schemas.json`.
|
||
- See [“Authentication and authorization” in the config docs](../../docs/config.md#authentication-and-authorization) for configuration knobs.
|
||
|
||
|
||
## Events (work-in-progress)
|
||
|
||
Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for `thread/started`, `turn/*`, and `item/*` notifications.
|
||
|
||
### Turn events
|
||
|
||
The app-server streams JSON-RPC notifications while a turn is running. Each turn starts with `turn/started` (initial `turn`) and ends with `turn/completed` (final `turn` plus token `usage`), and clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`.
|
||
|
||
#### Thread items
|
||
|
||
`ThreadItem` is the tagged union carried in turn responses and `item/*` notifications. Currently we support events for the following items:
|
||
- `userMessage` — `{id, content}` where `content` is a list of user inputs (`text`, `image`, or `localImage`).
|
||
- `agentMessage` — `{id, text}` containing the accumulated agent reply.
|
||
- `reasoning` — `{id, summary, content}` where `summary` holds streamed reasoning summaries (applicable for most OpenAI models) and `content` holds raw reasoning blocks (applicable for e.g. open source models).
|
||
- `mcpToolCall` — `{id, server, tool, status, arguments, result?, error?}` describing MCP calls; `status` is `inProgress`, `completed`, or `failed`.
|
||
- `webSearch` — `{id, query}` for a web search request issued by the agent.
|
||
|
||
All items emit two shared lifecycle events:
|
||
- `item/started` — emits the full `item` when a new unit of work begins so the UI can render it immediately; the `item.id` in this payload matches the `itemId` used by deltas.
|
||
- `item/completed` — sends the final `item` once that work finishes (e.g., after a tool call or message completes); treat this as the authoritative state.
|
||
|
||
There are additional item-specific events:
|
||
#### agentMessage
|
||
- `item/agentMessage/delta` — appends streamed text for the agent message; concatenate `delta` values for the same `itemId` in order to reconstruct the full reply.
|
||
#### reasoning
|
||
- `item/reasoning/summaryTextDelta` — streams readable reasoning summaries; `summaryIndex` increments when a new summary section opens.
|
||
- `item/reasoning/summaryPartAdded` — marks the boundary between reasoning summary sections for an `itemId`; subsequent `summaryTextDelta` entries share the same `summaryIndex`.
|
||
- `item/reasoning/textDelta` — streams raw reasoning text (only applicable for e.g. open source models); use `contentIndex` to group deltas that belong together before showing them in the UI.
|