Expose Codebase Investigator settings to the user (#10844)

This commit is contained in:
Silvio Junior
2025-10-13 22:30:32 -04:00
committed by GitHub
parent f56a561f02
commit 9185f68e52
8 changed files with 120 additions and 131 deletions

View File

@@ -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', () => {

View File

@@ -758,7 +758,8 @@ export async function loadCliConfig(
useModelRouter,
enableMessageBusIntegration:
settings.tools?.enableMessageBusIntegration ?? false,
enableSubagents: settings.experimental?.enableSubagents ?? false,
codebaseInvestigatorSettings:
settings.experimental?.codebaseInvestigatorSettings,
});
}

View File

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

View File

@@ -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,
},
},
},
},
},

View File

@@ -342,7 +342,7 @@ describe('SettingsDialog', () => {
await wait();
expect(lastFrame()).toContain('● Use Model Router');
expect(lastFrame()).toContain('● Codebase Investigator Max Num Turns');
unmount();
});

View File

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

View File

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

View File

@@ -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<ToolRegistry> {
@@ -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();