Appearance
一次 Run 的时间预算怎么传下去:Timeout、Deadline 与 Retry 分类
前置:Tool Timeout / Error;Run State Machine
核心问题:一个 Run 只剩 8 秒时,下游 Model/Tool timeout 应怎样分配,哪些失败值得重试而不会把 Deadline 彻底耗尽?
一次 Run 预算 30 秒,模型已用 20 秒后,后续 Tool 批次不能每个再等 30 秒。把 deadline at 传进 ToolExecutor,每个 retry 前重新检查剩余预算。
Mental Model
text
Operation intent → deadline/budget/policy → attempt → result/error → retry/degrade/wait/cancel → durable decision失败处理必须围绕 logical operation 建模:每个 attempt 都可观测,副作用可去重,重试不能突破总 deadline/budget,人工等待也要持久化。
Timeout Budget
Timeout Budget 是一个 Run 或 logical operation 可用于等待的总时间预算。Model、Tool、queue、retry 等子步骤只能消费剩余预算,而不能在每次重试时重新获得完整 timeout。
应依据剩余 deadline 动态收紧。
🔍 源码对照:MCP stall 使用独立 expiry policy 做诊断
精确定位
- 文件:
source/host/runner/turn-observation.ts - Symbol:
beginMcpExecObservation()→MCP_EXEC_STALL_THRESHOLD_MS - 深入索引:Code Atlas:04-turn-observation
ts
function beginMcpExecObservation(call) {
const started = performanceNow()
const stall = expiry.arm(call.toolCallId, () =>
diagnostic({ kind: "stalled", toolCallId: call.toolCallId })
)
return (errorClass) => {
stall.dispose()
if (errorClass)
diagnostic({ kind: "error", errorClass,
durationMs: performanceNow() - started })
}
}对照理解
- “没有返回”不等于一个普通 exception。源码给每个 MCP call 单独 arm 一个 stall timer,并用
toolCallId关联诊断。 - Timeout/Stall 的 Observation 与是否允许 Retry 是两件事:前者描述发生了什么,后者由 Reliability Policy 结合副作用语义决定。
- 这也是为什么 deadline、timeout、retry 不应该揉成 Tool Executor 内的一个
try/except。
源码说明
stream attempt、transient error、clock skew guard、observation 被拆成独立逻辑,说明“超时/延迟”不是一个单一秒数。模型 stream、Tool execution、await/stall observation、跨时钟 telemetry 都可能有不同语义。Runtime 需要先分类失败发生在哪一层,才能决定 retry。
工程边界
不要复制案例里的时间常量。Deadline 必须来自上层 Run budget,并向下派生;某一层标记 transient 也不代表整个 action 在业务上可安全重试。
Shadow Harness 对照
- Error taxonomy 先区分 transport/provider/tool/semantic/outcome-unknown,再映射 retry policy。
- Timeout 记录阶段与 elapsed/deadline,而不是只抛
TimeoutError。
验证
同样 30 秒超时分别发生在 model connect、stream 中途、read-only tool、side-effect tool,验证四种 case 得到不同 retry decision。
Deadline
Timeout 限制某一次调用,Deadline 限制整个任务最晚什么时候结束。
例如 Run 还剩 8 秒,Tool 默认 timeout 是 30 秒:真正可用的等待时间只能取剩余 deadline,而不是继续给 Tool 30 秒。
text
remaining = deadline - now
child_timeout = min(configured_timeout, remaining)这能防止 Model、Tool、HTTP 各自都有 timeout,最后端到端耗时却层层累加。
Retry Classification
只对 transient 且可安全重放错误 retry;指数退避+抖动减少同步重试风暴,retry budget 防止失败链持续烧成本。重试前先按错误分类:transient network/429/5xx 可能可重试;validation、permission、not-found 通常不可重试;unknown side-effect outcome 需要幂等或 reconciliation。分类错误比 backoff 参数错误更危险。
Retry policy 是错误语义函数,不是固定次数。
最小实现 / 数据结构
python
for attempt in retry_policy.attempts(error_class):
try:
return await asyncio.wait_for(operation(), timeout=attempt.timeout)
except TransientError as exc:
await retry_policy.backoff(attempt, exc)在 Shadow Harness 中怎么落地
TimeoutPolicy、RetryPolicy、IdempotencyStore、ApprovalPolicy、Guardrail 与 RunState 协作;HITL 通过 durable pause/resume 实现。
Failure Modes
只有局部 Timeout 没有端到端 Deadline
症状: 每个 Tool 都获得完整 timeout/retry,整个 Run 远超用户 deadline。
为什么危险: 局部没超时但整体 SLO 和资源预算失控。
处理: Run 保存 absolute deadline,子调用按 remaining time 派生 timeout;retry 共享预算。
对非幂等 Tool 自动 Retry
症状: 发送邮件/扣款/创建资源超时后直接再次调用,未确认第一次是否已成功。
为什么危险: 网络超时不等于远端失败,重复调用会制造重复副作用。
处理: 使用 idempotency key/execution record;uncertain outcome 先 reconcile,再决定 retry。
如何验证
| Case | 断言 |
|---|---|
| 只有局部 Timeout 没有端到端 Deadline | Run deadline=1s、多子调用时总 wall time 仍受上限约束。 |
| 对非幂等 Tool 自动 Retry | 在远端成功、本地超时位置注入故障,恢复后业务副作用仍只发生一次。 |
关联源码
自测
- Run 总 deadline=10s,第一次调用已耗时 8s。Retry policy 为什么不能再给下一次完整 10s timeout?
- connect timeout、read timeout、overall deadline 分别保护什么边界?把它们混成一个数会丢掉什么控制能力?