JuliusBrussee/caveman · error · Error
cave_budget_conflicting_cap
cave_budget_conflicting_cap
Error message
cave_budget_conflicting_cap
What it means
maxCostUsd (the older error-terminating cap) and options.budget (the reserve-and-clamp budget contract that returns a planned partial result) are two different stop-semantics contracts for the same money. Carrying both would leave the run's own stop behavior undecided, so streamAgentInternal rejects the combination at start, before any spend.
Solutions
- Delete maxCostUsd and express the cap via budget: { maxUsd } (or maxTokens) — this gives reserve-and-clamp semantics.
- Alternatively keep maxCostUsd for hard error-terminating behavior and remove budget.
- Audit config merge code to ensure only one cap style survives the merge.
Example fix
// before
runAgent(def, input, { maxCostUsd: 5, budget: { maxUsd: 10 } });
// after
runAgent(def, input, { budget: { maxUsd: 10 } }); Defensive patterns
Strategy: validation
Validate before calling
if (opts.maxCostUsd !== undefined && opts.budget !== undefined) {
throw new Error("maxCostUsd and budget are mutually exclusive");
} Type guard
function hasSingleCostCap(o) {
return !(o.maxCostUsd !== undefined && o.budget !== undefined);
} Try / catch
try {
await runAgent(def, input, opts);
} catch (err) {
if (err.message === "cave_budget_conflicting_cap") {
const { maxCostUsd, ...rest } = opts;
await runAgent(def, input, rest); // prefer budget semantics
} else throw err;
} Prevention
- Standardize on one cap style (prefer budget) across your codebase.
- Strip legacy maxCostUsd during config merges/migrations.
- Document the intended stop semantics (error vs planned partial result) in your run wrappers.
When it happens
Trigger: Calling runAgent/streamAgent with both options.maxCostUsd and options.budget defined in the same RunOptions object.
Common situations: Merging an options object that sets maxCostUsd with a newer config layer that sets budget; incremental migration from the legacy maxCostUsd API to the budget API where the old field was never removed.
Related errors
- cave_budget_controller_without_budget
- cave_budget_denomination_unavailable
- cave_execution_authorization_ambiguous
- cave_${field}_invalid
- cave_${slot}_budget_exceeded
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/f03f488a80f2401a.
Report an issue: GitHub.
Appendix: 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 3ee70a1026)