Skip to content

Commit c431053

Browse files
hugocasaclaude
andauthored
fix(frontend): prevent duplicate and reserved agent tool names (#8367)
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a079dd5 commit c431053

File tree

9 files changed

+81
-37
lines changed

9 files changed

+81
-37
lines changed

frontend/src/lib/components/copilot/MetadataGen.svelte

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import { yamlStringifyExceptKeys } from './utils'
1313
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'
1414
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
15-
import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
15+
import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
1616
import {
1717
inputBaseClass,
1818
inputBorderClass,
@@ -117,6 +117,7 @@ Generate a tool name for the script below:
117117
elementProps?: Record<string, any>
118118
class?: string
119119
onChange?: (content: string) => void
120+
siblingToolNames?: string[]
120121
}
121122
122123
let {
@@ -130,9 +131,16 @@ Generate a tool name for the script below:
130131
elementType = 'input',
131132
elementProps = {},
132133
class: clazz = '',
133-
onChange = undefined
134+
onChange = undefined,
135+
siblingToolNames = undefined
134136
}: Props = $props()
135137
138+
let toolNameError = $derived(
139+
promptConfigName === 'agentToolFunctionName'
140+
? getToolNameError(content ?? '', undefined, siblingToolNames)
141+
: undefined
142+
)
143+
136144
let el: HTMLElement | undefined = $state()
137145
let generatedContent = $state('')
138146
let active = $state(false)
@@ -347,16 +355,16 @@ Generate a tool name for the script below:
347355
inputBaseClass,
348356
inputSizeClasses.md,
349357
inputBorderClass({
350-
error: promptConfigName === 'agentToolFunctionName' && !validateToolName(content ?? '')
358+
error: !!toolNameError
351359
}),
352360
'w-full'
353361
)}
354362
onfocus={() => (focused = true)}
355363
onblur={() => (focused = false)}
356364
/>
357-
{#if promptConfigName === 'agentToolFunctionName' && !validateToolName(content ?? '')}
365+
{#if toolNameError}
358366
<p class="text-3xs text-red-400 leading-tight mt-0.5">
359-
Invalid tool name, should only contain letters, numbers and underscores
367+
{toolNameError}
360368
</p>
361369
{/if}
362370
{/if}

frontend/src/lib/components/flows/common/FlowCard.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
action?: import('svelte').Snippet
1313
children?: import('svelte').Snippet
1414
isAgentTool?: boolean
15+
siblingToolNames?: string[]
1516
}
1617
1718
let {
@@ -23,7 +24,8 @@
2324
header,
2425
action,
2526
children,
26-
isAgentTool = false
27+
isAgentTool = false,
28+
siblingToolNames = undefined
2729
}: Props = $props()
2830
</script>
2931

@@ -38,6 +40,7 @@
3840
{flowModuleValue}
3941
{action}
4042
{isAgentTool}
43+
{siblingToolNames}
4144
>
4245
{@render header?.()}
4346
</FlowCardHeader>

frontend/src/lib/components/flows/common/FlowCardHeader.svelte

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { Flag, Lock, RefreshCw, Unlock } from 'lucide-svelte'
1919
import { createEventDispatcher, untrack } from 'svelte'
2020
import { twMerge } from 'tailwind-merge'
21-
import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
21+
import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
2222
import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub'
2323
2424
interface Props {
@@ -28,6 +28,7 @@
2828
children?: import('svelte').Snippet
2929
action?: import('svelte').Snippet
3030
isAgentTool?: boolean
31+
siblingToolNames?: string[]
3132
}
3233
3334
let {
@@ -36,9 +37,14 @@
3637
summary = $bindable(undefined),
3738
children,
3839
action,
39-
isAgentTool = false
40+
isAgentTool = false,
41+
siblingToolNames = undefined
4042
}: Props = $props()
4143
44+
let toolNameError = $derived(
45+
isAgentTool ? getToolNameError(summary ?? '', undefined, siblingToolNames) : undefined
46+
)
47+
4248
let latestHash: string | undefined = $state(undefined)
4349
4450
// Extract version_id from hub path (format: hub/{version_id}/{app}/{summary})
@@ -103,6 +109,7 @@
103109
elementProps={{
104110
placeholder: isAgentTool ? 'Tool name' : 'Summary'
105111
}}
112+
{siblingToolNames}
106113
/>
107114
{:else if flowModuleValue.type === 'script' && 'path' in flowModuleValue && flowModuleValue.path}
108115
<IconedPath path={flowModuleValue.path} hash={flowModuleValue.hash} class="grow" />
@@ -173,14 +180,16 @@
173180
/>
174181
</div>
175182
{/if}
176-
<input
177-
bind:value={summary}
178-
placeholder={isAgentTool ? 'Tool name' : 'Summary'}
179-
class={twMerge(
180-
'w-full grow',
181-
isAgentTool && !validateToolName(summary ?? '') && '!border-red-400'
182-
)}
183-
/>
183+
<div class="flex flex-col w-full grow">
184+
<input
185+
bind:value={summary}
186+
placeholder={isAgentTool ? 'Tool name' : 'Summary'}
187+
class={twMerge('w-full grow', toolNameError && '!border-red-400')}
188+
/>
189+
{#if toolNameError}
190+
<p class="text-3xs text-red-400 leading-tight mt-0.5">{toolNameError}</p>
191+
{/if}
192+
</div>
184193
{:else if flowModuleValue.type === 'flow'}
185194
<Badge color="indigo" capitalize>flow</Badge>
186195
<input bind:value={summary} placeholder="Summary" class="w-full grow" />

frontend/src/lib/components/flows/content/AgentToolWrapper.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
previousModule?: FlowModule | undefined
1515
forceTestTab?: Record<string, boolean>
1616
highlightArg?: Record<string, string | undefined>
17+
siblingToolNames?: string[]
1718
}
1819
1920
let {
@@ -23,7 +24,8 @@
2324
parentModule = undefined,
2425
previousModule = undefined,
2526
forceTestTab,
26-
highlightArg
27+
highlightArg,
28+
siblingToolNames = undefined
2729
}: Props = $props()
2830
</script>
2931

@@ -43,6 +45,7 @@
4345
forceTestTab={forceTestTab?.[tool.id]}
4446
highlightArg={highlightArg?.[tool.id]}
4547
isAgentTool={true}
48+
{siblingToolNames}
4649
/>
4750
{:else if isMcpTool(tool)}
4851
<!-- MCP tool - use McpToolEditor -->

frontend/src/lib/components/flows/content/FlowEditorPanel.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,7 @@
101101
)
102102
let canMoveSelected = $derived(
103103
resolvedModuleIds.length > 0 &&
104-
areContiguousSiblings(
105-
locateModules(resolvedModuleIds, flowStore.val.value.modules ?? [])
106-
)
104+
areContiguousSiblings(locateModules(resolvedModuleIds, flowStore.val.value.modules ?? []))
107105
)
108106
</script>
109107

