{"record":{"id":"e25b07a8c6f58aee","repo":"windmill-labs/windmill","slug":"unknown-tool-call-functionname-probably-not-i","errorCode":null,"errorMessage":"Unknown tool call: ${functionName}. Probably not in the correct mode, use the change_mode tool to switch to the correct mode.","messagePattern":"Unknown tool call: (.+?)\\. Probably not in the correct mode, use the change_mode tool to switch to the correct mode\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/lib/components/copilot/chat/shared.ts","lineNumber":717,"sourceCode":"\ttools,\n\tfunctionName,\n\targs,\n\tworkspace,\n\thelpers,\n\ttoolCallbacks,\n\ttoolId\n}: {\n\ttools: Tool<T>[]\n\tfunctionName: string\n\targs: any\n\tworkspace: string\n\thelpers: T\n\ttoolCallbacks: ToolCallbacks\n\ttoolId: string\n}): Promise<string> {\n\tconst tool = tools.find((t) => t.def.function.name === functionName)\n\tif (!tool) {\n\t\tthrow new Error(\n\t\t\t`Unknown tool call: ${functionName}. Probably not in the correct mode, use the change_mode tool to switch to the correct mode.`\n\t\t)\n\t}\n\tconst result = await tool.fn({ args, workspace, helpers, toolCallbacks, toolId })\n\ttoolCompletionListener?.(functionName, args)\n\treturn result\n}\n\ntype MaybePromise<T> = T | Promise<T>\n\n/** A refused tool call: the row the user reads, and the result the model gets. A bare string\n * is both at once. */\nexport type ToolRejection = string | { label: string; result: string }\n\nfunction normalizeToolRejection(\n\trejection: ToolRejection | undefined\n): { label: string; result: string } | undefined {\n\tif (rejection === undefined) return undefined","sourceCodeStart":699,"sourceCodeEnd":735,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/frontend/src/lib/components/copilot/chat/shared.ts#L699-L735","documentation":"callTool looks up the requested function name among the tools registered for the CURRENT chat mode. If no tool matches, it throws this error hinting that the model is probably in the wrong mode and should call change_mode to switch. It signals a mode/toolset mismatch between what the model emitted and the active tool list.","triggerScenarios":"The model emitted a tool call whose function name exists only in another mode's toolset (e.g. an app-mode or flow-mode tool while the chat is in script mode), or a stale/renamed tool name was sent, and processToolCall passed it to callTool where tools.find returned undefined.","commonSituations":"User switches topics mid-chat (asks a flow question while in script mode) and the model guesses a flow tool name; the model hallucinates a tool name not in any toolset; frontend mode state drifted from the toolset that was registered in the LLM request.","solutions":["Follow the hint: have the model call the change_mode tool to switch to the mode that owns the tool, then retry the call.","Verify the frontend/chat mode matches the user's intent (script vs flow vs app) before continuing the conversation.","If the tool name looks right but fails, check for recent renames of the tool def and re-send with the current name.","Restart the chat so the toolset registration and model state are consistent."],"exampleFix":"// before (model in script mode calls flow tool)\n{\"name\": \"update_flow_value\", ...} // -> Unknown tool call\n// after\n{\"name\": \"change_mode\", \"arguments\": {\"new_mode\": \"flow\"}}\nthen\n{\"name\": \"update_flow_value\", ...}","handlingStrategy":"try-catch","validationCode":"const available = tools.map((t) => t.def.function.name)\nif (!available.includes(functionName)) {\n  // ask the model to call change_mode first, or remap the name\n  return requestModeSwitch(knownAliasFor(functionName))\n}","typeGuard":"function isKnownTool<T extends { def: { function: { name: string } } }>(tools: T[], name: string): T | undefined {\n  return tools.find((t) => t.def.function.name === name)\n}","tryCatchPattern":"try {\n  result = await callTool({ functionName, tools, ... })\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Unknown tool call:')) {\n    // feed the message back to the model instructing a change_mode call, then retry\n  }\n}","preventionTips":["Keep the tool list sent to the LLM in sync with the active chat mode.","Register the change_mode tool in every mode's toolset so the model can recover itself.","Validate tool names against the active toolset before forwarding model output.","Alias deprecated/renamed tool names to their new definitions."],"tags":["copilot","tool-routing","mode-mismatch"],"backgroundTag":"unknown-tool","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"}