结论先行: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 模型:
Submission:一次提交,包含提交 id、Op和 trace 信息。Op:客户端能要求 Agent 做什么。Event:一次输出事件,绑定原始 submission。EventMsg:Agent 能向客户端报告什么。
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 是线程句柄,向外暴露 submit、submit_with_trace、submit_user_input_with_client_user_message_id 等方法。它本身不是复杂状态机,而是把线程级请求委托给具体 Session。
真正的单线程运行状态在 codex-rs/core/src/session/session.rs 的 Session 中。它持有事件通道、agent 状态、会话状态、当前 active turn、输入队列、realtime conversation、安全审查状态和服务集合。这里的核心约束是:一个 session 同一时间最多只有一个活跃 turn task,但它可以被用户输入、中断、审批回执、动态工具响应等异步事件影响。
这接近 Actor 模型:外部提交消息,session 串行化关键状态变化,内部允许模型流、工具执行和审批等待等异步过程存在。
边界条件集中在这里:
- 中断必须同时产生用户可见事件和模型可见历史修正。
- mid-turn fork 必须选择一个稳定历史边界,不能把半个工具调用当成完成 turn。
- rollback 要修剪历史,同时避免留下失效的上下文 baseline。
- 自动 idle turn 不能抢占用户触发的 pending turn。
- 审批、动态工具响应和 MCP elicitation 必须回到正确 call id 和活跃 turn。
- thread settings 更新不能破坏当前 cwd、workspace roots、permission profile 和 sandbox 的一致性。
所以 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_turn 用 can_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.rs 的 TurnContext 是单个 turn 的运行快照。它携带:
- turn id、trace id、session source、parent thread、thread source。
- config、模型信息、provider、reasoning 配置、tool mode。
- resolved environments、cwd、shell、当前日期和时区。
- approval policy、permission profile、network proxy、Windows sandbox level、shell environment policy。
- dynamic tools、MCP/app 能力、skills load outcome、extension data。
- telemetry、turn timing、turn metadata state。
- developer instructions、compact prompt、user instructions、collaboration mode、personality。
它是执行层和安全层的交界面。模型请求工具时,工具拿到的不是一段裸字符串,而是包含权限、环境、模型配置、取消 token、diff tracker 和 call id 的 turn-scoped invocation。
这个设计的防御性很强:权限和环境随 turn 显式传递,工具可以统一依据 TurnContext 做 sandbox、approval、telemetry 和事件上报。
但长期成本也明显:TurnContext 容易变成“所有模块都需要”的大上下文。它降低传参成本,同时弱化模块边界。新增字段前应该先问:这是 turn 的不变量,还是某个工具、入口、扩展服务自己的局部依赖?如果只是局部依赖,继续塞进 TurnContext 会增加隐性耦合。
上下文与压缩
codex-rs/core/src/context_manager/history.rs 的 ContextManager 管理模型历史。它不是简单 Vec<Message>,而是包含:
items: Vec<ResponseItem>:按时间排列的历史项。history_version:历史被 compact、rollback、replace 等重写时递增。token_info:模型返回或估算的 token 使用信息。reference_context_item:用于配置和环境上下文 diff 的 baseline。
ContextManager::record_items 只记录 API message,并按截断策略处理工具输出。for_prompt 在发送模型前会规范化历史,丢弃不适合的项,并根据模型 input modalities 过滤图片:如果模型不支持图片,消息和工具输出里的图片会被剥离。
这说明上下文层有三个职责:
- 保存 canonical 模型历史。
- 把历史转换为当前模型可接受的 prompt 输入。
- 控制 token 成本、图片模态和长会话恢复边界。
compaction 也不能理解成“摘要功能”。它是长会话成本控制、缓存稳定性和恢复边界的一部分。run_turn 在采样前和采样后都会检查是否需要 compact;mid-turn auto compact 会把 continuation 放在 pending input 之前,以免压缩打断正在进行的模型-工具链路。
维护上下文注入时有一个硬约束:任何进入模型上下文的新片段都必须有明确结构、大小上限和恢复语义。无界文本、无界工具输出、无界列表都会破坏长会话成本和模型缓存,也会让 thread resume 变得不可预测。
工具路由与执行
Codex 的工具层不是把 JSON 反序列化后直接调用函数。它是副作用边界。
codex-rs/core/src/tools/router.rs 的 ToolRouter 做两件事:
- 维护本 turn 对模型可见的 tool specs。
- 把模型输出的
ResponseItem转为统一ToolCall。
ToolRouter::build_tool_call 目前会把三类模型输出收敛成同一形态:
ResponseItem::FunctionCall->ToolPayload::FunctionResponseItem::CustomToolCall->ToolPayload::Custom- client 执行的
ResponseItem::ToolSearchCall->ToolPayload::ToolSearch
统一后的 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 定义了 CoreToolRuntime 和 ToolRegistry。CoreToolRuntime 是本地工具的 typed runtime contract:工具处理器不仅要实现执行,还可以提供 exposure、并行能力、tool search 信息、参数 diff consumer、pre/post tool hook payload、telemetry tags 和取消语义。
ToolRegistry::dispatch_any_with_terminal_outcome 是工具执行生命周期的中心。它会:
- 增加 active turn 的 tool call 计数。
- 查找工具并校验 payload kind。
- 发出 tool start 生命周期事件。
- 运行 pre-tool-use hook,允许阻断或改写输入。
- 执行工具 handler。
- 记录 telemetry、sandbox tag、成功状态和输出 preview。
- 运行 post-tool-use hook,允许追加上下文或替换模型可见输出。
- 发出 tool finish 生命周期事件。
- 把
ToolOutput转为模型可见ResponseInputItem。
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 里直接做副作用,就是架构漏洞:协议可能看得到动作,安全和审计层却看不到完整生命周期。
工具与安全层的维护风险
工具安全不是单个开关,而是多个策略叠加:
- approval policy 决定是否需要用户审批。
- permission profile 描述文件系统和网络权限。
- sandbox policy 把权限投影到平台沙盒。
- shell environment policy 控制执行环境。
- guardian review 处理高风险动作。
- hook runtime 在工具前后提供阻断、改写和补充上下文能力。
- lifecycle event 和 telemetry 让副作用可观察、可审计。
这个多层模型的优点是主动防御:模型生成工具调用并不等于工具能执行,工具执行成功也不等于原始输出会原样回到模型。
漏洞是边界横跨模块。文件权限、网络权限、Windows sandbox、MCP server 能力、动态工具授权、插件暴露策略、工具搜索、hook 改写都可能影响最终执行。维护时要避免在某个局部模块“顺手执行”副作用。正确做法是把新能力注册成工具,让 ToolRouter、ToolRegistry、CoreToolRuntime 和生命周期事件接管执行链。
状态与持久化
Codex 的持久化边界在 codex-rs/thread-store/src/store.rs 的 ThreadStore trait。它是 storage-neutral boundary,负责:
create_threadresume_threadappend_itemspersist_threadflush_threadshutdown_threaddiscard_threadload_historyread_threadlist_threadssearch_threadslist_turnslist_itemsupdate_thread_metadataarchive_threadunarchive_thread
这里的关键设计是: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 维护。
状态不只是聊天记录。它至少包括:
- thread metadata。
- rollout history。
- turn lifecycle。
- active turn snapshot。
- token usage。
- thread settings。
- fork/subagent 关系。
- interrupted turn marker。
- thread goal。
- session source 和 client metadata。
最难的是 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.rs 的 MessageProcessor::new 是组合根。它装配:
- process-scoped thread store。
ThreadManager。- app-server extension event sink。
- thread state manager。
- skills watcher。
- plugin startup tasks。
- goal service。
- auth refresh bridge。
- environment manager、config manager、feedback、analytics、state db。
- 一组 request processor。
这些 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 方法映射到共享运行时和周边服务。ThreadRequestProcessor、TurnRequestProcessor 这类处理器可以触发 Agent 行为,但不应该复制 Session 或 run_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
这些文件的职责分别是:
protocol.rs:内部协议总线,定义Op、Event、EventMsg。thread_manager.rs:线程创建、恢复、fork、提交和持久化入口。codex_thread.rs:线程句柄,封装对 session 的提交和查询。session/session.rs:单线程会话状态机。session/turn.rs:run_turn模型-工具循环。session/turn_context.rs:turn-scoped 配置、环境、权限和模型快照。context_manager/history.rs:模型历史、token 信息、history version、reference context 和 prompt 规范化。tools/router.rs:模型工具输出到统一ToolCall的路由。tools/registry.rs:工具 runtime、hook、telemetry、生命周期和模型可见输出。tools/context.rs:ToolInvocation、ToolOutput和工具 payload/output 抽象。thread-store/src/store.rs:存储中立的 thread 持久化边界。app-server/message_processor.rs:JSON-RPC 组合根。
读码时不要从具体 shell 工具或 TUI widget 开始。先把协议、thread、session、turn、context、tool runtime 这条主干建起来,再看入口和展示层。
设计评价
优点:
- 运行单位正确:以 thread 管生命周期,以 turn 管事务,而不是以 HTTP 请求或一次 CLI 输入建模。
- 协议边界明确:
Op和EventMsg让多入口、多前端复用同一套运行语义。 run_turn把模型采样、pending input、工具结果、压缩和 hook 收束在一个事务循环里。- 上下文管理有版本、token、reference context 和 prompt 规范化,能支撑长会话和恢复。
- 工具 runtime 统一了副作用执行、hook、telemetry、生命周期事件和模型回填。
- ThreadStore 保持存储中立,把 metadata 策略留在 store 之上。
- app-server 复用 core 运行时,没有另起一套 Agent 内核。
设计漏洞:
codex-core过重,新能力天然想往 core 里塞,长期会增加编译、理解和变更成本。protocol.rs正在总线化,新增Op/EventMsg会扩大跨 crate、跨客户端的同步成本。TurnContext是高耦合上下文大包,字段增长会让模块边界变模糊。- 工具安全边界横跨 router、registry、handlers、sandbox、approval、hook、MCP、extension 和 app-server,局部修改容易漏掉全链路约束。
- app-server API 面扩张后,请求串行化、实验字段、TS schema、README 和测试 fixture 都会成为维护负担。
边界条件:
- mid-turn resume/fork/rollback 必须明确稳定历史边界。
- interrupt 既是用户可见控制流,也是模型可见历史修正。
- pending input 不能随意插入正在进行的模型-工具 continuation。
- compact 不能破坏工具调用和工具输出的配对关系。
- tool approval、dynamic tool response、MCP elicitation 必须绑定正确 call id。
- 配置热更新不能让 cwd、workspace roots、permission profile、sandbox policy 相互漂移。
长期维护成本:
- 新增协议项要同步 Rust、TypeScript、app-server schema、TUI 渲染、事件 projection 和测试。
- 新增工具要同时考虑权限、沙盒、审批、hook、telemetry、事件、持久化和模型回填。
- 新增上下文注入项必须有硬上限,否则会破坏 token 成本、缓存命中和恢复稳定性。
- 新增 app-server API 要避免把业务逻辑固化在 request processor 里。
- 新增 core 概念前要先判断是否真的属于
codex-core,还是应该放到更小 crate 或 extension/service 边界。
最终模型
Codex Rust 的架构模型不是“前端 + 后端 + 模型调用”,而是:
一个以 thread 为生命周期、
以 turn 为执行事务、
以 Op/EventMsg 为协议边界、
以 ContextManager 为模型历史边界、
以 ToolRegistry/CoreToolRuntime 为副作用出口、
以 ThreadStore 为持久化边界的事件驱动 Agent 运行时。
理解这一点后,维护 Codex 的判断标准就很明确:
- 入口只做协议适配,不复制 Agent 状态机。
- 新行为先进入
Op/EventMsg或既有协议语义,再进入 session/turn。 - 模型可见内容必须经过 context manager,并有大小和恢复边界。
- 外部副作用必须经过 tool runtime。
- thread history 是 canonical state,不是展示日志。
- app-server API 扩张必须同步序列化、实验 gating、schema、TS 类型和文档。
只要这些边界保持清晰,Codex 可以继续扩展入口、工具、插件、MCP、app-server API 和多 Agent 能力;一旦这些边界被绕过,系统会很快退化成一组难以恢复、难以审计、难以测试的异步副作用。