雨天小六

读懂 Codex(二):一句“帮我修好它”是怎样被执行的

· 更新于 2026-07-30 · 专栏:读懂 Codex

#Codex#Agent Runtime#App Server#软件架构

用户在 Codex 终端中输入:

资料页点击保存没有反应,请修一下。

界面很快开始显示状态:任务已经启动,Codex 正在搜索代码,随后运行命令、修改文件并汇报测试结果。用户看到的是一条连续的执行记录,内部却没有一个从头运行到尾的“修复函数”。这项任务会经过客户端协议、线程状态、输入队列、模型流、工具执行和事件输出等多个边界。

本章沿这条请求追踪一次完整执行。重点是每一层接收什么、交出什么,以及模型调用和工具调用为什么会在同一项任务中反复出现。

客户端先建立 Thread

Codex 有多种交互入口。终端界面适合持续对话,codex exec 适合脚本和 CI,编辑器或其他富客户端可以通过 App Server 接入。App Server 是 Codex 为富客户端提供的双向协议服务。各类客户端负责收集输入和展示结果,不负责运行 Agent 循环。

在当前开源实现中,终端界面和 codex exec 都使用 App Server Client。这个客户端既可以连接进程内的 App Server,也可以连接远程 App Server。对上层界面来说,两种连接方式使用同一组请求和事件。

客户端开始工作前要取得一个 Thread。Thread 是用户与 Codex 之间的一段可持续会话,其中可以包含多次请求及其执行记录。新任务可以调用 thread/start 创建 Thread;继续旧任务时,可以使用 thread/resume 恢复已有 Thread。

Thread 除了在界面中组织消息,还确定后续输入追加到哪段历史,并为模型配置、工作目录、工具状态和持久化记录提供共同的生命周期。用户第二次说“把刚才的测试也补上”时,Codex 必须知道“刚才”属于哪段执行历史。

Core 是承载 Agent Runtime 的核心层。其中的 ThreadManager 负责创建、恢复和查找 Thread。每个正在运行的 Thread 都有一个长期存在的 Session。Session 持有对话历史、当前任务、配置快照和多种共享服务。客户端不会直接操作 Session,而是通过 CodexThread 提交操作并接收事件。

这几层可以先压缩成下面的关系:

Codex 客户端、App Server 与 Core Runtime 的组件边界
图 2-1:客户端只处理请求与展示;App Server 映射公开协议;Core Runtime 持有 Thread、任务循环、模型与工具状态。实线表示主要调用或数据流,虚线表示事件返回。移动端可横向滑动,点击可查看 SVG 原图。

App Server 位于客户端协议和 Core Runtime 之间。它校验公开请求,把请求转换为 Core 能处理的操作,再把内部事件投影成客户端可以稳定消费的通知。这一层使客户端不必依赖 Runtime 内部的 Rust 类型。

turn/start 把用户请求送入 Core

Thread 准备好以后,客户端用 turn/start 提交当前请求。按照 App Server 的公开协议,一次请求可以写成:

{
  "method": "turn/start",
  "id": 30,
  "params": {
    "threadId": "thr_123",
    "input": [
      {
        "type": "text",
        "text": "资料页点击保存没有反应,请修一下。"
      }
    ],
    "cwd": "/workspace/project",
    "approvalPolicy": "unlessTrusted",
    "sandboxPolicy": {
      "type": "workspaceWrite",
      "writableRoots": ["/workspace/project"],
      "networkAccess": false
    }
  }
}

请求中除了用户文字,还可以携带工作目录、模型、推理强度、审批策略和沙箱策略等覆盖项。App Server 先确认 Thread 存在,再检查输入大小和这些设置是否合法。校验通过后,它把公开输入转换为 Core 输入项,并构造 Op::UserInput

Op 是 Operation 的缩写,表示客户端希望 Runtime 执行的一种操作。用户输入只是其中一种。中断、关闭、审批答复、回滚和上下文压缩也会以不同的 Op 进入同一条控制通道。

