From 4fda0e75ddfd445d2bcaeee62aa0e3348d57a5a2 Mon Sep 17 00:00:00 2001 From: Abhi Date: Mon, 11 May 2026 10:06:12 -0400 Subject: [PATCH] feat(core): enforce 512 tool limit and add warning for ignored tools - Limits active tools to 512 in ToolRegistry to prevent Gemini API errors. - Prioritizes built-in tools and command-discovered tools over MCP tools. - Adds a warning message on startup and context refresh when tools are ignored. --- packages/core/src/config/config.test.ts | 81 +++++++++++++++++++ packages/core/src/config/config.ts | 22 +++++ packages/core/src/tools/tool-registry.test.ts | 58 +++++++++++++ packages/core/src/tools/tool-registry.ts | 51 +++++++++++- 4 files changed, 210 insertions(+), 2 deletions(-) diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 440cde681b..900c1a74ea 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -100,6 +100,9 @@ vi.mock('../tools/tool-registry', () => { ToolRegistryMock.prototype.getTool = vi.fn(); ToolRegistryMock.prototype.getAllToolNames = vi.fn(() => []); ToolRegistryMock.prototype.getFunctionDeclarations = vi.fn(() => []); + ToolRegistryMock.prototype.getToolLimitReport = vi + .fn() + .mockReturnValue({ totalActive: 0, allowedCount: 0, ignoredTools: [] }); return { ToolRegistry: ToolRegistryMock }; }); @@ -4279,3 +4282,81 @@ describe('ADKSettings', () => { expect(config.getAgentSessionNoninteractiveEnabled()).toBe(true); }); }); + +describe('Config Tool Limit Warning', () => { + const localParams: ConfigParameters = { + sessionId: 'test-session-id', + targetDir: '/test/dir', + debugMode: false, + model: 'test-model', + cwd: '/tmp', + }; + + it('should emit warning feedback when tools are ignored', () => { + const config = new Config(localParams); + + const mockReport = { + totalActive: 520, + allowedCount: 512, + ignoredTools: ['mcp_server_tool-1', 'mcp_server_tool-2'], + }; + + const mockToolRegistry = { + getToolLimitReport: vi.fn().mockReturnValue(mockReport), + }; + (config as unknown as { _toolRegistry: unknown })._toolRegistry = + mockToolRegistry; + + const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback'); + + config.checkAndWarnToolLimit(); + + expect(emitFeedbackSpy).toHaveBeenCalledOnce(); + expect(emitFeedbackSpy).toHaveBeenCalledWith( + 'warning', + expect.stringContaining('Tool limit exceeded'), + ); + expect(emitFeedbackSpy).toHaveBeenCalledWith( + 'warning', + expect.stringContaining('mcp_server_tool-1'), + ); + }); + + it('should deduplicate/suppress warnings if the ignored count has not changed', () => { + const config = new Config(localParams); + + const mockReport = { + totalActive: 520, + allowedCount: 512, + ignoredTools: ['mcp_server_tool-1', 'mcp_server_tool-2'], + }; + + const mockToolRegistry = { + getToolLimitReport: vi.fn().mockReturnValue(mockReport), + }; + (config as unknown as { _toolRegistry: unknown })._toolRegistry = + mockToolRegistry; + + const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback'); + + config.checkAndWarnToolLimit(); + expect(emitFeedbackSpy).toHaveBeenCalledTimes(1); + + config.checkAndWarnToolLimit(); + expect(emitFeedbackSpy).toHaveBeenCalledTimes(1); + + const newMockReport = { + totalActive: 521, + allowedCount: 512, + ignoredTools: [ + 'mcp_server_tool-1', + 'mcp_server_tool-2', + 'mcp_server_tool-3', + ], + }; + mockToolRegistry.getToolLimitReport.mockReturnValue(newMockReport); + + config.checkAndWarnToolLimit(); + expect(emitFeedbackSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index f74ae4d7f5..39c58c286a 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -898,6 +898,7 @@ export class Config implements McpContext, AgentLoopContext { private initialized = false; private initPromise: Promise | undefined; private mcpInitializationPromise: Promise | null = null; + private lastIgnoredToolsCount = 0; readonly storage: Storage; private readonly fileExclusions: FileExclusions; private readonly eventEmitter?: EventEmitter; @@ -1510,6 +1511,7 @@ export class Config implements McpContext, AgentLoopContext { debugLogger.error('Error initializing MCP clients:', result.reason); } } + this.checkAndWarnToolLimit(); }); if (!this.interactive || this.acpMode) { @@ -2463,6 +2465,25 @@ export class Config implements McpContext, AgentLoopContext { return this.userMemory; } + /** + * Checks if any tools were ignored due to the 512 limit and emits a warning feedback event if changed. + */ + checkAndWarnToolLimit(): void { + if (!this._toolRegistry) { + return; + } + const report = this._toolRegistry.getToolLimitReport(); + if (report.ignoredTools.length > 0) { + if (report.ignoredTools.length !== this.lastIgnoredToolsCount) { + this.lastIgnoredToolsCount = report.ignoredTools.length; + const message = `⚠️ Tool limit exceeded: Maximum supported tool count is 512. The first 512 tools have been registered (built-in tools prioritized, then discovered and MCP tools). The following ${report.ignoredTools.length} tool(s) are being ignored to prevent API errors: ${report.ignoredTools.join(', ')}.`; + coreEvents.emitFeedback('warning', message); + } + } else { + this.lastIgnoredToolsCount = 0; + } + } + /** * Refreshes the MCP context, including memory, tools, and system instructions. */ @@ -2479,6 +2500,7 @@ export class Config implements McpContext, AgentLoopContext { await this._geminiClient.setTools(); this._geminiClient.updateSystemInstruction(); } + this.checkAndWarnToolLimit(); } setUserMemory(newUserMemory: string | HierarchicalMemory): void { diff --git a/packages/core/src/tools/tool-registry.test.ts b/packages/core/src/tools/tool-registry.test.ts index 0f1e79ca25..dc9c99b5d8 100644 --- a/packages/core/src/tools/tool-registry.test.ts +++ b/packages/core/src/tools/tool-registry.test.ts @@ -889,6 +889,64 @@ describe('ToolRegistry', () => { expect(description).toBe(JSON.stringify(params)); }); }); + + describe('512 tool limit and reporting', () => { + it('should correctly cap the number of active tools to 512', () => { + for (let i = 0; i < 520; i++) { + const tool = new MockTool({ + name: `tool-${i}`, + displayName: `Tool ${i}`, + }); + toolRegistry.registerTool(tool); + } + + const activeTools = (toolRegistry as any).getActiveTools(); + expect(activeTools).toHaveLength(512); + + expect(toolRegistry.getAllTools()).toHaveLength(512); + expect(toolRegistry.getAllToolNames()).toHaveLength(512); + expect(toolRegistry.getFunctionDeclarations()).toHaveLength(512); + }); + + it('should return a correct report listing ignored tools', () => { + const builtIn = new MockTool({ name: 'my-builtin' }); + toolRegistry.registerTool(builtIn); + + for (let i = 0; i < 515; i++) { + const tool = createMCPTool('test-server', `mcp-tool-${i}`, `desc ${i}`); + toolRegistry.registerTool(tool); + } + + toolRegistry.sortTools(); + + const report = toolRegistry.getToolLimitReport(); + expect(report.totalActive).toBe(516); + expect(report.allowedCount).toBe(512); + expect(report.ignoredTools).toHaveLength(4); + expect(report.ignoredTools[0]).toContain('mcp_test-server_mcp-tool-'); + }); + + it('should return undefined in getTool for a tool cut off by the 512 limit', () => { + const allowedTool = new MockTool({ name: 'builtin-allowed' }); + toolRegistry.registerTool(allowedTool); + + const toolsList: string[] = []; + for (let i = 0; i < 515; i++) { + const padded = String(i).padStart(3, '0'); + const tool = createMCPTool('server', `tool-${padded}`, 'desc'); + toolRegistry.registerTool(tool); + toolsList.push(tool.getFullyQualifiedName()); + } + + toolRegistry.sortTools(); + + const lastToolName = toolsList[514]; + + expect(toolRegistry.getTool('builtin-allowed')).toBe(allowedTool); + expect(toolRegistry.getTool(toolsList[0])).toBeDefined(); + expect(toolRegistry.getTool(lastToolName)).toBeUndefined(); + }); + }); }); /** diff --git a/packages/core/src/tools/tool-registry.ts b/packages/core/src/tools/tool-registry.ts index cee5f22a8e..30fdeca074 100644 --- a/packages/core/src/tools/tool-registry.ts +++ b/packages/core/src/tools/tool-registry.ts @@ -543,7 +543,49 @@ export class ToolRegistry { } /** - * @returns All the tools that are not excluded. + * Generates a report on the number of active, allowed, and ignored tools due to the 512 cap. + */ + getToolLimitReport(): { + totalActive: number; + allowedCount: number; + ignoredTools: string[]; + } { + const toolMetadata = this.buildToolMetadata(); + const allKnownNames = new Set(this.allKnownTools.keys()); + const excludedTools = + this.expandExcludeToolsWithAliases( + this.config.getExcludeTools(toolMetadata, allKnownNames), + ) ?? new Set([]); + const activeTools: AnyDeclarativeTool[] = []; + for (const tool of this.allKnownTools.values()) { + if (this.isActiveTool(tool, excludedTools)) { + activeTools.push(tool); + } + } + + const MAX_TOOLS_LIMIT = 512; + if (activeTools.length > MAX_TOOLS_LIMIT) { + const ignored = activeTools + .slice(MAX_TOOLS_LIMIT) + .map((t) => + t instanceof DiscoveredMCPTool ? t.getFullyQualifiedName() : t.name, + ); + return { + totalActive: activeTools.length, + allowedCount: MAX_TOOLS_LIMIT, + ignoredTools: ignored, + }; + } + + return { + totalActive: activeTools.length, + allowedCount: activeTools.length, + ignoredTools: [], + }; + } + + /** + * @returns All the tools that are not excluded and fit within the 512 limit. */ private getActiveTools(): AnyDeclarativeTool[] { const toolMetadata = this.buildToolMetadata(); @@ -558,6 +600,11 @@ export class ToolRegistry { activeTools.push(tool); } } + + const MAX_TOOLS_LIMIT = 512; + if (activeTools.length > MAX_TOOLS_LIMIT) { + return activeTools.slice(0, MAX_TOOLS_LIMIT); + } return activeTools; } @@ -800,7 +847,7 @@ export class ToolRegistry { } } - if (tool && this.isActiveTool(tool)) { + if (tool && this.getActiveTools().includes(tool)) { return tool; } return;