Skip to content
预计 1 分钟右侧目录可快速定位

一次 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 做诊断 ​

精确定位

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 没有端到端 DeadlineRun deadline=1s、多子调用时总 wall time 仍受上限约束。
对非幂等 Tool 自动 Retry在远端成功、本地超时位置注入故障,恢复后业务副作用仍只发生一次。

关联源码 ​

自测 ​

  1. Run 总 deadline=10s,第一次调用已耗时 8s。Retry policy 为什么不能再给下一次完整 10s timeout?
  2. connect timeout、read timeout、overall deadline 分别保护什么边界?把它们混成一个数会丢掉什么控制能力?

Sources ​

AI Engineering · Agent · Harness · Backend Systems