JuliusBrussee/caveman · error · Error
cave_budget_controller_without_budget
cave_budget_controller_without_budget
Error message
cave_budget_controller_without_budget
What it means
A budgetController releases staged budget tranches, but it is meaningful only when the run actually carries a BudgetMeter (i.e. options.budget was supplied). A controller without a budget would be a silent no-op at the checkpoint that expected it to matter, so the runtime fails closed instead.
Solutions
- Supply a budget alongside the controller, e.g. budget: { initialUsd: 1, maxUsd: 10 } so checkpoints have tranches to release.
- Remove the budgetController option if the run is intentionally unbudgeted.
- Construct the controller and budget together in one config object so they cannot drift apart.
Example fix
// before
runAgent(def, input, { budgetController: controller });
// after
runAgent(def, input, {
budget: { initialUsd: 1, maxUsd: 10 },
budgetController: controller,
}); Defensive patterns
Strategy: validation
Validate before calling
if (opts.budgetController !== undefined && opts.budget === undefined) {
throw new Error("budgetController requires a budget");
} Type guard
function controllerHasBudget(o) {
return o.budgetController === undefined || o.budget !== undefined;
} Try / catch
try {
await runAgent(def, input, opts);
} catch (err) {
if (err.message === "cave_budget_controller_without_budget") {
await runAgent(def, input, {
...opts,
budget: { initialUsd: 1, maxUsd: 10 },
});
} else throw err;
} Prevention
- Always construct budget and budgetController together in one place.
- Do not reuse a controller handle in a run that carries no budget option.
- Review conditional config builders so dropping budget also drops the controller.
When it happens
Trigger: Calling runAgent/streamAgent with options.budgetController set while options.budget is undefined (no budget/budgetMeter present in the execution context).
Common situations: Reusing a createBudgetController() handle across runs where the new run forgot its budget option; conditional config where the budget block was dropped but the controller stayed; passing a controller for a nested run that inherits no meter.
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_budget_conflicting_cap
- 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/2c4f49489f6994fd.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/runtime.ts:1098
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.
if (options.budgetController !== undefined && budgetMeter === undefined) {
throw new Error("cave_budget_controller_without_budget");
}
const breakers = options.breakers === undefined
? undefined
: new BreakerState(normalizeRunBreakers(options.breakers, budgetMeter !== undefined));
const efficiencyPlan = options.lockedBuild?.selected_plan ?? options.candidatePlan;
const buildIdentity = options.lockedBuild === undefined
? undefined
: {
buildSha256: options.lockedBuild.build_sha256,
planSha256: options.lockedBuild.plan_sha256,
};
const runId = crypto.randomUUID();
yield { type: "run_start", runId, agentId: definition.id };
let conversation: ConversationTransaction | undefined;
let activeAbort: (() => void) | undefined;
let activeExecution: Promise<void> | undefined;
let sandboxSourceSnapshot: SandboxSourceSnapshot | undefined;
const pendingSpendReservations: SpendReservation[][] = [];View on GitHub (pinned to 3ee70a1026)