mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
Add experiment logging and add caching experiment (#12862)
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const ExperimentFlags = {
|
||||||
|
CONTEXT_COMPRESSION_THRESHOLD:
|
||||||
|
'GeminiCLIContextCompression__threshold_fraction',
|
||||||
|
USER_CACHING: 'GcliUserCaching__user_caching',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ExperimentFlagName =
|
||||||
|
(typeof ExperimentFlags)[keyof typeof ExperimentFlags];
|
||||||
@@ -8,6 +8,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|||||||
import type { Mock } from 'vitest';
|
import type { Mock } from 'vitest';
|
||||||
import type { ConfigParameters, SandboxConfig } from './config.js';
|
import type { ConfigParameters, SandboxConfig } from './config.js';
|
||||||
import { Config, DEFAULT_FILE_FILTERING_OPTIONS } from './config.js';
|
import { Config, DEFAULT_FILE_FILTERING_OPTIONS } from './config.js';
|
||||||
|
import { ExperimentFlags } from '../code_assist/experiments/flagNames.js';
|
||||||
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
import { ApprovalMode } from '../policy/types.js';
|
import { ApprovalMode } from '../policy/types.js';
|
||||||
import type { HookDefinition } from '../hooks/types.js';
|
import type { HookDefinition } from '../hooks/types.js';
|
||||||
import { HookType, HookEventName } from '../hooks/types.js';
|
import { HookType, HookEventName } from '../hooks/types.js';
|
||||||
@@ -247,7 +249,7 @@ describe('Server Config (config.ts)', () => {
|
|||||||
...baseParams,
|
...baseParams,
|
||||||
experiments: {
|
experiments: {
|
||||||
flags: {
|
flags: {
|
||||||
GeminiCLIContextCompression__threshold_fraction: {
|
[ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD]: {
|
||||||
floatValue: 0.8,
|
floatValue: 0.8,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -261,7 +263,7 @@ describe('Server Config (config.ts)', () => {
|
|||||||
...baseParams,
|
...baseParams,
|
||||||
experiments: {
|
experiments: {
|
||||||
flags: {
|
flags: {
|
||||||
GeminiCLIContextCompression__threshold_fraction: {
|
[ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD]: {
|
||||||
floatValue: 0.0,
|
floatValue: 0.0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -275,6 +277,43 @@ describe('Server Config (config.ts)', () => {
|
|||||||
expect(await config.getCompressionThreshold()).toBeUndefined();
|
expect(await config.getCompressionThreshold()).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getUserCaching', () => {
|
||||||
|
it('should return the remote experiment flag when available', async () => {
|
||||||
|
const config = new Config({
|
||||||
|
...baseParams,
|
||||||
|
experiments: {
|
||||||
|
flags: {
|
||||||
|
[ExperimentFlags.USER_CACHING]: {
|
||||||
|
boolValue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
experimentIds: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(await config.getUserCaching()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the remote flag is false', async () => {
|
||||||
|
const config = new Config({
|
||||||
|
...baseParams,
|
||||||
|
experiments: {
|
||||||
|
flags: {
|
||||||
|
[ExperimentFlags.USER_CACHING]: {
|
||||||
|
boolValue: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
experimentIds: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(await config.getUserCaching()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if there are no experiments', async () => {
|
||||||
|
const config = new Config(baseParams);
|
||||||
|
expect(await config.getUserCaching()).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('refreshAuth', () => {
|
describe('refreshAuth', () => {
|
||||||
@@ -1482,3 +1521,64 @@ describe('Config getExperiments', () => {
|
|||||||
expect(retrievedExps).toBe(mockExps); // Should return the same reference
|
expect(retrievedExps).toBe(mockExps); // Should return the same reference
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Config setExperiments logging', () => {
|
||||||
|
const baseParams: ConfigParameters = {
|
||||||
|
cwd: '/tmp',
|
||||||
|
targetDir: '/path/to/target',
|
||||||
|
debugMode: false,
|
||||||
|
sessionId: 'test-session-id',
|
||||||
|
model: 'gemini-pro',
|
||||||
|
usageStatisticsEnabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('logs a sorted, non-truncated summary of experiments when they are set', () => {
|
||||||
|
const config = new Config(baseParams);
|
||||||
|
const debugSpy = vi
|
||||||
|
.spyOn(debugLogger, 'debug')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
const experiments = {
|
||||||
|
flags: {
|
||||||
|
ZetaFlag: {
|
||||||
|
boolValue: true,
|
||||||
|
stringValue: 'zeta',
|
||||||
|
int32ListValue: { values: [1, 2] },
|
||||||
|
},
|
||||||
|
AlphaFlag: {
|
||||||
|
boolValue: false,
|
||||||
|
stringValue: 'alpha',
|
||||||
|
stringListValue: { values: ['a', 'b', 'c'] },
|
||||||
|
},
|
||||||
|
MiddleFlag: {
|
||||||
|
// Intentionally sparse to ensure undefined values are omitted
|
||||||
|
floatValue: 0.42,
|
||||||
|
int32ListValue: { values: [] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
experimentIds: [101, 99],
|
||||||
|
};
|
||||||
|
|
||||||
|
config.setExperiments(experiments);
|
||||||
|
|
||||||
|
const logCall = debugSpy.mock.calls.find(
|
||||||
|
([message]) => message === 'Experiments loaded',
|
||||||
|
);
|
||||||
|
expect(logCall).toBeDefined();
|
||||||
|
const loggedSummary = logCall?.[1] as string;
|
||||||
|
expect(typeof loggedSummary).toBe('string');
|
||||||
|
expect(loggedSummary).toContain('experimentIds');
|
||||||
|
expect(loggedSummary).toContain('101');
|
||||||
|
expect(loggedSummary).toContain('AlphaFlag');
|
||||||
|
expect(loggedSummary).toContain('ZetaFlag');
|
||||||
|
const alphaIndex = loggedSummary.indexOf('AlphaFlag');
|
||||||
|
const zetaIndex = loggedSummary.indexOf('ZetaFlag');
|
||||||
|
expect(alphaIndex).toBeGreaterThan(-1);
|
||||||
|
expect(zetaIndex).toBeGreaterThan(-1);
|
||||||
|
expect(alphaIndex).toBeLessThan(zetaIndex);
|
||||||
|
expect(loggedSummary).toContain('\n');
|
||||||
|
expect(loggedSummary).not.toContain('stringListLength: 0');
|
||||||
|
expect(loggedSummary).not.toContain('int32ListLength: 0');
|
||||||
|
|
||||||
|
debugSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
import { inspect } from 'node:util';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import type {
|
import type {
|
||||||
ContentGenerator,
|
ContentGenerator,
|
||||||
@@ -84,6 +85,7 @@ import { AgentRegistry } from '../agents/registry.js';
|
|||||||
import { setGlobalProxy } from '../utils/fetch.js';
|
import { setGlobalProxy } from '../utils/fetch.js';
|
||||||
import { SubagentToolWrapper } from '../agents/subagent-tool-wrapper.js';
|
import { SubagentToolWrapper } from '../agents/subagent-tool-wrapper.js';
|
||||||
import { getExperiments } from '../code_assist/experiments/experiments.js';
|
import { getExperiments } from '../code_assist/experiments/experiments.js';
|
||||||
|
import { ExperimentFlags } from '../code_assist/experiments/flagNames.js';
|
||||||
import { debugLogger } from '../utils/debugLogger.js';
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
|
|
||||||
import { ApprovalMode } from '../policy/types.js';
|
import { ApprovalMode } from '../policy/types.js';
|
||||||
@@ -1144,16 +1146,10 @@ export class Config {
|
|||||||
return this.compressionThreshold;
|
return this.compressionThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.experimentsPromise) {
|
await this.ensureExperimentsLoaded();
|
||||||
try {
|
|
||||||
await this.experimentsPromise;
|
|
||||||
} catch (e) {
|
|
||||||
debugLogger.debug('Failed to fetch experiments', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteThreshold =
|
const remoteThreshold =
|
||||||
this.experiments?.flags['GeminiCLIContextCompression__threshold_fraction']
|
this.experiments?.flags[ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD]
|
||||||
?.floatValue;
|
?.floatValue;
|
||||||
if (remoteThreshold === 0) {
|
if (remoteThreshold === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -1161,6 +1157,23 @@ export class Config {
|
|||||||
return remoteThreshold;
|
return remoteThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserCaching(): Promise<boolean | undefined> {
|
||||||
|
await this.ensureExperimentsLoaded();
|
||||||
|
|
||||||
|
return this.experiments?.flags[ExperimentFlags.USER_CACHING]?.boolValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureExperimentsLoaded(): Promise<void> {
|
||||||
|
if (!this.experimentsPromise) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.experimentsPromise;
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger.debug('Failed to fetch experiments', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isInteractiveShellEnabled(): boolean {
|
isInteractiveShellEnabled(): boolean {
|
||||||
return (
|
return (
|
||||||
this.interactive &&
|
this.interactive &&
|
||||||
@@ -1413,6 +1426,44 @@ export class Config {
|
|||||||
*/
|
*/
|
||||||
setExperiments(experiments: Experiments): void {
|
setExperiments(experiments: Experiments): void {
|
||||||
this.experiments = experiments;
|
this.experiments = experiments;
|
||||||
|
const flagSummaries = Object.entries(experiments.flags ?? {})
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
|
.map(([name, flag]) => {
|
||||||
|
const summary: Record<string, unknown> = { name };
|
||||||
|
if (flag.boolValue !== undefined) {
|
||||||
|
summary['boolValue'] = flag.boolValue;
|
||||||
|
}
|
||||||
|
if (flag.floatValue !== undefined) {
|
||||||
|
summary['floatValue'] = flag.floatValue;
|
||||||
|
}
|
||||||
|
if (flag.intValue !== undefined) {
|
||||||
|
summary['intValue'] = flag.intValue;
|
||||||
|
}
|
||||||
|
if (flag.stringValue !== undefined) {
|
||||||
|
summary['stringValue'] = flag.stringValue;
|
||||||
|
}
|
||||||
|
const int32Length = flag.int32ListValue?.values?.length ?? 0;
|
||||||
|
if (int32Length > 0) {
|
||||||
|
summary['int32ListLength'] = int32Length;
|
||||||
|
}
|
||||||
|
const stringListLength = flag.stringListValue?.values?.length ?? 0;
|
||||||
|
if (stringListLength > 0) {
|
||||||
|
summary['stringListLength'] = stringListLength;
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
});
|
||||||
|
const summary = {
|
||||||
|
experimentIds: experiments.experimentIds ?? [],
|
||||||
|
flags: flagSummaries,
|
||||||
|
};
|
||||||
|
const summaryString = inspect(summary, {
|
||||||
|
depth: null,
|
||||||
|
maxArrayLength: null,
|
||||||
|
maxStringLength: null,
|
||||||
|
breakLength: 80,
|
||||||
|
compact: false,
|
||||||
|
});
|
||||||
|
debugLogger.debug('Experiments loaded', summaryString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Export model constants for use in CLI
|
// Export model constants for use in CLI
|
||||||
|
|||||||
Reference in New Issue
Block a user