Core 不直接把 Op::UserInput 传给模型。CodexThread 先把它包装成 Submission。Submission 包含一个唯一 ID 和具体操作,随后进入 Session 的有界输入队列。这个 ID 也会成为当前 Turn 的 ID。

Turn 表示一次用户请求以及 Codex 为完成该请求开展的全部工作。一次 Turn 可以包含多次模型调用、多次工具调用和大量流式事件。App Server 在操作成功入队后立即返回一个状态为 inProgress 的 Turn,不会等待修复任务结束后才响应 turn/start

输入队列提供了两个保证。第一,来自客户端的操作有明确顺序。用户输入、审批结果和中断信号不会以任意顺序同时修改 Session。第二,队列容量有限,生产速度超过 Runtime 的接收能力时会形成背压,而不是无限积累输入。

队列按顺序分派操作,不代表整个 Agent 只能串行工作。分派循环接到用户输入后会启动异步 Task,随后继续处理新的控制操作。模型响应可以流式到达,工具可以在满足约束时并行执行,中断和审批答复也能在任务运行期间进入 Session。

Session 为 Turn 启动一个 Task

Session 的提交循环收到 Op::UserInput 后,先检查当前是否已有活跃 Turn。空闲状态下,它为新请求建立 TurnContext,并启动 RegularTask。若已有任务正在运行,新输入也可能被用来调整当前 Turn;这条分支将在下一章讨论。

TurnContext 是本次 Turn 的主要运行配置。它包含模型、工作目录、权限策略和请求标识等信息。这里需要强调“主要”两个字:部分环境和工具状态可以在同一 Turn 的后续模型调用之间刷新,不能把整个执行过程理解成一份永远不变的配置。

Task 是 Session 中的一种执行策略。普通用户请求使用 RegularTask,代码审查、上下文压缩等工作可以使用其他 Task。Task 启动时,Session 会登记活跃 Turn,创建取消令牌,并把实际工作放进异步任务。

RegularTask 首先发出 TurnStarted 事件,然后进入 run_turn。取消令牌会沿调用链传给模型流和工具运行时。用户触发中断后,各层在可取消的等待点停止工作,Session 再清理活跃状态并发出中止事件。这种方式属于协作式取消:执行组件必须显式观察取消信号,Runtime 不能假定所有外部操作都能瞬间终止。

到这里,用户输入完成了从产品协议到运行时任务的转换:

turn/start

App Server 校验并映射输入

Op::UserInput

Submission 进入 Session 输入队列

创建 TurnContext 和 RegularTask

发出 TurnStarted,进入 run_turn

接下来才会发生第一次模型调用。

一次 Turn 包含多个 Step

run_turn 负责维持模型与工具之间的反馈循环。为了说明循环中的时间边界,本书把一次模型采样及其输出处理称为一个 Step。这里的“采样”是一次完整模型请求,不是生成单个 token 的采样动作。

进入第一个 Step 前,Runtime 会处理必要的上下文压缩,记录用户输入,加载本轮涉及的技能、插件和扩展信息,并建立 ModelClientSession。ModelClientSession 是当前 Turn 内复用的模型传输会话。后续 Step 可以复用连接和路由状态,不需要把每次模型请求都当作互不相关的网络调用。

每次采样前,Session 都会捕获一份 StepContext。它表示本次模型请求实际看到的执行快照,其中包括当前环境、可用工具以及由这些工具构造出的路由器。Runtime 使用同一份 StepContext 完成三项工作:

  1. 生成当前环境的 World State,也就是模型需要了解的工作目录、权限和其他运行状态。
  2. 生成模型可见的工具规格,让模型知道工具名称、参数和用途。
  3. 构造 ToolRouter,使模型随后发出的工具调用能够落到与这些规格对应的执行器。

工具规格和工具路由必须来自同一份快照。假设模型看到一个名为 run_command 的工具,但执行时 Runtime 已经换成另一套路由配置,模型生成的合法参数也可能无法执行。StepContext 把“模型看见什么”和“Runtime 能执行什么”绑定在同一个 Step 内。

