From 207ce8649cf7026c62eea2b1b2f462c7df8c4e5a Mon Sep 17 00:00:00 2001 From: hugocasa Date: Tue, 14 Jul 2026 12:12:00 +0200 Subject: [PATCH] fix(ai-agent): don't mark repeated tool calls as failed in flow graph (#10075) * fix(ai-agent): don't mark repeated tool calls as failed in flow graph Co-Authored-By: Claude Opus 4.8 (1M context) * test(ai-agent): cover reporter's mixed repeated-tool-call scenario Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../graph/renderers/nodes/AIToolNode.svelte | 7 +- .../graph/renderers/nodes/AIToolNode.test.ts | 100 ++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 frontend/src/lib/components/graph/renderers/nodes/AIToolNode.test.ts diff --git a/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte index 5fd064c033..b6ff05c193 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte @@ -160,7 +160,12 @@ data: { tool: tool.name, type: tool.type, - nameError: getToolNameError(tool.name, tool.type, siblingNames), + // agentActions are runtime tool calls: the same tool called multiple times + // yields duplicate names, which is expected and must not read as a Failure. + // Only validate names in the editor, where they define the static tool set. + nameError: agentActions + ? undefined + : getToolNameError(tool.name, tool.type, siblingNames), eventHandlers, moduleId: tool.id, insertable, diff --git a/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.test.ts b/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.test.ts new file mode 100644 index 0000000000..cdc6fa3f3b --- /dev/null +++ b/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, vi } from 'vitest' + +// Mock the component wrapper so importing the .svelte module doesn't pull in the +// full render-time dependency graph. +vi.mock('./NodeWrapper.svelte', () => ({ default: {} })) + +import { computeAIToolNodes } from './AIToolNode.svelte' + +const eventHandlers = {} as any + +function aiAgentNode(id: string, tools: any[]): any { + return { + id, + type: 'module', + position: { x: 0, y: 0 }, + data: { module: { id, value: { type: 'aiagent', tools } } } + } +} + +describe('computeAIToolNodes', () => { + it('does not flag duplicate names when the same tool is called multiple times at runtime', () => { + // One statically-defined tool that the agent called twice. The runtime + // agent_actions therefore carry the same function_name twice — this is + // expected and must not surface as a `nameError` (which renders as Failure). + const node = aiAgentNode('agent', [ + { id: 'tool_a', summary: 'my_tool', value: { tool_type: 'flowmodule', type: 'script' } } + ]) + const flowModuleStates = { + agent: { + type: 'Success', + agent_actions: [ + { type: 'tool_call', function_name: 'my_tool', module_id: 'tool_a', job_id: 'j1' }, + { type: 'tool_call', function_name: 'my_tool', module_id: 'tool_a', job_id: 'j2' } + ] + } + } as any + + const { toolNodes } = computeAIToolNodes([node], eventHandlers, false, flowModuleStates) + + expect(toolNodes.length).toBe(2) + for (const n of toolNodes) { + expect((n.data as any).nameError).toBeUndefined() + } + }) + + it('does not flag any node in a mixed run where one tool repeats (reporter scenario)', () => { + // Repo-intel run: query_stored called 3x plus two single calls, all succeeded. + // Before the fix the three query_stored nodes rendered red (Failure) purely + // from the duplicate-name check, while the unique tools stayed green. + const node = aiAgentNode('chat', [ + { id: 'q', summary: 'query_stored', value: { tool_type: 'flowmodule', type: 'script' } }, + { id: 'h', summary: 'hybrid_search', value: { tool_type: 'flowmodule', type: 'script' } }, + { + id: 't', + summary: 'trace_outbound_calls', + value: { tool_type: 'flowmodule', type: 'script' } + } + ]) + const call = (name: string, job: string) => ({ + type: 'tool_call', + function_name: name, + module_id: name[0], + job_id: job + }) + const flowModuleStates = { + chat: { + type: 'Success', + agent_actions: [ + call('query_stored', 'j1'), + call('query_stored', 'j2'), + call('query_stored', 'j3'), + call('hybrid_search', 'j4'), + call('trace_outbound_calls', 'j5') + ] + } + } as any + + const { toolNodes } = computeAIToolNodes([node], eventHandlers, false, flowModuleStates) + + expect(toolNodes.length).toBe(5) + for (const n of toolNodes) { + expect((n.data as any).nameError).toBeUndefined() + } + }) + + it('still flags genuinely duplicate tool names in the editor (static tool set)', () => { + const node = aiAgentNode('agent2', [ + { id: 't1', summary: 'dup', value: { tool_type: 'flowmodule', type: 'script' } }, + { id: 't2', summary: 'dup', value: { tool_type: 'flowmodule', type: 'script' } } + ]) + + const { toolNodes } = computeAIToolNodes([node], eventHandlers, true, undefined) + + const toolCallNodes = toolNodes.filter((n) => n.type === 'aiTool') + expect(toolCallNodes.length).toBe(2) + for (const n of toolCallNodes) { + expect((n.data as any).nameError).toBe('Duplicate tool name') + } + }) +})