fix: restore ai agent tool deletion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-04-07 12:52:27 +02:00
parent a46aa641f9
commit adfc73765d
3 changed files with 170 additions and 8 deletions
@@ -0,0 +1,68 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('../aiProviderStorage', () => ({
loadStoredConfig: () => undefined
}))
vi.mock('./flowInfers', () => ({
AI_AGENT_SCHEMA: { properties: {} }
}))
import type { FlowModule } from '$lib/gen'
import { removeAgentToolByIdDeep } from './agentToolUtils'
function makeRawModule(id: string): FlowModule {
return {
id,
summary: id,
value: { type: 'rawscript', content: '', language: 'python3' } as any
} as FlowModule
}
function makeAiAgent(id: string, tools: any[]): FlowModule {
return {
id,
summary: id,
value: {
type: 'aiagent',
tools,
input_transforms: {}
} as any
} as FlowModule
}
function makeFlowModuleTool(module: FlowModule) {
return {
id: module.id,
summary: module.summary,
value: {
tool_type: 'flowmodule',
...module.value
}
}
}
describe('removeAgentToolByIdDeep', () => {
it('removes a direct tool from an ai agent', () => {
const tool = makeFlowModuleTool(makeRawModule('lookup_user'))
const agent = makeAiAgent('agent', [tool])
const removed: string[] = []
expect(removeAgentToolByIdDeep([agent], 'lookup_user', (x) => removed.push(x.id))).toBe(true)
expect((agent.value as any).tools).toEqual([])
expect(removed).toEqual(['lookup_user'])
})
it('removes a nested tool from a nested ai agent tool', () => {
const nestedTool = makeFlowModuleTool(makeRawModule('create_ticket'))
const nestedAgent = makeAiAgent('support_agent', [nestedTool])
const rootAgent = makeAiAgent('root_agent', [makeFlowModuleTool(nestedAgent)])
const removed: string[] = []
expect(removeAgentToolByIdDeep([rootAgent], 'create_ticket', (x) => removed.push(x.id))).toBe(
true
)
expect((((rootAgent.value as any).tools as any[])[0].value as any).tools).toEqual([])
expect(removed).toEqual(['create_ticket'])
})
})
@@ -108,6 +108,66 @@ export function createWebsearchTool(id: string): WebsearchTool {
}
}
/**
* Remove an AI agent tool by id, recursively traversing nested modules and nested AI agents.
* Returns true when a matching tool was found and removed.
*/
export function removeAgentToolByIdDeep(
modules: FlowModule[],
id: string,
onRemove?: (tool: AgentTool) => void
): boolean {
for (const module of modules) {
if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') {
if (removeAgentToolByIdDeep(module.value.modules, id, onRemove)) {
return true
}
continue
}
if (module.value.type === 'branchall') {
for (const branch of module.value.branches) {
if (removeAgentToolByIdDeep(branch.modules, id, onRemove)) {
return true
}
}
continue
}
if (module.value.type === 'branchone') {
if (removeAgentToolByIdDeep(module.value.default, id, onRemove)) {
return true
}
for (const branch of module.value.branches) {
if (removeAgentToolByIdDeep(branch.modules, id, onRemove)) {
return true
}
}
continue
}
if (module.value.type !== 'aiagent') {
continue
}
const toolIndex = module.value.tools.findIndex((tool) => tool.id === id)
if (toolIndex !== -1) {
const [removed] = module.value.tools.splice(toolIndex, 1)
onRemove?.(removed)
return true
}
const nestedToolModules = module.value.tools
.filter(isFlowModuleTool)
.map((tool) => agentToolToFlowModule(tool))
if (removeAgentToolByIdDeep(nestedToolModules, id, onRemove)) {
return true
}
}
return false
}
/**
* Convert a FlowModuleTool to a FlowModule for use with loadFlowModuleState etc.
* Strips the extra `tool_type` field and maps AgentTool fields to FlowModule fields.
@@ -50,7 +50,8 @@
createWebsearchTool,
createAiAgentTool,
SPECIAL_TOOL_KINDS,
agentToolToFlowModule
agentToolToFlowModule,
removeAgentToolByIdDeep
} from '../agentToolUtils'
import { loadFlowModuleState } from '../flowStateUtils.svelte'
import { getNoteEditorContext } from '$lib/components/graph/noteEditor.svelte'
@@ -257,7 +258,7 @@
* Tools are always leaf nodes, so we just need to delete their state directly
*/
function removeAgentToolById(tools: AgentTool[], id: string): AgentTool[] {
const index = tools.findIndex((tool) => tool.id == id)
const index = tools.findIndex((tool) => tool.id === id)
if (index != -1) {
const [removed] = tools.splice(index, 1)
deleteFlowStateById(removed.id, flowStateStore)
@@ -383,6 +384,8 @@
}
export function deleteMultiple(ids: string[]) {
const structureIds = ids.filter((id) => findInStructure(proxy.items, id))
const toolIds = ids.filter((id) => !findInStructure(proxy.items, id))
const deletingSet = new Set(ids)
const allDeps: Record<string, string[]> = {}
for (const id of ids) {
@@ -395,18 +398,30 @@
}
const opts = { displayState: groupDisplayState }
const { emptiedGroups, duplicateGroups, commit } = proxy.prepareMutation((tree) => {
for (const id of ids) {
const found = findInStructure(tree, id)
if (found) found.parentChildren.splice(found.index, 1)
}
}, opts)
const { emptiedGroups, duplicateGroups, commit } =
structureIds.length > 0
? proxy.prepareMutation((tree) => {
for (const id of structureIds) {
const found = findInStructure(tree, id)
if (found) found.parentChildren.splice(found.index, 1)
}
}, opts)
: {
emptiedGroups: [],
duplicateGroups: [],
commit: () => {}
}
const affectedGroups = [...emptiedGroups, ...duplicateGroups]
const cb = () => {
push(history, flowStore.val)
commit({ removeDuplicates: duplicateGroups.length > 0 })
for (const id of toolIds) {
removeAgentToolByIdDeep(flowStore.val.value.modules, id, (removed) => {
deleteFlowStateById(removed.id, flowStateStore)
})
}
for (const id of ids) {
delete flowStateStore.val[id]
}
@@ -678,6 +693,25 @@
return
}
if (!findInStructure(proxy.items, id)) {
const cb = () => {
push(history, flowStore.val)
selectNextId(id)
const removed = removeAgentToolByIdDeep(flowStore.val.value.modules, id, (tool) => {
deleteFlowStateById(tool.id, flowStateStore)
})
if (!removed) return
refreshStateStore(flowStore)
onDelete?.(id)
}
if (Object.keys(dependents).length > 0) {
deleteCallback = cb
} else {
cb()
}
return
}
const dsOpts = { displayState: groupDisplayState }
const { emptiedGroups, duplicateGroups, commit } = proxy.prepareMutation((tree) => {
const found = findInStructure(tree, id)