JuliusBrussee/caveman · error

caveman agent: invalid tool name

Error message

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

What it means

tool() validates the tool name against /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/ before building the ToolDefinition. A name must start with a letter, contain only letters, digits, underscores and hyphens, and be at most 128 chars. Anything else throws this error so invalid identifiers never reach the model/provider layer.

Solutions

  1. Rename the tool to match ^[a-zA-Z][a-zA-Z0-9_-]{0,127}$ (letter first, then letters/digits/_/-)
  2. Sanitize dynamic names: strip invalid chars, replace separators with underscores, and ensure the first character is a letter
  3. Truncate or shorten generated names to 128 characters or fewer

Example fix

// before
tool({ name: `${repoId}.search`, ... }) // dot invalid, may start with digit
// after
const name = `tool_${repoId}_search`.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/^[^a-zA-Z]/, "t").slice(0, 128);
tool({ name, ... })
Defensive patterns

Strategy: validation

Validate before calling

const TOOL_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/;
function validToolName(name: unknown): name is string {
  return typeof name === "string" && TOOL_NAME_RE.test(name);
}

Type guard

function isToolName(v: unknown): v is `${string}` {
  return typeof v === "string" && /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(v);
}

Try / catch

try {
  const def = tool(options);
} catch (err) {
  if (err instanceof Error && err.message.includes("invalid tool name")) {
    // sanitize options.name and retry once
  } else throw err;
}

Prevention

When it happens

Trigger: tool({ name: "1st_tool" }) (starts with digit), tool({ name: "my tool" }) (space), tool({ name: "read.file" }) (dot), names > 128 chars, or an undefined/non-string name.

Common situations: Generating tool names dynamically (e.g. prefixing with a numeric id or including a namespace dot); copying a display name with spaces; names built by slugify functions that leave dots or slashes; exceeding the 128-char cap with long generated names.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


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

Appendix: 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 3ee70a1026)