JuliusBrussee/caveman · error
maxLoadedTools must be a positive integer
Error message
maxLoadedTools must be a positive integer
What it means
The `tools()` factory validates `config.maxLoadedTools` at construction time. It must be a positive integer (>= 1), and `undefined` is allowed to fall back to `initialToolCount`-driven behavior. This fails fast so a bad budget doesn't silently break lazy tool loading later.
Solutions
- Ensure maxLoadedTools is a positive safe integer before calling tools()
- Omit maxLoadedTools entirely to rely on initialToolCount
- Clamp/round the value: Math.max(1, Math.round(n))
Example fix
// before
const toolsApi = cave.tools({ catalog, maxLoadedTools: Number(opts.limit) });
// after
const limit = Number(opts.limit);
const maxLoadedTools = Number.isSafeInteger(limit) && limit >= 1 ? limit : undefined;
const toolsApi = cave.tools({ catalog, maxLoadedTools }); Defensive patterns
Strategy: validation
Validate before calling
function validMaxLoadedTools(n) {
return n === undefined || (Number.isSafeInteger(n) && n >= 1);
}
if (!validMaxLoadedTools(cfg.maxLoadedTools)) throw new Error("maxLoadedTools must be a positive integer"); Type guard
const isPositiveInt = (v: unknown): v is number => typeof v === "number" && Number.isSafeInteger(v) && v >= 1;
Try / catch
try {
const api = cave.tools({ catalog, maxLoadedTools });
} catch (e) {
if (e.message.includes("maxLoadedTools must be a positive integer")) {
return cave.tools({ catalog }); // fall back to defaults
}
throw e;
} Prevention
- Validate numeric config from env/CLI with a schema (zod etc.) before constructing
- Use Math.max(1, Math.round(x)) when deriving limits from computed values
- Omit the option instead of passing 0/null
When it happens
Trigger: Calling `tools({ catalog, maxLoadedTools: 0 })`, a negative number, a non-integer like 2.5, or NaN/Infinity (all fail `Number.isSafeInteger`).
Common situations: Computing the limit from user config or env parsing that yields 0, a float after division, or an unparsed NaN string.
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
- maxLoadedTools must be at least the alwaysLoad tool count
- cave_mastra_max_steps_invalid
- caveman agent: context id is required
- caveman agent: memory recallBudget must be a non-negative…
- invalid_deadline
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/e169980f749d0609.
Report an issue: GitHub.
Appendix: source
Thrown at packages/sdk/typescript/src/index.ts:478
*
* strategy "all" → initial = whole catalog; search() still calls the server.
* strategy "deferred" → initial = alwaysLoad tools (subset sent on first turn);
* search() calls the server to load relevant tools on demand.
*
* Breaking change from 1.0: search() is now async and returns ToolSearchResult
* (previously sync, returning CaveTool[]). Callers must await search().
*/
tools(config: { catalog: CaveTool[]; strategy?: "all" | "deferred"; initialToolCount?: number; maxLoadedTools?: number }) {
const strategy = config.strategy ?? "all";
if (strategy !== "all" && strategy !== "deferred") {
throw new Error("strategy must be all or deferred");
}
const initialToolCount = config.initialToolCount ?? 8;
if (!Number.isSafeInteger(initialToolCount) || initialToolCount < 0) {
throw new Error("initialToolCount must be a non-negative integer");
}
if (config.maxLoadedTools !== undefined && (!Number.isSafeInteger(config.maxLoadedTools) || config.maxLoadedTools < 1)) {
throw new Error("maxLoadedTools must be a positive integer");
}
let initial: CaveTool[];
if (strategy === "all") {
initial = config.catalog.slice();
} else {
const always = config.catalog.filter((tool) => tool.alwaysLoad);
if (config.maxLoadedTools !== undefined && always.length > config.maxLoadedTools) {
throw new Error("maxLoadedTools must be at least the alwaysLoad tool count");
}
const lazy = config.catalog.filter((tool) => !tool.alwaysLoad);
const lazyLimit = config.maxLoadedTools === undefined
? initialToolCount
: Math.min(initialToolCount, config.maxLoadedTools - always.length);
initial = always.concat(lazy.slice(0, lazyLimit));
}
const cave = this;
return {View on GitHub (pinned to 3ee70a1026)