JuliusBrussee/caveman · error
cave_budget_controller_without_budget
cave_budget_controller_without_budget
Error message
cave_budget_controller_without_budget
What it means
Thrown at run start when RunOptions.budgetController is supplied without RunOptions.budget. A controller exists only to release additional tranches up to a budget's max at developer-controlled checkpoints; with no budget meter bound it would be a silent no-op at exactly the checkpoint that expected it to matter, so the run fails closed.
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 27d5a3981a)
Solutions
- Add the budget the controller releases into: budget: { maxUsd: X, initialUsd: Y } (or maxTokens/initialTokens)
- Or remove budgetController when running without a budget
- Keep controller and budget sourced from the same config branch so they are set or unset together
Example fix
// before
await agent.run(input, { budgetController: controller });
// after
await agent.run(input, {
budget: { maxUsd: 10, initialUsd: 2 },
budgetController: controller,
}); Defensive patterns
Strategy: validation
Validate before calling
const budgeted = config.budget !== undefined;
const opts = budgeted
? { budget: config.budget, budgetController: controller }
: {}; // controller only when budget is set Type guard
const controllerIsValid = (o: { budget?: unknown; budgetController?: unknown }): boolean =>
o.budgetController === undefined || o.budget !== undefined; Prevention
- Set budget and budgetController from the same config branch
- Treat a controller without a budget as a wiring bug caught in code review
- Keep staged-budget setup (budget + initialUsd + controller) in one helper function
When it happens
Trigger: Passing { budgetController: createBudgetController(...) } while omitting budget, or setting budget only in some code paths (e.g. only when a flag is set) while always setting the controller.
Common situations: Refactoring staged budgeting: the controller wiring survives but the budget field is dropped during cleanup; conditionally disabling the budget via destructuring while leaving the controller in the options object.
Related errors
- caveman agent: run maxCostUsd must be positive
- cave_budget_denomination_ambiguous
- cave_budget_max_invalid
- cave_budget_initial_invalid
- cave_budget_output_floor_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/2c4f49489f6994fd.
Report an issue: GitHub.