56 lines
6.6 KiB
Markdown
56 lines
6.6 KiB
Markdown
# Codex app-server protocol
|
|
|
|
How `internal/codex` drives `codex app-server`. The server's own schema is the reference: `codex app-server generate-json-schema --experimental --out DIR` writes it, and `codex_app_server_protocol.schemas.json` in DIR is the whole bundle (top-level definitions plus a nested `v2` set).
|
|
|
|
## Binary and version
|
|
|
|
byre runs `codex` from `PATH`. On the owner's machine that is the CLI inside the ChatGPT desktop app (`ls -la ~/.local/bin/codex` shows the link), so the version moves when the app updates itself; `codex --version` prints it. byre passes `-c check_for_update_on_startup=false` so the CLI never checks for or offers updates of its own. `SchemaVersion` in `internal/codex/protocol.gen.go` names the version the types were generated from; when it differs from `codex --version`, regenerate and re-record (below).
|
|
|
|
## Generated types
|
|
|
|
`go generate ./internal/codex` runs `internal/codex/gen`: it asks the CLI for the experimental schema, then writes `protocol.gen.go` with a Go type for every params and response type of the client methods listed in `clientMethods` (`internal/codex/gen/main.go`), every server notification and every server request, and everything they reference. The experimental schema is used because the server sends experimental fields (such as `environments` on a thread) even to clients that don't opt into them.
|
|
|
|
- A `oneOf`/`anyOf` whose alternatives are all objects becomes one struct with every alternative's fields (a field whose type differs between alternatives is `json.RawMessage`), plus `<Type>Type<Value>` constants for its `type` discriminator, as for `ThreadItem`.
|
|
- Alternatives that are all strings become a string type with a constant per value; a mix of strings and objects (such as `AskForApproval`) is `json.RawMessage`.
|
|
- Optional and nullable fields get `omitempty`; optional structs are pointers.
|
|
- `Method<Name>` constants name every method; `clientResults`, `notificationParams`, `serverRequestParams` and `serverRequestResponses` map methods to their types.
|
|
|
|
## Launch and framing
|
|
|
|
```
|
|
codex app-server -c check_for_update_on_startup=false
|
|
```
|
|
|
|
Run in the session's working directory, with `GIT_AUTHOR_*`/`GIT_COMMITTER_*` set to the commit identity; shell commands inherit them. NDJSON both ways on stdio. Messages are JSON-RPC 2.0 without the `"jsonrpc"` member: requests `{"id","method","params"}`, responses `{"id","result"}` or `{"id","error":{"code","message"}}`, notifications `{"method","params"}`. The server adds `emittedAtMs` to its notifications. The server also reads the user's `~/.codex/config.toml`, so its MCP servers, plugins and `notify` program apply to byre's sessions too.
|
|
|
|
## Session lifecycle
|
|
|
|
1. `initialize` with `clientInfo`, then the `initialized` notification.
|
|
2. `thread/start` (`cwd`, `model`, `approvalPolicy` `never`, `sandbox` `danger-full-access`, `developerInstructions` for byre's guidance), or `thread/resume` with `threadId` and the same settings. The server picks the thread id, returned as `thread.id`; resuming keeps it. `Session.ID` is the thread id.
|
|
3. `turn/start` with `threadId` and `input`, a list of `UserInput`: `{"type":"text","text":...}`, and for each image `{"type":"image","url":"data:<type>;base64,..."}` before the text. `model` on it switches the model for that turn and every later one (`Session.SetModel` holds the model for the next `Send`); `thread/read` reports the thread's current `model`. The response carries the new turn; `turn/started` follows.
|
|
4. `turn/interrupt` with `threadId` and the active `turnId` ends the turn: `turn/completed` arrives with `status` `interrupted` within milliseconds, a running command is killed, and the thread stays usable. `Session` tracks the active turn from `turn/started` and `turn/completed`.
|
|
5. `thread/compact/start` runs compaction as a turn of its own: `turn/started`, an item of type `contextCompaction`, `turn/completed`. The thread keeps its id and remembers the conversation in summary.
|
|
|
|
Closing stdin ends the process.
|
|
|
|
## Events
|
|
|
|
- `turn/started`, `turn/completed` (`turn.status` `completed`, `interrupted` or `failed` with `turn.error`). A failed request to the model also sends an `error` notification first, with `willRetry`.
|
|
- `item/started` and `item/completed` with a `ThreadItem`: `userMessage`, `agentMessage` (`text`), `reasoning`, `commandExecution` (`command`, `aggregatedOutput`, `exitCode`), `fileChange`, `mcpToolCall`, `contextCompaction` and others.
|
|
- `item/agentMessage/delta` (`itemId`, `delta`) streams an agent message; the deltas join to the completed item's `text`. `item/commandExecution/outputDelta` and the reasoning deltas stream the same way.
|
|
- `thread/tokenUsage/updated` carries `last` and `total` token counts and `modelContextWindow`; `account/rateLimits/updated` the account's rate-limit windows; `thread/status/changed`, `mcpServer/startupStatus/updated`, `account/updated` and `remoteControl/status/changed` also arrive.
|
|
- Lines that aren't a JSON object, don't parse, or carry a notification method outside `notificationParams` go to `Options.OnBadLine`; parsed notifications are still delivered as events. `Event.Decode` returns the notification's generated params type.
|
|
|
|
Server requests (approvals, user input, dynamic tool calls) shouldn't arrive with approvals off; if one does, command and file-change approvals are accepted and anything else gets a JSON-RPC error.
|
|
|
|
## Transcripts and contract tests
|
|
|
|
`Options.Transcript` records every line in both directions as `{"dir":"send"|"recv","msg":...}`, deltas included, with any `"email"` value replaced by `[redacted]`.
|
|
|
|
The tests in `internal/codex/session_internal_test.go` are scenarios (commit identity, text deltas, interrupt, image input, resume, model switch, compaction), each run two ways:
|
|
|
|
- `TestLive` runs it against the real CLI with the models named by `testModel` and `switchModel`, and writes transcripts to `internal/codex/testdata/transcripts/` (gitignored).
|
|
- `TestContract` runs the same scenario against the committed transcripts in `internal/codex/testdata/contract/`. A replay server stands in for the CLI: it expects each recorded `send` line in turn (method and params equal, `cwd` aside) and writes each recorded `recv` line, so the client must send exactly what the recording shows and handle exactly what the server sent.
|
|
|
|
Both then decode every received line of their transcripts strictly into the generated types, so a field or method the types don't know fails the test. After a CLI update: `go generate ./internal/codex`, `go test ./internal/codex -run TestLive`, and when it passes, `cp internal/codex/testdata/transcripts/*.jsonl internal/codex/testdata/contract/`. Check the copies for anything private before committing them.
|