为什么需要一套运行时对象
如果 Codex 只调用一次模型,一个 request → response 就够了。但真实任务包含流式输出、工具子进程、审批等待、取消、恢复、压缩和多种界面。它需要一套稳定协议把“请求怎么进来”和“过程怎么出去”分开。
flowchart TB UI["CLI / TUI / App Server / SDK"] --> TH["CodexThread<br/>外部控制句柄"] TH --> SE["Session<br/>长期状态与提交循环"] SE --> TU["Turn<br/>一次用户任务"] TU --> ST1["Sampling step"] TU --> ST2["Tool step"] TU --> ST3["Follow-up sampling"] OP["Op<br/>提交 / 批准 / 取消 / 配置"] --> TH TH --> EV["Event stream<br/>消息 / exec / patch / completion"] EV --> UI
Thread:面向调用者的句柄
CodexThread 是调用方控制会话的主要通道。它可以提交 Op,也可以等待下一个 Event。这使 TUI、App Server 或其他宿主不必直接持有核心 Session 内部状态。
Session:长生命周期的状态容器
Session 创建时会解析配置、建立 Context、连接模型客户端和服务,并启动 submission_loop。它可以连续接受用户输入、审批响应、取消、压缩等操作。
长任务需要同时接受控制输入并持续产生异步状态,不能堵在一次函数返回上。
输入使用 Op 通道,输出使用 Event 通道;Session 持有跨 Turn 的上下文与服务。
协议与界面解耦后,TUI、无头 exec 和 App Server 可共享核心。
CodexThread 包装输入/输出通道;Session::spawn 启动 submission_loop。
事件消费者必须处理顺序、重复、取消和版本演进;内部状态并非都能从单个事件推断。
Op:外部对运行时说的话
协议中的 Op 是带 id 的操作。代表性类别包括:
- 用户输入;
- Shell、Patch 等批准或拒绝;
- 中断、关闭、压缩;
- 对某些运行配置或交互的响应。
Event:运行时对外说的话
Event 也带有 id,主体 EventMsg 覆盖:
- Turn 开始与完成;
- 用户/Agent 消息;
- 命令执行的开始、增量输出与结束;
- Patch 应用;
- 审批请求;
- MCP 工具;
- 错误、警告、token 使用等。
Turn 与 Step
Turn 是一次用户任务的业务边界。它可能包含多个执行步骤:
Turn
├─ 模型采样:决定搜索
├─ 工具执行:rg
├─ 模型采样:决定读文件
├─ 工具执行:read
├─ 模型采样:产生 patch
├─ 工具执行:apply_patch
├─ 工具执行:test
└─ 模型采样:最终总结
“Step”适合作为教学概念描述循环中的一次采样或动作;具体内部类型会随版本变化,读源码时应以冻结 commit 为准。
Rollout:可追溯的会话轨迹
仓库包含 rollout 相关模块,用于记录会话轨迹和元数据。它支撑恢复、调试或产品体验,但并不意味着每个运行模式都有完全相同的持久化保证。
源码可证实 codex-rs/core/src/rollout/ 和协议层的 rollout 类型可在本次冻结快照中确认。
一个界面如何消费事件
以命令执行为例,界面不必等命令结束才更新:
- 收到执行开始,创建一条命令卡片;
- 收到 stdout 增量,追加显示;
- 收到审批请求,展示决策控件;
- 收到执行完成,标记退出码;
- 收到 Turn 完成,收束整段任务。
这正是为什么协议需要细粒度 Event,而不是最后只返回一大段文本。
运行时边界的价值
| 关注点 | 放在哪里 |
|---|---|
| 模型决策 | sampling request / response |
| 长期上下文 | Session / ContextManager |
| 外部控制 | Op |
| 可观察过程 | Event |
| 真实副作用 | Tool handler + sandbox |
| 交互呈现 | CLI / TUI / App Server 客户端 |
下一章沿着真实函数名,把这张对象地图串成 端到端调用链。