先从一个很普通的场景开始:你在仓库里让 Codex 修一个 bug,必要时跑测试。 屏幕上看起来只是输入一句话,后面却会发生一串工程动作:读取仓库规则、选择上下文、 调用模型、执行命令、修改文件、处理测试结果、必要时请求审批,并把进度持续反馈给客户端。

所以第一篇不从文件树开始,也不要求你先记住所有 Rust 模块。 它只做一件事:把这条请求拆成几道关口。每过一道关口,我们问同一个问题: 现在谁接住了请求,下一步允许发生什么,什么会被记录下来?

读完这篇,你不需要背下每个文件名,但应该能复述这条主线: 用户意图先变成结构化任务,任务进入会话和 turn,turn 整理给模型看的材料, 工具请求经过权限关口,运行过程再变成事件和持久记录。

OpenAI 官方的 《深入解析 Codex 智能体循环》 提醒我们先把 Codex 当成一条会反复推进的 agent loop 来看:用户意图进入系统, 模型决定下一步,工具和环境把结果带回来,下一次模型调用再接着前进。本文把这条循环落到公开源码里, 看每一步由哪个结构接手、留下什么记录。

证据边界。 本文把官方文档当作产品层契约,把 openai/codex 的公开源码当作机制证据。 源码链接固定到同一个公开快照。文中会用一些工程归纳,例如“运行主线”“模型投影”“客户端投影”“权限关口”, 它们只描述公开源码里能看到的类型、函数和边界,不推断 OpenAI 私有服务内部实现。

这篇只追五个问题:

  1. 一句用户输入离开界面后,变成了什么结构化任务?
  2. 会话和 turn 分别负责哪部分运行状态?
  3. 模型真正看到的是哪份材料,为什么要先讲清上下文边界?
  4. 模型提出工具请求后,哪些关口决定能不能执行?
  5. 客户端看到的进度和恢复时使用的记录,为什么要分开看?

一、先把问题说小:一条请求会经过哪些关口

Codex 仓库很大,入口也很多。你可以从 CLI 读到命令解析,从 TUI 读到状态渲染, 从 app-server 读到多客户端协议,从 tools 读到 shell 和 patch,从 sandbox 读到隔离策略。 这些都重要,但如果一上来逐个目录展开,读者很快会失去主线。

总览篇的读法是:把“修一个 bug”这条请求当作线索。每一步只看一个交接动作, 先理解产品层发生了什么,再看源码里由什么结构承载。

关口 用户看到的事 源码里要问的问题 本文先给出的答案
入口 在某个界面里输入任务。 这句话怎样离开界面? 入口标记来源并提交任务,后面进入共享协议。
协议 任务开始排队。 排队的东西是文本,还是带类型的数据? Submission 包住 Op,把意图变成任务单。
会话 / turn Codex 开始处理。 谁负责本轮上下文、取消、模型流和工具循环? Session 管会话,run_turn 管一次工作单元。
上下文 / 模型 模型开始回答或请求工具。 模型看到的是完整历史,还是整理后的材料? 上下文管理把历史和环境整理成本轮模型视图。
工具权限 Codex 读文件、跑命令、改代码。 模型请求行动后,谁决定能不能执行? ToolRouter 分发请求,ToolOrchestrator 处理审批、sandbox 和执行。
事件 / 持久记录 界面显示进度和结果。 屏幕显示和恢复记录是不是同一层? Event 供客户端观察,RolloutItem 留下可恢复记录。

沿着这些关口继续深挖时,最先要弄清的是模型输入边界。只有知道本轮模型到底看到了什么, 工具、权限、事件和恢复才有可对齐的参照。

二、入口层:把一句话交给 protocol

Codex 可以从不同产品面进入:CLI、TUI、app-server、MCP、subagent 等。 源码里有一个很有用的信号: SessionSourceCliVSCodeExecMcpSubAgent 等来源放进同一个枚举。它提醒我们:入口会影响来源标签和交互方式,但请求离开入口后要进入共享协议层。

2.1 Submission 是任务单,Op 是任务类型

