Skip to content

Core运行时架构总览

从会话所有权、任务执行、上下文、模型客户端、工具系统和状态持久化六个职责域拆解 Codex Core 运行时架构。

基于rust-v0.150.0
CodexRustRuntimeArchitecture

Core运行时架构总览 ​

codex-core 是 Codex 本地 agent 的编排内核。它接收客户端提交的操作,维护 Thread 与 Turn 生命周期, 构造模型上下文和工具集合,驱动模型流与工具循环,再把结果同时投递到实时事件、会话历史和持久化 系统。

Core 不是一个“大而全”的应用进程:它不负责终端渲染,不拥有 App Server 的 JSON-RPC transport, 不直接实现所有平台沙箱,也不把每个扩展和模型 provider 都写在一个 crate 中。它的核心价值是让这些 边界在同一个运行时状态机中协作,并保持输入、执行、取消、事件和恢复的一致性。

本文只回答 Core 有哪些职责域、owner 如何连接、输入输出如何流动。读者进入具体 Session 字段、锁、 atomic、dirty refresh 或 pointer identity 时,应转入 Session核心数据结构,不在架构总览中重复字段参考。

1. Core 编排内核 ​

core/src/lib.rs 的第一条约束就体现了它的定位:

源码位置:codex-rs/core/src/lib.rs :: crate root

rust
//! Root of the `codex-core` library.

// Core 把用户可见输出交给协议/UI,库层直接打印会绕开事件与持久化链。
#![deny(clippy::print_stdout, clippy::print_stderr)]

库代码禁止直接写 stdout/stderr。用户可见输出必须转换为协议事件、交给 UI,或者进入 tracing。 这意味着 Core 的正常输出不是“打印一段文字”,而是 Event/EventMsg、结构化 item、tool output 和 持久化记录。

从外部看,Core 的公共入口可以归为四组:

公共边界代表类型调用方得到什么
Thread 管理ThreadManager、StartThreadOptions、NewThread创建、恢复、fork 和查找运行中的 thread
Thread 交互CodexThread提交 Op、接收 Event、查询状态和关闭会话
配置与依赖Config、AuthManager、EnvironmentManager、extension registry构造运行时所需的策略和服务
模型与协议辅助ModelClient、Prompt、ResponseEvent、parse_turn_item模型流和内部 item 的结构化边界

core-api 进一步把这些类型重导出成面向宿主的 facade。具体边界见 Core与Core-API边界;本文关注这些类型进入 Core 后如何组成运行时。

源码位置:codex-rs/core/src/codex_thread.rs :: CodexThread::submit, shutdown_and_wait

rust
impl CodexThread {
    // 对外 handle 不直接处理 Op,只把它交给 SessionIo 的 submission channel。
    pub async fn submit(&self, op: Op) -> CodexResult<String> {
        self.io.submit(op).await
    }

    // shutdown 同时等待 session loop 退出,而不是只把控制消息放进队列。
    pub async fn shutdown_and_wait(&self) -> CodexResult<()> {
        self.io.shutdown_and_wait().await
    }

    pub async fn wait_until_terminated(&self) {
        self.io.session_loop_termination.clone().await;
    }

    pub fn session_telemetry(&self) -> SessionTelemetry {
        self.session.services.session_telemetry.clone()
    }
}

这三个方法说明 CodexThread 是生命周期句柄:实际队列和 loop completion 都在 SessionIo,handle 本身没有另一套调度状态。

2. 六个职责域围绕 ​

Core 内部可以按职责拆成六个域。它们不是 Cargo crate 的机械分组,而是一次 Turn 真正穿过的运行时 边界。

职责域核心目录/文件负责什么不负责什么
Thread 与 Sessionthread_manager.rs、codex_thread.rs、session/所有权、生命周期、submission 与 event 通道UI thread list 和 JSON-RPC
Turn 与 Tasktasks/、session/turn.rstask 选择、单活 Turn、模型/工具循环、取消低层 HTTP 与 PTY 实现
上下文与模型context/、context_manager/、client*.rshistory、prompt、模型 session、Response streamprovider 配置存储与外部 UI 投影
工具与安全编排tools/、exec*.rs、safety.rstool plan、路由、审批、沙箱尝试、结果回灌OS sandbox 内核实现
状态与持久化state/、rollout.rs、state_db_bridge.rssession/turn 状态、rollout、SQLite bridgeApp Server 的客户端 Turn reducer
扩展与可观测性plugins/、skills.rs、mcp.rs、hook_runtime.rs、OTel扩展装配、lifecycle、trace 和 metricsmarketplace/transport 的全部底层实现

