JuliusBrussee/caveman · error · Error
cave_breaker_retry_requires_budget
Error message
cave_breaker_retry_requires_budget
What it means
A retry policy (`RunOptions.breakers.retry`) declares a worst-case spend in the run budget's denomination, and every retry takes a real hold from the run's BudgetMeter. Without a budget there is no denomination to reserve against, so `normalizeRunBreakers` throws `cave_breaker_retry_requires_budget` when `breakers.retry` is set and the run has no budget. This is a deliberate fail-closed design: retries without a metered wallet could spend without bound.
Source
Thrown at packages/agent/src/breakers.ts:89
hasBudget: boolean,
): NormalizedBreakers {
const repeatedToolCalls = breakers.repeatedToolCalls ?? DEFAULT_REPEATED_TOOL_CALLS;
const repeatedToolCallWindowTurns = breakers.repeatedToolCallWindowTurns ??
DEFAULT_REPEATED_TOOL_CALL_WINDOW_TURNS;
const noProgressTurns = breakers.noProgressTurns ?? DEFAULT_NO_PROGRESS_TURNS;
const maxToolCallsPerTurn = breakers.maxToolCallsPerTurn ?? DEFAULT_MAX_TOOL_CALLS_PER_TURN;
for (const value of [
repeatedToolCalls,
repeatedToolCallWindowTurns,
noProgressTurns,
maxToolCallsPerTurn,
]) {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error("cave_breaker_threshold_invalid");
}
}
if (breakers.retry !== undefined && !hasBudget) {
throw new Error("cave_breaker_retry_requires_budget");
}
if (breakers.retry !== undefined &&
(!Number.isFinite(breakers.retry.maxSpend) || breakers.retry.maxSpend <= 0)) {
throw new Error("cave_breaker_retry_spend_invalid");
}
const retryBackoffMs = breakers.retry?.backoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
if (!Number.isSafeInteger(retryBackoffMs) || retryBackoffMs < 0) {
throw new Error("cave_breaker_retry_backoff_invalid");
}
return Object.freeze({
repeatedToolCalls,
repeatedToolCallWindowTurns,
noProgressTurns,
maxToolCallsPerTurn,
retryMaxSpend: breakers.retry?.maxSpend,
retryBackoffMs,
});
}View on GitHub (pinned to 27d5a3981a)
Solutions
- Add a budget alongside retry: `run(agent, { budget: { maxUsd: 5 }, breakers: { retry: { maxSpend: 1 } } })`.
- Or drop `breakers.retry` if you don't need cost-bounded automatic retries.
Example fix
// before
run(agent, { breakers: { retry: { maxSpend: 1 } } }); // no budget
// after
run(agent, {
budget: { maxUsd: 5 },
breakers: { retry: { maxSpend: 1 } },
}); Defensive patterns
Strategy: validation
Validate before calling
if (options.breakers?.retry !== undefined && options.budget === undefined) {
throw new Error("breakers.retry requires RunOptions.budget — add budget: { maxUsd } or { maxTokens }");
} Try / catch
try {
run(agent, options);
} catch (err) {
if (err instanceof Error && err.message === "cave_breaker_retry_requires_budget") {
// add a budget or remove retry — a config fix, not a transient failure
}
} Prevention
- Always pair breakers.retry with an explicit RunOptions.budget.
- Note that maxCostUsd is not a budget; migrate to budget.maxUsd when adding retry.
- Centralize run-option construction so the budget/retry pairing is checked in one place.
When it happens
Trigger: Calling `run(agent, { breakers: { retry: { maxSpend: 1 } } })` without also setting `RunOptions.budget` (`{ maxUsd: ... }` or `{ maxTokens: ... }`); adding retry config to an existing run call that never had a budget; combining `retry` with `maxCostUsd` only (the older cap is not a budget).
Common situations: Copying a breakers snippet from docs into a run that uses no budget; migrating from `maxCostUsd` to breakers and assuming the cap satisfies the requirement.
Related errors
- cave_breaker_retry_spend_invalid
- cave_breaker_retry_backoff_invalid
- cave_breaker_threshold_invalid
- cave_vercel_terminal_failure
- cave_retry_accounting_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/1c94bb9c1dc6f5fe.
Report an issue: GitHub.