frontend/src/lib/components/flows/content/FlowModuleComponent.svelte

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
forceTestTab?: boolean
111111
highlightArg?: string
112112
isAgentTool?: boolean
113+
siblingToolNames?: string[]
113114
}
114115
115116
let {
@@ -125,7 +126,8 @@
125126
savedModule = undefined,
126127
forceTestTab = false,
127128
highlightArg = undefined,
128-
isAgentTool = false
129+
isAgentTool = false,
130+
siblingToolNames = undefined
129131
}: Props = $props()
130132
131133
let workspaceScriptTag: string | undefined = $state(undefined)
@@ -237,7 +239,9 @@
237239
}
238240
239241
let forceReload = $state(0)
240-
let editorPanelSize = $state(untrack(() => noEditor) ? 0 : flowModule.value.type == 'script' ? 30 : 50)
242+
let editorPanelSize = $state(
243+
untrack(() => noEditor) ? 0 : flowModule.value.type == 'script' ? 30 : 50
244+
)
241245
let editorSettingsPanelSize = $state(100 - untrack(() => editorPanelSize))
242246
let stepHistoryLoader = getStepHistoryLoaderContext()
243247
@@ -726,6 +730,7 @@
726730
}}
727731
bind:summary={flowModule.summary}
728732
{isAgentTool}
733+
{siblingToolNames}
729734
>
730735
{#snippet header()}
731736
<FlowModuleHeader
@@ -1062,8 +1067,8 @@
10621067
{enableAi}
10631068
{isAgentTool}
10641069
allowedAiTransforms={isAgentTool && flowModule.value.type === 'aiagent'
1065-
? ['user_message']
1066-
: undefined}
1070+
? ['user_message']
1071+
: undefined}
10671072
helperScript={retrieveDynCodeAndLang(flowModule.value)}
10681073
chatInputEnabled={flowStore.val.value?.chat_input_enabled ?? false}
10691074
/>

frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@
305305
{enableAi}
306306
{forceTestTab}
307307
{highlightArg}
308+
siblingToolNames={flowModule.value.tools.map((t) => t.summary ?? '')}
308309
/>
309310
{/if}
310311
{/each}

frontend/src/lib/components/graph/graphBuilder.svelte.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ export type AiToolN = {
300300
data: {
301301
tool: string
302302
type?: string
303+
nameError?: string
303304
eventHandlers: GraphEventHandlers
304305
moduleId: string
305306
insertable: boolean

frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
11
<script module lang="ts">
2-
export function validateToolName(name: string, type?: string) {
3-
if (type === 'websearch') return true
2+
import { forbiddenIds } from '$lib/components/flows/idUtils'
3+
4+
export function getToolNameError(
5+
name: string,
6+
type?: string,
7+
siblingNames?: string[]
8+
): string | undefined {
9+
if (type === 'websearch') return undefined
410
if (type === 'mcp') {
5-
return name.length > 0
11+
return name.length > 0 ? undefined : 'Tool name must not be empty'
12+
}
13+
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
14+
return 'Tool name must only contain letters, numbers and underscores'
15+
}
16+
if (forbiddenIds.includes(name)) {
17+
return `'${name}' is a reserved name`
618
}
7-
return /^[a-zA-Z0-9_]+$/.test(name)
19+
if (siblingNames && siblingNames.filter((n) => n === name).length > 1) {
20+
return 'Duplicate tool name'
21+
}
22+
return undefined
23+
}
24+
25+
export function validateToolName(name: string, type?: string) {
26+
return getToolNameError(name, type) === undefined
827
}
928
1029
export const AI_TOOL_BASE_OFFSET = 5
@@ -147,6 +166,7 @@
147166
}
148167
}
149168
169+
const siblingNames = tools.map((t) => t.name)
150170
const toolNodes: (Node & AiToolN)[] = tools.map((tool, i) => {
151171
let inputToolXGap = 12
152172
let inputToolWidth = (ROW_WIDTH - inputToolXGap) / 2
@@ -160,6 +180,7 @@
160180
data: {
161181
tool: tool.name,
162182
type: tool.type,
183+
nameError: getToolNameError(tool.name, tool.type, siblingNames),
163184
eventHandlers,
164185
moduleId: tool.id,
165186
insertable,
@@ -169,13 +190,13 @@
169190
width: inputToolWidth,
170191
position: {
171192
x:
172-
(tools.length === 1
193+
tools.length === 1
173194
? (ROW_WIDTH - inputToolWidth) / 2
174195
: (i + 1) % 2 === 0
175196
? inputToolWidth + inputToolXGap
176197
: isLastRow && tools.length % 2 === 1
177198
? (ROW_WIDTH - inputToolWidth) / 2
178-
: 0),
199+
: 0,
179200
y:
180201
baseOffset +
181202
rowOffset *
@@ -287,7 +308,7 @@
287308
const flowModuleState = $derived(data.flowModuleStates?.[data.moduleId])
288309
let colorClasses = $derived(
289310
getNodeColorClasses(
290-
!validateToolName(data.tool, data.type) ? 'Failure' : flowModuleState?.type,
311+
data.nameError ? 'Failure' : flowModuleState?.type,
291312
selectionManager?.getSelectedId() === data.moduleId
292313
)
293314
)
@@ -324,12 +345,7 @@
324345
<Wrench size={16} class="ml-1 shrink-0" />
325346
{/if}
326347

327-
<span
328-
class={twMerge(
329-
'text-3xs truncate flex-1',
330-
!validateToolName(data.tool, data.type) && 'text-red-400'
331-
)}
332-
>
348+
<span class={twMerge('text-3xs truncate flex-1', data.nameError && 'text-red-400')}>
333349
{data.tool || 'Missing name'}
334350
</span>
335351
</button>

0 commit comments

Comments
 (0)