Skip to content

Codex Rust 架构分析

Published: at 12:00 AM

结论先行:Codex Rust 不是一个“CLI 调模型”的程序,而是一个以 thread 为生命周期、以 turn 为事务、以 Op/EventMsg 为协议边界、以 tool runtime 为副作用出口的事件驱动 Agent 运行时。

更具体地说,它的核心模型是:

外部入口把用户动作转换为 Op

ThreadManager 找到或创建 CodexThread

Session 串行化线程状态

run_turn 驱动模型采样、上下文更新、工具执行和压缩

工具 runtime 统一处理副作用、安全、hook、telemetry 和模型可见输出

EventMsg 回到 TUI、app-server 或其他客户端

ThreadStore 持久化 canonical rollout history

所以维护 Codex 时,真正要判断的不是“这段代码在哪个界面触发”,而是它改变了哪一类状态,是否进入了正确的协议边界,副作用是否走统一工具出口,以及中断、恢复、fork、rollback 时还能不能得到一致历史。

总体分层

Codex Rust 可以按边界分为七层:

CLI / TUI / app-server / MCP / 扩展客户端

入口适配层:把外部协议转换为内部请求

协议层:Submission / Op / Event / EventMsg

线程与会话运行时:ThreadManager / CodexThread / Session

Turn 执行层:run_turn / TurnContext / 模型采样 / pending input / hooks

上下文层:ContextManager / history_version / token info / compaction

工具与安全层:ToolRouter / ToolRegistry / CoreToolRuntime / sandbox / approval

状态与持久化:ThreadStore / rollout items / metadata / resume / archive

这套分层的关键点是:前端不直接驱动模型循环,app-server 也不是 Agent 内核。外部入口只负责把动作转为协议对象;会话运行时负责串行化状态;run_turn 负责一个 turn 内的模型-工具循环;工具 runtime 是所有外部副作用的统一出口。

设计漏洞也从这里开始出现:协议枚举会膨胀,codex-core 会变重,TurnContext 会吸收越来越多字段,工具安全策略会横跨多个模块,app-server API 面会不断扩张。这些不是实现瑕疵,而是这个架构的主要维护成本。

协议边界

codex-rs/protocol/src/protocol.rs 是最重要的稳定边界。它定义了 Submission Queue / Event Queue 模型:

Op 不只是“用户发了一句话”。它覆盖中断、审批、动态工具回执、MCP elicitation、权限请求、thread settings 更新、compact、rollback、review 等控制动作。也就是说,Op 是所有入口进入 Agent 内核的命令边界。

EventMsg 覆盖 turn 生命周期、模型输出、工具开始和结束、token 计数、上下文压缩、安全警告、thread goal 更新、session 配置等可观察状态。客户端可以选择不同展示方式,但不应该绕开这条事件流去读取 core 内部状态。

这个设计的优点是统一:TUI、app-server 和其他入口可以复用同一套运行语义。缺点是 protocol.rs 会越来越像系统总线。任何新增行为都可能牵动 Rust protocol、core、TUI 渲染、app-server projection、TypeScript schema、fixture 和测试。维护时要问的第一个问题是:这是新的协议语义,还是已有事件的另一种展示?

线程与会话运行时

codex-rs/core/src/thread_manager.rs 把 Agent 建模为长生命周期 thread,而不是一次请求。

ThreadManager 持有进程内线程表:

ThreadId -> Arc<CodexThread>

它负责创建线程、恢复线程、fork 子线程或 subagent、根据 thread id 提交 Op、连接 thread store,并注入认证、模型管理、环境管理、MCP、插件、skills、analytics、extension registry 等共享服务。

CodexThread 是线程句柄,向外暴露 submitsubmit_with_tracesubmit_user_input_with_client_user_message_id 等方法。它本身不是复杂状态机,而是把线程级请求委托给具体 Session

真正的单线程运行状态在 codex-rs/core/src/session/session.rsSession 中。它持有事件通道、agent 状态、会话状态、当前 active turn、输入队列、realtime conversation、安全审查状态和服务集合。这里的核心约束是:一个 session 同一时间最多只有一个活跃 turn task,但它可以被用户输入、中断、审批回执、动态工具响应等异步事件影响。

