03
Anatomy of an AI Coding Agent — Article 3 of 8

The BrainInside the Core Engine

The Codex orchestrator, session management, prompt assembly, model communication, and the relentless tool-call loop that powers autonomous coding.

The Codex Orchestrator

If the nervous system is Codex's communication network, the Codex struct is the brain itself—the central orchestrator that holds everything together. It is the single entry point where submissions arrive, sessions are managed, and the entire agent lifecycle is coordinated.

The Codex struct has five critical components, each serving a distinct purpose:

pub struct Codex {
    // Receives submissions from the frontend
    tx_sub: Sender<Submission>,

    // Emits events back to the UI
    rx_event: Receiver<Event>,

    // Current status of the agent
    agent_status: Arc<AtomicCell<AgentStatus>>,

    // The persistent conversation session
    session: Arc<Session>,

    // Signal to stop the session loop
    session_loop_termination: CancellationToken,
}
CODEX ORCHESTRATOR The central struct Coordinates all components tx_sub Submission Channel rx_event Event Channel agent_status Arc<AtomicCell> session Arc<Session> loop_termination CancellationToken
Design Insight

The Codex struct doesn't do the work—it coordinates work. It holds the channels, owns the session, and manages the lifecycle. Think of it as the cortex: not a single neuron firing, but the structure that makes thought possible.

The Session

If the Codex struct is the brain's structure, the Session is its memory. It persists the entire conversation—every message, every tool call, every decision. When a turn ends and the next begins, the Session carries forward everything the agent knows.

pub struct Session {
    conversation_id: String,
    tx_event: Sender<Event>,
    agent_status: Arc<AtomicCell<AgentStatus>>,
    state: Mutex<SessionState>,
    features: FeatureFlags,
    active_turn: AtomicCell<Option<TurnId>>,
    services: ServiceContainer,
    js_repl: Option<JsRepl>,
}

Notice the Arc wrapping the Session in the Codex struct. Arc (Atomic Reference Counting) enables shared ownership across async tasks. Multiple parts of the system—the submission loop, the model client, tool executors—all need access to session data simultaneously. Arc lets them share it safely without cloning the entire state.

The Mutex<SessionState> protects mutable state behind a lock. Only one task at a time can modify the conversation history, preventing data races while still allowing concurrent reads of immutable fields.

Why Arc + Mutex?

Rust doesn't have a garbage collector. Arc provides reference-counted heap allocation so multiple owners can exist. Mutex ensures exclusive mutable access. Together, they give you safe shared mutable state in an async runtime—exactly what a multi-task AI agent needs.

The Submission Loop

The submission loop is the heartbeat of the entire system. It's an infinite async loop that waits for incoming submissions and dispatches them to the appropriate handler. Every user message, every approval decision, every configuration change flows through this single loop.

while let Ok(sub) = rx_sub.recv().await {
    match sub {
        Submission::UserTurn(msg) => {
            handle_user_turn(&session, msg).await;
        }
        Submission::ExecApproval(decision) => {
            handle_approval(&session, decision).await;
        }
        Submission::Cancel => {
            handle_cancel(&session).await;
        }
        // ... more handlers
    }
}
Submission Loop — The Heartbeat
Each pulse is a submission received and dispatched—UserTurn, Approval, Cancel, Config...

This pattern—while let Ok(sub) = rx_sub.recv().await—is elegantly simple. The loop sleeps until a submission arrives, processes it, and goes back to sleep. No polling, no busy-waiting. The async runtime wakes it only when there's work to do.

If the channel closes (all senders dropped), the loop terminates gracefully. This is how Codex achieves clean shutdown: drop the submission sender, and the brain stops thinking.

Building the Prompt

Every time the model needs to think, Codex assembles a prompt—a carefully layered stack of context that tells the model who it is, what it knows, what tools it has, and what the user wants. This isn't just string concatenation. It's a pipeline.

Prompt Assembly Pipeline
1
System Prompt
Identity, behavior rules, and base instructions
2
Conversation History
All prior messages, tool calls, and results
3
Tool Definitions
Shell, file operations, apply_patch, etc.
4
Skills Section
Domain-specific capabilities and instructions
5
MCP Tools
External tool servers discovered at runtime
6
User Instructions
The actual user message and task context

