From 9185f68e5294247af902b07334580e97b283a2f3 Mon Sep 17 00:00:00 2001 From: Silvio Junior Date: Mon, 13 Oct 2025 22:30:32 -0400 Subject: [PATCH] Expose Codebase Investigator settings to the user (#10844) --- packages/cli/src/config/config.test.ts | 53 ---------------- packages/cli/src/config/config.ts | 3 +- .../cli/src/config/settingsSchema.test.ts | 19 ------ packages/cli/src/config/settingsSchema.ts | 62 +++++++++++++++++-- .../src/ui/components/SettingsDialog.test.tsx | 2 +- packages/core/src/agents/registry.ts | 28 ++++++++- packages/core/src/config/config.test.ts | 58 +++++------------ packages/core/src/config/config.ts | 26 +++++--- 8 files changed, 120 insertions(+), 131 deletions(-) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 19756f939a..0ec7924fb2 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -2543,59 +2543,6 @@ describe('loadCliConfig useRipgrep', () => { expect(config.getUseModelRouter()).toBe(false); }); }); - - describe('loadCliConfig enableSubagents', () => { - it('should be false by default when enableSubagents is not set in settings', async () => { - process.argv = ['node', 'script.js']; - const argv = await parseArguments({} as Settings); - const settings: Settings = {}; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager( - ExtensionStorage.getUserExtensionsDir(), - argv.extensions, - ), - 'test-session', - argv, - ); - expect(config.getEnableSubagents()).toBe(false); - }); - - it('should be true when enableSubagents is set to true in settings', async () => { - process.argv = ['node', 'script.js']; - const argv = await parseArguments({} as Settings); - const settings: Settings = { experimental: { enableSubagents: true } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager( - ExtensionStorage.getUserExtensionsDir(), - argv.extensions, - ), - 'test-session', - argv, - ); - expect(config.getEnableSubagents()).toBe(true); - }); - - it('should be false when enableSubagents is explicitly set to false in settings', async () => { - process.argv = ['node', 'script.js']; - const argv = await parseArguments({} as Settings); - const settings: Settings = { experimental: { enableSubagents: false } }; - const config = await loadCliConfig( - settings, - [], - new ExtensionEnablementManager( - ExtensionStorage.getUserExtensionsDir(), - argv.extensions, - ), - 'test-session', - argv, - ); - expect(config.getEnableSubagents()).toBe(false); - }); - }); }); describe('screenReader configuration', () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index f68fb41313..6a807c9b0c 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -758,7 +758,8 @@ export async function loadCliConfig( useModelRouter, enableMessageBusIntegration: settings.tools?.enableMessageBusIntegration ?? false, - enableSubagents: settings.experimental?.enableSubagents ?? false, + codebaseInvestigatorSettings: + settings.experimental?.codebaseInvestigatorSettings, }); } diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index 2ec76712d8..4088151049 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -330,24 +330,5 @@ describe('SettingsSchema', () => { getSettingsSchema().experimental.properties.useModelRouter.default, ).toBe(false); }); - - it('should have enableSubagents setting in schema', () => { - expect( - getSettingsSchema().experimental.properties.enableSubagents, - ).toBeDefined(); - expect( - getSettingsSchema().experimental.properties.enableSubagents.type, - ).toBe('boolean'); - expect( - getSettingsSchema().experimental.properties.enableSubagents.category, - ).toBe('Experimental'); - expect( - getSettingsSchema().experimental.properties.enableSubagents.default, - ).toBe(false); - expect( - getSettingsSchema().experimental.properties.enableSubagents - .requiresRestart, - ).toBe(true); - }); }); }); diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index a57dbc6654..887dedb205 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -19,6 +19,7 @@ import type { import { DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, + DEFAULT_GEMINI_MODEL, } from '@google/gemini-cli-core'; import type { CustomTheme } from '../ui/themes/theme.js'; import type { SessionRetentionSettings } from './settings.js'; @@ -1065,14 +1066,65 @@ const SETTINGS_SCHEMA = { 'Enable model routing to route requests to the best model based on complexity.', showInDialog: true, }, - enableSubagents: { - type: 'boolean', - label: 'Enable Subagents', + codebaseInvestigatorSettings: { + type: 'object', + label: 'Codebase Investigator Settings', category: 'Experimental', requiresRestart: true, - default: false, - description: 'Enable experimental subagents.', + default: {}, + description: 'Configuration for Codebase Investigator.', showInDialog: false, + properties: { + enabled: { + type: 'boolean', + label: 'Enable Codebase Investigator', + category: 'Experimental', + requiresRestart: true, + default: false, + description: 'Enable the Codebase Investigator agent.', + showInDialog: true, + }, + maxNumTurns: { + type: 'number', + label: 'Codebase Investigator Max Num Turns', + category: 'Experimental', + requiresRestart: true, + default: 15, + description: + 'Maximum number of turns for the Codebase Investigator agent.', + showInDialog: true, + }, + maxTimeMinutes: { + type: 'number', + label: 'Max Time (Minutes)', + category: 'Experimental', + requiresRestart: true, + default: 5, + description: + 'Maximum time for the Codebase Investigator agent (in minutes).', + showInDialog: false, + }, + thinkingBudget: { + type: 'number', + label: 'Thinking Budget', + category: 'Experimental', + requiresRestart: true, + default: -1, + description: + 'The thinking budget for the Codebase Investigator agent.', + showInDialog: false, + }, + model: { + type: 'string', + label: 'Model', + category: 'Experimental', + requiresRestart: true, + default: DEFAULT_GEMINI_MODEL, + description: + 'The model to use for the Codebase Investigator agent.', + showInDialog: false, + }, + }, }, }, }, diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index 1c6d1c1de2..268325923b 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -342,7 +342,7 @@ describe('SettingsDialog', () => { await wait(); - expect(lastFrame()).toContain('● Use Model Router'); + expect(lastFrame()).toContain('● Codebase Investigator Max Num Turns'); unmount(); }); diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index dec30fdc05..c675179a5b 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -33,7 +33,33 @@ export class AgentRegistry { } private loadBuiltInAgents(): void { - this.registerAgent(CodebaseInvestigatorAgent); + const investigatorSettings = this.config.getCodebaseInvestigatorSettings(); + + // Only register the agent if it's enabled in the settings. + if (investigatorSettings?.enabled) { + const agentDef = { + ...CodebaseInvestigatorAgent, + modelConfig: { + ...CodebaseInvestigatorAgent.modelConfig, + model: + investigatorSettings.model ?? + CodebaseInvestigatorAgent.modelConfig.model, + thinkingBudget: + investigatorSettings.thinkingBudget ?? + CodebaseInvestigatorAgent.modelConfig.thinkingBudget, + }, + runConfig: { + ...CodebaseInvestigatorAgent.runConfig, + max_time_minutes: + investigatorSettings.maxTimeMinutes ?? + CodebaseInvestigatorAgent.runConfig.max_time_minutes, + max_turns: + investigatorSettings.maxNumTurns ?? + CodebaseInvestigatorAgent.runConfig.max_turns, + }, + }; + this.registerAgent(agentDef); + } } /** diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index e7ddd738ef..a23cabe0b6 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -133,6 +133,7 @@ vi.mock('../agents/registry.js', () => { const AgentRegistryMock = vi.fn(); AgentRegistryMock.prototype.initialize = vi.fn(); AgentRegistryMock.prototype.getAllDefinitions = vi.fn(() => []); + AgentRegistryMock.prototype.getDefinition = vi.fn(); return { AgentRegistry: AgentRegistryMock }; }); @@ -600,31 +601,6 @@ describe('Server Config (config.ts)', () => { }); }); - describe('EnableSubagents Configuration', () => { - it('should default enableSubagents to false when not provided', () => { - const config = new Config(baseParams); - expect(config.getEnableSubagents()).toBe(false); - }); - - it('should set enableSubagents to true when provided as true', () => { - const paramsWithSubagents: ConfigParameters = { - ...baseParams, - enableSubagents: true, - }; - const config = new Config(paramsWithSubagents); - expect(config.getEnableSubagents()).toBe(true); - }); - - it('should set enableSubagents to false when explicitly provided as false', () => { - const paramsWithSubagents: ConfigParameters = { - ...baseParams, - enableSubagents: false, - }; - const config = new Config(paramsWithSubagents); - expect(config.getEnableSubagents()).toBe(false); - }); - }); - describe('ContinueOnFailedApiCall Configuration', () => { it('should default continueOnFailedApiCall to false when not provided', () => { const config = new Config(baseParams); @@ -679,25 +655,26 @@ describe('Server Config (config.ts)', () => { expect(wasReadFileToolRegistered).toBe(false); }); - it('should register subagents as tools when enableSubagents is true', async () => { + it('should register subagents as tools when codebaseInvestigatorSettings.enabled is true', async () => { const params: ConfigParameters = { ...baseParams, - enableSubagents: true, + codebaseInvestigatorSettings: { enabled: true }, }; const config = new Config(params); - const mockAgentDefinitions = [ - { name: 'agent1', description: 'Agent 1', instructions: 'Inst 1' }, - { name: 'agent2', description: 'Agent 2', instructions: 'Inst 2' }, - ]; + const mockAgentDefinition = { + name: 'codebase-investigator', + description: 'Agent 1', + instructions: 'Inst 1', + }; const AgentRegistryMock = ( (await vi.importMock('../agents/registry.js')) as { AgentRegistry: Mock; } ).AgentRegistry; - AgentRegistryMock.prototype.getAllDefinitions.mockReturnValue( - mockAgentDefinitions, + AgentRegistryMock.prototype.getDefinition.mockReturnValue( + mockAgentDefinition, ); const SubagentToolWrapperMock = ( @@ -714,14 +691,9 @@ describe('Server Config (config.ts)', () => { } ).ToolRegistry.prototype.registerTool; - expect(SubagentToolWrapperMock).toHaveBeenCalledTimes(2); + expect(SubagentToolWrapperMock).toHaveBeenCalledTimes(1); expect(SubagentToolWrapperMock).toHaveBeenCalledWith( - mockAgentDefinitions[0], - config, - undefined, - ); - expect(SubagentToolWrapperMock).toHaveBeenCalledWith( - mockAgentDefinitions[1], + mockAgentDefinition, config, undefined, ); @@ -730,13 +702,13 @@ describe('Server Config (config.ts)', () => { const registeredWrappers = calls.filter( (call) => call[0] instanceof SubagentToolWrapperMock, ); - expect(registeredWrappers).toHaveLength(2); + expect(registeredWrappers).toHaveLength(1); }); - it('should not register subagents as tools when enableSubagents is false', async () => { + it('should not register subagents as tools when codebaseInvestigatorSettings.enabled is false', async () => { const params: ConfigParameters = { ...baseParams, - enableSubagents: false, + codebaseInvestigatorSettings: { enabled: false }, }; const config = new Config(params); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index cb255a3b62..e6a77e979f 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -116,6 +116,14 @@ export interface OutputSettings { format?: OutputFormat; } +export interface CodebaseInvestigatorSettings { + enabled?: boolean; + maxNumTurns?: number; + maxTimeMinutes?: number; + thinkingBudget?: number; + model?: string; +} + /** * All information required in CLI to handle an extension. Defined in Core so * that the collection of loaded, active, and inactive extensions can be passed @@ -268,7 +276,7 @@ export interface ConfigParameters { output?: OutputSettings; useModelRouter?: boolean; enableMessageBusIntegration?: boolean; - enableSubagents?: boolean; + codebaseInvestigatorSettings?: CodebaseInvestigatorSettings; continueOnFailedApiCall?: boolean; } @@ -361,7 +369,7 @@ export class Config { private readonly outputSettings: OutputSettings; private readonly useModelRouter: boolean; private readonly enableMessageBusIntegration: boolean; - private readonly enableSubagents: boolean; + private readonly codebaseInvestigatorSettings?: CodebaseInvestigatorSettings; private readonly continueOnFailedApiCall: boolean; constructor(params: ConfigParameters) { @@ -455,7 +463,7 @@ export class Config { this.useModelRouter = params.useModelRouter ?? false; this.enableMessageBusIntegration = params.enableMessageBusIntegration ?? false; - this.enableSubagents = params.enableSubagents ?? false; + this.codebaseInvestigatorSettings = params.codebaseInvestigatorSettings; this.continueOnFailedApiCall = params.continueOnFailedApiCall ?? true; this.extensionManagement = params.extensionManagement ?? true; this.storage = new Storage(this.targetDir); @@ -1039,8 +1047,8 @@ export class Config { return this.enableMessageBusIntegration; } - getEnableSubagents(): boolean { - return this.enableSubagents; + getCodebaseInvestigatorSettings(): CodebaseInvestigatorSettings | undefined { + return this.codebaseInvestigatorSettings; } async createToolRegistry(): Promise { @@ -1135,9 +1143,11 @@ export class Config { } // Register Subagents as Tools - if (this.getEnableSubagents()) { - const agentDefinitions = this.agentRegistry.getAllDefinitions(); - for (const definition of agentDefinitions) { + if (this.getCodebaseInvestigatorSettings()?.enabled) { + const definition = this.agentRegistry.getDefinition( + 'codebase_investigator', + ); + if (definition) { // We must respect the main allowed/exclude lists for agents too. const excludeTools = this.getExcludeTools() || []; const allowedTools = this.getAllowedTools();