进入 protocol 层后,用户输入不再只是一段文本。 Submission 像一张任务单:有 id,有真正要做的 op,还可以带客户端消息 id 和 trace。 Op 则说明任务类型:用户输入、thread settings、approval、interrupt、realtime conversation、tool refresh 都是不同的 operation。

形状示意:
  用户说:"修这个 bug,并跑对应测试"

  Submission {
    id: "...",
    op: Op::UserInput(...),
    metadata: 客户端消息 / trace
  }

这段只是形状示意,不是源码复刻。它要表达的是:请求开始进入系统时,已经有了任务身份和类型。 从这里开始,源码阅读就不再追“哪一句话发给模型”,而是追“哪一种 operation 被提交给运行时”。

三、会话层:Codex 门面接住任务

任务进入 core 后,会落到高层 Codex 门面。这里的门面可以理解成对外入口:外部不直接碰 session 内部状态,而是通过提交任务和读取事件来交互。 这个结构持有 tx_subrx_event、agent status 和 session。 submit() 给 operation 生成 submission id 并送入队列; next_event() 从事件队列取回运行时输出。

这里可以记住第一条稳定规则:客户端提交 operation,运行时返回 event。 某个界面怎样渲染进度,可以变化;operation / event 这对交接方式才是后续阅读的主线。

3.1 turn 是一次可推进、可取消、可记录的工作单元

进入 session 之后,请求会进入 turn。这里的 turn 不要理解成聊天软件里的一问一答, 更接近“一次有边界的工作单元”:它可以整理上下文、调用模型、处理工具请求、等待审批、 接收补充输入、被取消,也可以把中间过程发成事件。 run_turn 的注释把主循环说得很朴素:每次 sampling request 里,模型要么返回 function calls,要么返回 assistant message; 如果是 function call,运行时执行工具并把 output 放进下一次 sampling;如果只是 assistant message, 就记录历史并结束本轮。

这个循环真正难的地方不在“调用模型”四个字,而在调用前后要准备和收尾的东西: compact、上下文更新、skills/plugins 注入、session hooks、用户输入记录、模型请求构造、工具输出回灌、 事件发送和完成状态。这些名词后文会拆,这里只要抓住一件事: 它们都发生在模型采样前后,决定本轮工作单元能不能继续前进。

一条请求,简化后:
  提交有类型的 Op
  -> session 接收 Submission
  -> run_turn 接管这次工作单元
  -> 更新上下文 / skills / hooks
  -> 把历史整理成模型输入
  -> 接收模型流式输出
  -> 工具请求经过权限关口再执行
  -> 发出事件并写入持久记录

四、turn 层:先整理材料,再问模型

turn 开始后,模型不能直接看到整个世界。运行时要先决定本轮模型请求需要哪些材料。 TurnContext 就是这类本轮材料的结构化快照:模型、cwd、权限、approval policy、sandbox、技能上下文、输出 schema、 环境信息、网络策略和 session source 都在这里出现。

build_prompt() 把投影后的 input、router.model_visible_specs()、parallel tool call 能力、base instructions、 personality 和 output schema 合成 Prompt。这里先把它叫作“模型投影”: 运行时从保存下来的历史和当前环境里,挑出、整理出一份模型本轮可见的视图。 第二篇会专门拆 ContextManagerfor_prompt() 和 compact,这里先建立入口: 模型看到的是运行时整理过的材料,不是 UI 上所有文本的简单拼接。

五、工具层:模型提出动作,权限关口决定执行

现在看最容易误读的一步:模型要求 Codex 执行命令或修改文件。 源码里要分成两层看。第一层是“能力菜单”:运行时告诉模型本轮有哪些工具可以被请求。 这些工具 schema 来自 built_tools()ToolRouter: MCP tools、plugins、apps、dynamic tools 和 extension executors 会被合并成一套 turn-scoped router。 当模型真的返回 tool call 时, router dispatch 才把调用交给注册的工具执行器。

第二层是“能不能执行”。这一步由 ToolOrchestrator 文件头 直接把顺序写出来:approval、select sandbox、attempt、sandbox denial 后升级重试。 在具体实现里, orchestrator 会先根据 approval policy、filesystem sandbox policy 和工具自己的 requirement 做审批判断, 再选择初始 sandbox,并把 workspace roots、网络策略和平台 sandbox 参数一起带到 attempt。