这接近 Actor 模型:外部提交消息,session 串行化关键状态变化,内部允许模型流、工具执行和审批等待等异步过程存在。

边界条件集中在这里:

所以 Codex 的复杂度不是“怎么调用模型”,而是“长会话在异步输入和外部副作用下如何保持可恢复、可中断、可审计”。

Turn 执行层

codex-rs/core/src/session/turn.rs::run_turn 是普通 turn 的主循环。理解它,基本就理解了 Codex 的 Agent 内核。

一次 turn 的前置流程是:

pre-sampling compact

record context updates and set reference context item

resolve skills / plugins / extension turn input injections

run pending session start hooks

inspect and record initial TurnInput

merge connector selection

record injection ResponseItem into conversation history

track resolved config telemetry

进入模型-工具循环

这里有几个设计含义。

第一,compact 不是最后才做的清理动作。run_turn 在采样前就可能触发 pre-sampling compact,因为历史窗口和 token 成本会决定本轮能否安全请求模型。

第二,context update 先于模型采样进入历史。Session::record_context_updates_and_set_reference_context_item 会记录本轮配置、环境、上下文差异,并设置后续 diff 的 reference context item。

第三,skills、plugins 和 extension turn input 都被转成模型可见 ResponseItem 注入历史,而不是作为入口层私有逻辑偷偷影响 prompt。这保证恢复、调试和审计时能看到模型实际收到的上下文。

进入循环后,run_turn 做的是:

drain pending input if allowed

run hooks and record pending input

clone history and call for_prompt

build sampling request

consume model stream

if tool call: dispatch through ToolRouter / ToolRegistry

record tool output as model input item

check pending input and auto compact

run stop hooks / after-agent hooks

complete, abort, or emit error

pending input 的处理很关键。用户可能在模型运行时继续输入,但模型不一定能在当前采样中安全吸收这些输入。run_turncan_drain_pending_input 控制何时把 pending input 写入历史:本轮初始输入要先被采样;auto compact 之后,也要让模型或工具 continuation 先恢复,再决定是否吸收新的 steer。

错误和中断也在这个循环里收束。CodexErr::TurnAborted 不按普通错误上报;无效图片会尝试替换最后一轮工具图片,避免污染后续历史;其他错误会进入 turn error lifecycle,并通过 EventMsg::Error 返回客户端。

维护这个函数时要保持一个原则:它是 turn 事务边界。新增逻辑如果会影响模型可见历史、工具输出、压缩时机或 turn 生命周期,就应该在这里或它明确调用的子模块中建模,而不是散落到入口 processor。

TurnContext 边界

codex-rs/core/src/session/turn_context.rsTurnContext 是单个 turn 的运行快照。它携带:

它是执行层和安全层的交界面。模型请求工具时,工具拿到的不是一段裸字符串,而是包含权限、环境、模型配置、取消 token、diff tracker 和 call id 的 turn-scoped invocation。

这个设计的防御性很强:权限和环境随 turn 显式传递,工具可以统一依据 TurnContext 做 sandbox、approval、telemetry 和事件上报。

但长期成本也明显:TurnContext 容易变成“所有模块都需要”的大上下文。它降低传参成本,同时弱化模块边界。新增字段前应该先问:这是 turn 的不变量,还是某个工具、入口、扩展服务自己的局部依赖?如果只是局部依赖,继续塞进 TurnContext 会增加隐性耦合。

上下文与压缩

codex-rs/core/src/context_manager/history.rsContextManager 管理模型历史。它不是简单 Vec<Message>,而是包含:

ContextManager::record_items 只记录 API message,并按截断策略处理工具输出。for_prompt 在发送模型前会规范化历史,丢弃不适合的项,并根据模型 input modalities 过滤图片:如果模型不支持图片,消息和工具输出里的图片会被剥离。

这说明上下文层有三个职责:

