From dd54ddbbc12d6dc9d215335aa584172b19f88c92 Mon Sep 17 00:00:00 2001 From: Qiu <2889459163@qq.com> Date: Mon, 12 Oct 2026 15:06:16 +0800 Subject: [PATCH] fix(core): allow toolkit replacement to retain tool names --- .changeset/fix-toolkit-replacement.md | 5 + packages/core/src/tool/manager/ToolManager.ts | 14 ++- packages/core/src/tool/manager/index.spec.ts | 105 ++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-toolkit-replacement.md diff --git a/.changeset/fix-toolkit-replacement.md b/.changeset/fix-toolkit-replacement.md new file mode 100644 index 000000000..179d59efc --- /dev/null +++ b/.changeset/fix-toolkit-replacement.md @@ -0,0 +1,5 @@ +--- +"@voltagent/core": patch +--- + +Allow replacing a toolkit while retaining its tool names, while still rejecting conflicts with standalone tools and other toolkits. diff --git a/packages/core/src/tool/manager/ToolManager.ts b/packages/core/src/tool/manager/ToolManager.ts index ce74d37bf..7f6bb7125 100644 --- a/packages/core/src/tool/manager/ToolManager.ts +++ b/packages/core/src/tool/manager/ToolManager.ts @@ -34,7 +34,19 @@ export class ToolManager extends BaseToolManager this.hasToolInAny(toolName))) { + if ( + newToolkit + .getAllToolNames() + .some( + (toolName) => + this.baseTools.has(toolName) || + this.providerTools.has(toolName) || + [...this.toolkits.entries()].some( + ([toolkitName, manager]) => + toolkitName !== toolkit.name && manager.hasToolInAny(toolName), + ), + ) + ) { this.logger.warn( `Toolkit '${toolkit.name}' contains tools that conflict with existing tools. Skipping addition.`, ); diff --git a/packages/core/src/tool/manager/index.spec.ts b/packages/core/src/tool/manager/index.spec.ts index 73e37c25c..acc74cd8b 100644 --- a/packages/core/src/tool/manager/index.spec.ts +++ b/packages/core/src/tool/manager/index.spec.ts @@ -204,6 +204,76 @@ describe("ToolManager", () => { expect(toolManager.getToolkits()).toHaveLength(1); }); + it("should replace a toolkit while retaining a tool name", async () => { + const originalTool = createTool({ + name: "search", + description: "Original search", + parameters: z.object({ query: z.string() }), + execute: vi.fn().mockResolvedValue("original result"), + }); + const replacementTool = createTool({ + name: "search", + description: "Updated search", + parameters: z.object({ query: z.string(), limit: z.number() }), + execute: vi.fn().mockResolvedValue("updated result"), + }); + + expect( + toolManager.addToolkit(createToolkit({ name: "search-kit", tools: [originalTool] })), + ).toBe(true); + expect( + toolManager.addToolkit(createToolkit({ name: "search-kit", tools: [replacementTool] })), + ).toBe(true); + + expect(toolManager.getToolkits()).toHaveLength(1); + expect(toolManager.getAllToolNames()).toEqual(["search"]); + expect(toolManager.getToolByName("search")).toBe(replacementTool); + const preparedTools = toolManager.prepareToolsForExecution( + (tool) => (args, options) => tool.execute?.(args, options), + ); + expect(preparedTools.search.inputSchema).toBe(replacementTool.parameters); + await expect(preparedTools.search.execute({ query: "agent", limit: 3 })).resolves.toBe( + "updated result", + ); + }); + + it("should preserve the existing toolkit when a replacement conflicts with another toolkit", () => { + const originalToolkit = createToolkit({ name: "shared-kit", tools: [mockTool1] }); + const otherToolkit = createToolkit({ name: "other-kit", tools: [mockTool2] }); + expect(toolManager.addToolkit(originalToolkit)).toBe(true); + expect(toolManager.addToolkit(otherToolkit)).toBe(true); + + const replacementToolkit = createToolkit({ + name: "shared-kit", + tools: [mockTool1, mockTool2], + }); + expect(toolManager.addToolkit(replacementToolkit)).toBe(false); + expect(toolManager.getToolkits()).toHaveLength(2); + expect(toolManager.getToolByName(mockTool1.name)).toBe(mockTool1); + expect(toolManager.getToolkits()[0].getAllToolNames()).toEqual([mockTool1.name]); + expect(toolManager.getToolByName(mockTool2.name)).toBe(mockTool2); + }); + + it("should preserve the existing toolkit when a replacement conflicts with a standalone tool", () => { + const standaloneTool = createTool({ + name: mockTool1.name, + description: "Standalone search", + parameters: z.object({}), + execute: vi.fn(), + }); + expect( + toolManager.addToolkit(createToolkit({ name: "shared-kit", tools: [mockTool1] })), + ).toBe(true); + expect(toolManager.addStandaloneTool(standaloneTool)).toBe(true); + + expect( + toolManager.addToolkit(createToolkit({ name: "shared-kit", tools: [mockTool1] })), + ).toBe(false); + expect(toolManager.getToolkits()).toHaveLength(1); + expect(toolManager.getToolkits()[0].getToolByName(mockTool1.name)).toBe(mockTool1); + expect(toolManager.getToolByName(mockTool1.name)).toBe(standaloneTool); + }); + it("should skip toolkit when tools conflict with existing standalone tool", () => { const standaloneTool = createTool({ name: "conflicting-tool", @@ -385,6 +455,41 @@ describe("provider-defined tools", () => { ]); }); + it("should replace a toolkit while retaining a provider tool name", () => { + const originalTool = createProviderTool("provider-search"); + const replacementTool = { + ...originalTool, + description: "Updated provider search", + } as ProviderTool; + expect(manager.addToolkit(createToolkit({ name: "provider-kit", tools: [originalTool] }))).toBe( + true, + ); + expect( + manager.addToolkit(createToolkit({ name: "provider-kit", tools: [replacementTool] })), + ).toBe(true); + + expect(manager.getToolkits()).toHaveLength(1); + expect(manager.getAllProviderTools()).toEqual([replacementTool]); + expect(manager.getToolByName(replacementTool.name)).toBe(replacementTool); + expect(manager.prepareToolsForExecution(vi.fn())[replacementTool.name]).toBe(replacementTool); + }); + + it("should reject replacement after a conflicting standalone provider tool is added", () => { + const originalTool = createProviderTool("provider-search"); + const standaloneTool = { ...originalTool, description: "Standalone provider" } as ProviderTool; + expect(manager.addToolkit(createToolkit({ name: "provider-kit", tools: [originalTool] }))).toBe( + true, + ); + expect(manager.addStandaloneTool(standaloneTool)).toBe(true); + + expect(manager.addToolkit(createToolkit({ name: "provider-kit", tools: [originalTool] }))).toBe( + false, + ); + expect(manager.getToolkits()).toHaveLength(1); + expect(manager.getToolkits()[0].getToolByName(originalTool.name)).toBe(originalTool); + expect(manager.getToolByName(originalTool.name)).toBe(standaloneTool); + }); + it("should remove standalone provider tool via removeTool", () => { const providerTool = createProviderTool("to-remove"); manager.addStandaloneTool(providerTool as unknown as AgentTool);