用户可以说“Codex 改了文件”,源码阅读时要拆细一点: 模型提出 tool call,运行时通过 router、approval、hooks、sandbox 和具体 handler 后, 才可能产生真实副作用。

六、输出层:事件给客户端,rollout 留下恢复线索

运行时推进时,会不断发事件。 EventEventMsg 把输出也类型化:warning、context compacted、thread rolled back、turn started、turn complete、 agent message delta、tool call、token count、hook lifecycle 等都不是一段无结构日志。 客户端可以把这些事件渲染成聊天气泡、进度条、diff、审批弹窗或状态文本。

比如同一条 shell 命令,事件可以让界面实时显示“命令开始、输出增量、命令结束”; 之后写入 rollout 的材料,则让刷新或恢复时还能重建这件事发生过。前者服务当前观察, 后者服务后续恢复。

这里再引入另一种 projection:客户端投影。 它指同一份运行时事实,在不同客户端上可以显示成不同样子。TUI 关心终端里的增量渲染, app-server 关心 JSON-RPC item,未来别的客户端还可以有自己的视图。显示方式可以不同, 但它们都应该消费运行时发出的结构化事实。

恢复和审计还需要更耐久的记录: RolloutItem 会保存 session metadata、response items、compacted records、turn context 和 event messages。 所以后面讲 resume、rollback、compact 时,不能只看屏幕上还显示什么。 屏幕是当前视图,rollout 才是恢复时要回放的一层记录。

七、沿这条路线继续读

总览篇只负责建立路线。后面的文章会沿同一条请求路径,一次只拆一个边界。 这样读起来不会变成源码百科,也不会把所有概念一次性塞满。你可以把它当作一张阅读地图: 每一站都回到“谁接手、谁记录、谁展示”这三个问题。

阅读层 核心问题 主线机制
总览 一次用户请求怎样穿过入口、protocol、turn、工具关口和持久记录? SubmissionCodexrun_turnToolOrchestrator
上下文管理 每次问模型前,运行时怎样整理、压缩和恢复上下文? ContextManagerTurnContextfor_prompt()、compaction。
protocol 与事件流 客户端和运行时如何用有类型的 request / event 维持同一套事实? OpEventMsg、app-server protocol、generated schema。
工具系统 工具 schema 如何进入模型,tool call 又怎样被分发、并行和归档? ToolRouter、registry、MCP、dynamic tools、extension tools。
权限与 sandbox 模型请求副作用时,审批、hooks、sandbox、exec policy 分别拦在哪里? ToolOrchestrator、exec policy、hooks、sandboxing、apply_patch。
客户端投影 TUI、app-server 和持久记录如何展示同一套运行时证据? 事件映射、turn items、rollout、thread status。
扩展与多 agent skills、plugins、MCP、subagents 怎样作为本轮运行时输入继续进入同一条主线? skills manager、plugins manager、MCP exposure、tool_search、AgentControl。

八、读完第一篇,用这张检查表复盘

看到的现象 源码里重新问 这篇给出的抓手
用户在某个界面输入任务。 这个入口把任务交给了哪种 operation? Submission / Op
Codex 开始处理任务。 谁拥有本轮上下文、取消和完成条件? Session / run_turn
模型产生回复或工具请求。 它看到的是哪份整理后的材料? TurnContext / build_prompt()
Codex 运行命令或改文件。 工具请求经过哪些权限和 sandbox 关口? ToolRouter / ToolOrchestrator
客户端显示进度和结果。 哪些是客户端视图,哪些是恢复记录? EventMsg / RolloutItem

这就是第一篇要建立的阅读习惯:不要急着问“哪个目录最核心”,先问请求现在在哪一层, 这一层把什么结构交给下一层。下一篇进入上下文管理,因为一旦你知道请求会进入 turn, 下一个自然问题就是:turn 每次问模型前,到底把哪些材料放进模型视图?

参考源码与文档