先从一个很普通的场景开始:你在仓库里让 Codex 修一个 bug,必要时跑测试。 屏幕上看起来只是输入一句话,后面却会发生一串工程动作:读取仓库规则、选择上下文、 调用模型、执行命令、修改文件、处理测试结果、必要时请求审批,并把进度持续反馈给客户端。
所以第一篇不从文件树开始,也不要求你先记住所有 Rust 模块。 它只做一件事:把这条请求拆成几道关口。每过一道关口,我们问同一个问题: 现在谁接住了请求,下一步允许发生什么,什么会被记录下来?
读完这篇,你不需要背下每个文件名,但应该能复述这条主线: 用户意图先变成结构化任务,任务进入会话和 turn,turn 整理给模型看的材料, 工具请求经过权限关口,运行过程再变成事件和持久记录。
OpenAI 官方的 《深入解析 Codex 智能体循环》 提醒我们先把 Codex 当成一条会反复推进的 agent loop 来看:用户意图进入系统, 模型决定下一步,工具和环境把结果带回来,下一次模型调用再接着前进。本文把这条循环落到公开源码里, 看每一步由哪个结构接手、留下什么记录。
证据边界。 本文把官方文档当作产品层契约,把 openai/codex 的公开源码当作机制证据。 源码链接固定到同一个公开快照。文中会用一些工程归纳,例如“运行主线”“模型投影”“客户端投影”“权限关口”, 它们只描述公开源码里能看到的类型、函数和边界,不推断 OpenAI 私有服务内部实现。
这篇只追五个问题:
- 一句用户输入离开界面后,变成了什么结构化任务?
- 会话和 turn 分别负责哪部分运行状态?
- 模型真正看到的是哪份材料,为什么要先讲清上下文边界?
- 模型提出工具请求后,哪些关口决定能不能执行?
- 客户端看到的进度和恢复时使用的记录,为什么要分开看?
一、先把问题说小:一条请求会经过哪些关口
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 等。
源码里有一个很有用的信号:
SessionSource
把 Cli、VSCode、Exec、Mcp、SubAgent
等来源放进同一个枚举。它提醒我们:入口会影响来源标签和交互方式,但请求离开入口后要进入共享协议层。
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_sub、rx_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。这里先把它叫作“模型投影”:
运行时从保存下来的历史和当前环境里,挑出、整理出一份模型本轮可见的视图。
第二篇会专门拆 ContextManager、for_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 留下恢复线索
运行时推进时,会不断发事件。
Event 和 EventMsg
把输出也类型化: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、工具关口和持久记录? | Submission、Codex、run_turn、ToolOrchestrator。 |
| 上下文管理 | 每次问模型前,运行时怎样整理、压缩和恢复上下文? | ContextManager、TurnContext、for_prompt()、compaction。 |
| protocol 与事件流 | 客户端和运行时如何用有类型的 request / event 维持同一套事实? | Op、EventMsg、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 每次问模型前,到底把哪些材料放进模型视图?