JuliusBrussee/caveman · error
cave_${field}_invalid
Error message
cave_${field}_invalid What it means
Dynamic template error: field is "max_model_calls" or "max_tool_calls", producing cave_max_model_calls_invalid / cave_max_tool_calls_invalid. Caller-supplied ceilings (RunOptions.maxModelCalls, RunOptions.maxToolCalls) fail closed: any value that is not an integer >= 1 is a malformed cap, not 'no cap', and is rejected before the first provider call. Defaults are derived (64 calls without a plan; plan-derived with one) when the options are omitted.
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 27d5a3981a)
Solutions
- Pass an integer >= 1, e.g. maxModelCalls: 8, or omit to use the derived default
- For 'exactly one model call', use maxModelCalls: 1, never 0
- Sanitize computed values: Math.max(1, Math.floor(n))
Example fix
// before
await agent.run(input, { maxModelCalls: calls }); // calls = 0 from unset config
// after
const calls = Number(process.env.MAX_CALLS);
await agent.run(input, {
...(Number.isInteger(calls) && calls >= 1 ? { maxModelCalls: calls } : {}),
}); Defensive patterns
Strategy: validation
Validate before calling
function callCeiling(v: unknown): number | undefined {
return typeof v === "number" && Number.isInteger(v) && v >= 1 ? v : undefined;
}
const opts = {
...(callCeiling(config.maxModelCalls) !== undefined ? { maxModelCalls: callCeiling(config.maxModelCalls)! } : {}),
...(callCeiling(config.maxToolCalls) !== undefined ? { maxToolCalls: callCeiling(config.maxToolCalls)! } : {}),
}; Type guard
const isCallCeiling = (v: unknown): v is number => typeof v === "number" && Number.isInteger(v) && v >= 1;
Try / catch
try { await agent.run(input, opts); } catch (e) {
if (e instanceof Error && /^cave_(max_model_calls|max_tool_calls)_invalid$/.test(e.message)) {
// config bug: fix the ceiling value
} else throw e;
} Prevention
- Use 1 for single-shot runs, never 0
- Validate ceilings alongside other integer run options in one helper
- Omit the options to accept the derived defaults
When it happens
Trigger: Passing maxModelCalls: 0, a negative number, a float like 8.5, or NaN; same for maxToolCalls. Computed ceilings from config that default to 0 are the usual source.
Common situations: Config-driven caps where an unset value coerces to 0; trying to express 'one shot only' with 0 (should be 1); dividing budgets into call counts and passing a fractional result.
Related errors
- option not found
- cave_harness_adapter_version_invalid
- caveman agent: subagent maxInputChars must be a positive int
- caveman agent: run maxCostUsd must be positive
- cave_subagent_invocation_limit_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/30b6ae8ad442806f.
Report an issue: GitHub.