From c431053a1e24ef29cd551a86de4d013fd7f158be Mon Sep 17 00:00:00 2001 From: hugocasa Date: Fri, 13 Mar 2026 23:30:53 +0100 Subject: [PATCH] fix(frontend): prevent duplicate and reserved agent tool names (#8367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): prevent duplicate and reserved agent tool names Extend tool name validation to detect duplicates within an agent step and reserved names (like 'preprocessor', 'failure'). Show specific error messages in the editor panel and red styling in the graph view. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(frontend): remove duplicate banner for agent tool name errors The inline per-tool error messages are sufficient — the panel-level banner was redundant and showed a double error. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .../lib/components/copilot/MetadataGen.svelte | 18 +++++--- .../components/flows/common/FlowCard.svelte | 5 ++- .../flows/common/FlowCardHeader.svelte | 29 ++++++++----- .../flows/content/AgentToolWrapper.svelte | 5 ++- .../flows/content/FlowEditorPanel.svelte | 4 +- .../flows/content/FlowModuleComponent.svelte | 13 ++++-- .../flows/content/FlowModuleWrapper.svelte | 1 + .../components/graph/graphBuilder.svelte.ts | 1 + .../graph/renderers/nodes/AIToolNode.svelte | 42 +++++++++++++------ 9 files changed, 81 insertions(+), 37 deletions(-) diff --git a/frontend/src/lib/components/copilot/MetadataGen.svelte b/frontend/src/lib/components/copilot/MetadataGen.svelte index bb18b3dc1e..1ef4b2ff08 100644 --- a/frontend/src/lib/components/copilot/MetadataGen.svelte +++ b/frontend/src/lib/components/copilot/MetadataGen.svelte @@ -12,7 +12,7 @@ import { yamlStringifyExceptKeys } from './utils' import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs' import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte' - import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte' + import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte' import { inputBaseClass, inputBorderClass, @@ -117,6 +117,7 @@ Generate a tool name for the script below: elementProps?: Record class?: string onChange?: (content: string) => void + siblingToolNames?: string[] } let { @@ -130,9 +131,16 @@ Generate a tool name for the script below: elementType = 'input', elementProps = {}, class: clazz = '', - onChange = undefined + onChange = undefined, + siblingToolNames = undefined }: Props = $props() + let toolNameError = $derived( + promptConfigName === 'agentToolFunctionName' + ? getToolNameError(content ?? '', undefined, siblingToolNames) + : undefined + ) + let el: HTMLElement | undefined = $state() let generatedContent = $state('') let active = $state(false) @@ -347,16 +355,16 @@ Generate a tool name for the script below: inputBaseClass, inputSizeClasses.md, inputBorderClass({ - error: promptConfigName === 'agentToolFunctionName' && !validateToolName(content ?? '') + error: !!toolNameError }), 'w-full' )} onfocus={() => (focused = true)} onblur={() => (focused = false)} /> - {#if promptConfigName === 'agentToolFunctionName' && !validateToolName(content ?? '')} + {#if toolNameError}

- Invalid tool name, should only contain letters, numbers and underscores + {toolNameError}

{/if} {/if} diff --git a/frontend/src/lib/components/flows/common/FlowCard.svelte b/frontend/src/lib/components/flows/common/FlowCard.svelte index f780331a82..43cabc7aa0 100644 --- a/frontend/src/lib/components/flows/common/FlowCard.svelte +++ b/frontend/src/lib/components/flows/common/FlowCard.svelte @@ -12,6 +12,7 @@ action?: import('svelte').Snippet children?: import('svelte').Snippet isAgentTool?: boolean + siblingToolNames?: string[] } let { @@ -23,7 +24,8 @@ header, action, children, - isAgentTool = false + isAgentTool = false, + siblingToolNames = undefined }: Props = $props() @@ -38,6 +40,7 @@ {flowModuleValue} {action} {isAgentTool} + {siblingToolNames} > {@render header?.()} diff --git a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte index 401bf232b1..a5ff48053e 100644 --- a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte +++ b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte @@ -18,7 +18,7 @@ import { Flag, Lock, RefreshCw, Unlock } from 'lucide-svelte' import { createEventDispatcher, untrack } from 'svelte' import { twMerge } from 'tailwind-merge' - import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte' + import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte' import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub' interface Props { @@ -28,6 +28,7 @@ children?: import('svelte').Snippet action?: import('svelte').Snippet isAgentTool?: boolean + siblingToolNames?: string[] } let { @@ -36,9 +37,14 @@ summary = $bindable(undefined), children, action, - isAgentTool = false + isAgentTool = false, + siblingToolNames = undefined }: Props = $props() + let toolNameError = $derived( + isAgentTool ? getToolNameError(summary ?? '', undefined, siblingToolNames) : undefined + ) + let latestHash: string | undefined = $state(undefined) // Extract version_id from hub path (format: hub/{version_id}/{app}/{summary}) @@ -103,6 +109,7 @@ elementProps={{ placeholder: isAgentTool ? 'Tool name' : 'Summary' }} + {siblingToolNames} /> {:else if flowModuleValue.type === 'script' && 'path' in flowModuleValue && flowModuleValue.path} @@ -173,14 +180,16 @@ /> {/if} - +
+ + {#if toolNameError} +

{toolNameError}

+ {/if} +
{:else if flowModuleValue.type === 'flow'} flow diff --git a/frontend/src/lib/components/flows/content/AgentToolWrapper.svelte b/frontend/src/lib/components/flows/content/AgentToolWrapper.svelte index d624c2132f..9807e96603 100644 --- a/frontend/src/lib/components/flows/content/AgentToolWrapper.svelte +++ b/frontend/src/lib/components/flows/content/AgentToolWrapper.svelte @@ -14,6 +14,7 @@ previousModule?: FlowModule | undefined forceTestTab?: Record highlightArg?: Record + siblingToolNames?: string[] } let { @@ -23,7 +24,8 @@ parentModule = undefined, previousModule = undefined, forceTestTab, - highlightArg + highlightArg, + siblingToolNames = undefined }: Props = $props() @@ -43,6 +45,7 @@ forceTestTab={forceTestTab?.[tool.id]} highlightArg={highlightArg?.[tool.id]} isAgentTool={true} + {siblingToolNames} /> {:else if isMcpTool(tool)} diff --git a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte index c8418e5183..3d9a533f48 100644 --- a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte +++ b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte @@ -101,9 +101,7 @@ ) let canMoveSelected = $derived( resolvedModuleIds.length > 0 && - areContiguousSiblings( - locateModules(resolvedModuleIds, flowStore.val.value.modules ?? []) - ) + areContiguousSiblings(locateModules(resolvedModuleIds, flowStore.val.value.modules ?? [])) ) diff --git a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte index 4a43c2c38d..d9da29f01e 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte @@ -110,6 +110,7 @@ forceTestTab?: boolean highlightArg?: string isAgentTool?: boolean + siblingToolNames?: string[] } let { @@ -125,7 +126,8 @@ savedModule = undefined, forceTestTab = false, highlightArg = undefined, - isAgentTool = false + isAgentTool = false, + siblingToolNames = undefined }: Props = $props() let workspaceScriptTag: string | undefined = $state(undefined) @@ -237,7 +239,9 @@ } let forceReload = $state(0) - let editorPanelSize = $state(untrack(() => noEditor) ? 0 : flowModule.value.type == 'script' ? 30 : 50) + let editorPanelSize = $state( + untrack(() => noEditor) ? 0 : flowModule.value.type == 'script' ? 30 : 50 + ) let editorSettingsPanelSize = $state(100 - untrack(() => editorPanelSize)) let stepHistoryLoader = getStepHistoryLoaderContext() @@ -726,6 +730,7 @@ }} bind:summary={flowModule.summary} {isAgentTool} + {siblingToolNames} > {#snippet header()} diff --git a/frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte b/frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte index cb50e8a327..c58819d59c 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte @@ -305,6 +305,7 @@ {enableAi} {forceTestTab} {highlightArg} + siblingToolNames={flowModule.value.tools.map((t) => t.summary ?? '')} /> {/if} {/each} diff --git a/frontend/src/lib/components/graph/graphBuilder.svelte.ts b/frontend/src/lib/components/graph/graphBuilder.svelte.ts index 1d01b2e252..192b5c0c34 100644 --- a/frontend/src/lib/components/graph/graphBuilder.svelte.ts +++ b/frontend/src/lib/components/graph/graphBuilder.svelte.ts @@ -300,6 +300,7 @@ export type AiToolN = { data: { tool: string type?: string + nameError?: string eventHandlers: GraphEventHandlers moduleId: string insertable: boolean diff --git a/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte index 28e9d58fa4..1913d927c8 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte @@ -1,10 +1,29 @@