Each layer serves a distinct purpose. The system prompt sets the model's identity and behavioral constraints. The conversation history gives it memory. Tool definitions tell it what it can do. Skills section provides specialized knowledge. MCP tools extend the model's reach to external services. And finally, the user's instructions—the actual task at hand.

The order matters. The system prompt anchors the model's behavior. Everything else layers on top, with the user's message always last—recency bias in language models means the most recent tokens carry the most weight.

fn build_prompt(&self) -> Vec<Message> {
    let mut messages = Vec::new();

    // Layer 1: System prompt
    messages.push(system_prompt(&self.config));

    // Layer 2: Conversation history (possibly compacted)
    messages.extend(self.state.conversation_history());

    // Layer 3-5: Tools, skills, MCP injected via API params

    // Layer 6: Current user message
    messages.push(user_message(input));

    messages
}

The ModelClient

The ModelClient is how Codex talks to OpenAI. It abstracts away the transport layer—whether the connection uses WebSockets for persistent, bidirectional streaming or SSE (Server-Sent Events) for one-way streaming over HTTP.

pub struct ModelClientSession {
    // State of the current turn
    turn_state: TurnState,

    // WebSocket connection (if using Realtime API)
    websocket: Option<WebSocketStream>,

    // Last request for retry/debug
    last_request: Option<RequestPayload>,
}

WebSocket vs SSE

WebSocket transport gives you a persistent, full-duplex connection. The model can stream tokens while the client sends interrupts. This is the Realtime API—lower latency, richer interaction, but requires maintaining a connection.

SSE transport is simpler: the client sends a request, and the server streams back events over a single HTTP connection. Each turn is a new request. Simpler to implement, easier to debug, and works through more proxies.

The ModelClient abstracts both behind a unified interface, so the rest of the system doesn't care which transport is in use.

Streaming Responses

The model doesn't return a complete response all at once. It streams it—token by token for text, and as structured events for tool calls. The ResponseEvent enum captures every type of event the model can emit:

enum ResponseEvent {
    // A piece of text or code being generated
    ContentBlockDelta {
        index: usize,
        delta: String,
    },

    // The model wants to call a tool
    FunctionCall {
        name: String,
        call_id: String,
        arguments: String,
    },

    // Model is done with this turn
    Done,
}

ContentBlockDelta delivers text incrementally—this is what you see as the AI types in real-time. Each delta is a small chunk of tokens appended to the current response block.

FunctionCall is the key event. When the model decides it needs to execute a tool—run a shell command, read a file, apply a patch—it emits a FunctionCall with the tool name and arguments. This kicks off the tool call loop.

Done signals the turn is complete. No more tokens, no more tool calls. The system can finalize and await the next user input.

The Tool Call Loop

This is where the magic happens. The tool call loop is a feedback cycle between the model and the execution environment. The model thinks, decides it needs to act, the system executes the action, and feeds the result back to the model for further reasoning.

Model Output FunctionCall event Approval Check Policy evaluation Sandbox Exec Isolated execution Result stdout, stderr, exit Back to Model Next iteration REPEATS UNTIL MODEL EMITS Done Rejected? User notified

The loop continues until the model emits a Done event—signaling it has finished its reasoning and doesn't need any more tool calls. A single user prompt might trigger zero tool calls (a pure text response) or dozens (a complex refactoring task).

Each iteration through the loop adds to the conversation history: the tool call, its result, and the model's subsequent reasoning. This gives the model full context on what it has tried and what happened.

The Turn Lifecycle

A single "turn" in Codex is a rich lifecycle with distinct stages. Click each stage below to see what happens at each point:

Turn Lifecycle Timeline
T0
Submit
T1
Build
T2
Send
T3
Stream
T4
Tool Call
T5
Approve
T6
Execute
T7
Complete
T0 — Submission Received: The user's message arrives via the submission channel. The submission loop wakes up and dispatches it to handle_user_turn. Agent status transitions to "active."

MCP Connections

The Model Context Protocol (MCP) extends Codex's brain beyond its built-in tools. The McpConnectionManager maintains connections to external tool servers—databases, APIs, custom services—and makes them available to the model as if they were native tools.