准备完成后,Runtime 从对话历史生成模型输入,加入基础指令、当前 World State 和工具规格,再通过 ModelClientSession 发起流式请求。模型返回的内容不是只有最终文字,还可能包含推理摘要、普通消息、工具调用和用量信息等结构化响应项。

工具结果触发下一次采样

第一次模型采样时,模型还没有读取项目文件。对于“保存没有反应”这类请求,合理输出通常是一个工具调用,例如搜索页面组件。Runtime 收到完整工具调用后解析参数,通过 ToolRouter 找到执行器,再由 ToolCallRuntime 管理并行和取消边界。run_turn 负责收集执行结果。

一次可能的执行过程如下:

Step模型本次获得的新增信息模型输出Runtime 的处理
1用户请求、项目规则、工具规格搜索保存按钮相关代码执行代码搜索并记录匹配位置
2搜索结果读取资料页组件和请求封装读取文件并记录内容
3文件内容运行相关测试或复现命令执行命令并记录退出状态与输出
4测试错误修改认证参数并再次测试应用补丁,执行验证命令
5修改差异和验证结果给出最终说明结束 Turn

表中的步骤只是示例。实际模型可能一次请求多个文件,也可能并行调用几个互不依赖的工具。关键约束没有变化:工具结果必须先进入历史,才能成为下一次模型采样的输入。

每个工具调用都有调用 ID。Runtime 将工具结果与原调用配对,记录到对话历史和持久化执行记录中。模型在下一次请求里会收到与调用 ID 对应的结构化结果,界面无须把执行结果拼成一段说明。成功输出、命令退出码和可恢复错误都可以通过这条路径返回模型。

模型发出工具调用后,当前采样结果会标记为需要继续。run_turn 收集仍在执行的工具结果,检查是否有用户追加输入或扩展要求续写,然后捕获下一份 StepContext 并再次调用模型。循环持续到以下条件都满足:模型没有留下需要处理的工具调用,没有待处理输入,也没有 Hook 要求继续。

因此,一次 Turn 和一次模型请求之间没有一一对应关系。Turn 是用户任务的边界,Step 是模型观察当前状态并决定下一步的边界。工具调用位于两次 Step 之间,负责把代码仓库中的新事实带回模型。

一次 Codex Turn 中的模型采样、工具执行与事件返回
图 2-2:turn/start 只启动 Turn。虚线框内的模型采样和工具执行可以重复多次,直到模型给出最终回复、任务失败或用户中断。移动端可横向滑动,点击可查看 SVG 原图。

执行进度通过事件返回客户端

模型流和工具执行都在异步进行,客户端不能等到最后才知道状态。Core 使用 Event 输出运行中的变化。Event 包含关联 ID 和具体的 EventMsg,内容覆盖 Turn 生命周期、消息增量、命令执行、文件修改、审批、计划、差异、Token 用量、警告和错误。

App Server 持续读取这些 Core 事件,并将其转换为公开通知。例如:

Core EventMsg                     App Server 通知
──────────────────────────────    ───────────────────────────
TurnStarted                    →  turn/started
AgentMessageContentDelta       →  item/agentMessage/delta
命令或文件项开始、完成           →  item/started、item/completed
TurnComplete / TurnAborted     →  turn/completed(带最终状态)

客户端根据通知类型更新界面。文本增量追加到当前消息,命令事件显示执行状态,文件事件更新差异视图,审批请求暂停相关操作并等待用户决定。客户端无须从自然语言中猜测“Codex 现在是否正在运行命令”。协议已经把状态和内容分开。

这种事件投影也隔离了内部实现变化。Core 可以增加更细的运行时事件,App Server 决定哪些字段成为公开协议。代价是两层状态必须保持一致:漏掉一个结束事件,客户端可能一直显示任务进行中;同一事件重复投影,也会造成消息或工具项重复。

完成、中断和失败走不同路径