Core 对这些基础设施有依赖,但不吞并其职责。例如 ToolRouter 决定调用哪个 runtime,实际进程创建 由 exec/PTY 层完成;Core 计算 sandbox 尝试,Seatbelt、bubblewrap 或 Windows token 则由平台 crate 强制。

3. 所有权主轴 ​

Core 的运行时所有权不是 ThreadManager 持有一棵包含全部状态的巨型对象树。它分为三个层次:

  • ThreadManagerState 持有共享服务和 ThreadId → Arc<CodexThread> 活跃映射;
  • CodexThread 是宿主持有的交互 handle,组合 Arc<Session> 与 SessionIo;
  • Session 才拥有一个 agent thread 的可变状态、当前 Turn、输入队列和 thread-scoped services。

ThreadManager 从 map 中移除一个 thread,不等于对象已经销毁。map value 是 Arc<CodexThread>,App Server listener、agent control 或其他调用方仍可能持有引用。真正的 shutdown 需要提交控制操作、 等待 Session loop 终止并释放 thread-scoped resources。

Thread 创建还有一个重要握手:ThreadManagerState::spawn_thread() 调用 Session::spawn() 后, finalize_thread_spawn() 必须先从 SessionIo 读到 INITIAL_SUBMIT_ID 对应的 EventMsg::SessionConfigured,之后才把 CodexThread 插入 map。第一个事件不符合契约时返回 SessionConfiguredNotFirstEvent;并发出现相同 ThreadId 时,后创建的 Session 会被关闭。

Thread 的对外状态与内部 Session 生命周期并非完全相同。下面的状态图展示从构造、首事件握手、空闲、 活动到终止的主要转换;SystemError 表示运行时无法继续提供正常服务,不是普通 Turn 失败。

普通模型错误通常只闭合当前 Turn,Session 仍可回到 Idle;只有系统级错误才进入 SystemError。 这一区分决定客户端是允许下一轮输入,还是只能关闭并重新创建 Thread。

4. Session生命周期 ​

Session 源码注释给出一个关键不变量:一个 Session 同时最多只有一个 running task,但可以被用户 输入中断。架构总览只需要区分三类 owner:

架构域代表类型生命周期在主链中的责任
Thread级服务SessionServices整个已加载Thread连接模型、MCP、执行、扩展与store
跨Turn事实SessionState多个Turn保存history、configuration和窗口状态
当前执行切片ActiveTurn、RunningTask、TurnState当前Task/Turn单活、取消和交互waiter

Core运行时架构总览 只依赖这三个架构域互相分离,不逐字段判断该使用 Mutex、Semaphore、OnceLock、atomic 还是 ArcSwap。完整字段总览、同步原语、snapshot-then-act、dirty guard、pointer identity及状态测试统一见 Session核心数据结构。

TurnContext 与 StepContext 是执行过程中向下游传递的快照,不是第四个 Session owner:前者固定一次 Turn的策略/身份,后者固定一次sampling实际使用的环境、MCP binding和ToolRouter。字段级参考将在后续 Context专题展开。

4.1 总览边界 ​

总览不再复制 Session struct 或 service/state/turn 字段。调试某个字段时直接进入 Session核心数据结构:先判断字段 属于 Thread 服务、跨Turn事实还是当前执行,再由该专题选择同步原语和测试。

5. Session创建注册 ​

Thread 创建是运行时控制面的主路径。新建、恢复、fork 和 subagent 最终都转换成 ThreadSpawnRequest,再进入同一个 spawn_thread():

恢复活跃 thread 时还有一条 fast path:若 map 中同 ID 的 thread 仍在运行且 rollout path 一致,直接 返回现有 CodexThread,避免创建第二个 Session。若 path 不同则拒绝;若旧 handle 已不运行,则先从 map 移除,再走完整恢复构造。

Session 初始化本身会并行准备 persistence/state DB、认证与 MCP projection、plugins/skills、模型和 环境。SessionServices、初始 SessionState、MCP prewarm、network proxy 与 rollout recorder 都在 这个阶段装配,但对外可观察的完成点仍是 SessionConfigured 握手。

6. Submission loop ​

调用方不会直接调用 run_turn()。CodexThread::submit(Op) 先经 SessionIo 生成 submission ID, 再送入 Session 的 input channel。Session loop 按 Op 类型选择不同 handler:

