{"record":{"id":"17b88bbdc1367066","repo":"windmill-labs/windmill","slug":"invalid-ai-agent-tool-name-s-invalidtoolnames","errorCode":null,"errorMessage":"Invalid AI agent tool name(s): ${invalidToolNames.map((t) => `agent \"${t.agentId}\" tool \"${t.toolId}\" is named ${JSON.stringify(t.name)} - ${t.error}`).join('; ')}. The tool's \"summary\" is the name the agent calls it by: use underscores instead of spaces (e.g. \"search_docs\").","messagePattern":"Invalid AI agent tool name\\(s\\): (.+?)\" tool \"(.+?)\" is named (.+?) - (.+?)`\\)\\.join\\('; '\\)\\}\\. The tool's \"summary\" is the name the agent calls it by: use underscores instead of spaces \\(e\\.g\\. \"search_docs\"\\)\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/lib/components/copilot/chat/flow/editableFlowJson.ts","lineNumber":285,"sourceCode":"\n\t// Not expressible in the schema: `provider` is required only when the step is standalone, and\n\t// making AiAgent a conditional union breaks the FlowModuleValue discriminated union it belongs to.\n\tconst providerless = collectProviderlessAgentIds(parsedModules)\n\tif (providerless.length > 0) {\n\t\tthrow new Error(\n\t\t\t`AI agent modules ${providerless\n\t\t\t\t.map((id) => `\"${id}\"`)\n\t\t\t\t.join(\n\t\t\t\t\t', '\n\t\t\t\t)} need a provider input transform, or an \"agent\" path linking them to a saved agent`\n\t\t)\n\t}\n\n\t// An agent tool's `summary` is the name the LLM sees; the worker rejects anything outside\n\t// `^[a-zA-Z0-9_]+$`, so a flow written with a spaced name saves but fails on every run.\n\tconst invalidToolNames = collectInvalidAgentToolNames(parsedModules)\n\tif (invalidToolNames.length > 0) {\n\t\tthrow new Error(\n\t\t\t`Invalid AI agent tool name(s): ${invalidToolNames\n\t\t\t\t.map(\n\t\t\t\t\t(t) =>\n\t\t\t\t\t\t`agent \"${t.agentId}\" tool \"${t.toolId}\" is named ${JSON.stringify(t.name)} - ${t.error}`\n\t\t\t\t)\n\t\t\t\t.join(\n\t\t\t\t\t'; '\n\t\t\t\t)}. The tool's \"summary\" is the name the agent calls it by: use underscores instead of spaces (e.g. \"search_docs\").`\n\t\t)\n\t}\n\n\tvalidateAiAgentProviders(parsedModules, ctx.aiProviders, ctx.aiProviderWarnings)\n\n\treturn parsedModules\n}\n\nexport function validateFlowSchema(rawSchema: unknown): Record<string, any> | null {\n\tif (rawSchema == null) return null","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/copilot/chat/flow/editableFlowJson.ts#L267-L303","documentation":"validateFlowModules rejects AiAgent tools whose `summary` (the name the LLM calls the tool by) contains characters outside ^[a-zA-Z0-9_]+$. The worker enforces this regex at run time, so a flow with a spaced tool name saves fine but fails on every run — this check moves the failure to edit time. The message lists each offending agent id, tool id, current name, and the specific reason.","triggerScenarios":"Calling flowTools/modules with a flow JSON where an aiagent module's tool has a `summary` like \"Search Docs\" or contains hyphens, spaces, or unicode instead of snake_case identifiers.","commonSituations":"An LLM copies a human-readable tool title into `summary`; a user writes tool names with spaces; localized or formatted names are pasted into tool summaries.","solutions":["Rename each listed tool's `summary` to a snake_case identifier matching ^[a-zA-Z0-9_]+$ (e.g. \"search_docs\").","Re-run the modules tool after fixing all listed agent/tool pairs.","Keep tool summaries as identifier-style names and put human descriptions in the tool's `description` field instead."],"exampleFix":"// before\n{\"type\":\"aiagent\",\"tools\":[{\"id\":\"t1\",\"summary\":\"Search Docs\"}]}\n// after\n{\"type\":\"aiagent\",\"tools\":[{\"id\":\"t1\",\"summary\":\"search_docs\"}]}","handlingStrategy":"validation","validationCode":"const TOOL_NAME_RE = /^[a-zA-Z0-9_]+$/\nconst bad = []\nfor (const m of flow.modules ?? []) {\n  if (m?.value?.type !== 'aiagent') continue\n  for (const t of m.value.tools ?? []) {\n    if (typeof t?.summary === 'string' && !TOOL_NAME_RE.test(t.summary))\n      bad.push({agentId: m.id, toolId: t.id, name: t.summary})\n  }\n}\nif (bad.length) throw new Error(`Invalid tool summaries: ${JSON.stringify(bad)}`)","typeGuard":"function isValidToolName(name) {\n  return typeof name === 'string' && /^[a-zA-Z0-9_]+$/.test(name)\n}","tryCatchPattern":"try {\n  const editable = validateEditableFlowJson(rawFlow)\n} catch (e) {\n  if (String(e.message).startsWith('Invalid AI agent tool name')) {\n    // ask the model to rename each listed tool summary to snake_case\n  } else throw e\n}","preventionTips":["Generate tool summaries as snake_case identifiers only","Put human-readable text in tool descriptions, never in summaries","Run the tool-name regex over agent JSON before submitting a patch"],"tags":["validation","ai-agent","naming"],"backgroundTag":"invalid-identifier-name","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}