# 2. Agent Runtime

# 2.1 原理

Runtime 是 Harness 的执行内核。它把“模型输出的一串 Token”解释成带状态的动作,并保证动作满足顺序、权限、资源和生命周期约束。

建议把 Runtime 建模为层级状态机:

Session                         // 表示可持续多个用户交互、可恢复的会话容器
  └─ Turn                       // 表示围绕一次用户目标展开的业务执行单元
      ├─ Step(model sampling)   // 表示一次确定上下文快照下的模型采样
      ├─ ToolCall[N]            // 表示该 Step 产生的零到多个结构化动作提议
      ├─ Approval/Sandbox execution // 表示工具在审批和 OS 隔离边界内执行
      └─ Follow-up Step         // 表示工具结果加入 Context 后触发的下一次模型采样

典型状态:

  • Session:created → running → idle/requires_action → closed
  • Turn:queued → preparing → sampling → executing_tools → compacting → completed/aborted/failed
  • ToolCall:proposed → validating → awaiting_approval → running → succeeded/failed/cancelled

Runtime 必须处理:

  • 取消传播:Session 取消应向 Turn、采样请求、工具子进程传播;工具局部失败未必终止整个 Turn。
  • 背压:模型流、工具进度和 UI 消费速度不一致,需要队列上限和丢弃策略。
  • 幂等:恢复后不能重复提交已成功的高副作用工具;需要 call id、状态持久化与去重。
  • 公平性:并行 Agent/工具应受全局并发、租户配额、CPU/内存/Token 预算约束。

# 2.2 源码细节

Claude Code 的 StreamingToolExecutor 为每个工具保存状态与 Promise,进度消息立即 yield,完成结果只 yield 一次;取消控制器是父查询控制器的子级,能够区分“局部工具中止”和“结束整个查询”:StreamingToolExecutor.ts:40StreamingToolExecutor.ts:298StreamingToolExecutor.ts:408

Codex 的 Runtime 以 SessionTask → run_turn → run_sampling_request → ToolRouter 分层。run_turn 在每个 Step 捕获一次上下文快照,保证“发给模型的上下文、工具定义和随后解析工具调用”使用同一个视图,避免检查完成后、真正执行前数据发生变化的问题(TOCTOU):turn.rs:297

# 2.3 伪代码

async function executeTool(call, runtime):                            // 定义单个 ToolCall 的受控执行入口
    assert state(call) == PROPOSED                                   // 只允许尚未执行的调用进入流程,防止重复执行
    transition(call, VALIDATING)                                     // 记录状态迁移,供 UI、恢复和审计使用
    spec = runtime.registry.lookup(call.name)                         // 从注册表解析工具实现、Schema 和安全元数据
    args = spec.schema.parse(call.arguments)                         // 对模型参数做严格结构校验并拒绝未知字段
    decision = await runtime.permissions.decide(spec, args)          // 结合规则、模式、资源和风险计算权限决策
    if decision == ASK:                                              // ASK 表示必须由用户或上级策略进一步确认
        transition(call, AWAITING_APPROVAL)                          // 显式进入等待审批状态,避免被误判为卡死
        decision = await runtime.ui.requestApproval(call)            // 展示规范化动作并等待批准或拒绝
    if decision != ALLOW:                                            // DENY 或用户拒绝都不能进入真实执行阶段
        return terminalResult(call, REJECTED)                        // 生成结构化拒绝结果并结束该 ToolCall
    transition(call, RUNNING)                                        // 权限通过后才将调用标记为实际运行
    try:                                                              // 捕获取消、沙箱错误和工具业务错误
        result = await runtime.sandbox.run(spec, args, call.cancelToken) // 在沙箱中执行,并传播调用级取消信号
        return terminalResult(call, SUCCEEDED, result)               // 规范化成功结果并持久化终态
    catch Cancelled:                                                  // 单独识别用户取消或父级取消传播
        return terminalResult(call, CANCELLED)                       // 保存取消终态,避免恢复时自动重跑
    catch error:                                                      // 捕获除取消外的执行异常
        return terminalResult(call, FAILED, normalize(error))        // 将异常脱敏、分类后返回统一失败结果

# 2.4 面试题

问:为什么一次 Turn 中要有多个 Step?

模型第一次采样可能只产生 ToolCall。工具结果追加到上下文后还要再次采样,直到模型给出终态答案或满足停止条件。因此 Turn 是业务交互单位,Step 是一次模型推理单位。

问:Runtime 如何避免工具重复执行?

使用稳定 call_id、执行前写入 intent/started 事件、成功后写 terminal event;恢复时由 reducer 重建状态,只重试明确可重试且幂等的调用。支付、发布等不可幂等动作需业务幂等键或人工确认。

# 2.5 原理深化:Runtime 用所有权和终态约束副作用

Claude Code 的 StreamingToolExecutor 为每个 ToolUse 保存 queued/executing/completed/yielded 状态和独立 Promise。模型流尚未结束时工具可以开始执行,但完整结果只能加入下一轮 Context 一次;父级 AbortController 触发后,会向工具级控制器传播,而单个工具取消不必反向终止整个 Query:StreamingToolExecutor.ts:40StreamingToolExecutor.ts:320StreamingToolExecutor.ts:408

这说明 Runtime 的关键原理不是“异步执行”,而是状态所有权:模型拥有动作提议权,Tool Executor 拥有运行态,权限组件拥有放行决策,Session Store 拥有可恢复事实,只有 Runtime 能把它们合并成终态。任何组件都不能仅凭局部成功宣告 ToolCall 完成;例如子进程退出只说明执行结束,还要完成结果规范化、持久化和事件发送。

Codex 在 Turn 开始时冻结 StepContextrun_sampling_request 使用该快照并产生 Response Items;工具结果被记录后,下一 Step 才重新读取可变配置和 World State。这个边界避免了“审批时看到 cwd=A、执行时已经变成 cwd=B”这种检查与执行对象不一致的问题(TOCTOU):turn.rs:198turn.rs:297

Runtime 实现时要明确三类超时:模型首 Token/完整响应超时、工具执行超时、Turn 总墙钟预算。工具超时只结束当前 call;Turn 超时需要取消仍在运行的采样、子进程和子 Agent,并将未完成副作用标成 unknown/interrupted,不能伪造成 failed 后自动重试。

恢复时应从持久化终态判断动作:succeeded 直接复用结果,cancelled 保持取消,running 根据工具幂等性进入重放或人工确认。Runtime 的正确性不在“并发越多越好”,而在每个状态只有合法前驱、每个终态只写一次。

最后更新: 2026/8/5 22:05:21