pub struct McpConnectionManager {
    connections: HashMap<String, McpConnection>,
    tool_registry: ToolRegistry,
}

impl McpConnectionManager {
    // Discover tools from a server
    pub async fn discover(&mut self, uri: &str) {
        let conn = McpConnection::connect(uri).await;
        let tools = conn.list_tools().await;
        self.tool_registry.register_all(tools);
    }

    // Route a tool call to the right server
    pub async fn route(&self, call: ToolCall) -> ToolResult {
        let conn = self.connections.get(&call.server);
        conn.execute(call).await
    }
}

The discovery phase happens at startup: Codex connects to each configured MCP server, queries its available tools (name, description, parameters), and registers them. When the model emits a FunctionCall targeting an MCP tool, the connection manager routes the call to the correct server.

This is how Codex becomes extensible. You don't modify the core engine to add new capabilities. You spin up an MCP server and Codex discovers it automatically.

The Safety Layer

Every tool call passes through the safety layer before execution. At its core is the ApprovalPolicy enum—four modes that determine how much autonomy the agent has:

enum ApprovalPolicy {
    AlwaysAllow,      // Full autonomy, no prompts
    AlwaysReject,     // Block all tool calls
    AskIfDangerous,   // Auto-approve safe, ask for risky
    AskAlways,        // Prompt for every single call
}
AlwaysAllow
Full autonomy. Every tool call executes immediately without human approval. Maximum speed, maximum trust.
Full Auto
AlwaysReject
Complete lockdown. No tool call can execute. The model can only generate text responses. Read-only mode.
Locked
AskIfDangerous
Smart filtering. Safe operations (read files, list dirs) auto-approve. Writes, deletes, and network calls require human confirmation.
Balanced
AskAlways
Maximum oversight. Every tool call, no matter how safe, requires explicit human approval before execution.
Supervised

The default for Codex is AskIfDangerous—the balanced middle ground. The system maintains an internal classification of which operations are "safe" (reading) versus "dangerous" (writing, executing, network access). This classification is the allow list that we'll explore in detail in Article 7.

Context Windows

Language models have a finite context window—a maximum number of tokens they can process in a single request. For Codex, managing this limit is critical because conversations accumulate history rapidly: every message, every tool call result, every code snippet eats into the budget.

Compaction

When the conversation approaches the token limit, Codex compacts it. Older messages are summarized or removed while preserving the essential context. The model receives a condensed version of the conversation that fits within the window while retaining the information needed to continue reasoning coherently.

Truncation

For tool call results—especially large ones like file contents or command output—Codex applies truncation. If a tool result exceeds a threshold, it's trimmed with a note: [truncated, showing first N lines]. The model knows the output was cut and can request more if needed.

fn fit_within_context(
    messages: &mut Vec<Message>,
    max_tokens: usize,
) {
    let total = count_tokens(&messages);
    if total > max_tokens {
        // Strategy 1: Truncate long tool results
        truncate_tool_results(messages);

        // Strategy 2: Compact old turns
        compact_history(messages, max_tokens);

        // Strategy 3: Drop oldest messages
        while count_tokens(&messages) > max_tokens {
            messages.remove(1); // Keep system prompt at [0]
        }
    }
}
The Priority Stack

Notice the strategy order: truncate results first (cheapest), then compact history (moderate), then drop old messages (last resort). The system prompt is never dropped—it's at index 0 and always preserved. The most recent messages are kept because the model needs fresh context to continue coherently.

Key Takeaways

The Codex struct is the central orchestrator holding channels, session, and lifecycle controls—it coordinates, not computes.
The Session persists conversation state using Arc for shared ownership and Mutex for safe mutable access across async tasks.
The submission loop is the heartbeat—an async recv loop that dispatches every user action to the right handler.
Prompts are assembled in layers: system prompt, history, tools, skills, MCP, user message—order matters for model attention.
The tool call loop is a feedback cycle: model thinks, calls tool, gets result, thinks again—repeating until done.
MCP connections make Codex extensible—external tool servers discovered and routed at runtime without core changes.
The safety layer with four approval policies gives users control over the autonomy-oversight tradeoff.
Context management uses a priority strategy: truncate results, compact history, then drop oldest—never the system prompt.
← Prev: The Nervous System Article 3 of 8 Next: The Vault →