mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
Add experiment logging and add caching experiment (#12862)
This commit is contained in:
14
packages/core/src/code_assist/experiments/flagNames.ts
Normal file
14
packages/core/src/code_assist/experiments/flagNames.ts
Normal file
@@ -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 { ConfigParameters, SandboxConfig } 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 type { HookDefinition } from '../hooks/types.js';
|
||||
import { HookType, HookEventName } from '../hooks/types.js';
|
||||
@@ -247,7 +249,7 @@ describe('Server Config (config.ts)', () => {
|
||||
...baseParams,
|
||||
experiments: {
|
||||
flags: {
|
||||
GeminiCLIContextCompression__threshold_fraction: {
|
||||
[ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD]: {
|
||||
floatValue: 0.8,
|
||||
},
|
||||
},
|
||||
@@ -261,7 +263,7 @@ describe('Server Config (config.ts)', () => {
|
||||
...baseParams,
|
||||
experiments: {
|
||||
flags: {
|
||||
GeminiCLIContextCompression__threshold_fraction: {
|
||||
[ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD]: {
|
||||
floatValue: 0.0,
|
||||
},
|
||||
},
|
||||
@@ -275,6 +277,43 @@ describe('Server Config (config.ts)', () => {
|
||||
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', () => {
|
||||
@@ -1482,3 +1521,64 @@ describe('Config getExperiments', () => {
|
||||
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 { inspect } from 'node:util';
|
||||
import process from 'node:process';
|
||||
import type {
|
||||
ContentGenerator,
|
||||
@@ -84,6 +85,7 @@ import { AgentRegistry } from '../agents/registry.js';
|
||||
import { setGlobalProxy } from '../utils/fetch.js';
|
||||
import { SubagentToolWrapper } from '../agents/subagent-tool-wrapper.js';
|
||||
import { getExperiments } from '../code_assist/experiments/experiments.js';
|
||||
import { ExperimentFlags } from '../code_assist/experiments/flagNames.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
import { ApprovalMode } from '../policy/types.js';
|
||||
@@ -1144,16 +1146,10 @@ export class Config {
|
||||
return this.compressionThreshold;
|
||||
}
|
||||
|
||||
if (this.experimentsPromise) {
|
||||
try {
|
||||
await this.experimentsPromise;
|
||||
} catch (e) {
|
||||
debugLogger.debug('Failed to fetch experiments', e);
|
||||
}
|
||||
}
|
||||
await this.ensureExperimentsLoaded();
|
||||
|
||||
const remoteThreshold =
|
||||
this.experiments?.flags['GeminiCLIContextCompression__threshold_fraction']
|
||||
this.experiments?.flags[ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD]
|
||||
?.floatValue;
|
||||
if (remoteThreshold === 0) {
|
||||
return undefined;
|
||||
@@ -1161,6 +1157,23 @@ export class Config {
|
||||
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 {
|
||||
return (
|
||||
this.interactive &&
|
||||
@@ -1413,6 +1426,44 @@ export class Config {
|
||||
*/
|
||||
setExperiments(experiments: Experiments): void {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user