The Protocol Layer defines the asynchronous communication contract between Codex clients (TUI, CLI, App Server) and the core agent system (codex-core). It implements a Submission Queue / Event Queue (SQ/EQ) pattern using bounded async channels where clients submit Submission messages containing Op variants and receive Event messages containing EventMsg variants. This decouples user interfaces from agent execution, enabling real-time streaming of model output, tool execution, and approval requests.
The protocol is implemented primarily in two locations:
codex-rs/protocol/src/protocol.rs defines Submission, Op, Event, and EventMsg typescodex-rs/core/src/codex.rs implements the Codex struct with channel pairs and the submission_loop functionFor information about how this protocol is used in specific client implementations, see User Interfaces. For details on tool execution triggered by protocol operations, see Tool System.
Sources: codex-rs/protocol/src/protocol.rs1-50 codex-rs/core/src/codex.rs257-507
The protocol is implemented via the Codex struct which owns two async channels:
Channel Specifications:
SUBMISSION_CHANNEL_CAPACITY)async_channel::unbounded)The submission_loop function (spawned by Codex::spawn) consumes submissions from rx_sub and routes them to appropriate handlers in the Session.
Sources: codex-rs/core/src/codex.rs272-280 codex-rs/core/src/codex.rs293 codex-rs/core/src/codex.rs451-453
Diagram: Codex Channel Architecture
Sources: codex-rs/core/src/codex.rs272-280 codex-rs/core/src/codex.rs312-314 codex-rs/core/src/codex.rs449-467
The submission_loop function runs until Op::Shutdown:
Diagram: submission_loop Control Flow
Code path: Codex::spawn → tokio::spawn(submission_loop) → loop { rx_sub.recv().await } → match submission.op
Sources: codex-rs/core/src/codex.rs451-453 codex-rs/core/src/codex.rs1246-1791
The Submission struct wraps an operation with a correlation ID:
| Field | Type | Description |
|---|---|---|
id | String | Unique identifier for correlating events |
op | Op | The operation to execute |
Sources: codex-rs/protocol/src/protocol.rs68-75
The Op enum defines all operations that clients can submit to the agent:
Diagram: Op Variant Taxonomy
Sources: codex-rs/protocol/src/protocol.rs84-318
The primary operation for submitting user input with full turn context. Preferred over legacy UserInput as it provides complete per-turn configuration:
| Field | Type | Description |
|---|---|---|
items | Vec<UserInput> | User input items (text, files, images) |
cwd | PathBuf | Working directory for tool execution |
approval_policy | AskForApproval | Command approval policy |
sandbox_policy | SandboxPolicy | Tool execution sandbox policy |
model | String | Model slug to use |
effort | Option<ReasoningEffortConfig> | Reasoning effort level |
summary | ReasoningSummaryConfig | Reasoning summary preference |
collaboration_mode | Option<CollaborationMode> | Multi-agent mode |
personality | Option<Personality> | Agent personality |
final_output_json_schema | Option<Value> | JSON Schema constraint for final output |
Code path:
submission_loop receives Submission { op: Op::UserTurn, ... }Session::handle_user_turnSession::build_turn_context (constructs TurnContext)Session::spawn_task(RegularTask::new(...))tokio::spawn(task.execute())RegularTask::execute streams events via Session::send_eventSources: codex-rs/protocol/src/protocol.rs162-197 codex-rs/core/src/codex.rs1299-1368 codex-rs/core/src/tasks.rs1-50
Aborts the current turn by triggering the cancellation_token on the active SessionTask. The agent responds with EventMsg::TurnAborted containing the abort reason.
Code path:
submission_loop receives Submission { op: Op::Interrupt, ... }session.active_turn lockactive_turn.cancellation_token.cancel().or_cancel() combinatorEventMsg::TurnAborted { reason: TurnAbortReason::Interrupted }Implementation details: The ActiveTurn struct contains:
Tasks poll the token via async_operation.or_cancel(&turn_ctx.cancellation_token), which converts Err(Cancelled) into TurnAbortReason::Interrupted.
Sources: codex-rs/protocol/src/protocol.rs130-131 codex-rs/core/src/codex.rs1406-1430 codex-rs/core/src/state.rs1-50
Resolves an approval request for command execution. Delivered via oneshot channel to the waiting tool orchestrator:
| Field | Type | Description |
|---|---|---|
id | String | Submission ID of the approval request |
turn_id | Option<String> | Turn ID when available |
decision | ReviewDecision | User's approval decision (Approve/Deny/AddToRules) |
Code path: submission_loop → Op::ExecApproval match arm → lookup approval_tx from pending_approvals → send decision via oneshot
Sources: codex-rs/protocol/src/protocol.rs200-209 codex-rs/core/src/codex.rs1482-1510
Drops the last N user turns from in-memory context without reverting filesystem changes. Implemented by popping turns from SessionState.context and updating the rollout recorder:
| Field | Type | Description |
|---|---|---|
num_turns | u32 | Number of turns to rollback |
Code path: submission_loop → Op::ThreadRollback match arm → Session::rollback_turns → ContextManager::rollback_user_turns → emit EventMsg::ThreadRolledBack
Sources: codex-rs/protocol/src/protocol.rs319 codex-rs/core/src/codex.rs1679-1716
The Event struct wraps an event message with correlation metadata:
| Field | Type | Description |
|---|---|---|
id | String | Submission ID that triggered this event |
msg | EventMsg | The event payload |
Events are sent via Session::send_event:
Sources: codex-rs/protocol/src/protocol.rs689-696 codex-rs/core/src/codex.rs973-984
The EventMsg enum defines all events that the agent can emit:
Diagram: EventMsg Variant Categories
Sources: codex-rs/protocol/src/protocol.rs698-886
Signals that the agent has begun processing a turn. Emitted by SessionTask::execute after spawning:
| Field | Type | Description |
|---|---|---|
turn_id | String | Submission ID for this turn |
cwd | PathBuf | Working directory |
approval_policy | AskForApproval | Active approval policy |
sandbox_policy | SandboxPolicy | Active sandbox policy |
model | String | Model being used |
reasoning_effort | Option<ReasoningEffortConfig> | Reasoning effort level |
personality | Option<Personality> | Agent personality |
collaboration_mode | Option<CollaborationMode> | Multi-agent mode |
Emission point:
RegularTask::execute(session, turn_ctx)
→ session.send_event(EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_ctx.sub_id.clone(),
cwd: turn_ctx.cwd.clone(),
// ... other fields from turn_ctx
}))
Sources: codex-rs/protocol/src/protocol.rs1031-1042 codex-rs/core/src/tasks.rs180-210
Signals that the agent has finished all actions. Emitted by SessionTask::execute cleanup:
| Field | Type | Description |
|---|---|---|
last_agent_message | Option<String> | Final assistant message text |
tokens_used_this_turn | Option<TokenUsage> | Token consumption for this turn |
total_token_usage | Option<TotalTokenUsageBreakdown> | Cumulative token usage |
Emission point: RegularTask::execute → cleanup → Session::send_event(TurnComplete)
Sources: codex-rs/protocol/src/protocol.rs1044-1052 codex-rs/core/src/codex.rs2393-2410
Contains a complete message from the agent. Emitted after streaming AgentMessageDelta events complete:
| Field | Type | Description |
|---|---|---|
message | String | Agent's complete text response |
phase | Option<MessagePhase> | Message phase (Commentary/Answer for preamble models) |
Emission point:
ModelClientSession::stream(prompt)
→ yields ResponseEvent::Message(text)
→ handle_non_tool_response_item(ResponseItem::Message { content, phase })
→ session.send_event(EventMsg::AgentMessage(AgentMessageEvent {
message: content,
phase,
}))
Sources: codex-rs/protocol/src/protocol.rs1060-1063 codex-rs/core/src/stream_events_utils.rs127-152
Contains an incremental update to the agent's message. Emitted during streaming response processing:
| Field | Type | Description |
|---|---|---|
delta | String | Text to append to current message |
phase | Option<MessagePhase> | Message phase (Commentary/Answer) |
Emission point:
ModelClientSession::stream(prompt)
→ yields ResponseEvent::ContentItemDelta(ContentItem::Message { delta, phase })
→ handle_response_item_delta(ContentItem::Message { delta, phase })
→ session.send_event(EventMsg::AgentMessageDelta(AgentMessageDeltaEvent {
delta,
phase,
}))
These deltas are accumulated in ActiveTurn::active_assistant_message until the message completes.
Sources: codex-rs/protocol/src/protocol.rs1065-1072 codex-rs/core/src/stream_events_utils.rs58-85
Notifies that a shell command is starting. Emitted by ToolEmitter before process spawn:
| Field | Type | Description |
|---|---|---|
call_id | String | Unique identifier for this tool call |
command | Vec<String> | Command and arguments |
parsed_cmd | Vec<ParsedCommand> | Parsed command structure |
cwd | PathBuf | Working directory |
source | ExecCommandSource | Origin (AgentTool/UnifiedExecStartup/etc) |
auto_approved | bool | Whether approval was bypassed |
sandboxed | bool | Whether command runs in sandbox |
reason | Option<String> | Agent's justification for command |
Emission point:
UnifiedExecProcessManager::exec_command(...)
→ ToolOrchestrator::check_approval(...)
→ approval_granted
→ ToolEmitter::emit_exec_begin(ExecCommandBeginEvent { ... })
→ session.send_event(EventMsg::ExecCommandBegin(...))
Sources: codex-rs/protocol/src/protocol.rs1132-1141 codex-rs/core/src/tools/emitter.rs79-95
Notifies that a shell command has completed. Emitted by ToolEmitter after process exit:
| Field | Type | Description |
|---|---|---|
call_id | String | Matches ExecCommandBegin.call_id |
exit_code | i32 | Process exit code |
status | ExecCommandStatus | Success/Failure/Timeout |
aggregated_output | String | Combined stdout/stderr (capped via HeadTailBuffer) |
duration | Duration | Execution time |
timed_out | bool | Whether command exceeded timeout |
Emission point:
UnifiedExecProcessManager::watch_process(process_id)
→ process exits or times out
→ ToolEmitter::emit_exec_end(ExecCommandEndEvent {
call_id,
exit_code: status.code(),
status: if timed_out { Timeout } else { ... },
aggregated_output,
duration: start.elapsed(),
timed_out,
})
→ session.send_event(EventMsg::ExecCommandEnd(...))
Sources: codex-rs/protocol/src/protocol.rs1147-1156 codex-rs/core/src/tools/emitter.rs113-140 codex-rs/core/src/unified_exec.rs200-250
Requests user approval for command execution. Emitted by ToolOrchestrator when approval policy triggers:
| Field | Type | Description |
|---|---|---|
turn_id | String | Current turn identifier |
command | Vec<String> | Command to approve |
cwd | PathBuf | Execution directory |
call_id | String | Tool call identifier |
reason | Option<String> | Agent's reasoning |
parsed_cmd | Option<ParsedCommand> | Command analysis result |
proposed_execpolicy_amendment | Option<ExecPolicyAmendment> | Suggested rule addition |
network_approval_context | Option<NetworkApprovalContext> | Network access details |
Emission point: ToolOrchestrator::check_approval → create oneshot channel → emit ExecApprovalRequest → await response
Sources: codex-rs/protocol/src/protocol.rs1159-1172 codex-rs/core/src/tools/orchestrator.rs158-215
Signals an error during submission execution. Emitted by various error paths in submission_loop or task execution:
| Field | Type | Description |
|---|---|---|
message | String | Human-readable error description |
info | Option<CodexErrorInfo> | Structured error type for client handling |
Common emission points:
Session::handle_codex_error when model API failsSources: codex-rs/protocol/src/protocol.rs1000-1004 codex-rs/core/src/codex.rs1906-1934
Each Submission is assigned a UUIDv7 when created:
All Event messages generated during that submission's execution carry the same ID in their Event.id field.
Sources: codex-rs/core/src/codex.rs469-475
Diagram: Submission-Event Correlation with Active Turn
Implementation: The Session stores the current submission ID and passes it to send_event:
Sources: codex-rs/protocol/src/protocol.rs73-79 codex-rs/protocol/src/protocol.rs689-696 codex-rs/core/src/codex.rs973-984
Determines when users are prompted to approve commands:
| Variant | Description |
|---|---|
UnlessTrusted | Approve only known-safe read-only commands automatically |
OnFailure | Auto-approve in sandbox; escalate on failure |
OnRequest | Model decides when to request approval (default) |
Never | Never prompt user; return failures to model |
Sources: codex-rs/protocol/src/protocol.rs320-359
Determines execution restrictions for tool calls:
| Variant | Disk Access | Network | Use Case |
|---|---|---|---|
DangerFullAccess | Full write | Enabled | Development/testing only |
ReadOnly | Read-only | Disabled | Safe exploration |
ExternalSandbox | Full write | Configurable | Pre-sandboxed environments |
WorkspaceWrite | Cwd + TMPDIR | Configurable | Typical usage |
Sources: codex-rs/protocol/src/protocol.rs379-426
SandboxPolicy::WorkspaceWrite uses WritableRoot to define writable paths with read-only exceptions:
| Field | Type | Description |
|---|---|---|
root | AbsolutePathBuf | Writable root path |
read_only_subpaths | Vec<AbsolutePathBuf> | Protected subpaths (e.g., .git, .codex) |
The system automatically protects .git, .agents, and .codex directories from agent modification.
Sources: codex-rs/protocol/src/protocol.rs428-457 codex-rs/protocol/src/protocol.rs511-621
The TUI uses ChatWidget to manage submission/event flow:
Diagram: TUI Submission Flow
Code paths:
ChatComposer::handle_key_event → InputResult::Submitted → ChatWidget::submit_user_input → self.codex_op_tx.send(Op::UserTurn) → AppEvent::CodexOp → codex.submit(op)App::run event loop → codex.next_event().await → app_event_tx.send(AppEvent::CodexEvent) → ChatWidget::handle_codex_event → match on EventMsgSources: codex-rs/tui/src/chatwidget.rs2471-2550 codex-rs/tui/src/app.rs815-940 codex-rs/tui/src/bottom_pane/chat_composer.rs1066-1120
Exec mode uses EventProcessor trait to handle events non-interactively:
EventProcessorWithHumanOutput formats events with ANSI colorsSources: codex-rs/exec/src/event_processor.rs1-46 codex-rs/exec/src/event_processor_with_human_output.rs1-936
App Server translates protocol events to JSON-RPC notifications for IDE integration:
bespoke_event_handling converts EventMsg to ServerNotificationSources: Referenced in high-level diagrams
The MCP server wraps Codex as an MCP tool, running complete sessions within tool calls. The CodexToolRunner::run_codex_tool_session implements the event loop:
Diagram: MCP Server Event Loop
Key implementation details:
sessions: HashMap<ThreadId, Codex> stores active sessions for subsequent repliesis_safe_command() heuristicSources: codex-rs/mcp-server/src/codex_tool_runner.rs59-141 codex-rs/mcp-server/src/codex_tool_runner.rs191-393
The TUI's ChatWidget::handle_codex_event processes events and updates UI state:
Diagram: ChatWidget Event Processing
Key code paths:
Sources: codex-rs/tui/src/chatwidget.rs2471-3100 codex-rs/tui/src/chatwidget.rs1886-2050
Approval events pause turn execution until client submits a resolution:
Diagram: Approval Request Flow
Sources: codex-rs/protocol/src/protocol.rs789 codex-rs/protocol/src/protocol.rs194-200
The RolloutRecorder filters events before persisting to JSONL rollout files:
Code path: Session → RolloutRecorder::record_event(event) → is_persisted_event(event, mode) → write to JSONL if true
Sources: codex-rs/core/src/rollout/mod.rs40-80 codex-rs/core/src/rollout/policy.rs1-20
Diagram: Event Persistence Decision Tree
Implementation: The is_persisted_event function in rollout/policy.rs uses pattern matching:
Use cases:
Sources: codex-rs/core/src/rollout/policy.rs1-171 codex-rs/core/src/rollout/mod.rs95-123
Structured error types provide actionable information to clients:
| Variant | Description |
|---|---|
ContextWindowExceeded | Context length limit exceeded |
UsageLimitExceeded | User quota exhausted |
ModelCap | Rate limit hit; includes reset time |
HttpConnectionFailed | Failed to connect to API |
ResponseStreamConnectionFailed | SSE stream connection failed |
ResponseStreamDisconnected | Stream disconnected mid-turn |
Unauthorized | Invalid or expired credentials |
BadRequest | Invalid request parameters |
SandboxError | Sandbox execution failed |
ThreadRollbackFailed | Rollback operation failed |
Sources: codex-rs/protocol/src/protocol.rs969-1000
Derived from event sequences, AgentStatus represents the agent's lifecycle state:
| Variant | Description |
|---|---|
PendingInit | Waiting for initialization |
Running | Processing turn |
Completed(Option<String>) | Finished; includes final message |
Errored(String) | Encountered error |
Shutdown | Agent terminated |
NotFound | Thread does not exist |
This enum is used in collaboration events to track spawned agent states.
Sources: codex-rs/protocol/src/protocol.rs948-966
DynamicToolCallRequest and DynamicToolResponse enable runtime tool definition:
Sources: codex-rs/protocol/src/protocol.rs229-235
Multi-agent collaboration uses specialized event pairs:
CollabAgentSpawnBegin/End: Agent spawningCollabAgentInteractionBegin/End: Inter-agent messagesCollabWaitingBegin/End: Waiting for agents to completeCollabCloseBegin/End: Agent terminationEach event includes sender/receiver thread IDs and operation status.
Refresh this wiki