mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-20 15:00:54 -07:00
feat(core): implement tool preselection to reduce context size
- Created ToolPreselectionService using the classifier model to select only relevant tools for a given prompt. - Integrated pre-selection into LocalAgentExecutor and GeminiClient to automatically filter the tool registry. - Added `general.toolPreselection` toggle in configuration (enabled by default). - Added comprehensive unit tests and an E2E scenario confirming accurate tool reduction without loss of function. - Fixes #17113
This commit is contained in:
@@ -50,6 +50,7 @@ export const GeneralistAgent = (
|
||||
const tools = config.getToolRegistry().getAllToolNames();
|
||||
return {
|
||||
tools,
|
||||
preselectTools: true,
|
||||
};
|
||||
},
|
||||
get promptConfig() {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from '../core/geminiChat.js';
|
||||
import {
|
||||
type FunctionCall,
|
||||
type FunctionDeclaration,
|
||||
type Part,
|
||||
type GenerateContentResponse,
|
||||
type Content,
|
||||
@@ -71,6 +72,7 @@ import type {
|
||||
import type { AgentRegistry } from './registry.js';
|
||||
import { getModelConfigAlias } from './registry.js';
|
||||
import type { ModelRouterService } from '../routing/modelRouterService.js';
|
||||
import { ToolPreselectionService } from '../services/toolPreselectionService.js';
|
||||
|
||||
const {
|
||||
mockSendMessageStream,
|
||||
@@ -552,6 +554,70 @@ describe('LocalAgentExecutor', () => {
|
||||
});
|
||||
|
||||
describe('run (Execution Loop and Logic)', () => {
|
||||
it('should pre-select tools when preselectTools is enabled', async () => {
|
||||
const definition = createTestDefinition([
|
||||
LS_TOOL_NAME,
|
||||
READ_FILE_TOOL_NAME,
|
||||
]);
|
||||
definition.toolConfig!.preselectTools = true;
|
||||
definition.promptConfig.query = '${goal}';
|
||||
|
||||
// Mock tool pre-selection to only keep LS
|
||||
const selectToolsSpy = vi
|
||||
.spyOn(ToolPreselectionService.prototype, 'selectTools')
|
||||
.mockResolvedValue([LS_TOOL_NAME]);
|
||||
|
||||
// Mock a response to terminate the loop
|
||||
mockSendMessageStream.mockImplementation(async () =>
|
||||
(async function* () {
|
||||
yield {
|
||||
type: StreamEventType.CHUNK,
|
||||
value: createMockResponseChunk(
|
||||
[],
|
||||
[
|
||||
{
|
||||
name: TASK_COMPLETE_TOOL_NAME,
|
||||
args: { result: 'done' },
|
||||
id: 'call1',
|
||||
},
|
||||
],
|
||||
),
|
||||
} as StreamEvent;
|
||||
})(),
|
||||
);
|
||||
|
||||
const executor = await LocalAgentExecutor.create(
|
||||
definition,
|
||||
mockConfig,
|
||||
onActivity,
|
||||
);
|
||||
|
||||
const inputs = { goal: 'Test pre-selection' };
|
||||
await executor.run(inputs, signal);
|
||||
|
||||
// Verify ToolPreselectionService was called
|
||||
expect(selectToolsSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Test pre-selection'),
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ name: LS_TOOL_NAME }),
|
||||
expect.objectContaining({ name: READ_FILE_TOOL_NAME }),
|
||||
]),
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
|
||||
// Verify GeminiChat was initialized with ONLY LS and complete_task
|
||||
const chatConstructorArgs = MockedGeminiChat.mock.calls[0];
|
||||
const tools = chatConstructorArgs[2]![0]
|
||||
.functionDeclarations as FunctionDeclaration[];
|
||||
const toolNames = tools.map((t) => t.name);
|
||||
|
||||
expect(toolNames).toContain(LS_TOOL_NAME);
|
||||
expect(toolNames).toContain(TASK_COMPLETE_TOOL_NAME);
|
||||
expect(toolNames).not.toContain(READ_FILE_TOOL_NAME);
|
||||
|
||||
selectToolsSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should log AgentFinish with error if run throws', async () => {
|
||||
const definition = createTestDefinition();
|
||||
// Make the definition invalid to cause an error during run
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
Schema,
|
||||
} from '@google/genai';
|
||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
||||
import { ToolPreselectionService } from '../services/toolPreselectionService.js';
|
||||
import {
|
||||
DiscoveredMCPTool,
|
||||
MCP_QUALIFIED_NAME_SEPARATOR,
|
||||
@@ -460,11 +461,41 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
};
|
||||
|
||||
tools = this.prepareToolsList();
|
||||
chat = await this.createChatObject(augmentedInputs, tools);
|
||||
|
||||
const query = this.definition.promptConfig.query
|
||||
? templateString(this.definition.promptConfig.query, augmentedInputs)
|
||||
: DEFAULT_QUERY_STRING;
|
||||
|
||||
// Tool Pre-selection
|
||||
if (
|
||||
this.definition.toolConfig?.preselectTools &&
|
||||
this.runtimeContext.isToolPreselectionEnabled()
|
||||
) {
|
||||
const preselector = new ToolPreselectionService(this.runtimeContext);
|
||||
const selectedToolNames = await preselector.selectTools(
|
||||
query,
|
||||
tools,
|
||||
combinedSignal,
|
||||
);
|
||||
|
||||
// Filter the agent's tool registry to only include selected tools.
|
||||
// We MUST always include the task completion tool.
|
||||
const toolsToKeep = new Set(selectedToolNames);
|
||||
toolsToKeep.add(TASK_COMPLETE_TOOL_NAME);
|
||||
|
||||
const allAgentTools = this.toolRegistry.getAllToolNames();
|
||||
for (const toolName of allAgentTools) {
|
||||
if (!toolsToKeep.has(toolName)) {
|
||||
this.toolRegistry.unregisterTool(toolName);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-prepare the tools list after filtering the registry.
|
||||
tools = this.prepareToolsList();
|
||||
}
|
||||
|
||||
chat = await this.createChatObject(augmentedInputs, tools);
|
||||
|
||||
const pendingHintsQueue: string[] = [];
|
||||
const hintListener = (hint: string) => {
|
||||
pendingHintsQueue.push(hint);
|
||||
|
||||
@@ -410,6 +410,13 @@ export class AgentRegistry {
|
||||
);
|
||||
}
|
||||
|
||||
if (overrides.preselectTools !== undefined) {
|
||||
merged.toolConfig = {
|
||||
...(definition.toolConfig ?? { tools: [] }),
|
||||
preselectTools: overrides.preselectTools,
|
||||
};
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,11 @@ export interface PromptConfig {
|
||||
*/
|
||||
export interface ToolConfig {
|
||||
tools: Array<string | FunctionDeclaration | AnyDeclarativeTool>;
|
||||
/**
|
||||
* Whether to pre-select a subset of tools based on the user request.
|
||||
* This can help reduce context size and improve performance.
|
||||
*/
|
||||
preselectTools?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user