compaction 也不能理解成“摘要功能”。它是长会话成本控制、缓存稳定性和恢复边界的一部分。run_turn 在采样前和采样后都会检查是否需要 compact;mid-turn auto compact 会把 continuation 放在 pending input 之前,以免压缩打断正在进行的模型-工具链路。

维护上下文注入时有一个硬约束:任何进入模型上下文的新片段都必须有明确结构、大小上限和恢复语义。无界文本、无界工具输出、无界列表都会破坏长会话成本和模型缓存,也会让 thread resume 变得不可预测。

工具路由与执行

Codex 的工具层不是把 JSON 反序列化后直接调用函数。它是副作用边界。

codex-rs/core/src/tools/router.rsToolRouter 做两件事:

ToolRouter::build_tool_call 目前会把三类模型输出收敛成同一形态:

统一后的 ToolCall 只有三个核心字段:

tool_name
call_id
payload

真正执行时,ToolRouter 会构造 ToolInvocation

Session
TurnContext
CancellationToken
SharedTurnDiffTracker
call_id
tool_name
ToolCallSource
ToolPayload

然后交给 ToolRegistry

codex-rs/core/src/tools/registry.rs 定义了 CoreToolRuntimeToolRegistryCoreToolRuntime 是本地工具的 typed runtime contract:工具处理器不仅要实现执行,还可以提供 exposure、并行能力、tool search 信息、参数 diff consumer、pre/post tool hook payload、telemetry tags 和取消语义。

ToolRegistry::dispatch_any_with_terminal_outcome 是工具执行生命周期的中心。它会:

codex-rs/core/src/tools/context.rs 里的 ToolOutput 抽象统一了不同工具的回填形式:shell、patch、MCP、tool search、动态工具都必须最终变成模型可消费的 response item,或者变成 code-mode 结果。

安全边界也在这里收束。approval policy、permission profile、sandbox policy、network policy、guardian、pre/post hook、telemetry 和 lifecycle event 虽然分布在不同模块,但工具执行必须经过统一 runtime。新增工具如果在 app-server processor、CLI 入口或某个临时 task 里直接做副作用,就是架构漏洞:协议可能看得到动作,安全和审计层却看不到完整生命周期。

工具与安全层的维护风险

工具安全不是单个开关,而是多个策略叠加:

这个多层模型的优点是主动防御:模型生成工具调用并不等于工具能执行,工具执行成功也不等于原始输出会原样回到模型。

漏洞是边界横跨模块。文件权限、网络权限、Windows sandbox、MCP server 能力、动态工具授权、插件暴露策略、工具搜索、hook 改写都可能影响最终执行。维护时要避免在某个局部模块“顺手执行”副作用。正确做法是把新能力注册成工具,让 ToolRouterToolRegistryCoreToolRuntime 和生命周期事件接管执行链。

状态与持久化

Codex 的持久化边界在 codex-rs/thread-store/src/store.rsThreadStore trait。它是 storage-neutral boundary,负责:

这里的关键设计是:rollout item 是 canonical history,metadata 更新策略位于 store 之上。append_items 的注释明确说明它是 raw history API,不从 item 内容推断 metadata;需要 metadata 更新的调用方应该在 store 之上准备明确事实,再调用 update_thread_metadata

这让存储层保持中立:它负责 durable/readable,不负责解释 Agent 语义。Agent 语义由 core 的 session、turn、context manager、thread manager 维护。

状态不只是聊天记录。它至少包括:

最难的是 mid-turn。一个 turn 可能已经写入用户输入、上下文更新、部分模型输出、工具调用开始事件,但还没有完成。中断、fork、rollback、resume 都必须回答同一个问题:当前历史是稳定边界,还是未完成动作?

Codex 的处理方向是把 turn lifecycle 显式写入事件和历史,并在中断时写入模型可见的 aborted marker。这样恢复后的模型不会误以为上一轮自然完成。

长期风险在于 fork、rollback、compaction 和 metadata update 都会重写或解释历史。只要某个模块把 rollout 当“展示日志”而不是 canonical model history,就容易破坏恢复语义。

app-server 入口

