用户在 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 提交操作并接收事件。
这几层可以先压缩成下面的关系:
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 完成三项工作:
- 生成当前环境的 World State,也就是模型需要了解的工作目录、权限和其他运行状态。
- 生成模型可见的工具规格,让模型知道工具名称、参数和用途。
- 构造 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 之间,负责把代码仓库中的新事实带回模型。
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 将它转换为状态为 completed 的 turn/completed 通知。
错误发生在哪一层,会决定 Turn 是否还能继续。
| 情况 | Runtime 的处理 | 客户端看到的结果 |
|---|---|---|
turn/start 参数无效或 Thread 不存在 | App Server 拒绝请求,不启动 Task | JSON-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 个单元:
- CLI、TUI、Exec 与 App Server 的进程入口
- 配置文件、Profile、命令行覆盖和受管配置的合并顺序
- Feature Gate 怎样决定实际启用的 Runtime 分支
- ThreadManager 的创建、恢复、派生与注册
- CodexThread 的提交端、事件端与状态查询端
- Session 初始化时并行建立的服务和失败收尾
- Op 协议的类别、所有者和分派入口
- EventMsg、Raw Response Item 与界面事件的层次
- 普通 User Turn 的端到端调用链
- 工具 Turn 的端到端调用链
- Compact、Review、UserShell 等非普通 Task 的入口
- TUI 怎样把用户动作逐层翻译到 Core Op
- App Server 怎样把 JSON-RPC 翻译为 Core 操作和通知
- Exec Server 与远程执行环境的协议边界
评论
登录后即可评论