{"record":{"id":"27d492768c5bfa95","repo":"JuliusBrussee/caveman","slug":"caveman-agent-invalid-tool-name-json-stringify","errorCode":null,"errorMessage":"caveman agent: invalid tool name ${JSON.stringify(options.name)}","messagePattern":"caveman agent: invalid tool name (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/primitives.ts","lineNumber":129,"sourceCode":"  inputJSONSchema?: never;\n}\n\nexport function tool<TInput extends TSchema, TResult>(\n  options: ToolOptions<TInput, TResult>,\n): ToolDefinition<Static<TInput>, TResult>;\nexport function tool<Input, Output, TResult>(\n  options: StandardToolOptions<Input, Output, TResult>,\n): ToolDefinition<Output, TResult>;\nexport function tool<Input, Output, TResult>(\n  options: StandardJSONToolOptions<Input, Output, TResult>,\n): ToolDefinition<Output, TResult>;\nexport function tool(\n  options: ToolOptions<TSchema, unknown> |\n    StandardToolOptions<unknown, unknown, unknown> |\n    StandardJSONToolOptions<unknown, unknown, unknown>,\n): ToolDefinition<unknown, unknown> {\n  if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(options.name)) {\n    throw new Error(`caveman agent: invalid tool name ${JSON.stringify(options.name)}`);\n  }\n  if (![\"read\", \"write\", \"idempotent\", \"external\"].includes(options.effect)) {\n    throw new Error(`caveman agent: unknown tool effect ${JSON.stringify(options.effect)}`);\n  }\n  const result = typeof options.result === \"object\"\n    ? artifactResultPolicy(options.result)\n    : options.result ?? \"auto\";\n  if (![\"auto\", \"inline\", \"page\", \"compress\", \"exact_ccr\"].includes(result)) {\n    throw new Error(`caveman agent: unknown tool result policy ${JSON.stringify(result)}`);\n  }\n  const timeoutMs = options.timeoutMs ?? 30_000;\n  if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {\n    throw new Error(\"caveman agent: tool timeoutMs must be a positive integer\");\n  }\n  const standard = standardToolSchema(options.input);\n  let input: TSchema;\n  if (standard === undefined) {\n    input = options.input as TSchema;","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/primitives.ts#L111-L147","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Rename the tool to match the pattern, e.g. \"fs_read_file\" or \"searchFiles\"","Slugify generated names: name.replace(/[^a-zA-Z0-9_-]+/g, \"_\").replace(/^[^a-zA-Z]+/, \"\").slice(0, 128)","Validate user-provided tool names at your own boundary and reject early with better context","Keep a constant table of tool names instead of deriving them dynamically"],"exampleFix":"// before\nconst t = tool({ name: \"fs.readFile\", effect: \"read\", execute: ... });\n\n// after\nconst t = tool({ name: \"fs_read_file\", effect: \"read\", execute: ... });","handlingStrategy":"validation","validationCode":"const TOOL_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/;\nfunction safeToolName(raw: string): string {\n  const name = raw.replace(/[^a-zA-Z0-9_-]+/g, \"_\").replace(/^[^a-zA-Z]+/, \"\").slice(0, 128);\n  if (!TOOL_NAME_RE.test(name)) throw new Error(`cannot derive valid tool name from ${JSON.stringify(raw)}`);\n  return name;\n}","typeGuard":"const isValidToolName = (name: string): boolean =>\n  /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(name);","tryCatchPattern":null,"preventionTips":["Define tool names as constants, not derived strings","Validate user-supplied tool names before registration and surface your own error message","Remember the 128-character cap when generating names from templates"],"tags":["validation","tool-names","api-misuse","llm-compat"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}