mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b82b98e71b | |||
| b269464df2 | |||
| 5eeddaef52 | |||
| 1de614f38e | |||
| 9f7a3d30ea | |||
| dd4df1bd11 | |||
| 2404e4fae8 |
+2
-13
@@ -49,19 +49,8 @@ Gemini CLI comes with the following built-in sub-agents:
|
||||
dependencies.
|
||||
- **When to use:** "How does the authentication system work?", "Map out the
|
||||
dependencies of the `AgentRegistry` class."
|
||||
- **Configuration:** Enabled by default. You can configure it in
|
||||
`settings.json`. Example (forcing a specific model):
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"codebaseInvestigatorSettings": {
|
||||
"enabled": true,
|
||||
"maxNumTurns": 20,
|
||||
"model": "gemini-2.5-pro"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Configuration:** Enabled by default. You can configure it using agent
|
||||
overrides in `settings.json`.
|
||||
|
||||
### CLI Help Agent
|
||||
|
||||
|
||||
Generated
+24
-1
@@ -2253,6 +2253,7 @@
|
||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.2",
|
||||
@@ -2433,6 +2434,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -2466,6 +2468,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
|
||||
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
@@ -2834,6 +2837,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
|
||||
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
@@ -2867,6 +2871,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
|
||||
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1"
|
||||
@@ -2919,6 +2924,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
|
||||
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.0.1",
|
||||
"@opentelemetry/resources": "2.0.1",
|
||||
@@ -4134,6 +4140,7 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4428,6 +4435,7 @@
|
||||
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.35.0",
|
||||
"@typescript-eslint/types": "8.35.0",
|
||||
@@ -5420,6 +5428,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8429,6 +8438,7 @@
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -8969,6 +8979,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -10570,6 +10581,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
|
||||
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -14354,6 +14366,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14364,6 +14377,7 @@
|
||||
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"shell-quote": "^1.6.1",
|
||||
"ws": "^7"
|
||||
@@ -16600,6 +16614,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16823,7 +16838,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
@@ -16831,6 +16847,7 @@
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -17003,6 +17020,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -17210,6 +17228,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -17323,6 +17342,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -17335,6 +17355,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -18039,6 +18060,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -18335,6 +18357,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type ServerGeminiErrorEvent,
|
||||
type ServerGeminiStreamEvent,
|
||||
type ToolCallConfirmationDetails,
|
||||
type SerializableConfirmationDetails,
|
||||
type Config,
|
||||
type UserTierId,
|
||||
type AnsiOutput,
|
||||
@@ -65,7 +66,10 @@ export class Task {
|
||||
scheduler: CoreToolScheduler;
|
||||
config: Config;
|
||||
geminiClient: GeminiClient;
|
||||
pendingToolConfirmationDetails: Map<string, ToolCallConfirmationDetails>;
|
||||
pendingToolConfirmationDetails: Map<
|
||||
string,
|
||||
ToolCallConfirmationDetails | SerializableConfirmationDetails
|
||||
>;
|
||||
taskState: TaskState;
|
||||
eventBus?: ExecutionEventBus;
|
||||
completedToolCalls: CompletedToolCall[];
|
||||
@@ -378,7 +382,7 @@ export class Task {
|
||||
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
|
||||
this.pendingToolConfirmationDetails.set(
|
||||
tc.request.callId,
|
||||
tc.confirmationDetails as ToolCallConfirmationDetails,
|
||||
tc.confirmationDetails,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -411,10 +415,12 @@ export class Task {
|
||||
);
|
||||
toolCalls.forEach((tc: ToolCall) => {
|
||||
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
(tc.confirmationDetails as ToolCallConfirmationDetails).onConfirm(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
);
|
||||
if ('onConfirm' in tc.confirmationDetails) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
tc.confirmationDetails.onConfirm(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
);
|
||||
}
|
||||
this.pendingToolConfirmationDetails.delete(tc.request.callId);
|
||||
}
|
||||
});
|
||||
@@ -803,6 +809,13 @@ export class Task {
|
||||
// This will trigger the scheduler to continue or cancel the specific tool.
|
||||
// The scheduler's onToolCallsUpdate will then reflect the new state (e.g., executing or cancelled).
|
||||
|
||||
if (!('onConfirm' in confirmationDetails)) {
|
||||
logger.error(
|
||||
`[Task] Serializable confirmation details not supported yet in a2a-server for callId: ${callId}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If `edit` tool call, pass updated payload if presesent
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
const payload = part.data['newContent']
|
||||
|
||||
@@ -804,6 +804,7 @@ export async function loadCliConfig(
|
||||
output: {
|
||||
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,
|
||||
},
|
||||
adaptiveThinking: settings.experimental?.adaptiveThinking,
|
||||
fakeResponses: argv.fakeResponses,
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors,
|
||||
|
||||
@@ -2154,64 +2154,9 @@ describe('Settings Loading and Merging', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should migrate experimental agent settings in system scope in memory but not save', () => {
|
||||
const systemSettingsContent = {
|
||||
experimental: {
|
||||
codebaseInvestigatorSettings: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
(fs.readFileSync as Mock).mockImplementation(
|
||||
(p: fs.PathOrFileDescriptor) => {
|
||||
if (p === getSystemSettingsPath()) {
|
||||
return JSON.stringify(systemSettingsContent);
|
||||
}
|
||||
return '{}';
|
||||
},
|
||||
);
|
||||
|
||||
const feedbackSpy = mockCoreEvents.emitFeedback;
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Verify it was migrated in memory
|
||||
expect(settings.system.settings.agents?.overrides).toMatchObject({
|
||||
codebase_investigator: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify it was NOT saved back to disk
|
||||
expect(updateSettingsFilePreservingFormat).not.toHaveBeenCalledWith(
|
||||
getSystemSettingsPath(),
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
// Verify warnings were shown
|
||||
expect(feedbackSpy).toHaveBeenCalledWith(
|
||||
'warning',
|
||||
expect.stringContaining(
|
||||
'The system configuration contains deprecated settings: [experimental.codebaseInvestigatorSettings]',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should migrate experimental agent settings to agents overrides', () => {
|
||||
it('should not throw if experimental settings are missing', () => {
|
||||
const userSettingsContent = {
|
||||
experimental: {
|
||||
codebaseInvestigatorSettings: {
|
||||
enabled: true,
|
||||
maxNumTurns: 15,
|
||||
maxTimeMinutes: 5,
|
||||
thinkingBudget: 16384,
|
||||
model: 'gemini-1.5-pro',
|
||||
},
|
||||
cliHelpAgentSettings: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
experimental: {},
|
||||
};
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
@@ -2223,29 +2168,7 @@ describe('Settings Loading and Merging', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const settings = loadSettings(MOCK_WORKSPACE_DIR);
|
||||
|
||||
// Verify migration to agents.overrides
|
||||
expect(settings.user.settings.agents?.overrides).toMatchObject({
|
||||
codebase_investigator: {
|
||||
enabled: true,
|
||||
runConfig: {
|
||||
maxTurns: 15,
|
||||
maxTimeMinutes: 5,
|
||||
},
|
||||
modelConfig: {
|
||||
model: 'gemini-1.5-pro',
|
||||
generateContentConfig: {
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 16384,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cli_help: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
expect(() => loadSettings(MOCK_WORKSPACE_DIR)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -904,21 +904,6 @@ export function migrateDeprecatedSettings(
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate experimental agent settings
|
||||
const experimentalModified = migrateExperimentalSettings(
|
||||
settings,
|
||||
loadedSettings,
|
||||
scope,
|
||||
removeDeprecated,
|
||||
foundDeprecated,
|
||||
);
|
||||
|
||||
if (experimentalModified) {
|
||||
if (!settingsFile.readOnly) {
|
||||
anyModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsFile.readOnly && foundDeprecated.length > 0) {
|
||||
systemWarnings.set(scope, foundDeprecated);
|
||||
}
|
||||
@@ -983,105 +968,3 @@ export function saveModelChange(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateExperimentalSettings(
|
||||
settings: Settings,
|
||||
loadedSettings: LoadedSettings,
|
||||
scope: LoadableSettingScope,
|
||||
removeDeprecated: boolean,
|
||||
foundDeprecated?: string[],
|
||||
): boolean {
|
||||
const experimentalSettings = settings.experimental as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
if (experimentalSettings) {
|
||||
const agentsSettings = {
|
||||
...(settings.agents as Record<string, unknown> | undefined),
|
||||
};
|
||||
const agentsOverrides = {
|
||||
...((agentsSettings['overrides'] as Record<string, unknown>) || {}),
|
||||
};
|
||||
let modified = false;
|
||||
|
||||
const migrateExperimental = (
|
||||
oldKey: string,
|
||||
migrateFn: (oldValue: Record<string, unknown>) => void,
|
||||
) => {
|
||||
const old = experimentalSettings[oldKey];
|
||||
if (old) {
|
||||
foundDeprecated?.push(`experimental.${oldKey}`);
|
||||
migrateFn(old as Record<string, unknown>);
|
||||
modified = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Migrate codebaseInvestigatorSettings -> agents.overrides.codebase_investigator
|
||||
migrateExperimental('codebaseInvestigatorSettings', (old) => {
|
||||
const override = {
|
||||
...(agentsOverrides['codebase_investigator'] as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
};
|
||||
|
||||
if (old['enabled'] !== undefined) override['enabled'] = old['enabled'];
|
||||
|
||||
const runConfig = {
|
||||
...(override['runConfig'] as Record<string, unknown> | undefined),
|
||||
};
|
||||
if (old['maxNumTurns'] !== undefined)
|
||||
runConfig['maxTurns'] = old['maxNumTurns'];
|
||||
if (old['maxTimeMinutes'] !== undefined)
|
||||
runConfig['maxTimeMinutes'] = old['maxTimeMinutes'];
|
||||
if (Object.keys(runConfig).length > 0) override['runConfig'] = runConfig;
|
||||
|
||||
if (old['model'] !== undefined || old['thinkingBudget'] !== undefined) {
|
||||
const modelConfig = {
|
||||
...(override['modelConfig'] as Record<string, unknown> | undefined),
|
||||
};
|
||||
if (old['model'] !== undefined) modelConfig['model'] = old['model'];
|
||||
if (old['thinkingBudget'] !== undefined) {
|
||||
const generateContentConfig = {
|
||||
...(modelConfig['generateContentConfig'] as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
};
|
||||
const thinkingConfig = {
|
||||
...(generateContentConfig['thinkingConfig'] as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
};
|
||||
thinkingConfig['thinkingBudget'] = old['thinkingBudget'];
|
||||
generateContentConfig['thinkingConfig'] = thinkingConfig;
|
||||
modelConfig['generateContentConfig'] = generateContentConfig;
|
||||
}
|
||||
override['modelConfig'] = modelConfig;
|
||||
}
|
||||
|
||||
agentsOverrides['codebase_investigator'] = override;
|
||||
});
|
||||
|
||||
// Migrate cliHelpAgentSettings -> agents.overrides.cli_help
|
||||
migrateExperimental('cliHelpAgentSettings', (old) => {
|
||||
const override = {
|
||||
...(agentsOverrides['cli_help'] as Record<string, unknown> | undefined),
|
||||
};
|
||||
if (old['enabled'] !== undefined) override['enabled'] = old['enabled'];
|
||||
agentsOverrides['cli_help'] = override;
|
||||
});
|
||||
|
||||
if (modified) {
|
||||
agentsSettings['overrides'] = agentsOverrides;
|
||||
loadedSettings.setValue(scope, 'agents', agentsSettings);
|
||||
|
||||
if (removeDeprecated) {
|
||||
const newExperimental = { ...experimentalSettings };
|
||||
delete newExperimental['codebaseInvestigatorSettings'];
|
||||
delete newExperimental['cliHelpAgentSettings'];
|
||||
loadedSettings.setValue(scope, 'experimental', newExperimental);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1552,6 +1552,37 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable planning features (Plan Mode and tools).',
|
||||
showInDialog: true,
|
||||
},
|
||||
adaptiveThinking: {
|
||||
type: 'object',
|
||||
label: 'Adaptive Thinking Settings',
|
||||
category: 'Experimental',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description: 'Configuration for Adaptive Thinking Budget.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Adaptive Thinking',
|
||||
category: 'Experimental',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Enable adaptive thinking budget based on task complexity.',
|
||||
showInDialog: true,
|
||||
},
|
||||
classifierModel: {
|
||||
type: 'string',
|
||||
label: 'Classifier Model',
|
||||
category: 'Experimental',
|
||||
requiresRestart: false,
|
||||
default: 'classifier',
|
||||
description:
|
||||
'The model (or alias) to use for complexity classification.',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ import type {
|
||||
import { ModelConfigService } from '../services/modelConfigService.js';
|
||||
import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js';
|
||||
import { ContextManager } from '../services/contextManager.js';
|
||||
import { AdaptiveBudgetService } from '../services/adaptiveBudgetService.js';
|
||||
import type { GenerateContentParameters } from '@google/genai';
|
||||
|
||||
// Re-export OAuth config type
|
||||
@@ -446,6 +447,10 @@ export interface ConfigParameters {
|
||||
policyEngineConfig?: PolicyEngineConfig;
|
||||
output?: OutputSettings;
|
||||
disableModelRouterForAuth?: AuthType[];
|
||||
adaptiveThinking?: {
|
||||
enabled?: boolean;
|
||||
classifierModel?: string;
|
||||
};
|
||||
continueOnFailedApiCall?: boolean;
|
||||
retryFetchErrors?: boolean;
|
||||
enableShellOutputEfficiency?: boolean;
|
||||
@@ -592,6 +597,10 @@ export class Config {
|
||||
private readonly messageBus: MessageBus;
|
||||
private readonly policyEngine: PolicyEngine;
|
||||
private readonly outputSettings: OutputSettings;
|
||||
private readonly adaptiveThinking: {
|
||||
enabled: boolean;
|
||||
classifierModel: string;
|
||||
};
|
||||
private readonly continueOnFailedApiCall: boolean;
|
||||
private readonly retryFetchErrors: boolean;
|
||||
private readonly enableShellOutputEfficiency: boolean;
|
||||
@@ -634,6 +643,7 @@ export class Config {
|
||||
private readonly planEnabled: boolean;
|
||||
private contextManager?: ContextManager;
|
||||
private terminalBackground: string | undefined = undefined;
|
||||
private adaptiveBudgetService!: AdaptiveBudgetService;
|
||||
private remoteAdminSettings: AdminControlsSettings | undefined;
|
||||
private latestApiRequest: GenerateContentParameters | undefined;
|
||||
private lastModeSwitchTime: number = Date.now();
|
||||
@@ -781,6 +791,10 @@ export class Config {
|
||||
this.enableHooks = params.enableHooks ?? true;
|
||||
this.disabledHooks = params.disabledHooks ?? [];
|
||||
|
||||
this.adaptiveThinking = {
|
||||
enabled: params.adaptiveThinking?.enabled ?? false,
|
||||
classifierModel: params.adaptiveThinking?.classifierModel ?? 'classifier',
|
||||
};
|
||||
this.continueOnFailedApiCall = params.continueOnFailedApiCall ?? true;
|
||||
this.enableShellOutputEfficiency =
|
||||
params.enableShellOutputEfficiency ?? true;
|
||||
@@ -967,6 +981,13 @@ export class Config {
|
||||
await this.contextManager.refresh();
|
||||
}
|
||||
|
||||
this.adaptiveBudgetService = new AdaptiveBudgetService(this);
|
||||
if (this.adaptiveThinking.enabled) {
|
||||
debugLogger.debug(
|
||||
`Adaptive Thinking Budget enabled (classifier: ${this.adaptiveThinking.classifierModel})`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.geminiClient.initialize();
|
||||
this.syncPlanModeTools();
|
||||
}
|
||||
@@ -975,6 +996,10 @@ export class Config {
|
||||
return this.contentGenerator;
|
||||
}
|
||||
|
||||
getAdaptiveBudgetService(): AdaptiveBudgetService {
|
||||
return this.adaptiveBudgetService;
|
||||
}
|
||||
|
||||
async refreshAuth(authMethod: AuthType) {
|
||||
// Reset availability service when switching auth
|
||||
this.modelAvailabilityService.reset();
|
||||
@@ -2143,6 +2168,10 @@ export class Config {
|
||||
return this.enableHooksUI;
|
||||
}
|
||||
|
||||
getAdaptiveThinkingConfig(): { enabled: boolean; classifierModel: string } {
|
||||
return this.adaptiveThinking;
|
||||
}
|
||||
|
||||
async createToolRegistry(): Promise<ToolRegistry> {
|
||||
const registry = new ToolRegistry(this, this.messageBus);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { retryWithBackoff } from '../utils/retry.js';
|
||||
import type { ValidationRequiredError } from '../utils/googleQuotaErrors.js';
|
||||
import { getErrorMessage } from '../utils/errors.js';
|
||||
import { tokenLimit } from './tokenLimits.js';
|
||||
import { partListUnionToString } from './geminiRequest.js';
|
||||
import type {
|
||||
ChatRecordingService,
|
||||
ResumedSessionData,
|
||||
@@ -64,6 +65,7 @@ import { resolveModel } from '../config/models.js';
|
||||
import type { RetryAvailabilityContext } from '../utils/retry.js';
|
||||
import { partToString } from '../utils/partUtils.js';
|
||||
import { coreEvents, CoreEvent } from '../utils/events.js';
|
||||
import type { AdaptiveBudgetResult } from '../services/adaptiveBudgetService.js';
|
||||
|
||||
const MAX_TURNS = 100;
|
||||
|
||||
@@ -88,6 +90,7 @@ export class GeminiClient {
|
||||
private readonly toolOutputMaskingService: ToolOutputMaskingService;
|
||||
private lastPromptId: string;
|
||||
private currentSequenceModel: string | null = null;
|
||||
private currentAdaptiveConfig: AdaptiveBudgetResult | null = null;
|
||||
private lastSentIdeContext: IdeContext | undefined;
|
||||
private forceFullIdeContext = true;
|
||||
|
||||
@@ -642,6 +645,42 @@ export class GeminiClient {
|
||||
|
||||
// availability logic
|
||||
const modelConfigKey: ModelConfigKey = { model: modelToUse };
|
||||
|
||||
// Adaptive Thinking Budget Integration
|
||||
if (
|
||||
!isInvalidStreamRetry &&
|
||||
this.config.getAdaptiveThinkingConfig().enabled &&
|
||||
prompt_id !== 'adaptive-budget-classifier'
|
||||
) {
|
||||
if (this.currentAdaptiveConfig) {
|
||||
modelConfigKey.thinkingBudget =
|
||||
this.currentAdaptiveConfig.thinkingBudget;
|
||||
modelConfigKey.thinkingLevel = this.currentAdaptiveConfig.thinkingLevel;
|
||||
} else {
|
||||
const userMessage = partListUnionToString(request);
|
||||
if (userMessage) {
|
||||
const adaptiveConfig = await this.config
|
||||
.getAdaptiveBudgetService()
|
||||
.determineAdaptiveConfig(
|
||||
userMessage,
|
||||
modelToUse,
|
||||
this.getHistory(),
|
||||
);
|
||||
|
||||
if (adaptiveConfig) {
|
||||
this.currentAdaptiveConfig = adaptiveConfig;
|
||||
modelConfigKey.thinkingBudget = adaptiveConfig.thinkingBudget;
|
||||
modelConfigKey.thinkingLevel = adaptiveConfig.thinkingLevel;
|
||||
this.getChat().recordAdaptiveThinking({
|
||||
complexity: adaptiveConfig.complexity,
|
||||
thinkingBudget: adaptiveConfig.thinkingBudget,
|
||||
thinkingLevel: adaptiveConfig.thinkingLevel,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { model: finalModel } = applyModelSelection(
|
||||
this.config,
|
||||
modelConfigKey,
|
||||
@@ -781,6 +820,7 @@ export class GeminiClient {
|
||||
this.hookStateMap.delete(this.lastPromptId);
|
||||
this.lastPromptId = prompt_id;
|
||||
this.currentSequenceModel = null;
|
||||
this.currentAdaptiveConfig = null;
|
||||
}
|
||||
|
||||
if (hooksEnabled && messageBus) {
|
||||
|
||||
@@ -260,6 +260,14 @@ export class GeminiChat {
|
||||
this.systemInstruction = sysInstr;
|
||||
}
|
||||
|
||||
recordAdaptiveThinking(info: {
|
||||
complexity: number;
|
||||
thinkingBudget?: number;
|
||||
thinkingLevel?: string;
|
||||
}) {
|
||||
this.chatRecordingService.recordAdaptiveThinking(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the model and returns the response in chunks.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
AdaptiveBudgetService,
|
||||
ComplexityLevel,
|
||||
} from './adaptiveBudgetService.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
|
||||
describe('AdaptiveBudgetService', () => {
|
||||
it('should map complexity levels to correct V2 budgets', () => {
|
||||
const service = new AdaptiveBudgetService({} as Config);
|
||||
expect(service.getThinkingBudgetV2(ComplexityLevel.SIMPLE)).toBe(1024);
|
||||
expect(service.getThinkingBudgetV2(ComplexityLevel.MODERATE)).toBe(4096);
|
||||
expect(service.getThinkingBudgetV2(ComplexityLevel.HIGH)).toBe(16384);
|
||||
expect(service.getThinkingBudgetV2(ComplexityLevel.EXTREME)).toBe(32768);
|
||||
});
|
||||
|
||||
it('should map complexity levels to correct V3 levels', () => {
|
||||
const service = new AdaptiveBudgetService({} as Config);
|
||||
expect(service.getThinkingLevelV3(ComplexityLevel.SIMPLE)).toBe('LOW');
|
||||
expect(service.getThinkingLevelV3(ComplexityLevel.MODERATE)).toBe('LOW');
|
||||
expect(service.getThinkingLevelV3(ComplexityLevel.HIGH)).toBe('HIGH');
|
||||
expect(service.getThinkingLevelV3(ComplexityLevel.EXTREME)).toBe('HIGH');
|
||||
});
|
||||
|
||||
it('should determine adaptive config based on LLM response', async () => {
|
||||
const mockGenerateContent = vi.fn().mockResolvedValue({
|
||||
candidates: [{ content: { parts: [{ text: '3' }] } }],
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
getBaseLlmClient: () => ({
|
||||
generateContent: mockGenerateContent,
|
||||
}),
|
||||
getAdaptiveThinkingConfig: () => ({
|
||||
enabled: true,
|
||||
classifierModel: 'gemini-2.0-flash',
|
||||
}),
|
||||
} as unknown as Config;
|
||||
|
||||
const service = new AdaptiveBudgetService(mockConfig);
|
||||
const result = await service.determineAdaptiveConfig(
|
||||
'Complex task',
|
||||
'gemini-2.5-pro',
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result?.complexity).toBe(ComplexityLevel.HIGH);
|
||||
expect(result?.thinkingBudget).toBe(16384);
|
||||
expect(mockGenerateContent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle Gemini 3 models with thinkingLevel', async () => {
|
||||
const mockConfig = {
|
||||
getBaseLlmClient: () => ({
|
||||
generateContent: vi.fn().mockResolvedValue({
|
||||
candidates: [{ content: { parts: [{ text: '1' }] } }],
|
||||
}),
|
||||
}),
|
||||
getAdaptiveThinkingConfig: () => ({
|
||||
enabled: true,
|
||||
classifierModel: 'gemini-2.0-flash',
|
||||
}),
|
||||
} as unknown as Config;
|
||||
|
||||
const service = new AdaptiveBudgetService(mockConfig);
|
||||
const result = await service.determineAdaptiveConfig(
|
||||
'Hi',
|
||||
'gemini-3-pro-preview',
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result?.complexity).toBe(ComplexityLevel.SIMPLE);
|
||||
expect(result?.thinkingLevel).toBe('LOW');
|
||||
expect(result?.thinkingBudget).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import type { Config } from '../config/config.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
import { isGemini2Model, isPreviewModel } from '../config/models.js';
|
||||
import type { Content } from '@google/genai';
|
||||
import {
|
||||
isFunctionCall,
|
||||
isFunctionResponse,
|
||||
} from '../utils/messageInspectors.js';
|
||||
|
||||
export enum ComplexityLevel {
|
||||
SIMPLE = 1,
|
||||
MODERATE = 2,
|
||||
HIGH = 3,
|
||||
EXTREME = 4,
|
||||
}
|
||||
|
||||
export const BUDGET_MAPPING_V2: Record<ComplexityLevel, number> = {
|
||||
[ComplexityLevel.SIMPLE]: 1024,
|
||||
[ComplexityLevel.MODERATE]: 4096,
|
||||
[ComplexityLevel.HIGH]: 16384,
|
||||
[ComplexityLevel.EXTREME]: 32768,
|
||||
};
|
||||
|
||||
export const LEVEL_MAPPING_V3: Record<ComplexityLevel, string> = {
|
||||
[ComplexityLevel.SIMPLE]: 'LOW',
|
||||
[ComplexityLevel.MODERATE]: 'LOW',
|
||||
[ComplexityLevel.HIGH]: 'HIGH',
|
||||
[ComplexityLevel.EXTREME]: 'HIGH',
|
||||
};
|
||||
|
||||
const HISTORY_TURNS_FOR_CONTEXT = 4;
|
||||
const HISTORY_SEARCH_WINDOW = 20;
|
||||
|
||||
export interface AdaptiveBudgetResult {
|
||||
complexity: ComplexityLevel;
|
||||
thinkingBudget?: number;
|
||||
thinkingLevel?: string;
|
||||
strategyNote?: string;
|
||||
}
|
||||
|
||||
export class AdaptiveBudgetService {
|
||||
constructor(private config: Config) {}
|
||||
|
||||
/**
|
||||
* Analyzes the user prompt and determines the optimal thinking configuration.
|
||||
*
|
||||
* Note on future scaling (per arXiv:2512.19585):
|
||||
* At Complexity 4 (Extreme), we should consider:
|
||||
* 1. Best-of-N: Generate multiple solutions.
|
||||
* 2. LLM-as-a-Judge: Use a strong model to evaluate candidates.
|
||||
* 3. Compiler Verification: Check code correctness via environment tools.
|
||||
*/
|
||||
async determineAdaptiveConfig(
|
||||
userPrompt: string,
|
||||
model: string,
|
||||
history: Content[],
|
||||
): Promise<AdaptiveBudgetResult | undefined> {
|
||||
const { classifierModel } = this.config.getAdaptiveThinkingConfig();
|
||||
|
||||
try {
|
||||
const llm = this.config.getBaseLlmClient();
|
||||
debugLogger.debug(
|
||||
`AdaptiveBudgetService: Classifying prompt complexity using ${classifierModel}...`,
|
||||
);
|
||||
|
||||
// situational context: provide the last N turns of the history
|
||||
const historySlice = history.slice(-HISTORY_SEARCH_WINDOW);
|
||||
|
||||
// Filter out tool-related turns to keep context focused on conversation.
|
||||
const cleanHistory = historySlice.filter(
|
||||
(content) => !isFunctionCall(content) && !isFunctionResponse(content),
|
||||
);
|
||||
|
||||
// Take the last N turns from the *cleaned* history.
|
||||
const finalHistory = cleanHistory.slice(-HISTORY_TURNS_FOR_CONTEXT);
|
||||
|
||||
const systemPrompt = `You are a complexity classifier for a coding assistant.
|
||||
Analyze the user's request and determine the complexity of the task.
|
||||
Output ONLY a single integer from 1 to 4 based on the following scale:
|
||||
|
||||
1 (Simple): Quick fixes, syntax questions, simple explanations, greetings.
|
||||
2 (Moderate): Function-level logic, writing small scripts, standard debugging.
|
||||
3 (High): Module-level refactoring, complex feature implementation, multi-file changes.
|
||||
4 (Extreme): Architecture design, deep root-cause analysis of obscure bugs, large-scale migrations.
|
||||
|
||||
Request: ${userPrompt}
|
||||
Complexity Level:`;
|
||||
|
||||
const response = await llm.generateContent({
|
||||
modelConfigKey: { model: classifierModel },
|
||||
contents: [
|
||||
...finalHistory,
|
||||
{ role: 'user', parts: [{ text: systemPrompt }] },
|
||||
],
|
||||
promptId: 'adaptive-budget-classifier',
|
||||
abortSignal: new AbortController().signal,
|
||||
});
|
||||
|
||||
const text = response.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
|
||||
if (!text) {
|
||||
debugLogger.debug(
|
||||
'AdaptiveBudgetService: No response from classifier.',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const level = parseInt(text, 10) as ComplexityLevel;
|
||||
if (isNaN(level) || level < 1 || level > 4) {
|
||||
debugLogger.debug(
|
||||
`AdaptiveBudgetService: Invalid complexity level returned: ${text}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result: AdaptiveBudgetResult = { complexity: level };
|
||||
|
||||
// Determine mapping based on model version
|
||||
// Gemini 3 uses ThinkingLevel, Gemini 2.x uses thinkingBudget
|
||||
if (isPreviewModel(model)) {
|
||||
result.thinkingLevel = LEVEL_MAPPING_V3[level] ?? 'HIGH';
|
||||
} else if (isGemini2Model(model)) {
|
||||
result.thinkingBudget = BUDGET_MAPPING_V2[level];
|
||||
}
|
||||
|
||||
if (level === ComplexityLevel.EXTREME) {
|
||||
result.strategyNote =
|
||||
'EXTREME complexity detected. Future implementations should use Best-of-N + Verification.';
|
||||
}
|
||||
|
||||
debugLogger.debug(
|
||||
`AdaptiveBudgetService: Complexity ${level} -> Thinking Param: ${result.thinkingLevel || result.thinkingBudget}`,
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
debugLogger.error(
|
||||
'AdaptiveBudgetService: Error classifying complexity',
|
||||
error,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getThinkingBudgetV2(level: ComplexityLevel): number {
|
||||
return BUDGET_MAPPING_V2[level];
|
||||
}
|
||||
|
||||
getThinkingLevelV3(level: ComplexityLevel): string {
|
||||
return LEVEL_MAPPING_V3[level] ?? 'HIGH';
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,11 @@ export type ConversationRecordExtra =
|
||||
thoughts?: Array<ThoughtSummary & { timestamp: string }>;
|
||||
tokens?: TokensSummary | null;
|
||||
model?: string;
|
||||
adaptiveThinking?: {
|
||||
complexity: number;
|
||||
thinkingBudget?: number;
|
||||
thinkingLevel?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -130,6 +135,11 @@ export class ChatRecordingService {
|
||||
private projectHash: string;
|
||||
private queuedThoughts: Array<ThoughtSummary & { timestamp: string }> = [];
|
||||
private queuedTokens: TokensSummary | null = null;
|
||||
private queuedAdaptiveThinking?: {
|
||||
complexity: number;
|
||||
thinkingBudget?: number;
|
||||
thinkingLevel?: string;
|
||||
};
|
||||
private config: Config;
|
||||
|
||||
constructor(config: Config) {
|
||||
@@ -186,6 +196,7 @@ export class ChatRecordingService {
|
||||
// Clear any queued data since this is a fresh start
|
||||
this.queuedThoughts = [];
|
||||
this.queuedTokens = null;
|
||||
this.queuedAdaptiveThinking = undefined;
|
||||
} catch (error) {
|
||||
// Handle disk full (ENOSPC) gracefully - disable recording but allow CLI to continue
|
||||
if (
|
||||
@@ -230,6 +241,11 @@ export class ChatRecordingService {
|
||||
type: ConversationRecordExtra['type'];
|
||||
content: PartListUnion;
|
||||
displayContent?: PartListUnion;
|
||||
adaptiveThinking?: {
|
||||
complexity: number;
|
||||
thinkingBudget?: number;
|
||||
thinkingLevel?: string;
|
||||
};
|
||||
}): void {
|
||||
if (!this.conversationFile) return;
|
||||
|
||||
@@ -247,9 +263,12 @@ export class ChatRecordingService {
|
||||
thoughts: this.queuedThoughts,
|
||||
tokens: this.queuedTokens,
|
||||
model: message.model,
|
||||
adaptiveThinking:
|
||||
message.adaptiveThinking || this.queuedAdaptiveThinking,
|
||||
});
|
||||
this.queuedThoughts = [];
|
||||
this.queuedTokens = null;
|
||||
this.queuedAdaptiveThinking = undefined;
|
||||
} else {
|
||||
// Or else just add it.
|
||||
conversation.messages.push(msg);
|
||||
@@ -261,6 +280,18 @@ export class ChatRecordingService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues adaptive thinking info to be recorded with the next Gemini message.
|
||||
*/
|
||||
recordAdaptiveThinking(info: {
|
||||
complexity: number;
|
||||
thinkingBudget?: number;
|
||||
thinkingLevel?: string;
|
||||
}): void {
|
||||
if (!this.conversationFile) return;
|
||||
this.queuedAdaptiveThinking = info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a thought from the assistant's reasoning process.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { GenerateContentConfig } from '@google/genai';
|
||||
import type { GenerateContentConfig, ThinkingLevel } from '@google/genai';
|
||||
|
||||
// The primary key for the ModelConfig is the model string. However, we also
|
||||
// support a secondary key to limit the override scope, typically an agent name.
|
||||
@@ -26,6 +26,10 @@ export interface ModelConfigKey {
|
||||
// This allows overrides to specify different settings (e.g., higher temperature)
|
||||
// specifically for retry scenarios.
|
||||
isRetry?: boolean;
|
||||
|
||||
// Dynamic thinking configuration determined at runtime (e.g. via complexity classification)
|
||||
thinkingBudget?: number;
|
||||
thinkingLevel?: ThinkingLevel | string;
|
||||
}
|
||||
|
||||
export interface ModelConfig {
|
||||
@@ -150,6 +154,35 @@ export class ModelConfigService {
|
||||
);
|
||||
}
|
||||
|
||||
// Apply dynamic thinking parameters from context if present
|
||||
if (
|
||||
context.thinkingBudget !== undefined ||
|
||||
context.thinkingLevel !== undefined
|
||||
) {
|
||||
const thinkingConfig = {
|
||||
...(currentConfig.generateContentConfig?.thinkingConfig as object),
|
||||
...(context.thinkingBudget !== undefined
|
||||
? { thinkingBudget: context.thinkingBudget }
|
||||
: {}),
|
||||
...(context.thinkingLevel !== undefined
|
||||
? { thinkingLevel: context.thinkingLevel }
|
||||
: {}),
|
||||
};
|
||||
|
||||
// Ensure we don't have BOTH if one was a default from an alias
|
||||
if (context.thinkingLevel) {
|
||||
delete (thinkingConfig as Record<string, unknown>)['thinkingBudget'];
|
||||
}
|
||||
if (context.thinkingBudget) {
|
||||
delete (thinkingConfig as Record<string, unknown>)['thinkingLevel'];
|
||||
}
|
||||
|
||||
currentConfig.generateContentConfig = {
|
||||
...currentConfig.generateContentConfig,
|
||||
thinkingConfig: thinkingConfig as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
model: currentConfig.model,
|
||||
generateContentConfig: currentConfig.generateContentConfig ?? {},
|
||||
|
||||
@@ -553,7 +553,7 @@ function toGenerateContentConfigAttributes(
|
||||
if (!config) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
const attributes: LogAttributes = {
|
||||
'gen_ai.request.temperature': config.temperature,
|
||||
'gen_ai.request.top_p': config.topP,
|
||||
'gen_ai.request.top_k': config.topK,
|
||||
@@ -568,6 +568,20 @@ function toGenerateContentConfigAttributes(
|
||||
toSystemInstruction(config.systemInstruction),
|
||||
),
|
||||
};
|
||||
|
||||
if (config.thinkingConfig) {
|
||||
const thinkingConfig = config.thinkingConfig;
|
||||
if (thinkingConfig.thinkingBudget !== undefined) {
|
||||
attributes['gen_ai.request.thinking_budget'] =
|
||||
thinkingConfig.thinkingBudget;
|
||||
}
|
||||
if (thinkingConfig.thinkingLevel !== undefined) {
|
||||
attributes['gen_ai.request.thinking_level'] =
|
||||
thinkingConfig.thinkingLevel;
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
export class ApiResponseEvent implements BaseTelemetryEvent {
|
||||
@@ -632,6 +646,17 @@ export class ApiResponseEvent implements BaseTelemetryEvent {
|
||||
status_code: this.status_code,
|
||||
finish_reasons: this.finish_reasons,
|
||||
};
|
||||
|
||||
if (this.prompt.generate_content_config?.thinkingConfig) {
|
||||
const thinkingConfig = this.prompt.generate_content_config.thinkingConfig;
|
||||
if (thinkingConfig.thinkingBudget !== undefined) {
|
||||
attributes['thinking_budget'] = thinkingConfig.thinkingBudget;
|
||||
}
|
||||
if (thinkingConfig.thinkingLevel !== undefined) {
|
||||
attributes['thinking_level'] = thinkingConfig.thinkingLevel;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.response_text) {
|
||||
attributes['response_text'] = this.response_text;
|
||||
}
|
||||
|
||||
@@ -1487,6 +1487,30 @@
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"adaptiveThinking": {
|
||||
"title": "Adaptive Thinking Settings",
|
||||
"description": "Configuration for Adaptive Thinking Budget.",
|
||||
"markdownDescription": "Configuration for Adaptive Thinking Budget.\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `{}`",
|
||||
"default": {},
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"title": "Enable Adaptive Thinking",
|
||||
"description": "Enable adaptive thinking budget based on task complexity.",
|
||||
"markdownDescription": "Enable adaptive thinking budget based on task complexity.\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `false`",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"classifierModel": {
|
||||
"title": "Classifier Model",
|
||||
"description": "The model (or alias) to use for complexity classification.",
|
||||
"markdownDescription": "The model (or alias) to use for complexity classification.\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `classifier`",
|
||||
"default": "classifier",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"plan": {
|
||||
"title": "Plan",
|
||||
"description": "Enable planning features (Plan Mode and tools).",
|
||||
|
||||
Reference in New Issue
Block a user