Op 类别控制效果
UserInput / UserTurnadmission、构造 TurnContext、启动 RegularTask
Interrupt取消 active task、记录中断语义、清理 waiter
approval / user-input response根据 call/request ID 唤醒 TurnState 中的 oneshot
config/model/mode update更新允许热变更的 Session 配置
compact / review / user shell启动专用 task 或执行路径
MCP、plugin、environment refresh刷新 thread-scoped runtime 或下次 step 快照
Shutdown停止 task、flush rollout、回收服务并终止 event stream

控制平面与数据平面分开后,审批 response 和 interrupt 才能在模型/工具 task 等待时继续被 Session 处理。若把整个 Turn 直接 await 在 submission loop 中,循环就无法消费这些控制操作。

源码位置:codex-rs/core/src/session/handlers.rs :: submission_loop(分派节选)

rust
pub(super) async fn submission_loop(
    sess: Arc<Session>,
    config: Arc<Config>,
    rx_sub: Receiver<Submission>,
) {
    let mut shutdown_received = false;
    while let Ok(sub) = rx_sub.recv().await {
        let dispatch_span = submission_dispatch_span(&sub);
        let should_exit = async {
            match sub.op.clone() {
                // Interrupt在控制循环中处理,不等待活动Turn数据平面返回。
                Op::Interrupt => {
                    interrupt(&sess).await;
                    false
                }
                Op::CleanBackgroundTerminals => {
                    clean_background_terminals(&sess).await;
                    false
                }
                Op::UserInput { .. } => {
                    user_input_or_turn(
                        &sess,
                        sub.id.clone(),
                        sub.op,
                        sub.client_user_message_id,
                        sub.parent_turn_id,
                    )
                    .await;
                    false
                }
                // ThreadSettings更新和Turn启动共享同一顺序控制面。
                Op::ThreadSettings { thread_settings } => {
                    update_thread_settings(&sess, sub.id.clone(), thread_settings).await;
                    false
                }

完整 match 还包含审批回复、MCP/Realtime、compact、review、rollback 和 Shutdown。总览只展示三种不同 控制效果;逐 Op 行为见 Session运行时处理。

7. Task 层 ​

SessionTask 把 Regular、Review、Compact 等长任务统一为 kind()、span name 和 async run()。 Session 启动 task 时创建 child cancellation token、完成 Notify、OTel span 和 AbortOnDropHandle,随后把 RunningTask 安装进 ActiveTurn。

源码位置:codex-rs/core/src/tasks/mod.rs :: Session::spawn_task

rust
pub async fn spawn_task<T: SessionTask>(
    self: &Arc<Self>,
    turn_context: Arc<TurnContext>,
    input: Vec<TurnInput>,
    task: T,
) {
    // 新Task先按Replaced语义终止旧Task,单活约束不交给具体Task自行维护。
    self.abort_all_tasks(TurnAbortReason::Replaced).await;
    self.clear_connector_selection().await;
    self.start_task(turn_context, input, task, MailboxParentProvenance::Ignore)
        .await;
}

承载 task 的 Tokio task 完成时执行统一收尾:

  1. 运行具体 SessionTask::run();
  2. flush rollout;失败时发送 warning,但继续进入 terminal handling;
  3. 未被取消时调用 on_task_finished();
  4. 通知等待者 task 已完成;
  5. drop/替换 handle 时仍有 abort 兜底。

普通 RegularTask 先发送 TurnStarted,获取 startup-prewarmed model client session,然后调用 run_turn()。若 input queue 仍有 pending input,它会在同一 TurnContext 下再次进入 run_turn()。

run_turn() 执行的是数据平面:

  1. 必要时进行 pre-sampling compaction;
  2. 解析本轮输入要求的 MCP server、plugin 和 Skill;
  3. 捕获第一个 StepContext,记录 world/context updates;
  4. 注入用户输入、Hook 和扩展上下文;
  5. 构造 Prompt 与当前 ToolRouter;
  6. 读取 ResponseEvent stream;
  7. 记录模型 item,执行工具并把 output 放回 history;
  8. 若需要继续则捕获下一 step 并再次采样;
  9. 没有工具续轮或收到终止条件时返回最后一条 agent message。

模型 client session 是 Turn-scoped 的,用于复用 WebSocket 和 sticky routing;ModelClient 本身放在 SessionServices 中跨 Turn 共享。两者名字相近,但缓存和生命周期不同。

8. Step边界 ​

Core 不会直接把 SessionState.history 原样发送给模型。一次 sampling request 的输入来自三部分:

  • ContextManager 中经过规范化、截断和 compaction 的 ResponseItem history;
  • Turn/Step 捕获的 developer context、environment、AGENTS、Skills、plugins 和 pending input;
  • ToolRouter::model_visible_specs() 给出的本 step 工具集合。

build_tool_router() 将 Core tool、MCP tool、extension tool、dynamic tool 与 hosted tool 汇合,同时 应用 feature、model capability、approval mode、tool search 和 exposure policy。registry 保存 runtime, visible specs 进入 prompt;hidden/deferred tool 可以存在于 registry,却不一定立即展示给模型。

模型返回 tool call 后,ToolRouter 将 ResponseItem 解析为统一调用,handler/runtime 负责参数与实际 行为,ToolOrchestrator 为 shell 类工具处理 exec policy、approval、sandbox 和条件升级。Core 最终 把 tool output 转成与 call ID 配对的 response item,写入 history,下一 step 才能让模型看到结果。

因此 StepContext 是一个一致性边界:同一次采样所广告的 tool spec,必须与之后用于执行该 call 的 router 属于同一捕获 step。不能在 call 返回时根据最新全局配置重新随意选择 runtime。

9. 结果分发 ​

模型或工具产生结果后,Core 通常需要更新三种不同表示:

输出链主要 API消费者是否完整持久化
模型历史record_conversation_items()下一次 sampling/compaction按 ResponseItem policy
实时事件send_event()CLI、App Server、TUI listenerdelta 多数不持久化
rollout/storepersist_rollout_items()、flush_rollout()resume、fork、history/read、诊断按 Rollout policy 筛选

event_mapping.rs::parse_turn_item() 把一部分 ResponseItem 转为 TurnItem,例如 user message、agent message、reasoning、web search 和 image generation。不是所有模型 item 都是可展示 item,也不是所有 EventMsg 都要进入 rollout。

Session 的 send_event() 还会处理 legacy event projection、telemetry 和 extension lifecycle。仅看到 UI 收到某个事件,不能推断它已经写盘;恢复问题必须继续检查 rollout policy 与 reconstruction。

源码位置:codex-rs/core/src/session/mod.rs :: Session::record_conversation_items

rust
#[tracing::instrument(level = "trace", skip_all, fields(item_count = items.len()))]
pub(crate) async fn record_conversation_items(
    &self,
    turn_context: &TurnContext,
    items: &[ResponseItem],
) {
    let (items, image_preparations) =
        self.prepare_conversation_items_for_history(turn_context, items);
    let items = items.as_ref();
    {
        let mut state = self.state.lock().await;
        state.current_time_reminder.note_recorded_items(items);
        // 先更新内存history,下一次sampling才能看到新item。
        state.record_items(
            items.iter(),
            turn_context.model_info.truncation_policy.into(),
        );
    }
    for image in image_preparations {
        self.services
            .analytics_events_client
            .track_image_preparation(ImagePreparationFact {
                turn_id: turn_context.sub_id.clone(),
                metadata: image,
            });
    }
    // durable rollout与raw item事件在history更新后依次执行。
    self.persist_rollout_response_items(items).await;
    self.send_raw_response_items(turn_context, items).await;
}

Event 走另一条入口,但同样把持久化放在实时交付之前:

源码位置:codex-rs/core/src/session/mod.rs :: Session::send_event_raw_with_persistence

rust
async fn send_event_raw_with_persistence(&self, event: Event, persist: bool) {
    if persist {
        // store仍会应用自己的Rollout policy,不是所有EventMsg都最终落盘。
        let rollout_items = vec![RolloutItem::EventMsg(event.msg.clone())];
        self.persist_rollout_items(&rollout_items).await;
    }
    self.services
        .rollout_thread_trace
        .record_protocol_event(&event.msg);
    self.deliver_event_raw(event).await;
}

10. 并发与锁 ​

Core 的关键并发策略可以概括为:

  • ThreadManager map 用 RwLock,查找和枚举并发,插入/移除独占;
  • 每个 Session 只允许一个 active task,但 submission loop 始终能消费控制消息;
  • SessionState、ActiveTurn 和 TurnState 使用独立锁,避免一个大锁覆盖全部生命周期;
  • model stream、工具调用和部分初始化通过独立 Tokio task 并发;
  • cancellation 使用层级 CancellationToken,task、turn、sampling 和 tool 可以持有 child token;
  • approval、elicitation 和 user input 用 call ID 对应的 oneshot waiter,不轮询共享字段;
  • rollout flush 在 task terminal handling 前执行,失败发 warning 并保留后续重试机会。

这里最重要的不变量是锁外 await。代码会在锁内解析或更新小块状态,然后 clone Arc/snapshot,释放 锁后才进行网络、MCP、文件或进程等待。分析死锁或卡顿时,应优先寻找“持锁跨 await”和两个锁的获取 顺序,而不是只统计 Mutex 数量。

11. 失败取消警告 ​

Core 不把所有异常都变成 Rust Err 返回到最外层:

情况主要表达运行时结果
无效请求或 Thread 不存在CodexErrsubmission/RPC 失败,不启动行为
Turn 被用户中断cancellation + TurnAbortReasontask 停止,发送 aborted terminal event
模型/工具可展示错误EventMsg::Error 或 tool output客户端可见,是否结束 Turn 由错误类型决定
rollout flush 失败warning event + recorder 保留 pending当前交互可继续,持久化后续重试
sandbox deniedSandboxErr::Denied仅满足策略时进入审批/重试,否则返回模型
background refresh 失败warning/telemetry/status保留旧 snapshot 或让下次 step 重试
invariant 破坏fatal CodexErr 或 panic/assert终止当前创建或测试立即失败

取消与错误也会触发 extension lifecycle:turn start、stop、abort、error 是独立回调。扩展若只监听 stop,不能假设 abort/error 一定先转成正常完成。

12. Core边界 ​

理解架构还要知道何时停止在 Core:

问题Core 的最后一站继续阅读的位置
JSON-RPC 方法为何这样命名ThreadManager/CodexThread APIapp-server 与 app-server-protocol
TUI 为什么显示某种布局EventMsg/TurnItemtui 的 reducer、cell 与 snapshot
HTTP/SSE 字节如何解析ModelClient provider 调用codex-api、http-client、model-provider
命令如何获得 PTYexec runtime 请求exec-server、utils/pty
文件权限如何被内核拒绝SandboxManager transformsandboxing、linux/windows sandbox
MCP transport 如何握手McpRuntime/McpBindingcodex-mcp、rmcp-client
rollout 文件如何扫描压缩Core persistence bridgerollout、thread-store、state
plugin 如何下载和安装Session 使用的 plugin outcomecore-plugins、plugin/marketplace crates

如果某篇 Core 代码分析开始解释终端 CSS、JSON-RPC wire rename 或 bubblewrap syscall,它已经越过了 当前模块的职责边界。

13. 源码导航 ​

阅读 Core 架构时,建议按下面顺序建立局部地图:

text
core/src/
├── lib.rs                    # 模块与公共导出
├── thread_manager.rs         # 多 Thread 所有权和共享依赖
├── codex_thread.rs           # 宿主交互 handle
├── session/
│   ├── session.rs            # Session 构造与字段
│   ├── mod.rs                # submission loop 与公共运行时操作
│   ├── turn_context.rs       # Turn 不可变快照
│   ├── step_context.rs       # sampling step 快照
│   └── turn.rs               # 模型/工具主循环
├── tasks/                    # Regular/Compact/Review/UserShell task
├── state/                    # services、SessionState、ActiveTurn/TurnState
├── context_manager/          # history、normalize、updates
├── client.rs                 # ModelClient 与 stream
├── tools/                    # spec、registry、router、runtime、approval
├── event_mapping.rs          # ResponseItem → TurnItem
└── rollout.rs                # 持久化 facade

架构级修改至少要从三类测试中选择与改动边界匹配的验证方式:

  • 状态和纯转换:实现旁的 *_tests.rs;
  • Thread/Turn/工具行为:core/tests/suite/ 的 TestCodex 集成测试;
  • 跨 App Server/远端 executor:App Server suite 或 remote environment tests。

一个有效的 Core 集成测试应同时观察输入和输出:用 Responses mock 检查发给模型的 prompt/tool history,用 EventMsg 检查客户端投影,必要时 flush 并重载 rollout 验证恢复。只断言函数返回值, 通常覆盖不到 Core 作为编排内核最容易出错的边界。

可以用三组代表性断言把架构图落到代码:submission_loop_channel_close_aborts_active_turn_before_thread_stop_lifecycle 先安排一个监听取消的活动 task,再关闭 submission sender,断言 turn_abort 先于 thread_stop; dropped_response_stream_traces_cancelled_partial_output 先让 mapper 看到一个完整 output item,再丢弃 consumer,断言 rollout 中 inference 状态为 Cancelled 且已观察 item 被保留; shutdown_and_wait_allows_multiple_waiters 则让两个 waiter 等待同一个 loop termination,断言二者都能 完成。三者分别证明关闭顺序、模型流取消和共享终止 future,不能单独证明所有工具或 App Server wire 路径。

可以用下面的只读搜索把本文的 Core ownership 主线落回源码:

bash
rg -n "struct Core|struct Session|submission_loop|record_response" codex-rs/core/src