Add experiment logging and add caching experiment (#12862)

This commit is contained in:
cornmander
2025-11-10 23:15:38 -05:00
committed by GitHub
parent 22b0550520
commit 4fbeac8b32
3 changed files with 175 additions and 10 deletions

View 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];

View File

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

View File

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