JuliusBrussee/caveman · error
cave_budget_conflicting_cap
cave_budget_conflicting_cap
Error message
cave_budget_conflicting_cap
What it means
Thrown at run start when both RunOptions.budget and RunOptions.maxCostUsd are set. They are two different contracts for the same money: exhausting maxCostUsd terminates with an error, while exhausting budget returns a planned partial result with a stopReason. Carrying both would leave the run's stop semantics undecided, so the conflict fails fast before the first provider call.
Source
Thrown at packages/agent/src/runtime.ts:1078
(!Number.isSafeInteger(options.maxSubagentInvocations) || options.maxSubagentInvocations <= 0 ||
options.maxSubagentInvocations > ABSOLUTE_SUBAGENT_INVOCATION_LIMIT)) {
throw new Error("cave_subagent_invocation_limit_invalid");
}
if (options.maxConcurrentSubagents !== undefined &&
(!Number.isSafeInteger(options.maxConcurrentSubagents) || options.maxConcurrentSubagents <= 0 ||
options.maxConcurrentSubagents > ABSOLUTE_SUBAGENT_INVOCATION_LIMIT)) {
throw new Error("cave_subagent_concurrency_limit_invalid");
}
if (options.lockedBuild !== undefined && options.candidatePlan !== undefined) {
throw new Error("cave_execution_authorization_ambiguous");
}
// Budget shape is settled before anything else happens: an ambiguous or
// unbounded budget must fail at run() start, not after the first dollar.
// maxCostUsd and budget are two different contracts for the same money —
// one terminates with an error, the other returns a planned partial result —
// so carrying both would leave the run's own stop semantics undecided.
if (options.budget !== undefined && options.maxCostUsd !== undefined) {
throw new Error("cave_budget_conflicting_cap");
}
const budgetMeter = executionContext.budgetMeter ?? (options.budget === undefined
? undefined
: new BudgetMeter(normalizeRunBudget(options.budget)));
if (options.deadlineMs !== undefined &&
(!Number.isSafeInteger(options.deadlineMs) || options.deadlineMs <= 0)) {
throw new Error("cave_run_deadline_invalid");
}
if (options.maxSubagentDepth !== undefined &&
(!Number.isSafeInteger(options.maxSubagentDepth) || options.maxSubagentDepth <= 0 ||
options.maxSubagentDepth > ABSOLUTE_SUBAGENT_DEPTH_LIMIT)) {
throw new Error("cave_subagent_depth_limit_invalid");
}
const deadlineAt = executionContext.deadlineAt ?? (options.deadlineMs === undefined
? undefined
: performance.now() + options.deadlineMs);
// A controller with nothing to release would be a silent no-op at the
// checkpoint that expected it to matter.View on GitHub (pinned to 27d5a3981a)
Solutions
- Keep exactly one cap: prefer RunOptions.budget (reserve-and-clamp, graceful stop) and delete maxCostUsd
- If you must keep the legacy error-terminating semantics, remove the budget field
- When merging option layers, make the budget cap an explicit override instead of spreading both
Example fix
// before
await agent.run(input, { ...defaults, budget: { maxUsd: 5 } }); // defaults has maxCostUsd: 10
// after
const { maxCostUsd: _omit, ...rest } = defaults;
await agent.run(input, { ...rest, budget: { maxUsd: 5 } }); Defensive patterns
Strategy: validation
Validate before calling
function singleCap<T extends { budget?: unknown; maxCostUsd?: unknown }>(o: T): T {
if (o.budget !== undefined && o.maxCostUsd !== undefined) {
const { maxCostUsd: _drop, ...rest } = o;
return rest as T; // or throw, per your policy
}
return o;
} Type guard
const hasBothCaps = (o: { budget?: unknown; maxCostUsd?: unknown }): boolean =>
o.budget !== undefined && o.maxCostUsd !== undefined; Prevention
- Pick one budget contract project-wide (prefer RunOptions.budget)
- When merging option layers, destructure out maxCostUsd before adding budget
- Add a lint/grep check that flags maxCostUsd appearing next to budget
When it happens
Trigger: run/stream called with { budget: { maxUsd: 5 }, maxCostUsd: 5 } or { budget: { maxTokens: 100000 }, maxCostUsd: 2 } in the same options object; merging two config objects where each contributed one of the caps.
Common situations: Layered defaults: a wrapper always sets maxCostUsd while the caller adds a budget; copy-paste migration from the legacy maxCostUsd to the newer budget without removing the old field; spread of a base options object into per-call options.
Related errors
- cave_budget_denomination_ambiguous
- cave_budget_max_invalid
- cave_budget_initial_invalid
- cave_budget_output_floor_invalid
- cave_budget_on_exhausted_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/f03f488a80f2401a.
Report an issue: GitHub.