Skip to content

2. Event System

Communication between UI and Daemon

Getting start introduces UI and Daemon communicates with custom protocol.

This passage focuses on the event system between UI and Daemon communication, and skip any details of the custom protocol, as the highlighted areas in the diagram.

sequenceDiagram
    autonumber
    actor User
    participant Daemon
    participant Model

    rect rgb(200, 220, 255)
    User ->>+ Daemon: Query Go LOC
    end

    Note over Daemon, Model: Interact with Model<br>......<br> get results.

    rect rgb(200, 220, 255)
    Daemon -->>- User: Deliver final message

    end

Event System

The event system defines the communication protocol between UI and Daemon using two types of messages:

  • Operations (Op): Messages sent from UI to Daemon, for example:

    • Op::UserTurn - user sends input
    • Op::ExecApproval - user approves command execution
  • Events (EventMsg): Messages sent from Daemon to UI

    • EventMsg::AgentMessage - model's response
    • EventMsg::ExecApprovalRequest - daemon requests approval

Communication Channel Characteristics

The UI-Daemon communication is full-duplex with two independent channels:

  • Submission Queue (SQ): UI → Daemon (carries Op messages)
  • Event Queue (EQ): Daemon → UI (carries EventMsg messages)

Each channel operates independently:

  • UI sends Op messages through SQ at any time
  • Daemon pushes EventMsg messages through EQ at any time
  • Neither channel blocks the other

Ordering guarantee:

  • Each channel preserves message order (FIFO)
  • Op::UserTurn(A) sent before Op::UserTurn(B) → Daemon receives A before B
  • EventMsg::X sent before EventMsg::Y → UI receives X before Y

Sequential Op behavior: When UI sends Op::A immediately followed by Op::B:

Scenario Protocol-level behavior Implementation detail
General case Daemon receives A, then receives B How Daemon processes the queue depends on Op types
Op::Interrupt followed by any Op Interrupt received first, takes effect immediately Interrupt preempts current operation
Multiple Op::UserTurn Received in order Subsequent turns typically abort previous work (Daemon's choice)

Key distinction: The protocol guarantees delivery order (A then B), but how they affect the system state depends on the Daemon's processing logic for each specific Op type.

Scope boundary: This section describes protocol-level guarantees:

  • Message delivery and ordering are guaranteed by the transport layer
  • How Daemon processes queued Op messages is an implementation detail
  • How UI handles received EventMsg messages is an implementation detail

Why Event System?

Unlike traditional HTTP request-response models where a client sends a request and receives a complete response, Codex interacts with the Model via Server-Sent Events (SSE) protocol.

The Model returns a stream of events instead of a single response:

event: response.output_text.delta
data: {"delta": "I'll"}

event: response.output_text.delta
data: {"delta": " analyze"}

event: response.output_item.done
data: {"type": "function_call", ...}

This means the Daemon natively receives and processes events from the Model, and consequently pushes events to the UI. Conversely, the UI pushes events (operations) to the Daemon to control the flow—approving actions, providing input, or interrupting execution.

Therefore, the event system is used for UI-Daemon communication not as a design pattern for abstraction, but because it fundamentally matches the Model's streaming architecture.

Event Semantics

Op Semantics

Operations from UI to Daemon carry intent, not implementation details. High-level categories:

  • Lifecycle: start work (UserTurn) or interrupt it (Interrupt).
  • Approvals: user decisions on safety checks (ExecApproval, PatchApproval).
  • Responses: user supplies missing info (UserInputAnswer).

Full definition and exhaustive list live in the protocol enum: enum Op at codex-rs/protocol/src/protocol.rs:L167.

EventMsg Semantics

Events from Daemon to UI represent lifecycle changes, streaming output, and user prompts. At a high level:

  • Lifecycle: turn started/completed/aborted; errors or warnings.
  • Output: streaming deltas and final items (assistant message, tool output, patch events).
  • Requests: prompts for approvals or missing user input.

Full definition and exhaustive list live in the protocol enum EventMsg at codex-rs/protocol/src/protocol.rs.

Task Examples

Example 1: Simple Task

User asks a simple question, LLM responds directly without tool execution.

UI      Op              EventMsg       Daemon
───     ────────────    ────────────    ────
        Op::UserTurn →
                        ← EventMsg::TurnStarted
                        ← EventMsg::AgentMessageDelta*
                        ← EventMsg::AgentMessage
                        ← EventMsg::TurnComplete

Example 2: Task with Tool Execution

User requests file deletion, LLM requires approval, executes command, then confirms.

UI      Op              EventMsg       Daemon
───     ────────────    ────────────    ────
        Op::UserTurn →
                        ← EventMsg::TurnStarted
                        ← EventMsg::AgentMessageDelta*
                        ← EventMsg::ExecApprovalRequest
        Op::ExecApproval →
                        ← EventMsg::ExecCommandBegin
                        ← EventMsg::ExecCommandOutputDelta*
                        ← EventMsg::ExecCommandEnd
                        ← EventMsg::AgentMessage
                        ← EventMsg::TurnComplete

Example 3: Task with Follow-up Questions

User asks to refactor code, LLM needs to know which function, asks user, then continues.

UI      Op              EventMsg       Daemon
───     ────────────    ────────────    ────
        Op::UserTurn →
                        ← EventMsg::TurnStarted
                        ← EventMsg::AgentMessage
                        ← EventMsg::RequestUserInput
        Op::UserInputAnswer →
                        ← EventMsg::AgentMessage
                        ← EventMsg::TurnComplete

Example 4: Interrupted Task

User realizes they asked the wrong question, interrupts current task and starts over.

UI      Op              EventMsg       Daemon
───     ────────────    ────────────    ────
        Op::UserTurn (Task A) →
                        ← EventMsg::TurnStarted
        Op::Interrupt →
                        ← EventMsg::TurnAborted
        Op::UserTurn (Task B) →
                        ← EventMsg::TurnStarted
        ← ...

Concurrent Op Handling

The Daemon maintains an internal state machine that determines how to handle concurrent or sequential Op messages. Different Op types trigger different state transitions.

Scenario: Multiple Op in sequence

Op sequence Daemon behavior State transition
UserTurn(A)UserTurn(B) Abort Task A, start Task B Running → Aborted → Running
UserTurnInterrupt Immediate preemption, no graceful shutdown Running → Aborted
ExecApprovalRequestInterrupt Abort the pending approval Waiting → Aborted
RequestUserInputInterrupt Cancel the information request Waiting → Aborted

Key principle: Only one Task runs at a time. New UserTurn or Interrupt always affects the current state.

Note: The detailed state machine implementation and concurrency control mechanisms will be covered in the next chapter "User Request Lifecycle". This section focuses on the protocol-level behavior—what happens when multiple Op messages are sent in sequence.