预算
基于已上报用量的运行级成本与 token 止损线。
你把一个 map 阶段指向你的仓库,让一个 scout agent 列出变更的文件。scout 返回 500 个。每一个都生成一个安全审查器。四个同时运行。等你注意到时,你已在一个本该花四毛钱的 flow 上花了十二美元。
budget 用来限制这类事故的爆炸半径。它是基于 provider 已上报累计成本或 token 的运行级止损线。观察到用量越线后,taskflow 不再准入新的模型调用,保留此前产生的所有部分输出,并返回诊断;已经在途的调用仍可能把用量推过阈值。应把它设在任何可能无界 fan-out 的 flow 上 —— map、parallel、loop、tournament、race。
预算执行不花费额外 token。它读取每个子代理已经写入的 usage 记录,并在每次重试、每个 fan-out 项、每次 DAG 层切换时检查。
在每个 fan-out flow 上设置预算
如果你的 flow 包含 map、parallel、loop 或 tournament,加一个 budget。这就是全部规则。
{
"name": "summarize-files",
"budget": { "maxUSD": 1.5 },
"concurrency": 4,
"phases": [
{
"id": "discover",
"type": "agent",
"output": "json",
"task": "List files under {args.dir}. Output a JSON array of { path }."
},
{
"id": "summarize",
"type": "map",
"over": "{steps.discover.json}",
"as": "file",
"concurrency": 8,
"task": "Read {file.path} and give a one-sentence summary."
}
]
}两个维度都可选。maxUSD 监测已上报累计成本;maxTokens 监测已上报累计输入 + 输出 token。设其一或两者。省略一个维度即关闭该止损线。
{
"name": "fan-out-audit",
"budget": {
"maxUSD": 2.0,
"maxTokens": 2000000
},
"phases": [ /* ... */ ]
}在 host 同时上报两个维度时,将 maxUSD 与 maxTokens 配对以纵深防御。一次模型切换或定价变化可能把花费从成本转移到 token(或反之)。
各 host 的支持范围取决于 CLI 实际上报的 usage:
| Host | 支持的预算维度 |
|---|---|
| Pi、Claude Code、OpenCode | maxUSD 和/或 maxTokens |
| Codex | 只支持 maxTokens;Codex 不上报 cost,因此拒绝 maxUSD |
| Grok Build 0.2.93、Hermes Agent | 全部不支持;这些 quiet-mode runner 不上报 token 或 cost,因此拒绝任何声明了 budget 的 flow |
schema 拒绝未知键(additionalProperties: false)。空的 budget: {} 合法但是空操作 —— maxUSD / maxTokens 至少应设置一个。
触及上限时会发生什么
观察到累计用量超出阈值后,运行时不再准入新的模型调用。运行以 blocked 结束 —— 不是 failed —— 并且已经产生的每个部分输出都被保留。活动调用在返回前尚无最终 usage,不能仅因预算而在调用中途撤销。
| 层 | 发生什么 |
|---|---|
| DAG 层 | 剩余阶段被记录为 skipped,带 error: "Budget exceeded: ..."。 |
进行中的 fan-out(map / parallel) | 循环停止生成。未开始的项目成为 stopReason: "budget-skipped",零 usage。 |
loop 迭代 | 下一次迭代不开始。reflexion loop 将此视为硬停止 —— 失败不作为 reflexion 输入反馈回去。 |
tournament 评委 | 评委被跳过;第一个存活变体获胜,带一条警告。 |
| 重试 | 重试循环中断 —— 不再尝试。 |
| 运行状态 | status 变为 "blocked"。finalOutput 为 "Budget exceeded — run halted." 加原因和最终阶段的部分输出。 |
被阻止的运行不是失败的运行。在超限前完成的每个阶段都保留其 output、json 和 usage。finalOutput 由实际产生输出的最后一个阶段组合而成 —— 因此一个预算被阻止的运行仍可向你的对话返回有用的部分工作。
blocked 表示预算或 gate 中止了工作流。failed 表示一个非 optional 阶段报错退出。检查 state.status 和逐阶段的 error / budgetTruncated 字段以区分两者。
比较是严格的
检查用严格 >,不是 >=。恰好落在上限上的运行不被阻止 —— 只有超过它的下一个美分或 token 才触发中止。maxUSD: 2.0 的预算允许成本恰好为 $2.000 的运行。
普通准入限制超支,race 是例外
provider 只有在调用返回后才上报 usage,因此基于已观察用量的预算无法保证零超支。即使只有一个活动调用,它也可能让用量从阈值下方跃迁到上方。
普通 budgeted DAG 层以及 map / parallel / tournament fan-out 会串行准入模型调用,不受正常 concurrency 设置影响。每次准入前都会检查守卫;观察到越线后不会再启动调用,因此新调用的超支限于已准入的那一次。race 为保持 first-success 语义,必须一起准入竞争分支,所以所有已活动的 race 分支都可能贡献超支。两种情况都不保证固定美元/token 上限,更不保证零超支。
各阶段类型的成本如何累计
预算核算读取 state.phases[*].usage,不同阶段类型对其填充方式不同:
| 阶段类型 | 计入预算的内容 |
|---|---|
agent | 子代理在每次尝试上的 usage。失败重试在重试决策前被累加。 |
parallel | 每个分支 usage 之和。 |
map | 每个已开始项目 usage 之和。预算跳过的项贡献零。 |
loop | 每次迭代累计的 usage,在循环中折入(而非覆盖)—— 因此越线会在准入下一次迭代前被观察到。 |
tournament | 每个变体的 usage 加上评委的(当评委运行时)。 |
flow | 子 flow 运行其自身预算(见下方)。父阶段记录子 flow 的累计 usage。 |
gate | gate 子代理的 usage。 |
approval | 零 —— 不运行子代理。 |
script | 零 —— 无 LLM token。 |
reduce | reducer 子代理的 usage。 |
缓存命中(cacheHit: "cross-run" | "run-only")报告缓存的 usage 但本次运行未花费新 token。cacheRead/cacheWrite/contextTokens 不计入 token 上限 —— 缓存读取几乎免费,contextTokens 是时点指标,不可累加。
重试与预算
每次重试尝试在重试决策之前将其 usage 推入累加器。因此一个失败三次才成功的阶段花费了全部三次尝试的 token —— 且第三次尝试本身可能将运行推过上限。
失败的重试确实计入预算。失败尝试没有"退款" —— 提供方已经对其计费。
声明为 idempotent: false 的阶段禁用隐式瞬态重试(运行时安全网),因此一个不稳定的提供方错误不会通过重跑副作用而静默双重计费。但显式 retry: { max } 仍被遵守 —— 且每次显式重试仍计入预算。
{
"id": "deploy",
"type": "agent",
"idempotent": false,
"task": "Run the production deploy. Report the rollout ID.",
"retry": { "max": 0 }
}子流程中的预算
flow 阶段运行子 taskflow。子流程的预算如何与父预算交互,取决于子流程是内联生成(def)还是按名加载(use)。
内联 def 子流程 —— 阈值钳制
生成的内联定义不能抬高父上限。运行时每个维度取 min(child, parent):
{
"id": "plan-and-execute",
"type": "flow",
"def": {
"name": "inner-plan",
"budget": { "maxUSD": 5.0 },
"phases": [ /* ... */ ]
}
}如果父 flow 有 budget: { maxUSD: 2.0 },内层子流程的有效阈值变为 $2.0,无论其声明的 $5.0。它只能声明比父更紧的阈值,但在途调用仍可能越线。如果钳制后没有有限维度剩下,子流程的 budget 被设为 undefined —— 但父的守卫仍能看到子流程的花费,因为父阶段记录了累计的子阶段 usage。
已保存 use 子流程 —— 软继承
按 use 加载的子流程执行其自身声明的预算。如果未声明,它继承父阈值作为尽力而为的每 flow 止损线。
此继承是尽力而为:花费不跨 flow 边界。父已花费总额不从子继承的上限中扣除,因此继承 $2.0 的子流程即使父已花费 $1.50,自身仍可花费至多 $2.0。父自身的守卫在子流程返回时仍会捕获合并总额。
递归被栈检查阻止,因此子流程不能无限嵌套以绕过自身预算。嵌套计数器在 flow 与内联 spawn(ctx_spawn)两条递归轴上统一。
在 fan out 前估算
对昂贵的逐项工作,先运行一个廉价的估算器阶段,让其输出决定是否继续。这把无界的 map 转为有界的。
{
"name": "bounded-review",
"budget": { "maxUSD": 3.0, "maxTokens": 3000000 },
"concurrency": 4,
"phases": [
{
"id": "discover",
"type": "agent",
"output": "json",
"task": "List files under src/. Output JSON: { items: [{ path }] }."
},
{
"id": "estimate",
"type": "agent",
"output": "json",
"context": ["{steps.discover.json}"],
"task": "Given these files, estimate tokens to review each. Output JSON: { items: [{ path, estTokens }], totalEstTokens }.",
"expect": {
"type": "object",
"required": ["items", "totalEstTokens"],
"properties": {
"items": { "type": "array", "items": { "type": "object" } },
"totalEstTokens": { "type": "number" }
}
}
},
{
"id": "review",
"type": "map",
"over": "{steps.estimate.json.items}",
"as": "item",
"when": "{steps.estimate.json.totalEstTokens} < 2500000",
"concurrency": 6,
"task": "Review {item.path} for security risks. List concrete findings."
},
{
"id": "fallback",
"type": "agent",
"when": "{steps.estimate.json.totalEstTokens} >= 2500000",
"task": "The estimated token cost ({steps.estimate.json.totalEstTokens}) exceeds half the budget. Summarize src/ at a high level instead of per-file review."
}
]
}review 上的 when 守卫在估算超过预算一半时完全跳过 fan-out,为 fallback 阶段留出余量。即使有此守卫,budget 仍能防止低估。
给 loop 和 tournament 各自的余量
reflexion loop 可能在精炼同一输出上花费多次迭代。tournament 运行 N 个变体加一个评委。将预算设得足够覆盖预期的迭代或变体数,再让守卫捕获失控情况。
{
"name": "pick-best-title",
"budget": { "maxUSD": 0.80 },
"phases": [
{
"id": "compete",
"type": "tournament",
"variants": 4,
"task": "Write a title for: {args.topic}.",
"judge": "Pick the single best title. Output only the winner."
}
]
}检查预算用量
持久化到 .pi/taskflows/runs/<flow>/<runId>.json 的每个运行记录都暴露了审计花费所需的信息:
| 字段 | 告诉你什么 |
|---|---|
def.budget | 生效的已观察用量阈值({ maxUSD?, maxTokens? })。 |
status | 超出预算时为 "blocked";否则为 "completed" / "failed" / "paused"。 |
phases.<id>.usage | 该阶段的 { input, output, cacheRead, cacheWrite, cost, contextTokens, turns }。input + output 是 token 上限所汇总的。 |
phases.<id>.budgetTruncated | 当 map / parallel fan-out 被上限(或 MAX_DYNAMIC_MAP_ITEMS 安全限制)截断时为 true。 |
phases.<id>.attempts | 包括重试在内的子代理总尝试次数。> 1 时表示发生了重试 —— 且每次尝试的花费都在 usage 中。 |
phases.<id>.cacheHit | 由缓存提供时为 "cross-run" 或 "run-only"(本次运行无新 token)。 |
phases.<id>.status | 从未运行的阶段为 "skipped",带 error: "Budget exceeded: ..."。 |
预算被阻止运行的终态 finalOutput 为:
Budget exceeded — run halted.
Reason: cost $2.143 exceeded cap $2.000
<final phase's partial output>要获得运行级总额而无需自己汇总阶段,读取 executeTaskflow 返回的值 —— result.totalUsage 是跨所有阶段的运行后累计。运行记录不存储顶层 totalUsage;TUI 在读取时计算。
常见错误
| 错误 | 原因 | 修复 |
|---|---|---|
Budget exceeded — run halted. Reason: cost $X exceeded cap $Y | 累计 cost 超过 maxUSD。 | 提高 maxUSD、减少 fan-out(concurrency / over 大小),或加 when 守卫跳过昂贵阶段。 |
Budget exceeded — run halted. Reason: tokens N exceeded cap M | 累计 input + output 超过 maxTokens。 | 提高 maxTokens、切换更廉价的 model,或预读更少 context(contextLimit)。 |
N item(s) skipped: budget exceeded | map / parallel fan-out 被截断。 | 在运行记录中检查 phases.<id>.budgetTruncated。提高预算或降低 concurrency。 |
judge skipped: run aborted or budget exceeded | tournament 在超限后到达评委步骤。 | 第一个存活变体被选为赢家。提高预算或减少 variants。 |
运行状态 blocked 但 finalOutput 看似完整 | 最终阶段在超限前完成,但更后的(非最终)阶段把总额推过了上限。 | 重排让昂贵阶段在最终阶段之前运行,或标记一个更廉价的阶段为 final: true。 |
blocked 运行不是 failed 运行。blocked 表示预算或 gate 中止了工作流;failed 表示非可选阶段报错退出。检查 state.status 和逐阶段的 error / budgetTruncated 字段以区分两者。
Last updated on