正常情况下,最后一次模型采样只返回助手消息,也没有待处理输入。run_turn 退出后,Session 统计本轮用量,清理活跃 Task,并发出 TurnComplete。App Server 将它转换为状态为 completedturn/completed 通知。

错误发生在哪一层,会决定 Turn 是否还能继续。

情况Runtime 的处理客户端看到的结果
turn/start 参数无效或 Thread 不存在App Server 拒绝请求,不启动 TaskJSON-RPC 错误
搜索无结果、命令退出非零、工具参数错误作为工具结果写回,允许模型调整方案工具项失败,Turn 通常继续
命令需要审批挂起相关工具调用,等待客户端答复审批请求和待处理工具项
用户中断当前 Turn触发取消,停止可取消的模型与工具工作turn/completed,状态为 interrupted
模型流中断且重试耗尽、内部状态损坏记录错误并终止当前 Turn错误事件及状态为 failed 的完成通知

把工具错误返回模型很重要。测试失败可能正是定位问题所需的证据,文件不存在也可能提示模型先搜索正确路径。如果所有非零退出都直接终止 Turn,Agent 无法利用失败结果修正计划。

可恢复并不等于无限重试。模型请求有传输重试和连接回退策略,工具也可以报告错误,但 Runtime 仍要受到上下文窗口、资源限制、超时和取消信号约束。失败路径必须最终收敛为可观察的事件和明确的 Turn 状态。

分层带来的收益与代价

从客户端到 Core 的协议边界,让 TUI、非交互命令和富客户端共享同一套任务语义。Submission 队列让控制操作按顺序进入 Session。异步 Task 使运行中的 Turn 仍可接收中断、审批和追加输入。StepContext 保证每次模型采样使用一致的环境与工具视图。结构化事件则让客户端能够稳定展示进度。

这些机制也引入了工程成本。一次用户请求可能产生多次模型调用,延迟和 Token 消耗随循环增长。工具并行、流式响应和用户中断会产生复杂的时序组合。公开通知与内部事件之间还需要维护一套状态投影。Runtime 的职责因此覆盖模型调用、操作顺序、状态快照、一致性、取消和错误传播。

本章追踪的完整路径可以收束为:

用户输入
  → 客户端发送 turn/start
  → App Server 映射为 Op::UserInput
  → Submission 进入 Session
  → RegularTask 启动 Turn
  → run_turn 构造 StepContext 和模型请求
  → 模型返回消息或工具调用
  → 工具结果写回历史
  → 需要时进入下一个 Step
  → Core 事件经 App Server 返回客户端
  → Turn 完成、失败或被中断

这张全景图暂时省略了几个关键细节:Thread 与 Session 谁拥有长期状态,TurnContext 和 StepContext 分别在何时创建,运行中的用户输入怎样进入当前任务,取消又怎样穿过异步边界。下一章将拆开 Thread、Turn、Task 与 Step 的生命周期,给出这些状态的所有权和切换条件。

源码级细节

这篇总览只建立一条任务主线。第二章的源码级拆解已经展开为 14 个单元:

  1. CLI、TUI、Exec 与 App Server 的进程入口
  2. 配置文件、Profile、命令行覆盖和受管配置的合并顺序
  3. Feature Gate 怎样决定实际启用的 Runtime 分支
  4. ThreadManager 的创建、恢复、派生与注册
  5. CodexThread 的提交端、事件端与状态查询端
  6. Session 初始化时并行建立的服务和失败收尾
  7. Op 协议的类别、所有者和分派入口
  8. EventMsg、Raw Response Item 与界面事件的层次
  9. 普通 User Turn 的端到端调用链
  10. 工具 Turn 的端到端调用链
  11. Compact、Review、UserShell 等非普通 Task 的入口
  12. TUI 怎样把用户动作逐层翻译到 Core Op
  13. App Server 怎样把 JSON-RPC 翻译为 Core 操作和通知
  14. Exec Server 与远程执行环境的协议边界

延伸阅读

评论


← 返回文章列表