app-server 是 JSON-RPC 入口,不是另一个 Agent 内核。

codex-rs/app-server/src/message_processor.rsMessageProcessor::new 是组合根。它装配:

这些 processor 包括 account、apps、catalog、command exec、process exec、config、environment、external agent config、feedback、fs、git、initialize、marketplace、MCP、plugin、remote control、search、thread goal、thread、turn、Windows sandbox 等。

这说明 app-server 的职责是把 JSON-RPC 方法映射到共享运行时和周边服务。ThreadRequestProcessorTurnRequestProcessor 这类处理器可以触发 Agent 行为,但不应该复制 Sessionrun_turn 的逻辑。

app-server 还有两个维护成本。

第一是请求串行化。MessageProcessor 持有 RequestSerializationQueues,说明并非所有 JSON-RPC 请求都能任意并发处理。某些请求必须按 connection、thread 或资源维度排队,否则会打乱 thread 状态、初始化状态或工具回执顺序。

第二是 API shape 同步。app-server protocol v2 的请求、响应、notification 需要同时维护 Rust serde、ts-rs 导出、schema fixture、实验 API gating 和 README 示例。新增 API 面时,成本不在 handler 本身,而在“协议、类型生成、文档、测试、兼容性”同时一致。

所以 app-server 的设计边界是:它可以组合服务、做协议转换、做请求排队、做 projection,但不应该成为 Agent 状态机的第二实现。

主数据流

把各层合起来,一次普通用户 turn 的数据流是:

客户端

  │ JSON-RPC / TUI action / internal call

入口层 processor

  │ 构造 Op::UserInput / Op::Interrupt / Op::ExecApproval ...

ThreadManager

  │ ThreadId -> CodexThread

CodexThread

  │ submit Submission

Session

  │ 更新 state、active_turn、input_queue

run_turn

  │ compact -> context update -> injections -> prompt -> sampling

ToolRouter / ToolRegistry

  │ tool lifecycle -> sandbox / approval / hooks / telemetry

ContextManager

  │ record model output and tool output as ResponseItem

EventMsg

  │ AgentMessage / ItemStarted / ItemCompleted / TokenCount / TurnComplete ...

入口层事件适配

  │ TUI render / app-server outgoing message / external client event

客户端

这个模型解释了为什么 Codex 的核心代码大量围绕队列、锁、watch channel、Arc、状态快照和事件类型展开。它不是同步请求响应程序,而是可中断、可恢复、可扩展工具能力的 Agent 运行时。

核心代码锚点

建议按这个顺序读源码:

codex-rs/protocol/src/protocol.rs

codex-rs/core/src/thread_manager.rs

codex-rs/core/src/codex_thread.rs

codex-rs/core/src/session/session.rs

codex-rs/core/src/session/turn.rs

codex-rs/core/src/session/turn_context.rs

codex-rs/core/src/context_manager/history.rs

codex-rs/core/src/tools/router.rs

codex-rs/core/src/tools/registry.rs

codex-rs/core/src/tools/context.rs

codex-rs/thread-store/src/store.rs

codex-rs/app-server/src/message_processor.rs

这些文件的职责分别是:

读码时不要从具体 shell 工具或 TUI widget 开始。先把协议、thread、session、turn、context、tool runtime 这条主干建起来,再看入口和展示层。

设计评价

优点:

设计漏洞:

边界条件:

长期维护成本:

最终模型

Codex Rust 的架构模型不是“前端 + 后端 + 模型调用”,而是:

一个以 thread 为生命周期、
以 turn 为执行事务、
以 Op/EventMsg 为协议边界、
以 ContextManager 为模型历史边界、
以 ToolRegistry/CoreToolRuntime 为副作用出口、
以 ThreadStore 为持久化边界的事件驱动 Agent 运行时。

理解这一点后,维护 Codex 的判断标准就很明确:

只要这些边界保持清晰,Codex 可以继续扩展入口、工具、插件、MCP、app-server API 和多 Agent 能力;一旦这些边界被绕过,系统会很快退化成一组难以恢复、难以审计、难以测试的异步副作用。