From e20cd87ef9a71b02a9070ecf3850ac6031fe3b9c Mon Sep 17 00:00:00 2001 From: hugocasa Date: Thu, 17 Sep 2026 14:42:08 +0200 Subject: [PATCH] feat: edit a nested AI agent's own tools from the flow editor Co-Authored-By: Claude Opus 5 --- .../components/flows/agentToolTree.test.ts | 20 +++++++--- .../src/lib/components/flows/agentToolTree.ts | 26 +++++++------ .../components/flows/common/FlowCard.svelte | 4 ++ .../flows/common/FlowCardHeader.svelte | 4 ++ .../flows/content/AgentToolRoster.svelte | 6 ++- .../flows/content/AgentToolWrapper.svelte | 9 ++++- .../flows/content/AiAgentStepInputs.svelte | 1 + .../flows/content/FlowModuleComponent.svelte | 31 +++++++++++++++- .../flows/content/FlowModuleWrapper.svelte | 37 ++++++++++++------- .../flows/map/FlowModuleSchemaMap.svelte | 12 +++++- .../flows/map/InsertModuleInner.svelte | 20 ++++++---- 11 files changed, 129 insertions(+), 41 deletions(-) diff --git a/frontend/src/lib/components/flows/agentToolTree.test.ts b/frontend/src/lib/components/flows/agentToolTree.test.ts index 3e5b49e3a6..70da082068 100644 --- a/frontend/src/lib/components/flows/agentToolTree.test.ts +++ b/frontend/src/lib/components/flows/agentToolTree.test.ts @@ -59,20 +59,26 @@ describe('findAgentToolOwner', () => { }) it('finds a nested tool owner inside a nested ai agent tool', () => { - const nestedAgent = makeAiAgent('support_agent', [makeFlowModuleTool(makeRawModule('create_ticket'))]) + const nestedAgent = makeAiAgent('support_agent', [ + makeFlowModuleTool(makeRawModule('create_ticket')) + ]) const rootAgent = makeAiAgent('root_agent', [makeFlowModuleTool(nestedAgent)]) - expect(findAgentToolOwner([rootAgent], 'create_ticket')).toMatchObject({ + const owner = findAgentToolOwner([rootAgent], 'create_ticket') + expect(owner).toMatchObject({ agentId: 'support_agent', toolIndex: 0, depth: 2 }) + expect(owner?.agents.map((agent) => agent.id)).toEqual(['root_agent', 'support_agent']) }) }) describe('removeAgentToolOwner', () => { it('removes the matched tool and returns its subtree ids', () => { - const nestedAgent = makeAiAgent('support_agent', [makeFlowModuleTool(makeRawModule('create_ticket'))]) + const nestedAgent = makeAiAgent('support_agent', [ + makeFlowModuleTool(makeRawModule('create_ticket')) + ]) const rootAgent = makeAiAgent('root_agent', [ makeFlowModuleTool(makeRawModule('lookup_user')), makeFlowModuleTool(nestedAgent) @@ -85,7 +91,9 @@ describe('removeAgentToolOwner', () => { removedIds: ['support_agent', 'create_ticket'] }) expect((rootAgent.value as any).tools).toHaveLength(1) - expect(((rootAgent.value as any).tools as any[]).map((tool) => tool.id)).toEqual(['lookup_user']) + expect(((rootAgent.value as any).tools as any[]).map((tool) => tool.id)).toEqual([ + 'lookup_user' + ]) }) }) @@ -93,7 +101,9 @@ describe('collectFlowNodeIds', () => { it('includes ai agent tool ids when deleting an ai agent flow module', () => { const agent = makeAiAgent('root_agent', [ makeFlowModuleTool(makeRawModule('lookup_user')), - makeFlowModuleTool(makeAiAgent('support_agent', [makeFlowModuleTool(makeRawModule('create_ticket'))])) + makeFlowModuleTool( + makeAiAgent('support_agent', [makeFlowModuleTool(makeRawModule('create_ticket'))]) + ) ]) expect(collectFlowNodeIds(agent)).toEqual([ diff --git a/frontend/src/lib/components/flows/agentToolTree.ts b/frontend/src/lib/components/flows/agentToolTree.ts index c30804c21d..5e01d5ef07 100644 --- a/frontend/src/lib/components/flows/agentToolTree.ts +++ b/frontend/src/lib/components/flows/agentToolTree.ts @@ -7,7 +7,7 @@ import { } from './agentToolUtils' import { forEachAiAgentModule } from './aiAgentModules' -type FlowNodeLike = Pick +type FlowNodeLike = Pick export type AgentToolOwner = { agentId: string @@ -15,6 +15,8 @@ export type AgentToolOwner = { toolIndex: number tool: AgentTool depth: number + /** Every agent the tool sits under, the step's own first and `agentId`'s last. */ + agents: FlowNodeLike[] } export type RemovedAgentTool = { @@ -26,7 +28,7 @@ export function findAgentToolOwner( modules: FlowModule[], toolId: string ): AgentToolOwner | undefined { - return findAgentToolOwnerInModules(modules, toolId, 0) + return findAgentToolOwnerInModules(modules, toolId, []) } export function removeAgentToolOwner(owner: AgentToolOwner): RemovedAgentTool | undefined { @@ -53,10 +55,10 @@ export function collectAgentToolIds(tool: AgentTool): string[] { function findAgentToolOwnerInModules( modules: FlowModule[], toolId: string, - depth: number + agents: FlowNodeLike[] ): AgentToolOwner | undefined { for (const module of modules) { - const owner = findAgentToolOwnerInNode(module, toolId, depth) + const owner = findAgentToolOwnerInNode(module, toolId, agents) if (owner) { return owner } @@ -68,15 +70,15 @@ function findAgentToolOwnerInModules( function findAgentToolOwnerInNode( node: FlowNodeLike, toolId: string, - depth: number + agents: FlowNodeLike[] ): AgentToolOwner | undefined { if (node.value.type === 'forloopflow' || node.value.type === 'whileloopflow') { - return findAgentToolOwnerInModules(node.value.modules, toolId, depth) + return findAgentToolOwnerInModules(node.value.modules, toolId, agents) } if (node.value.type === 'branchall') { for (const branch of node.value.branches) { - const owner = findAgentToolOwnerInModules(branch.modules, toolId, depth) + const owner = findAgentToolOwnerInModules(branch.modules, toolId, agents) if (owner) { return owner } @@ -85,12 +87,12 @@ function findAgentToolOwnerInNode( } if (node.value.type === 'branchone') { - const defaultOwner = findAgentToolOwnerInModules(node.value.default, toolId, depth) + const defaultOwner = findAgentToolOwnerInModules(node.value.default, toolId, agents) if (defaultOwner) { return defaultOwner } for (const branch of node.value.branches) { - const owner = findAgentToolOwnerInModules(branch.modules, toolId, depth) + const owner = findAgentToolOwnerInModules(branch.modules, toolId, agents) if (owner) { return owner } @@ -102,6 +104,7 @@ function findAgentToolOwnerInNode( return undefined } + const withNode = [...agents, node] // Absent for a linked agent, whose tools live in the resource rather than on the module. const tools = node.value.tools ?? [] const toolIndex = tools.findIndex((tool) => tool.id === toolId) @@ -111,7 +114,8 @@ function findAgentToolOwnerInNode( tools, toolIndex, tool: tools[toolIndex], - depth: depth + 1 + depth: withNode.length, + agents: withNode } } @@ -120,7 +124,7 @@ function findAgentToolOwnerInNode( continue } - const owner = findAgentToolOwnerInNode(tool as FlowNodeLike, toolId, depth + 1) + const owner = findAgentToolOwnerInNode(tool as FlowNodeLike, toolId, withNode) if (owner) { return owner } diff --git a/frontend/src/lib/components/flows/common/FlowCard.svelte b/frontend/src/lib/components/flows/common/FlowCard.svelte index 091d3a9feb..86c8ae7158 100644 --- a/frontend/src/lib/components/flows/common/FlowCard.svelte +++ b/frontend/src/lib/components/flows/common/FlowCard.svelte @@ -14,6 +14,8 @@ header?: import('svelte').Snippet action?: import('svelte').Snippet children?: import('svelte').Snippet + /** See `FlowCardHeader`. */ + trail?: import('svelte').Snippet isAgentTool?: boolean siblingToolNames?: string[] } @@ -30,6 +32,7 @@ header, action, children, + trail, isAgentTool = false, siblingToolNames = undefined }: Props = $props() @@ -49,6 +52,7 @@ {subtitleDocLink} {flowModuleValue} {action} + {trail} {isAgentTool} {siblingToolNames} > diff --git a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte index 063367cc85..1c373135e4 100644 --- a/frontend/src/lib/components/flows/common/FlowCardHeader.svelte +++ b/frontend/src/lib/components/flows/common/FlowCardHeader.svelte @@ -39,6 +39,8 @@ subtitleDocLink?: string | undefined children?: import('svelte').Snippet action?: import('svelte').Snippet + /** A line above the header naming where the step sits. */ + trail?: import('svelte').Snippet isAgentTool?: boolean siblingToolNames?: string[] } @@ -52,6 +54,7 @@ subtitleDocLink = undefined, children, action, + trail, isAgentTool = false, siblingToolNames = undefined }: Props = $props() @@ -192,6 +195,7 @@
+ {@render trail?.()}
diff --git a/frontend/src/lib/components/flows/content/AgentToolRoster.svelte b/frontend/src/lib/components/flows/content/AgentToolRoster.svelte index 33abff2ba8..78d048270c 100644 --- a/frontend/src/lib/components/flows/content/AgentToolRoster.svelte +++ b/frontend/src/lib/components/flows/content/AgentToolRoster.svelte @@ -20,6 +20,8 @@ emptyMessage?: string /** Where the picker's popover belongs, when the roster is not inside the flow editor. */ pickerPortal?: string + /** See `InsertModuleInner`. */ + allowAiAgentTool?: boolean } let { @@ -28,7 +30,8 @@ onAddTool = undefined, onDeleteTool = undefined, emptyMessage = 'No tools yet. Add one from the agent on the flow graph.', - pickerPortal = '#flow-editor' + pickerPortal = '#flow-editor', + allowAiAgentTool = true }: Props = $props() let funcDesc = $state('') @@ -81,6 +84,7 @@ (onAddTool?.(e.detail), close())} on:insert={(e) => (onAddTool?.(e.detail), close())} diff --git a/frontend/src/lib/components/flows/content/AgentToolWrapper.svelte b/frontend/src/lib/components/flows/content/AgentToolWrapper.svelte index 6ccadc2ee5..304d07d857 100644 --- a/frontend/src/lib/components/flows/content/AgentToolWrapper.svelte +++ b/frontend/src/lib/components/flows/content/AgentToolWrapper.svelte @@ -20,6 +20,9 @@ staticOnly?: boolean /** See `FlowModuleComponent`: set where there is no graph to select a nested tool on. */ noToolNavigation?: boolean + /** See `FlowModuleComponent`. */ + agentTrail?: Pick[] + flowModuleSchemaMap?: import('../map/FlowModuleSchemaMap.svelte').default } let { @@ -32,7 +35,9 @@ highlightArg, siblingToolNames = undefined, staticOnly = false, - noToolNavigation = false + noToolNavigation = false, + agentTrail = undefined, + flowModuleSchemaMap = undefined }: Props = $props() @@ -65,6 +70,8 @@ isAgentTool={true} {staticOnly} {noToolNavigation} + {agentTrail} + {flowModuleSchemaMap} bind:toolDescription={tool.description} {siblingToolNames} /> diff --git a/frontend/src/lib/components/flows/content/AiAgentStepInputs.svelte b/frontend/src/lib/components/flows/content/AiAgentStepInputs.svelte index 31abd3c0b5..c868f65efb 100644 --- a/frontend/src/lib/components/flows/content/AiAgentStepInputs.svelte +++ b/frontend/src/lib/components/flows/content/AiAgentStepInputs.svelte @@ -558,6 +558,7 @@ {onAddTool} {onDeleteTool} pickerPortal={toolPickerPortal} + allowAiAgentTool={!isAgentTool} /> {:else}