前九章已经把 Codex 拆了两遍。第一遍沿生产系统向下,追踪 Thread、Turn、Step、Prompt、模型传输、 工具、安全、多 Agent、扩展和持久化;第二遍在 Python Mini Codex 中重新组装关键合同,并用故障注入 检查取消、配对、部分写、进程清理和热更新。
现在要回答的不是“Codex 有哪些模块”,而是更难的三个问题:哪些复杂度来自任何工具型 Agent 都会 遇到的并发和故障;哪些复杂度只在 Codex 的产品规模下成立;如果从零搭建另一个 Runtime,应该按 什么顺序引入这些设计?
本章每条判断都回指前九章的源码和测试。源码能证明当前实现怎样工作,测试能证明特定行为合同, Mini Codex 能证明这些合同可以跨语言表达。三者都不能替代对目标系统需求的判断。
一张图先分开“合同”和“产品层”
中心的合同数量并不多:输入和事件有明确类型;状态有唯一所有者;一次请求使用一致快照;模型可见 ToolSpec 与执行 Handler 同源;ToolCall 与 ToolResult 按 call_id 配对;副作用前完成校验和授权; Queue、进程和 Agent 都有上限与终态;持久恢复从规范记录开始,而不是从 UI 日志猜测。
这些合同在只有一个模型和两个工具时也有意义,因为它们处理的不是产品规模,而是时间:调用发生前 知道什么,等待期间谁还能修改状态,失败后已经留下什么,下一次采样又应看见什么。
外圈的产品层则由明确需求触发。没有第二个客户端,不需要 App Server 版本投影;没有动态第三方工具, 不需要 MCP 目录刷新;不执行不可信进程,未必需要复制三平台沙箱矩阵;没有分页和搜索,不应先引入 SQLite 投影;没有可恢复子 Agent,不需要 Residency 和 AgentGraph。
事件驱动不是风格选择
一个同步函数可以完成“发送 Prompt,得到文本”。它无法在模型仍流式输出时接收 Interrupt,也无法 同时等待审批回答和后台进程,更无法让慢客户端对事件生产者施加反压。
Codex 的 Op → Session → EventMsg 结构把控制输入和观察输出拆开。Session 是活动 Turn 的所有者,
Turn 又负责让 Completed、Aborted 和 Failed 收敛为唯一终态。Delta 可以低延迟送到界面,但只有完整
ResponseItem 才进入模型历史和 Rollout。
值得迁移的不是 EventMsg 的全部 variant,而是以下条件:
活动任务期间仍需接收控制输入
→ 输入分派与任务执行必须解耦
→ 增量观察与规范历史必须分层
→ 取消在状态所有者处收敛
→ 每个 Turn 只产生一个终态
若目标程序完全同步、没有流式输出、审批和后台资源,直接返回值比事件总线更清楚。一旦上述任一
条件出现,继续把所有事情塞进一个 run() 通常只是把状态机藏在异常和布尔变量中。
快照解决的是时间一致性
TurnContext 与 StepContext 不是面向对象层级,而是两种稳定期。TurnContext 固定本次用户请求的模型、 cwd、权限基线和取消范围;StepContext 在每次模型采样前重新捕获工具计划、MCP binding 和 World State。
工具修改文件后,下一 Step 应看见新 World State。MCP 目录刷新后,下一 Step 可以暴露新工具。但一个 已经由旧 Schema 产生的 ToolCall 必须继续交给旧 Step 的 Router;否则热更新会改变过去请求的含义。
因此快照原则可以写成一句更精确的话:旧请求保持自洽,新请求看见新世界。它也解释了为什么 stream retry 不应该偷偷重建工具面,为什么 Handler 不应从全局变量读取 cwd 和权限。
Prompt 是编译产物,不是字符串
模型请求同时包含不同稳定期的内容:基础指令通常稳定,AGENTS 指导受目录作用域约束,History 按 Thread 增长,World State 按 Step 变化,ToolSpec 又必须与当前 Router 同快照。Compact 还会用一个 replacement checkpoint 替换旧前缀。
一个可靠的 PromptCompiler 先生成确定的语义快照,再让 HTTP/SSE 或 WebSocket Transport 选择完整或
增量编码。previous_response、prompt cache key 和稳定序列化都属于传输与性能层,不能决定模型应看
什么。权限、工具或历史真的变化时,允许 cache miss;错误复用旧语义比缓存失效严重得多。
这条设计即使没有 Provider Prompt Cache 也有收益:测试快照更稳定,录制回放可以校验 request fingerprint,故障诊断能比较具体输入层,而不是面对每次顺序都不同的大字符串。
ToolSpec 和 Handler 是一份计划的两面
模型看见 JSON Schema,Runtime 执行 Handler。只按名称在全局 Registry 中查找是不够的,因为工具 可能来自 Feature、MCP、Plugin、Extension 或远程环境,而且目录能在运行时刷新。
Codex 的 ToolPlan 同时构造 model_visible_specs 和 Registry,并把 Router 放进 StepContext。Hosted
工具可能只有模型面,Deferred 工具可能暂不显示;真正的不变量不是两个集合相等,而是每个需要本地
执行的模型调用都能在同一 generation 中找到唯一兼容实现。
ToolCall 被接受后,生命周期继续经过参数校验、Pre Hook、Policy、Approval、ExecutionEnvironment、 输出截断、Post Hook 和 ToolResult 提交。所有能阻止副作用的步骤都必须位于执行前。Post Hook 可以 记录或阻断后续流程,却不能回滚已经启动的进程、写入的文件或远端服务动作。
这里最容易被忽略的是 Result 写入失败:外部副作用可能已经发生,而历史还没有同 call_id Result。 恢复动作应先补足协议事实,不能因为“没有成功记录结果”就盲目重跑工具。
安全不是一张 allow/deny 表
应用 Policy 判断动作风险,Approval 记录用户是否授权,OS Sandbox/Network Proxy 强制真实资源边界。 三者不能互换:Policy 不限制 syscall;用户批准不自动缩小文件系统权限;Sandbox 不理解删除操作是否 符合用户意图。
最小安全链应先把模型参数解析成具体 argv、cwd、路径和网络需求,再用 Permission Profile 缩小请求, 由 Policy 决定是否可执行/可询问,Approval 只授权这个具体范围,最后由执行器物化为平台强制约束。
Workspace 路径检查可以阻止 Patch 使用 ../,却阻止不了 python -c 自行打开工作区外文件。Mini
Codex 因此把默认 Adapter 明确命名为无 OS isolation 的 Local Adapter。说清没有什么,比把应用检查
叫作沙箱更重要。
多份持久化状态为什么不是重复建设
Context History 回答下一次模型应看什么;Rollout 回答进程退出后怎样重放会话语义;ThreadStore 和 State DB 服务分页、搜索与元数据;AgentGraphStore 保存不能从聊天文本可靠推断的父子拓扑。
多权威的必要条件是每层回答不同问题,并且重建方向明确。规范 JSONL 已 Flush、SQLite 尚未更新时, 后者是可追赶投影;不能让派生索引反向覆盖 Rollout。Graph 写入失败时也不能从一条“已创建 Agent” 文本猜出完整拓扑。
这种分层的代价是真实的:短暂不一致、迁移、压缩引用保留、跨进程锁和更多故障点。小型 Runtime 可以从内存 History + JSONL 开始;只有分页、搜索、归档或可恢复 Agent 拓扑出现后再增加其他层。
无论存储多少层,Thread Rollback 都只替换有效会话历史。文件改动、已运行进程和远端操作不属于 对话日志事务,不能被一并撤销。
多 Agent 首先是一组资源约束
子 Agent 能把探索、测试和日志分析移出主上下文,也可以并行处理互不依赖的任务。每个子 Agent 同时 复制模型采样、工具资源、历史和恢复状态,因此消耗的总 token 通常更多。
Codex 将多 Agent 资源拆成注册容量、活动执行容量、已加载 Residency 和持久 AgentGraph。Agent
“已经创建”“正在运行”“当前驻留内存”和“可以恢复”是四种不同状态。一个 max_agents 计数器无法
表达这些约束,也无法安全处理并发 Spawn reservation。
值得迁移的是独立性门槛、有界 Mailbox、单一 final notification 和资源分层。写热点相同、依赖链 串行或结果无法压缩的任务,单 Agent 配合工具并发往往更合适。父 Agent 必须继续拥有需求、冲突合并 和最终验证,不能把三份未核对摘要直接拼成结论。
扩展能力必须保留信任边界
Skill 是工作流指令,MCP 是外部服务协议,Hook 是生命周期外部命令,Plugin 是安装和分发单元, Extension Contributor 是宿主进程内的强类型能力。它们可以组合,但运行位置和失败半径不能因为统一 “插件体验”而消失。
只需指导模型时用 Skill;需要认证服务和工具 Schema 时用 MCP;需要在已有生命周期点阻断/通知时 用 Hook;需要分发组合能力时用 Plugin;只有完全受信且必须贡献宿主对象的能力才做进程内 Extension。
不可信代码进入 Extension 可以崩溃或阻塞整个 Session。反过来,用 Hook 模拟原生 Tool 也缺少正确的 typed executor 与 ToolPlan 绑定。选择扩展面的第一问题是“代码在哪里运行、能做什么、失败由谁吸收”, 而不是 API 看起来是否统一。
Mini Codex 的测试边界
Mini Codex 当前有 27 项离线测试、1 项需要凭据才运行的 live smoke test,46 个 Python 源文件通过 mypy strict。它还提供 request fingerprint 录制回放和 20 Turn 单机微基准。
这些证据证明的是具体不变量:Interrupt 只产生一个 Aborted 终态;Writer 部分成功后只重试未确认 后缀;Registry 热更新不改变旧 ToolPlan;进程超时会 kill/reap;Mailbox 有反压。它们不证明完整 MCP、跨平台 OS sandbox、多 Agent Session、长时间压力或生产 SLA。
测试的正确读法是:
源码命题
→ 最小跨语言实现
→ 在提交点注入故障
→ 断言完整事件和残留状态
→ 结论只返回原命题
如果一段最小实现无法表达取消由谁拥有、写入何时确认或旧 ToolCall 使用哪个 Router,那么问题通常 不是 Python 不够像 Rust,而是研究还没有提炼出真正合同。
从零搭建时的建议顺序
先建立领域 Item、Operation/Event 和 Session owner,再增加 Cancellation 与 terminal-once;随后把 Prompt 做成纯编译,把 request/step 状态做成快照;再用同一 ToolPlan 生成 specs/handlers,建立完整 Call/Result 配对和副作用屏障。
只有需要执行不可信动作时加入 OS 隔离,需要跨进程恢复时加入规范日志,需要查询时加入派生索引, 需要动态外部能力时加入 MCP/Plugin,需要被测量证明的并行收益时加入多 Agent。
这个顺序没有要求复制 Codex 的 Rust 类型和 crate。换成 TypeScript、Go、actor model 或数据库队列, 只要以下问题仍有确定答案,合同就还在:谁拥有状态;变化何时可见;副作用何时发生;失败后剩什么; 恢复从哪份权威记录开始。
不照搬,也不能过度简化
Codex 的多 Surface、Transport、三平台沙箱、legacy Rollout、动态扩展和多 Agent 兼容矩阵,都有产品 需求背景。目标系统没有这些需求时,不应预先支付相同成本。
但“保持简单”不能删除核心正确性:Call/Result 配对不是产品装饰,ToolPlan 同快照不是大规模专属, 取消终态和副作用屏障也不能等出事故后再补。正确裁剪位于两种极端之间:不复制未被需求触发的产品 层,也不删除故障下维持协议自洽的 Runtime 合同。
第十章因此得到的不是一套标准架构图,而是一种审查方法。每增加一个层次,都要求写清触发需求、 状态所有者、协议变化、失败残留、迁移路径和可执行证据。回答不了其中任一项,就暂时不应该把这层 复杂度加入系统。
本章细节导航
- 10.1 Event-driven Runtime 的必要复杂度
- 10.2 Turn 稳定快照与 Step 动态快照
- 10.3 Prompt 编译和缓存友好设计
- 10.4 Tool Spec 与 Runtime 的同快照契约
- 10.5 工具生命周期与副作用控制
- 10.6 应用策略、人工审批和 OS 沙箱分层
- 10.7 多层持久化权威的收益与成本
- 10.8 多 Agent 的收益、容量和恢复成本
- 10.9 Hooks、MCP、Plugins 与 Extension 的边界
- 10.10 Mini Codex 故障测试证明了什么
- 10.11 可迁移到其他 Agent Runtime 的设计
- 10.12 不应机械照搬的 Codex 产品复杂度
评论
登录后即可评论