# 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:40、StreamingToolExecutor.ts:298、StreamingToolExecutor.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:40、StreamingToolExecutor.ts:320、StreamingToolExecutor.ts:408。
这说明 Runtime 的关键原理不是“异步执行”,而是状态所有权:模型拥有动作提议权,Tool Executor 拥有运行态,权限组件拥有放行决策,Session Store 拥有可恢复事实,只有 Runtime 能把它们合并成终态。任何组件都不能仅凭局部成功宣告 ToolCall 完成;例如子进程退出只说明执行结束,还要完成结果规范化、持久化和事件发送。
Codex 在 Turn 开始时冻结 StepContext,run_sampling_request 使用该快照并产生 Response Items;工具结果被记录后,下一 Step 才重新读取可变配置和 World State。这个边界避免了“审批时看到 cwd=A、执行时已经变成 cwd=B”这种检查与执行对象不一致的问题(TOCTOU):turn.rs:198、turn.rs:297。
Runtime 实现时要明确三类超时:模型首 Token/完整响应超时、工具执行超时、Turn 总墙钟预算。工具超时只结束当前 call;Turn 超时需要取消仍在运行的采样、子进程和子 Agent,并将未完成副作用标成 unknown/interrupted,不能伪造成 failed 后自动重试。
恢复时应从持久化终态判断动作:succeeded 直接复用结果,cancelled 保持取消,running 根据工具幂等性进入重放或人工确认。Runtime 的正确性不在“并发越多越好”,而在每个状态只有合法前驱、每个终态只写一次。