It is tempting to summarize this layer as “the SDK wraps the agent.” The source suggests a narrower and more useful reading. SDKs and app-server are adapter surfaces: they translate external calls into the thread, turn, setting, event, and notification shapes already owned by the Codex runtime. They do not reimplement the context ledger, permission model, or rollout path.
The core idea of this chapter is: the public interface
is a runtime adapter, not a second agent.
app-server turns JSON-RPC requests into typed
ClientRequest values, then routes them to request
processors. SDKs wrap that protocol into friendlier calls such
as Thread.run(), runStreamed(), or CLI
JSONL events.
Reading contract. This chapter tracks only the external entry boundary: transport, initialization, typed requests, thread and turn entry points, notification fanout, and SDK ergonomics. After reading it, you should be able to separate the app-server public contract, the core runtime owner, and the convenience layer added by each SDK.
Evidence boundary. Transport, lifecycle, and API surface come from the app-server README. Dispatch, processors, listener projection, and SDK routing come from verified public source. This article does not infer hosted-service internals, remote-control backend behavior, or private storage implementation.
1. Start by splitting “embed Codex”
A host application usually wants a simple action: send user input
and let Codex continue working. app-server exposes a richer shape
than a prompt -> answer call. Its README describes
codex app-server as the interface for rich clients,
communicates with JSON-RPC-shaped messages over transports such
as stdio JSONL, and reduces the public model to three primitives:
Thread, Turn, and Item.
| External goal | app-server shape | Runtime owner | Why a prompt API is too small |
|---|---|---|---|
| Open a conversation. | thread/start with cwd, model, permissions, environments, and config. |
ThreadManager creates a CodexThread. |
The runtime must establish identity, authority, rollout, and listeners. |
| Send user input. | turn/start with input, overrides, and additional context. |
CodexThread.submit(...) handles Op::UserInput. |
The input affects model view, tools, usage, events, and persistence. |
| Render progress. | turn/started, item/*, and turn/completed notifications. |
The listener maps core EventMsg into ServerNotification. |
Progress is a stream of facts, not only a final response. |
| Resume, fork, or roll back. | thread/resume, thread/fork, thread/rollback. |
The rollout and recovery ledger from Part X. | Continuation depends on replayable evidence, not only visible messages. |
That is the layer’s central discipline. IDEs, SDKs, CLI surfaces, and in-process hosts can differ in ergonomics. Once they enter the runtime, they should converge on the same typed requests, the same thread and turn owners, and the same event stream.
The smallest replayable shape is a sequence of requests and notifications, not one long answer. This abbreviated JSONL keeps only the fields needed to connect the README primitives to the source owners below:
{"id":1,"method":"initialize","params":{"clientInfo":{"name":"ide"}}}
{"method":"initialized","params":{}}
{"id":2,"method":"thread/start","params":{"cwd":"workspace","model":"gpt-5"}}
{"id":3,"method":"turn/start","params":{"threadId":"t1","input":[{"type":"text","text":"fix failing test"}]}}
{"method":"turn/started","params":{"threadId":"t1","turnId":"u1"}}
{"method":"item/completed","params":{"turnId":"u1","item":{"type":"tool_result"}}}
{"method":"turn/completed","params":{"threadId":"t1","turnId":"u1"}}
The useful part is the order. A connection is initialized first. A thread then establishes identity and boundaries. A turn carries one user input. Runtime progress returns through notifications. SDK convenience methods wrap this sequence; they do not replace the lifecycle.
2. app-server turns a transport into an initialized session
The first gate is not turn/start; it is
initialize. The lifecycle section in the README says
each transport connection must send one initialize
request, then an initialized notification. Requests
issued before that handshake are rejected. This gives the server a
per-connection client identity, capability set, and notification
opt-out configuration.
The source follows that order. process_request
deserializes JSON-RPC into a typed ClientRequest.
In-process embedders can bypass JSON deserialization through
process_client_request, but the comment says that it
preserves identical semantics by delegating to
handle_client_request. That shared handler treats
Initialize specially, then sends all other requests
through initialized dispatch.
public edge:
JSON-RPC request
-> serde ClientRequest
-> initialize gate
-> experimental gate
-> serialization scope
-> request processor
Initialized requests also pass through serialization scopes. The
protocol macro generates ClientRequest.serialization_scope();
app-server maps those scopes to global, thread, path, command,
process, filesystem watch, or OAuth queues. Shared reads can
proceed together; mutating work uses exclusive access. The public
edge therefore protects request ordering before core state is
touched.
3. thread/start creates a runtime container
ThreadStartParams carries model, provider, service
tier, cwd, workspace roots, approval policy, sandbox, permissions,
instructions, personality, environments, dynamic tools, and
capability roots. Read together, these fields show that
thread/start is not only “create a chat.” It
establishes the baseline that later turns will inherit.
When message_processor sees
ClientRequest::ThreadStart, it enters
thread_processor.thread_start. The processor rejects
incompatible permissions and sandbox
fields, parses environments, builds config overrides, then spawns
thread_start_task. The task ultimately calls
ThreadManager.start_thread_with_options(StartThreadOptions { ... }),
passing initial history, dynamic tools, service naming, tracing,
environment selection, and extension initialization into core.
The response is only half the startup story. app-server also
auto-attaches a conversation listener, updates thread watch
state, sends ThreadStartResponse, and then emits
thread/started. The caller is subscribed to the
event stream from the beginning.
4. turn/start becomes Op::UserInput
Once a thread exists, turn/start is the entry point
that actually drives the agent. TurnStartParams
carries the target thread, input items, optional Responses API
metadata, additional context, environment and cwd overrides,
workspace roots, approval and sandbox choices, permission profile,
model and reasoning settings, output schema, personality, and
collaboration mode. A turn is therefore not just appended text; it
may also update the runtime settings used by following turns.
turn_start_inner is a compact execution trace. It
loads the thread, checks that direct input is allowed, validates
input size, maps v2 input into core input items, maps additional
context, builds settings overrides, rejects incompatible
permissions and sandboxPolicy, and
previews overrides before accepting them. Only then does it build
Op::UserInput and submit it to CodexThread.
turn/start shape:
input + overrides + additionalContext
-> CoreInputItem[]
-> ThreadSettingsOverrides
-> Op::UserInput
-> CodexThread.submit_user_input_with_client_user_message_id()
The immediate TurnStartResponse contains an
in-progress turn with unloaded items. The interesting facts return
later through notifications.
5. Progress returns as notifications
A running turn may emit model deltas, tool activity, permission requests, plan updates, token usage, rollback state, thread status changes, and completion. A final response object would be too late for interactive clients and too narrow for approval or interrupt flows.
The listener path owns this fanout. ensure_conversation_listener
gets the thread from ThreadManager, subscribes the
connection to thread state, and starts a listener task. The task
reads conversation.next_event(), updates thread-local
state, finds subscribed connections, and calls
apply_bespoke_event_handling. That handler turns
EventMsg::TurnStarted into
ServerNotification::TurnStarted, item deltas into
server notifications through item_event_to_server_notification,
and completion into ServerNotification::TurnCompleted.
SDKs therefore need routing. stdio is one ordered stdout stream
containing both JSON-RPC responses and notifications. Python’s
MessageRouter gives each in-flight request and each
active turn its own queue. It routes responses by request id,
turn notifications by turn id, buffers early turn events until the
caller starts streaming, and wakes all waiters on transport
failure.
6. Python SDK wraps app-server stdio
The Python SDK follows app-server most closely. The
CodexClient docstring describes a typed JSON-RPC
client over stdio. Startup resolves a Codex executable, runs
codex app-server --listen stdio://, opens
stdin/stdout/stderr, and starts reader and stderr-drain threads.
Initialization sends initialize with
clientInfo and capabilities.experimentalApi,
then sends initialized.
The low-level client exposes app-server methods such as
thread_start, thread_resume,
thread_fork, turn_start,
turn_interrupt, and turn_steer. The
high-level Codex object turns those into application
ergonomics: construction starts and initializes the runtime;
thread_start() returns a Thread;
Thread.turn() builds TurnStartParams and
returns a TurnHandle; Thread.run()
consumes the stream and collects a TurnResult from
completed items, usage, and turn/completed.
| SDK layer | Caller sees | Still backed by | Boundary protected |
|---|---|---|---|
CodexClient |
Typed requests, notifications, wait and stream helpers. | stdio JSON-RPC plus app-server methods. | One stdout stream is not consumed by competing readers. |
Codex |
Login, account, and thread methods. | A runtime connection initialized during construction. | Callers do not hand-write the handshake. |
Thread |
run(), turn(), read(). |
turn/start plus notification stream. |
Synchronous run is event collection, not a new runtime contract. |
AsyncCodexClient |
Async thread, turn, and stream helpers. | The synchronous client executed in worker threads. | Blocking reads do not monopolize the event loop. |
7. TypeScript SDK wraps a lighter CLI route
The TypeScript SDK README is explicit: it wraps the
codex CLI from @openai/codex, spawning
the CLI and exchanging JSONL events over stdin/stdout. That is a
different surface from Python. Python sits next to app-server v2;
the TypeScript SDK currently drives codex exec --experimental-json.
CodexExec.run builds that command: it starts with
exec --experimental-json, then appends config
overrides, model, sandbox, working directory, additional
directories, output schema, reasoning effort, network access, web
search, approval policy, and optionally resume <threadId>.
It spawns the child process, writes input to stdin, reads stdout
line by line, and yields each JSONL line.
Thread.runStreamedInternal normalizes caller input
into prompt text and images, calls CodexExec.run,
parses each line as a ThreadEvent, stores the thread
id when it sees thread.started, and yields events.
run() then buffers item.completed and
turn.completed into a final result. The surface is
simpler, but it also exposes a narrower contract than app-server
v2.
8. In-process hosting removes the process boundary, not the protocol boundary
in_process.rs is useful because its file-level
comment states the design plainly. The module replaces
socket/stdio transports with bounded in-memory channels while
still running the existing MessageProcessor and
outbound routing. Incoming requests are typed
ClientRequest values, but responses still return
through the same JSON-RPC result envelope used by stdio and
websocket transports.
start() completes initialize and
initialized before returning a handle. Later requests
still enter process_client_request, and initialized
state, experimental capability, and notification opt-outs are
mirrored to outbound state. Even inside one process, Codex keeps
the app-server semantics instead of creating a hidden shortcut
around the gates.
9. What to carry forward
The external integration path now closes where public clients begin. Part I followed one request into the runtime. The middle chapters inspected context, tools, permissions, projection, extensions, hooks, cache discipline, and rollout recovery. This chapter shows how external surfaces enter that same runtime through app-server and SDK adapters. The next chapter returns to long-lived state: how stable lessons from old rollouts become memory for future threads.
| Integration need | Question to ask | Mechanism | Do not read it as |
|---|---|---|---|
| IDE or rich client. | Do you need the full thread, turn, event, and control surface? | codex app-server plus JSON-RPC notifications. |
A synchronous answer API. |
| Python automation. | Do you need app-server v2 methods and typed notifications? | Python SDK over app-server stdio. | Direct chat-text read/write. |
| Node or TypeScript task runner. | Is CLI JSONL enough for the workflow? | TypeScript SDK over codex exec --experimental-json. |
A full app-server client. |
| Same-process host. | Can it preserve app-server semantics? | in_process typed requests plus the same result envelope. |
A bypass around initialization, queues, and notifications. |
A public interface for an agent runtime should not be only a convenient mouthpiece for the model. It should bring external callers into the runtime’s existing owners, gates, ledger, and event stream. The more the runtime can do, the more valuable that boundary discipline becomes.
Sources
- app-server README: rich interfaces, transport, primitives, and initialization lifecycle
- app-server API overview: thread, turn, rollback, steer, interrupt
core-apipublic facade re-exporting thread and runtime primitivesClientRequestmacro and serialization scopeServerNotificationmacro and JSON-RPC notification projection- JSON-RPC and typed requests enter the same processing path
- initialize gate, experimental gate, and serialization queue dispatch
ThreadStart,ThreadResume, andThreadForkdispatchTurnStart,TurnSteer, andTurnInterruptdispatch- request serialization queue keys and exclusive/shared read access
ThreadStartParamspublic shapeThreadResumeParamsresume variantsThreadForkParamsfork variantsTurnStartParamspublic shapethread_start_innerandthread_start_taskturn_start_innerbuildsOp::UserInput- turn settings override validation
- conversation listener subscription
- listener task reads core events and projects them
TurnStartedandTurnCompletenotification handling- item delta events become
ServerNotification TurnCompletednotification emission- in-process app-server runtime host design note
- in-process start performs initialize / initialized
- in-process request still enters
process_client_request - Python SDK
CodexClientconfig, app-server startup, and initialization - Python SDK thread start/resume/fork wrappers
- Python SDK turn start, wait, and stream helpers
- Python SDK
MessageRouterroutes responses and notifications - Python SDK high-level
Codexandthread_start - Python SDK
Thread.run/Thread.turn - Python SDK collects
TurnResult - TypeScript SDK README: CLI JSONL wrapper, streaming, and resume
- TypeScript SDK
CodexExec.runbuildscodex exec --experimental-json - TypeScript SDK spawns CLI and reads stdout lines
- TypeScript SDK
Thread.runStreamed/run