mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-15 14:23:02 -07:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user