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

  1. Supply a budget alongside the controller, e.g. budget: { initialUsd: 1, maxUsd: 10 } so checkpoints have tranches to release.
  2. Remove the budgetController option if the run is intentionally unbudgeted.
  3. 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

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


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)