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.
This commit is contained in:
Abhi
2026-05-11 10:06:12 -04:00
parent 8a3fde4c33
commit 4fda0e75dd
4 changed files with 210 additions and 2 deletions

View File

@@ -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);
});
});

View File

@@ -898,6 +898,7 @@ export class Config implements McpContext, AgentLoopContext {
private initialized = false;
private initPromise: Promise<void> | undefined;
private mcpInitializationPromise: Promise<void> | 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 {

View File

@@ -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();
});
});
});
/**

View File

@@ -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;