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
//! 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
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 与 Session | thread_manager.rs、codex_thread.rs、session/ | 所有权、生命周期、submission 与 event 通道 | UI thread list 和 JSON-RPC |
| Turn 与 Task | tasks/、session/turn.rs | task 选择、单活 Turn、模型/工具循环、取消 | 低层 HTTP 与 PTY 实现 |
| 上下文与模型 | context/、context_manager/、client*.rs | history、prompt、模型 session、Response stream | provider 配置存储与外部 UI 投影 |
| 工具与安全编排 | tools/、exec*.rs、safety.rs | tool plan、路由、审批、沙箱尝试、结果回灌 | OS sandbox 内核实现 |
| 状态与持久化 | state/、rollout.rs、state_db_bridge.rs | session/turn 状态、rollout、SQLite bridge | App Server 的客户端 Turn reducer |
| 扩展与可观测性 | plugins/、skills.rs、mcp.rs、hook_runtime.rs、OTel | 扩展装配、lifecycle、trace 和 metrics | marketplace/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 / UserTurn | admission、构造 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(分派节选)
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
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 完成时执行统一收尾:
- 运行具体
SessionTask::run(); - flush rollout;失败时发送 warning,但继续进入 terminal handling;
- 未被取消时调用
on_task_finished(); - 通知等待者 task 已完成;
- drop/替换 handle 时仍有 abort 兜底。
普通 RegularTask 先发送 TurnStarted,获取 startup-prewarmed model client session,然后调用 run_turn()。若 input queue 仍有 pending input,它会在同一 TurnContext 下再次进入 run_turn()。
run_turn() 执行的是数据平面:
- 必要时进行 pre-sampling compaction;
- 解析本轮输入要求的 MCP server、plugin 和 Skill;
- 捕获第一个
StepContext,记录 world/context updates; - 注入用户输入、Hook 和扩展上下文;
- 构造
Prompt与当前ToolRouter; - 读取
ResponseEventstream; - 记录模型 item,执行工具并把 output 放回 history;
- 若需要继续则捕获下一 step 并再次采样;
- 没有工具续轮或收到终止条件时返回最后一条 agent message。
模型 client session 是 Turn-scoped 的,用于复用 WebSocket 和 sticky routing;ModelClient 本身放在 SessionServices 中跨 Turn 共享。两者名字相近,但缓存和生命周期不同。
8. Step边界
Core 不会直接把 SessionState.history 原样发送给模型。一次 sampling request 的输入来自三部分:
ContextManager中经过规范化、截断和 compaction 的ResponseItemhistory;- 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 listener | delta 多数不持久化 |
| rollout/store | persist_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
#[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
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 不存在 | CodexErr | submission/RPC 失败,不启动行为 |
| Turn 被用户中断 | cancellation + TurnAbortReason | task 停止,发送 aborted terminal event |
| 模型/工具可展示错误 | EventMsg::Error 或 tool output | 客户端可见,是否结束 Turn 由错误类型决定 |
| rollout flush 失败 | warning event + recorder 保留 pending | 当前交互可继续,持久化后续重试 |
| sandbox denied | SandboxErr::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 API | app-server 与 app-server-protocol |
| TUI 为什么显示某种布局 | EventMsg/TurnItem | tui 的 reducer、cell 与 snapshot |
| HTTP/SSE 字节如何解析 | ModelClient provider 调用 | codex-api、http-client、model-provider |
| 命令如何获得 PTY | exec runtime 请求 | exec-server、utils/pty |
| 文件权限如何被内核拒绝 | SandboxManager transform | sandboxing、linux/windows sandbox |
| MCP transport 如何握手 | McpRuntime/McpBinding | codex-mcp、rmcp-client |
| rollout 文件如何扫描压缩 | Core persistence bridge | rollout、thread-store、state |
| plugin 如何下载和安装 | Session 使用的 plugin outcome | core-plugins、plugin/marketplace crates |
如果某篇 Core 代码分析开始解释终端 CSS、JSON-RPC wire rename 或 bubblewrap syscall,它已经越过了 当前模块的职责边界。
13. 源码导航
阅读 Core 架构时,建议按下面顺序建立局部地图:
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 主线落回源码:
rg -n "struct Core|struct Session|submission_loop|record_response" codex-rs/core/src