JuliusBrussee/caveman · error · Error

caveman agent: invalid tool name ${JSON.stringify(options.na

Error message

caveman agent: invalid tool name ${JSON.stringify(options.name)}

What it means

The tool() factory validates that the tool name matches /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/ — it must start with a letter and contain only letters, digits, underscores, and hyphens, max 128 chars. This keeps names compatible with model function-calling constraints across providers. Any other shape (spaces, dots, leading digits, unicode, empty) throws at definition time with the offending name JSON-stringified.

Source

Thrown at packages/agent/src/primitives.ts:129

  inputJSONSchema?: never;
}

export function tool<TInput extends TSchema, TResult>(
  options: ToolOptions<TInput, TResult>,
): ToolDefinition<Static<TInput>, TResult>;
export function tool<Input, Output, TResult>(
  options: StandardToolOptions<Input, Output, TResult>,
): ToolDefinition<Output, TResult>;
export function tool<Input, Output, TResult>(
  options: StandardJSONToolOptions<Input, Output, TResult>,
): ToolDefinition<Output, TResult>;
export function tool(
  options: ToolOptions<TSchema, unknown> |
    StandardToolOptions<unknown, unknown, unknown> |
    StandardJSONToolOptions<unknown, unknown, unknown>,
): ToolDefinition<unknown, unknown> {
  if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(options.name)) {
    throw new Error(`caveman agent: invalid tool name ${JSON.stringify(options.name)}`);
  }
  if (!["read", "write", "idempotent", "external"].includes(options.effect)) {
    throw new Error(`caveman agent: unknown tool effect ${JSON.stringify(options.effect)}`);
  }
  const result = typeof options.result === "object"
    ? artifactResultPolicy(options.result)
    : options.result ?? "auto";
  if (!["auto", "inline", "page", "compress", "exact_ccr"].includes(result)) {
    throw new Error(`caveman agent: unknown tool result policy ${JSON.stringify(result)}`);
  }
  const timeoutMs = options.timeoutMs ?? 30_000;
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
    throw new Error("caveman agent: tool timeoutMs must be a positive integer");
  }
  const standard = standardToolSchema(options.input);
  let input: TSchema;
  if (standard === undefined) {
    input = options.input as TSchema;

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Rename the tool to match the pattern, e.g. "fs_read_file" or "searchFiles"
  2. Slugify generated names: name.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^[^a-zA-Z]+/, "").slice(0, 128)
  3. Validate user-provided tool names at your own boundary and reject early with better context
  4. Keep a constant table of tool names instead of deriving them dynamically

Example fix

// before
const t = tool({ name: "fs.readFile", effect: "read", execute: ... });

// after
const t = tool({ name: "fs_read_file", effect: "read", execute: ... });
Defensive patterns

Strategy: validation

Validate before calling

const TOOL_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/;
function safeToolName(raw: string): string {
  const name = raw.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^[^a-zA-Z]+/, "").slice(0, 128);
  if (!TOOL_NAME_RE.test(name)) throw new Error(`cannot derive valid tool name from ${JSON.stringify(raw)}`);
  return name;
}

Type guard

const isValidToolName = (name: string): boolean =>
  /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(name);

Prevention

When it happens

Trigger: Naming a tool "fs.readFile", "search files", "2fa_check", "", "my tool!", or a name longer than 128 characters. The validation happens synchronously inside tool(), so the throw occurs during setup, not during a run.

Common situations: Deriving tool names from user input, file names, or i18n strings; converting an OpenAI-style function name with dots; or generating names from templates that occasionally emit an empty suffix.

Related errors


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