JuliusBrussee/caveman · error · Error

cave_${field}_invalid

cave_${field}_invalid

Error message

cave_${field}_invalid

What it means

Template error thrown by the callCeilingOverride validator when a caller-supplied ceiling (e.g. maxModelCalls) is not a positive integer. The library treats a malformed cap as fail-closed data rather than "no cap", rejecting before any provider call.

Solutions

  1. Pass a positive integer (>= 1) for the ceiling option
  2. Coerce/round the value before calling: Math.max(1, Math.round(value))
  3. Omit the option to use the derived default (64 without an efficiency plan)

Example fix

// before
streamAgent({ maxModelCalls: 0 });
// after
streamAgent({ maxModelCalls: Math.max(1, Math.round(userCap)) });
Defensive patterns

Strategy: validation

Validate before calling

function assertCeiling(v) { if (v !== undefined && (!Number.isInteger(v) || v < 1)) throw new Error(`invalid ceiling: ${v}`); }
assertCeiling(opts.maxModelCalls);

Type guard

const isValidCeiling = (v: unknown): v is number => typeof v === "number" && Number.isInteger(v) && v >= 1;

Try / catch

try { await streamAgent(opts); } catch (e) { if (/^cave_.*_invalid$/.test(e.message)) { /* fix the named ceiling option */ } else throw e; }

Prevention

When it happens

Trigger: Passing options.maxModelCalls (or any validated ceiling field) as 0, a negative number, a float (e.g. 1.5), or a non-integer value to streamAgent.

Common situations: Reading the cap from env/config as a string-ish or computed value, dividing to compute a cap, or defaulting with 0 intending "unlimited".

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/30b6ae8ad442806f. Report an issue: GitHub.

Appendix: source

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

    let modelCalls = 0;
    let compactionsUsed = 0;
    // Incremented the moment a compaction takes a reservation, so a paid
    // attempt counts even when its summary is later discarded.
    let compactionsSpent = 0;
    let previousSummary: ContextSummary | undefined;
    // Provider-reported, never assumed: the last call either read a cached
    // prefix or it did not. Before the first call there is nothing to report.
    let lastCallCacheState: "warm" | "cold" | "unknown" = "unknown";
    const toolCalls: string[] = [];
    let turnStateChanged = false;
    const startedAt = performance.now();
    // Caller-supplied ceilings override the derived defaults. They
    // fail closed: a non-positive or non-integer value is a malformed cap, not
    // "no cap", so it is rejected before the first provider call.
    const callCeilingOverride = (value: number | undefined, field: string): number | undefined => {
      if (value === undefined) return undefined;
      if (!Number.isInteger(value) || value < 1) {
        throw new Error(`cave_${field}_invalid`);
      }
      return value;
    };
    const maxModelCalls = callCeilingOverride(options.maxModelCalls, "max_model_calls") ??
      (efficiencyPlan === undefined
        ? 64
        : 1 + Math.max(1, Math.ceil(efficiencyPlan.budgets.retry_cascade_reserve / 256)));
    const maxToolCalls = callCeilingOverride(options.maxToolCalls, "max_tool_calls") ??
      (efficiencyPlan === undefined
        ? 64
        : Math.max(1, definition.tools.length) * Math.max(1, maxModelCalls - 1));
    const accountedAssistantMessages = new WeakSet<object>();
    const accountProviderMessage = (message: AssistantMessage): void => {
      // beforeToolCall runs after the provider message but before its tools.
      // Account there so a call that consumed the deadline or wallet cannot
      // launch fresh side effects. turn_end calls this too for tool-free turns;
      // object identity makes the operation exactly once on either path.
      if (accountedAssistantMessages.has(message)) {

View on GitHub (pinned to 3ee70a1026)