Files
byre/docs/codex-protocol.md
T
2026-10-20 23:03:08 -07:00

5.5 KiB

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 the types (below).

Generated types

go generate ./internal/codex regenerates protocol.gen.go from the CLI's schema; run it after a CLI update. It uses the experimental schema because the server sends experimental fields (such as environments on a thread) even to clients that don't opt into them.

Launch and framing

codex app-server -c check_for_update_on_startup=false

Shell commands inherit the app-server's environment. 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 reads config.toml from CODEX_HOME (default ~/.codex), so the MCP servers, plugins and notify program configured there apply to the session; -c mcp_servers={} doesn't remove them, a separate CODEX_HOME does.

Session lifecycle

  1. initialize with clientInfo and capabilities.experimentalApi true (dynamic tools need it: without it thread/start with dynamicTools fails with thread/start.dynamicTools requires experimentalApi capability), then the initialized notification.
  2. thread/start (cwd, model, approvalPolicy never, sandbox danger-full-access, developerInstructions for byre's guidance, dynamicTools for the tool schemas), or thread/resume with threadId and the same settings but no tools, or thread/fork (threadId, lastTurnId, the last turn the fork keeps; the turn must not be in progress) with the same settings. The server picks the thread id, returned as thread.id; resuming keeps it, forking makes a new one. A thread's dynamic tools are stored with it, so resumed and forked threads keep them.
  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. Additional turn context uses additionalContext, for example {"byre":{"kind":"application","value":...}}. model on it switches the model for that turn and every later one; thread/read reports the thread's current model. The response carries the new turn; turn/started follows.
  4. turn/steer with threadId, expectedTurnId (the active turn) and input adds input to the running turn; the model reads it at its next step without the turn ending.
  5. 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.
  6. 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.

Dynamic tools

A dynamicTools entry of type namespace holds tools as functions with their names, descriptions and input schemas. Names and namespaces starting with mcp__ are reserved (dynamic tool name is reserved), so byre's tools are ask_human and so on in the namespace byre; the model refers to them as byre__ask_human. A call arrives as the server request item/tool/call (namespace, tool, arguments, callId, turnId); the client answers {"success":true,"contentItems":[{"type":"inputText","text":...}]}, or success false with the error text. The call also shows as a dynamicToolCall item.

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 (the same snapshot account/rateLimits/read returns on request, as rateLimits); thread/status/changed, thread/readState/changed, mcpServer/startupStatus/updated, account/updated and remoteControl/status/changed also arrive.

Besides item/tool/call, server requests (approvals, user input) shouldn't arrive with approvals off; if one does, command and file-change approvals are accepted and anything else gets a JSON-RPC error.