JuliusBrussee/caveman · error
cave_breaker_retry_requires_budget
cave_breaker_retry_requires_budget
Error message
cave_breaker_retry_requires_budget
What it means
normalizeRunBreakers validates run breaker configuration. A `retry` breaker inherently spends more budget (repeated model calls), so it is only meaningful when a budget is configured. If `breakers.retry` is provided but no budget (hasBudget) exists on the run, the library throws this sentinel error to prevent an unbounded retry loop with no spending cap.
Solutions
- Add a budget to the run config (maxUsd or maxTokens) alongside the retry breaker.
- Remove the `retry` breaker if you intentionally want an uncapped run.
- Ensure the budget is passed to the same normalize/validate call so hasBudget is true.
Example fix
// before
breakers({ retry: { maxSpend: 0.5 } })
// after
breakers({ budget: { maxUsd: 2 }, retry: { maxSpend: 0.5 } }) Defensive patterns
Strategy: validation
Validate before calling
function assertRetryNeedsBudget(breakers) {
if (breakers?.retry && (breakers.budget === undefined || (breakers.budget.maxUsd === undefined && breakers.budget.maxTokens === undefined))) {
throw new Error('cave_breaker_retry_requires_budget: retry requires a budget');
}
} Try / catch
try { normalizeRunBreakers(cfg); } catch (e) {
if (e.message === 'cave_breaker_retry_requires_budget') {
cfg = { ...cfg, budget: { maxUsd: cfg.budget?.maxUsd ?? DEFAULT_BUDGET_USD } };
} else throw e;
} Prevention
- Always pair retry breakers with an explicit budget in the same config object.
- Lint configs: any `retry:` key must be sibling to a budget key.
- Document retry spend implications so configs are not tuned independently.
When it happens
Trigger: Passing a breakers object with `retry: { maxSpend, backoffMs, ... }` set while the run configuration has no budget (no maxUsd/maxTokens) — e.g. calling breakers normalization with retry options on a run lacking normalizeRunBudget.
Common situations: Developers copy a retry-tuning snippet from docs but forget the budget section; an existing config gains retry settings while budget stays unset; a CLI/UI lets users tune retries independently of the spend cap.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- cave_breaker_retry_backoff_invalid
- cave_breaker_retry_spend_invalid
- cave_budget_denomination_ambiguous
- cave_budget_initial_invalid
- cave_budget_max_invalid
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/1c94bb9c1dc6f5fe.
Report an issue: GitHub.
Appendix: 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 3ee70a1026)