JuliusBrussee/caveman · error

caveman agent: run maxCostUsd must be positive

Error message

caveman agent: run maxCostUsd must be positive

What it means

Thrown by rootExecutionContext() when the legacy dollar cap RunOptions.maxCostUsd is present but not a usable number: it must be finite and strictly greater than zero. maxCostUsd is the older error-terminating cost cap; a zero, negative, NaN, or Infinity value is treated as a malformed cap, not 'no cap', so it fails closed before any work starts. Note it also cannot be combined with RunOptions.budget (see cave_budget_conflicting_cap).

Source

Thrown at packages/agent/src/runtime.ts:774

  costUsd: number;
  unpriced: boolean;
  incomplete: boolean;
  /**
   * True once any descendant run reported `observe-only`. A graph whose traffic
   * partly bypassed the gateway cannot be labelled plainly optimized, so the
   * root under-claims instead of averaging.
   */
  observeOnly: boolean;
};

function rootExecutionContext(
  definition: AgentDefinition,
  maxCostUsd?: number,
  options: Pick<RunOptions, "budget" | "breakers" | "maxSubagentInvocations" | "maxConcurrentSubagents"> = {},
): InternalExecutionContext {
  validateAgentGraph(definition);
  if (maxCostUsd !== undefined && (!Number.isFinite(maxCostUsd) || maxCostUsd <= 0)) {
    throw new Error("caveman agent: run maxCostUsd must be positive");
  }
  const rootBudget: InvocationLedger["rootBudget"] = maxCostUsd !== undefined
    ? "legacy_usd"
    : options.budget !== undefined && "maxUsd" in options.budget
      ? "usd"
      : options.budget !== undefined && "maxTokens" in options.budget ? "tokens" : "absent";
  const invocationTrace = Object.freeze({
    traceId: randomBytes(16).toString("hex"),
    spanId: randomBytes(8).toString("hex"),
    parentSpanId: "",
  });
  return Object.freeze({
    rootDefinitionSha256: agentDefinitionSHA256(definition),
    agentPath: Object.freeze([]),
    // Root cap is one more ancestor ledger: root turns reserve against it, and
    // every descendant stacks its own ledger on top of it.
    spendLedgers: Object.freeze(maxCostUsd === undefined ? [] : [{
      limitUsd: maxCostUsd,

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Pass a positive finite number, e.g. maxCostUsd: 5, or omit it entirely to run uncapped
  2. If you meant 'spend nothing', do not run the agent at all — 0 is rejected by design
  3. Validate/normalize values parsed from env vars or config before handing them to the run (Number.isFinite check)
  4. If you also set RunOptions.budget, remove maxCostUsd — the two contracts cannot coexist

Example fix

// before
const cap = Number(process.env.MAX_COST);
await agent.run(input, { maxCostUsd: cap }); // NaN when unset

// after
const cap = Number(process.env.MAX_COST);
await agent.run(input, {
  ...(Number.isFinite(cap) && cap > 0 ? { maxCostUsd: cap } : {}),
});
Defensive patterns

Strategy: validation

Validate before calling

function toPositiveCap(v: unknown): number | undefined {
  return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined;
}
// before the run:
const maxCostUsd = toPositiveCap(config.maxCostUsd);
if (config.maxCostUsd !== undefined && maxCostUsd === undefined) {
  throw new Error(`invalid maxCostUsd: ${String(config.maxCostUsd)}`);
}

Type guard

const isPositiveCap = (v: unknown): v is number =>
  typeof v === "number" && Number.isFinite(v) && v > 0;

Try / catch

try { await agent.run(input, opts); } catch (e) {
  if (e instanceof Error && e.message.includes("maxCostUsd must be positive")) {
    // config bug: fix the cap at its source, do not retry
  } else throw e;
}

Prevention

When it happens

Trigger: Calling run/stream with maxCostUsd: 0, a negative number, NaN, or Infinity. Passing a value read from an unset env var (NaN) or a computed cap that rounds to 0.

Common situations: maxCostUsd: Number(process.env.MY_CAP) with the variable unset or empty; a safety 'disable spending' attempt by setting the cap to 0; dividing a budget by a count and getting 0 or a non-finite value.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/83ed14a957255580. Report an issue: GitHub.