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.

Codex client projection fanout from EventMsg into app-server ServerNotification, TUI streaming, rollout, and resume

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