Compare commits

..

1 Commits

Author SHA1 Message Date
Jacob Richman 9cedc6131e Speculative changes to address react bug with overflowing state 2026-04-12 18:34:32 -07:00
28 changed files with 383 additions and 1540 deletions
-73
View File
@@ -1,73 +0,0 @@
---
name: experimentation
description: Guide developers through the process of adding new remote experiments (feature flags) to the Gemini CLI codebase.
---
# Experimentation Skill
This skill assists developers in adding net-new remote experiments (feature flags) to the Gemini CLI codebase. You MUST follow these steps sequentially to ensure consistency, type safety, and proper validation.
## Key Files
- `packages/core/src/code_assist/experiments/flagNames.ts`: Definitions and metadata for all experiment flags.
- `packages/core/src/config/config.ts`: Unified configuration object where strongly typed wrappers for experiments are added.
- `packages/cli/src/config/settingsSchema.ts`: Schema definitions for `settings.json`.
- `packages/core/src/config/config.experiment.test.ts`: Regression tests for experiment resolution.
## Core Pattern: Unified Configuration
Gemini CLI uses a unified `Config` object as the source of truth. Every experiment should be accessed via the `config.getExperimentValue(flagId)` method, which internally handles the priority of overrides:
**Command Line Argument (--experiment-*) > Local Setting (experimental.*) > Remote Experiment > Default Value.**
---
## Workflow: Adding a New Experiment
### 1. List Current Experiments
- **File:** `packages/core/src/code_assist/experiments/flagNames.ts`
- **MANDATORY:** Read this file and explicitly list EVERY existing experiment name and its numeric ID to the user in your response. This is a critical step to ensure a unique ID and name are chosen. Do NOT just read the file silently.
- **Analyze Usage:** Search for `getExperimentValue` in `packages/core/src/config/config.ts` to see how similar experiments are wrapped and used.
### 2. Ask for Details (Behavior & Intent)
- **CRITICAL:** You MUST ask the user:
1. "What is the name of the new flag?" (e.g., `enable-new-thing`)
2. "What is the data type?" (boolean, number, or string)
3. "What should the behavior be when the flag is enabled vs. disabled?"
4. "What are the architectural impacts (routing, tools, prompt construction, etc.)?"
- Do not proceed until the behavior is clearly defined and documented.
### 3. Add Experiment ID and Metadata
- **File:** `packages/core/src/code_assist/experiments/flagNames.ts`
- Add a new constant to the `ExperimentFlags` object with a unique numeric ID.
- Add a corresponding entry to `ExperimentMetadata`, including `description`, `type`, and `defaultValue`.
### 4. Add Config Entry and Schema
- **Step A (Wrapper):** In `packages/core/src/config/config.ts`, add a strongly typed wrapper method:
```typescript
isNewThingEnabled(): boolean {
return this.getExperimentValue<boolean>(ExperimentFlags.ENABLE_NEW_THING) ?? false;
}
```
- **Step B (Schema):** In `packages/cli/src/config/settingsSchema.ts`, add the kebab-case name to the `experimental.properties` section to enable IDE autocompletion.
- **Step C (Regenerate):** Run `npm run schema:settings` to update `schemas/settings.schema.json`.
### 5. Implement Code Change with Debug Logging
- **Usage:** Implement the logic defined in Step 2.
- **Logging:** At the point of usage, add a debug log to confirm the flag's state:
```typescript
debugLogger.debug('New thing enabled:', config.isNewThingEnabled());
```
- **Telemetry (Crucial):** Prompt the user to think about success metrics. Ask: "How will we know if this feature is working as intended?" and help them add telemetry calls to capture these insights.
### 6. Update Tests
- **File:** `packages/core/src/config/config.experiment.test.ts`
- Add a test case to verify that the flag resolves correctly across all priority levels (CLI, Settings, Remote, Default).
- Example: `expect(config.isNewThingEnabled()).toBe(true)` when passed via `experimentalCliArgs`.
### 7. Final Validation and PR Preparation
- **Validation Action:** Run the CLI with debugging enabled and the new flag set via the command line:
```bash
npm run start -- --debug --experiment your-flag-name=value --prompt "test"
```
- **Log Verification:** Check the console for the "Experimental CLI args" normalization log and your custom debug log.
- **Branching:** If not already on a dedicated branch, help the user create one (e.g., `git checkout -b exp/feature-name`).
- **Pre-flight:** Remind the user to run local tests and linting (`npm run lint:all`, `npm test` or `npm run preflight`) before preparing a Pull Request.
Binary file not shown.
+18 -49
View File
@@ -106,7 +106,6 @@ export interface CliArgs {
rawOutput: boolean | undefined;
acceptRawOutputRisk: boolean | undefined;
isCommand: boolean | undefined;
[key: string]: unknown;
}
/**
@@ -444,22 +443,13 @@ export async function parseArguments(
.option('accept-raw-output-risk', {
type: 'boolean',
description: 'Suppress the security warning when using --raw-output.',
})
.option('experiment', {
type: 'array',
string: true,
nargs: 1,
description:
'Override experiment flags locally (format: flag=value, comma-separated or multiple --experiment)',
coerce: (exps: string[]) =>
// Handle comma-separated values
exps.flatMap((e) => e.split(',').map((s) => s.trim())),
}),
)
.version(await getVersion()) // This will enable the --version flag based on package.json
.alias('v', 'version')
.help()
.alias('h', 'help')
.strict()
.demandCommand(0, 0) // Allow base command to run with no subcommands
.exitProcess(false);
@@ -883,46 +873,27 @@ export async function loadCliConfig(
}
}
const experimentalCliArgs: Record<string, unknown> = Object.create(null);
if (argv['experiment'] && Array.isArray(argv['experiment'])) {
for (const entry of argv['experiment']) {
const [key, ...valueParts] = entry.split('=');
const value = valueParts.join('=');
if (key && value !== undefined) {
// Simple type inference for CLI args
if (value === 'true') experimentalCliArgs[key] = true;
else if (value === 'false') experimentalCliArgs[key] = false;
else if (!isNaN(Number(value)))
experimentalCliArgs[key] = Number(value);
else experimentalCliArgs[key] = value;
}
let clientName: string | undefined = undefined;
if (isAcpMode) {
const ide = detectIdeFromEnv();
if (
ide &&
(ide.name !== 'vscode' || process.env['TERM_PROGRAM'] === 'vscode')
) {
clientName = `acp-${ide.name}`;
}
}
let clientName: string | undefined = undefined;
if (isAcpMode) {
const ide = detectIdeFromEnv();
if (
ide &&
(ide.name !== 'vscode' || process.env['TERM_PROGRAM'] === 'vscode')
) {
clientName = `acp-${ide.name}`;
}
}
const useGeneralistProfile =
settings.experimental?.generalistProfile ?? false;
const useContextManagement =
settings.experimental?.contextManagement ?? false;
const contextManagement = {
...(useGeneralistProfile ? generalistProfile : {}),
...(useContextManagement ? settings?.contextManagement : {}),
enabled: useContextManagement || useGeneralistProfile,
};
const useGeneralistProfile =
settings.experimental?.generalistProfile ?? false;
const useContextManagement =
settings.experimental?.contextManagement ?? false;
const contextManagement = {
...(useGeneralistProfile ? generalistProfile : {}),
...(useContextManagement ? settings?.contextManagement : {}),
enabled: useContextManagement || useGeneralistProfile,
};
if (debugMode && Object.keys(experimentalCliArgs).length > 0) {
debugLogger.debug('Experimental CLI args:', experimentalCliArgs);
}
return new Config({
acpMode: isAcpMode,
clientName,
@@ -1014,8 +985,6 @@ if (debugMode && Object.keys(experimentalCliArgs).length > 0) {
planSettings: settings.general?.plan?.directory
? settings.general.plan
: (extensionPlanSettings ?? settings.general?.plan),
experimentalSettings: settings.experimental,
experimentalCliArgs,
enableEventDrivenScheduler: true,
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
@@ -2217,15 +2217,6 @@ const SETTINGS_SCHEMA = {
'Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.',
showInDialog: true,
},
'enable-awesome': {
type: 'boolean',
label: 'Enable Awesome',
category: 'Experimental',
requiresRestart: false,
default: false,
description: "When enabled, the ASCII art says 'matt'.",
showInDialog: true,
},
},
},
extensions: {
@@ -31,7 +31,6 @@ import { corgiCommand } from '../ui/commands/corgiCommand.js';
import { docsCommand } from '../ui/commands/docsCommand.js';
import { directoryCommand } from '../ui/commands/directoryCommand.js';
import { editorCommand } from '../ui/commands/editorCommand.js';
import { experimentCommand } from '../ui/commands/experimentCommand.js';
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
import { footerCommand } from '../ui/commands/footerCommand.js';
import { helpCommand } from '../ui/commands/helpCommand.js';
@@ -134,7 +133,6 @@ export class BuiltinCommandLoader implements ICommandLoader {
docsCommand,
directoryCommand,
editorCommand,
experimentCommand,
...(this.config?.getExtensionsEnabled() === false
? [
{
@@ -121,7 +121,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getCompressionThreshold: vi.fn().mockResolvedValue(undefined),
getUserCaching: vi.fn().mockResolvedValue(false),
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(false),
isAwesomeEnabled: vi.fn().mockReturnValue(false),
getClassifierThreshold: vi.fn().mockResolvedValue(undefined),
getBannerTextNoCapacityIssues: vi.fn().mockResolvedValue(''),
getBannerTextCapacityIssues: vi.fn().mockResolvedValue(''),
+79
View File
@@ -3290,6 +3290,85 @@ describe('AppContainer State Management', () => {
unmount();
});
it('does not reset the hint timer when overflowingIdsSize decreases', async () => {
const { unmount } = await act(async () => renderAppContainer());
await waitFor(() => expect(capturedOverflowActions).toBeTruthy());
act(() => {
capturedOverflowActions.addOverflowingId('test-id-1');
capturedOverflowActions.addOverflowingId('test-id-2');
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(true);
});
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
act(() => {
capturedOverflowActions.removeOverflowingId('test-id-2');
});
act(() => {
vi.advanceTimersByTime(1);
});
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(false);
});
unmount();
});
it('does not auto-reset the hint timer for new overflow while expanded', async () => {
const { stdin, unmount } = await act(async () => renderAppContainer());
await waitFor(() => expect(capturedOverflowActions).toBeTruthy());
act(() => {
capturedOverflowActions.addOverflowingId('test-id-1');
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(true);
});
act(() => {
stdin.write('\x0f'); // Ctrl+O
});
expect(capturedUIState.constrainHeight).toBe(false);
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 1000);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
act(() => {
capturedOverflowActions.addOverflowingId('test-id-2');
});
act(() => {
vi.advanceTimersByTime(1);
});
act(() => {
vi.advanceTimersByTime(1000);
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(false);
});
unmount();
});
it('toggles expansion state and resets the hint timer when Ctrl+O is pressed in Standard Mode', async () => {
const { stdin, unmount } = await act(async () => renderAppContainer());
await waitFor(() => expect(capturedOverflowActions).toBeTruthy());
+20 -8
View File
@@ -187,6 +187,7 @@ import {
isToolAwaitingConfirmation,
getAllToolCalls,
} from './utils/historyUtils.js';
import { shouldAutoTriggerExpandHint } from './utils/expandHint.js';
interface AppContainerProps {
config: Config;
@@ -329,24 +330,35 @@ export const AppContainer = (props: AppContainerProps) => {
const showIsExpandableHint = Boolean(expandHintTrigger);
const overflowState = useOverflowState();
const overflowingIdsSize = overflowState?.overflowingIds.size ?? 0;
const hasOverflowState = overflowingIdsSize > 0 || !constrainHeight;
const previousOverflowingIdsSizeRef = useRef(0);
/**
* Manages the visibility and x-second timer for the expansion hint.
*
* This effect triggers the timer countdown whenever an overflow is detected
* or the user manually toggles the expansion state with Ctrl+O.
* By depending on overflowingIdsSize, the timer resets when *new* views
* overflow, but avoids infinitely resetting during single-view streaming.
* while the response is actually constrained. The Ctrl+O handler still
* refreshes the hint manually when the user toggles expansion.
*
* In alternate buffer mode, we don't trigger the hint automatically on overflow
* to avoid noise, but the user can still trigger it manually with Ctrl+O.
* We only auto-refresh when the number of overflowing regions grows. That
* keeps the "show more" hint responsive for newly truncated content without
* retriggering on layout churn, overflow shrinkage, or while the content is
* already expanded.
*/
useEffect(() => {
if (hasOverflowState) {
const previousOverflowingIdsSize = previousOverflowingIdsSizeRef.current;
if (
shouldAutoTriggerExpandHint({
constrainHeight,
overflowingIdsSize,
previousOverflowingIdsSize,
})
) {
triggerExpandHint(true);
}
}, [hasOverflowState, overflowingIdsSize, triggerExpandHint]);
previousOverflowingIdsSizeRef.current = overflowingIdsSize;
}, [constrainHeight, overflowingIdsSize, triggerExpandHint]);
const [defaultBannerText, setDefaultBannerText] = useState('');
const [warningBannerText, setWarningBannerText] = useState('');
@@ -1,167 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// Trigger CI run for latest changes.
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { experimentCommand } from './experimentCommand.js';
import { CommandKind, type CommandContext } from './types.js';
import { MessageType } from '../types.js';
import { SettingScope } from '../../config/settings.js';
describe('experimentCommand', () => {
let mockContext: CommandContext;
beforeEach(() => {
mockContext = {
services: {
config: {
getExperimentValue: vi.fn(),
updateExperimentalSettings: vi.fn(),
},
settings: {
user: {
settings: {
experimental: {},
},
},
merged: {
experimental: {},
},
setValue: vi.fn(),
},
},
ui: {
addItem: vi.fn(),
},
session: {
stats: {},
sessionShellAllowlist: new Set(),
},
} as unknown as CommandContext;
});
it('should have the correct name and description', () => {
expect(experimentCommand.name).toBe('experiment');
expect(experimentCommand.description).toBe('Manage experimental features');
expect(experimentCommand.kind).toBe(CommandKind.BUILT_IN);
});
describe('list sub-command', () => {
const listCommand = experimentCommand.subCommands?.find(
(c) => c.name === 'list',
);
it('should list experiments', async () => {
(mockContext.services.config!.getExperimentValue as Mock).mockReturnValue(
true,
);
await listCommand?.action!(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining('enable-preview'),
}),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('Value: true'),
}),
);
});
});
describe('set sub-command', () => {
const setCommand = experimentCommand.subCommands?.find(
(c) => c.name === 'set',
);
it('should set a boolean experiment', async () => {
await setCommand?.action!(mockContext, 'enable-preview true');
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'experimental',
expect.objectContaining({
'enable-preview': true,
}),
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining(
'Experiment enable-preview set to true',
),
}),
);
});
it('should set a number experiment', async () => {
await setCommand?.action!(mockContext, 'classifier-threshold 0.5');
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'experimental',
expect.objectContaining({
'classifier-threshold': 0.5,
}),
);
});
it('should show error for unknown experiment', async () => {
await setCommand?.action!(mockContext, 'unknown-exp true');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: expect.stringContaining('Unknown experiment: unknown-exp'),
}),
);
});
});
describe('unset sub-command', () => {
const unsetCommand = experimentCommand.subCommands?.find(
(c) => c.name === 'unset',
);
it('should unset an experiment', async () => {
(mockContext.services.settings.user.settings as Record<string, unknown>)[
'experimental'
] = {
'enable-preview': true,
};
await unsetCommand?.action!(mockContext, 'enable-preview');
expect(mockContext.services.settings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'experimental',
{},
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining(
'Local user override for experiment enable-preview removed',
),
}),
);
});
it('should show error if no override exists', async () => {
await unsetCommand?.action!(mockContext, 'enable-preview');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: expect.stringContaining(
'No local user override found for experiment: enable-preview',
),
}),
);
});
});
});
@@ -1,207 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
ExperimentMetadata,
getExperimentFlagIdFromName,
getExperimentFlagName,
} from '@google/gemini-cli-core';
import { z } from 'zod';
import {
type CommandContext,
CommandKind,
type SlashCommand,
} from './types.js';
import { MessageType } from '../types.js';
import { SettingScope } from '../../config/settings.js';
const listExperimentsCommand: SlashCommand = {
name: 'list',
description: 'List all available experiments and their current values',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext) => {
const { config } = context.services;
if (!config) return;
const entries = Object.entries(ExperimentMetadata).filter(
([, metadata]) => !metadata.hidden,
);
if (entries.length === 0) {
context.ui.addItem({
type: MessageType.INFO,
text: 'No experiments available.',
});
return;
}
let output = 'Available Experiments:\n\n';
for (const [idStr, metadata] of entries) {
const id = parseInt(idStr, 10);
const name = getExperimentFlagName(id) || `ID: ${id}`;
const value = config.getExperimentValue(id);
output += `${name}\n`;
output += ` Value: ${value}\n`;
output += ` Description: ${metadata.description}\n`;
output += ` Default: ${metadata.defaultValue}\n\n`;
}
context.ui.addItem({
type: MessageType.INFO,
text: output.trim(),
});
},
};
const setExperimentCommand: SlashCommand = {
name: 'set',
description:
'Set a local override for an experiment. Usage: /experiment set <name> <value>',
kind: CommandKind.BUILT_IN,
autoExecute: false,
action: async (context: CommandContext, args: string) => {
const parts = args.trim().split(/\s+/).filter(Boolean);
if (parts.length < 2) {
context.ui.addItem({
type: MessageType.ERROR,
text: 'Usage: /experiment set <name> <value>',
});
return;
}
const name = parts[0];
const rawValue = args.trim().substring(name.length).trim();
const id = getExperimentFlagIdFromName(name);
if (id === undefined) {
context.ui.addItem({
type: MessageType.ERROR,
text: `Unknown experiment: ${name}`,
});
return;
}
const metadata = ExperimentMetadata[id];
let value: unknown;
// Helper to parse strings based on Zod schema type
const parseValue = (raw: string, schema: z.ZodTypeAny): unknown => {
if (schema instanceof z.ZodBoolean) {
if (raw === 'true' || raw === 'on') return true;
if (raw === 'false' || raw === 'off') return false;
return raw;
}
if (schema instanceof z.ZodNumber) {
return Number(raw);
}
return raw;
};
value = parseValue(rawValue, metadata.schema);
const result = metadata.schema.safeParse(value);
if (!result.success) {
context.ui.addItem({
type: MessageType.ERROR,
text: `Invalid value for ${name}: ${rawValue}. Error: ${result.error.errors[0].message}`,
});
return;
}
value = result.data;
const { settings, config } = context.services;
if (!config) return;
// SECURITY: Only persist user-scoped settings to prevent untrusted workspace settings
// from being promoted to the global user configuration.
const userExperimental = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...((settings.user.settings['experimental'] as Record<string, unknown>) ||
{}),
};
userExperimental[name] = value;
settings.setValue(SettingScope.User, 'experimental', userExperimental);
// Update the live config with the merged state so it takes effect immediately
const mergedExperimental = {
...((settings.merged.experimental as Record<string, unknown>) || {}),
...userExperimental,
};
config.updateExperimentalSettings(mergedExperimental);
context.ui.addItem({
type: MessageType.INFO,
text: `Experiment ${name} set to ${value} (persisted in user settings).`,
});
},
};
const unsetExperimentCommand: SlashCommand = {
name: 'unset',
description:
'Remove a local override for an experiment. Usage: /experiment unset <name>',
kind: CommandKind.BUILT_IN,
autoExecute: false,
action: async (context: CommandContext, args: string) => {
const name = args.trim();
if (!name) {
context.ui.addItem({
type: MessageType.ERROR,
text: 'Usage: /experiment unset <name>',
});
return;
}
const { settings, config } = context.services;
if (!config) return;
const userExperimental = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...((settings.user.settings['experimental'] as Record<string, unknown>) ||
{}),
};
if (!(name in userExperimental)) {
context.ui.addItem({
type: MessageType.ERROR,
text: `No local user override found for experiment: ${name}`,
});
return;
}
delete userExperimental[name];
settings.setValue(SettingScope.User, 'experimental', userExperimental);
// Update the live config with the new merged state
const mergedExperimental = {
...((settings.merged.experimental as Record<string, unknown>) || {}),
};
delete mergedExperimental[name];
config.updateExperimentalSettings(mergedExperimental);
context.ui.addItem({
type: MessageType.INFO,
text: `Local user override for experiment ${name} removed.`,
});
},
};
export const experimentCommand: SlashCommand = {
name: 'experiment',
description: 'Manage experimental features',
hidden: true,
kind: CommandKind.BUILT_IN,
autoExecute: false,
subCommands: [
listExperimentsCommand,
setExperimentCommand,
unsetExperimentCommand,
],
action: async (context: CommandContext, args: string) =>
listExperimentsCommand.action!(context, args),
};
@@ -57,14 +57,3 @@ export const tinyAsciiLogoCompactText = `
`;
export const mattAsciiLogo = `
`;
+1 -10
View File
@@ -7,16 +7,10 @@
import type React from 'react';
import { Box } from 'ink';
import { ThemedGradient } from './ThemedGradient.js';
import {
shortAsciiLogo,
longAsciiLogo,
tinyAsciiLogo,
mattAsciiLogo,
} from './AsciiArt.js';
import { shortAsciiLogo, longAsciiLogo, tinyAsciiLogo } from './AsciiArt.js';
import { getAsciiArtWidth } from '../utils/textUtils.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { useSnowfall } from '../hooks/useSnowfall.js';
import { useConfig } from '../contexts/ConfigContext.js';
interface HeaderProps {
customAsciiArt?: string; // For user-defined ASCII art
@@ -29,7 +23,6 @@ export const Header: React.FC<HeaderProps> = ({
version,
nightly,
}) => {
const config = useConfig();
const { columns: terminalWidth } = useTerminalSize();
let displayTitle;
const widthOfLongLogo = getAsciiArtWidth(longAsciiLogo);
@@ -37,8 +30,6 @@ export const Header: React.FC<HeaderProps> = ({
if (customAsciiArt) {
displayTitle = customAsciiArt;
} else if (config.isAwesomeEnabled()) {
displayTitle = mattAsciiLogo;
} else if (terminalWidth >= widthOfLongLogo) {
displayTitle = longAsciiLogo;
} else if (terminalWidth >= widthOfShortLogo) {
@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useTimedMessage } from './useTimedMessage.js';
describe('useTimedMessage', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('resets the timeout when the same message is retriggered', async () => {
const { result, unmount } = await renderHook(() => useTimedMessage(1000));
act(() => {
result.current[1]('hint');
});
expect(result.current[0]).toBe('hint');
act(() => {
vi.advanceTimersByTime(500);
});
act(() => {
result.current[1]('hint');
});
expect(result.current[0]).toBe('hint');
act(() => {
vi.advanceTimersByTime(500);
});
expect(result.current[0]).toBe('hint');
act(() => {
vi.advanceTimersByTime(500);
});
expect(result.current[0]).toBeNull();
unmount();
});
it('clears the message immediately when asked to hide it', async () => {
const { result, unmount } = await renderHook(() => useTimedMessage(1000));
act(() => {
result.current[1]('hint');
});
expect(result.current[0]).toBe('hint');
act(() => {
result.current[1](null);
});
expect(result.current[0]).toBeNull();
act(() => {
vi.advanceTimersByTime(1000);
});
expect(result.current[0]).toBeNull();
unmount();
});
});
+13 -2
View File
@@ -12,19 +12,29 @@ import { useState, useCallback, useRef, useEffect } from 'react';
*/
export function useTimedMessage<T>(durationMs: number) {
const [message, setMessage] = useState<T | null>(null);
const messageRef = useRef<T | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const showMessage = useCallback(
(msg: T | null) => {
setMessage(msg);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (msg !== null) {
timeoutRef.current = setTimeout(() => {
setMessage(null);
timeoutRef.current = null;
messageRef.current = null;
setMessage((prev) => (prev === null ? prev : null));
}, durationMs);
}
if (Object.is(messageRef.current, msg)) {
return;
}
messageRef.current = msg;
setMessage(msg);
},
[durationMs],
);
@@ -33,6 +43,7 @@ export function useTimedMessage<T>(durationMs: number) {
() => () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
},
[],
@@ -0,0 +1,50 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { shouldAutoTriggerExpandHint } from './expandHint.js';
describe('shouldAutoTriggerExpandHint', () => {
it('returns true when constrained content gains a new overflowing region', () => {
expect(
shouldAutoTriggerExpandHint({
constrainHeight: true,
overflowingIdsSize: 2,
previousOverflowingIdsSize: 1,
}),
).toBe(true);
});
it('returns false when overflowingIdsSize decreases', () => {
expect(
shouldAutoTriggerExpandHint({
constrainHeight: true,
overflowingIdsSize: 1,
previousOverflowingIdsSize: 2,
}),
).toBe(false);
});
it('returns false when overflowingIdsSize is unchanged', () => {
expect(
shouldAutoTriggerExpandHint({
constrainHeight: true,
overflowingIdsSize: 1,
previousOverflowingIdsSize: 1,
}),
).toBe(false);
});
it('returns false while content is already expanded', () => {
expect(
shouldAutoTriggerExpandHint({
constrainHeight: false,
overflowingIdsSize: 2,
previousOverflowingIdsSize: 1,
}),
).toBe(false);
});
});
+19
View File
@@ -0,0 +1,19 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
interface ExpandHintAutoTriggerParams {
constrainHeight: boolean;
overflowingIdsSize: number;
previousOverflowingIdsSize: number;
}
export function shouldAutoTriggerExpandHint({
constrainHeight,
overflowingIdsSize,
previousOverflowingIdsSize,
}: ExpandHintAutoTriggerParams): boolean {
return constrainHeight && overflowingIdsSize > previousOverflowingIdsSize;
}
+1 -7
View File
@@ -46,11 +46,5 @@ export * from './src/utils/googleQuotaErrors.js';
export type { GoogleApiError } from './src/utils/googleErrors.js';
export { getCodeAssistServer } from './src/code_assist/codeAssist.js';
export { getExperiments } from './src/code_assist/experiments/experiments.js';
export {
ExperimentFlags,
ExperimentMetadata,
type ExperimentMetadataEntry,
getExperimentFlagName,
getExperimentFlagIdFromName,
} from './src/code_assist/experiments/flagNames.js';
export { ExperimentFlags } from './src/code_assist/experiments/flagNames.js';
export { getErrorStatus, ModelNotFoundError } from './src/utils/httpErrors.js';
+10 -12
View File
@@ -4,24 +4,22 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { BaseToolInvocation } from '../tools/tools.js';
import type {
ToolResult,
ToolCallConfirmationDetails,
ToolConfirmationOutcome,
ExecuteOptions,
import {
BaseToolInvocation,
type ToolConfirmationOutcome,
type ToolResult,
type ToolCallConfirmationDetails,
type ExecuteOptions,
} from '../tools/tools.js';
import {
DEFAULT_QUERY_STRING,
type RemoteAgentInputs,
type RemoteAgentDefinition,
type AgentInputs,
type SubagentProgress,
getAgentCardLoadOptions,
getRemoteAgentTargetUrl,
} from './types.js';
import type {
RemoteAgentInputs,
RemoteAgentDefinition,
AgentInputs,
SubagentProgress,
} from './types.js';
import { type AgentLoopContext } from '../config/agent-loop-context.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import type {
@@ -4,8 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { z } from 'zod';
export const ExperimentFlags = {
CONTEXT_COMPRESSION_THRESHOLD: 45740197,
USER_CACHING: 45740198,
@@ -22,239 +20,7 @@ export const ExperimentFlags = {
PRO_MODEL_NO_ACCESS: 45768879,
GEMINI_3_1_FLASH_LITE_LAUNCHED: 45771641,
DEFAULT_REQUEST_TIMEOUT: 45773134,
ENABLE_AWESOME: 45758820,
// Migrated from hardcoded experimental settings
ENABLE_AGENTS: 45760001,
EXTENSION_MANAGEMENT: 45760002,
EXTENSION_CONFIG: 45760003,
EXTENSION_REGISTRY: 45760004,
EXTENSION_RELOADING: 45760005,
JIT_CONTEXT: 45760006,
USE_OSC52_PASTE: 45760007,
USE_OSC52_COPY: 45760008,
PLAN: 45760009,
MODEL_STEERING: 45760010,
DISABLE_LLM_CORRECTION: 45760011,
ENABLE_TOOL_OUTPUT_MASKING: 45760012,
} as const;
export type ExperimentFlagName =
(typeof ExperimentFlags)[keyof typeof ExperimentFlags];
export interface ExperimentMetadataEntry {
description: string;
schema: z.ZodTypeAny;
defaultValue: unknown;
hidden?: boolean;
/** The key used in settings.json (defaults to kebab-case version of flag name) */
settingKey?: string;
}
export const ExperimentMetadata: Record<number, ExperimentMetadataEntry> = {
[ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD]: {
description: 'Threshold at which context compression activates.',
schema: z.number(),
defaultValue: 0,
},
[ExperimentFlags.USER_CACHING]: {
description: 'Enables caching of user contexts.',
schema: z.boolean(),
defaultValue: false,
},
[ExperimentFlags.BANNER_TEXT_NO_CAPACITY_ISSUES]: {
description: 'Banner text displayed when there are no capacity issues.',
schema: z.string(),
defaultValue: '',
},
[ExperimentFlags.BANNER_TEXT_CAPACITY_ISSUES]: {
description: 'Banner text displayed during capacity issues.',
schema: z.string(),
defaultValue: '',
},
[ExperimentFlags.ENABLE_PREVIEW]: {
description: 'Enables preview features globally.',
schema: z.boolean(),
defaultValue: false,
},
[ExperimentFlags.ENABLE_NUMERICAL_ROUTING]: {
description: 'Enables numerical routing strategies for the model.',
schema: z.boolean(),
defaultValue: false,
},
[ExperimentFlags.CLASSIFIER_THRESHOLD]: {
description: 'Threshold for the intent classifier.',
schema: z.number(),
defaultValue: 0.5,
},
[ExperimentFlags.ENABLE_ADMIN_CONTROLS]: {
description: 'Enables admin control features in the CLI.',
schema: z.boolean(),
defaultValue: false,
},
[ExperimentFlags.MASKING_PROTECTION_THRESHOLD]: {
description: 'Threshold for masking protection logic.',
schema: z.number(),
defaultValue: 0,
settingKey: 'toolOutputMasking.toolProtectionThreshold',
},
[ExperimentFlags.MASKING_PRUNABLE_THRESHOLD]: {
description: 'Threshold for prunable masking.',
schema: z.number(),
defaultValue: 0,
settingKey: 'toolOutputMasking.minPrunableTokensThreshold',
},
[ExperimentFlags.MASKING_PROTECT_LATEST_TURN]: {
description: 'Protects the latest turn from being masked.',
schema: z.boolean(),
defaultValue: true,
settingKey: 'toolOutputMasking.protectLatestTurn',
},
// Migrated settings (marked hidden to keep /experiment list focused)
[ExperimentFlags.ENABLE_AGENTS]: {
description: 'Enable local and remote subagents.',
schema: z.boolean(),
defaultValue: false,
hidden: true,
settingKey: 'enableAgents',
},
[ExperimentFlags.EXTENSION_MANAGEMENT]: {
description: 'Enable extension management features.',
schema: z.boolean(),
defaultValue: true,
hidden: true,
settingKey: 'extensionManagement',
},
[ExperimentFlags.EXTENSION_CONFIG]: {
description: 'Enable requesting and fetching of extension settings.',
schema: z.boolean(),
defaultValue: true,
hidden: true,
settingKey: 'extensionConfig',
},
[ExperimentFlags.EXTENSION_REGISTRY]: {
description: 'Enable extension registry explore UI.',
schema: z.boolean(),
defaultValue: false,
hidden: true,
settingKey: 'extensionRegistry',
},
[ExperimentFlags.EXTENSION_RELOADING]: {
description: 'Enables extension loading/unloading within the CLI session.',
schema: z.boolean(),
defaultValue: false,
hidden: true,
settingKey: 'extensionReloading',
},
[ExperimentFlags.JIT_CONTEXT]: {
description: 'Enable Just-In-Time (JIT) context loading.',
schema: z.boolean(),
defaultValue: false,
hidden: true,
settingKey: 'jitContext',
},
[ExperimentFlags.USE_OSC52_PASTE]: {
description: 'Use OSC 52 for pasting.',
schema: z.boolean(),
defaultValue: false,
hidden: true,
settingKey: 'useOSC52Paste',
},
[ExperimentFlags.USE_OSC52_COPY]: {
description: 'Use OSC 52 for copying.',
schema: z.boolean(),
defaultValue: false,
hidden: true,
settingKey: 'useOSC52Copy',
},
[ExperimentFlags.PLAN]: {
description: 'Enable planning features (Plan Mode and tools).',
schema: z.boolean(),
defaultValue: false,
hidden: true,
settingKey: 'plan',
},
[ExperimentFlags.MODEL_STEERING]: {
description: 'Enable model steering (user hints).',
schema: z.boolean(),
defaultValue: false,
hidden: true,
settingKey: 'modelSteering',
},
[ExperimentFlags.DISABLE_LLM_CORRECTION]: {
description: 'Disable LLM-based error correction for edit tools.',
schema: z.boolean(),
defaultValue: true,
hidden: true,
settingKey: 'disableLLMCorrection',
},
[ExperimentFlags.ENABLE_TOOL_OUTPUT_MASKING]: {
description: 'Enables tool output masking to save tokens.',
schema: z.boolean(),
defaultValue: true,
hidden: true,
settingKey: 'toolOutputMasking.enabled',
},
[ExperimentFlags.GEMINI_3_1_PRO_LAUNCHED]: {
description: 'Indicates if Gemini 3.1 Pro has been launched.',
schema: z.boolean(),
defaultValue: false,
},
[ExperimentFlags.PRO_MODEL_NO_ACCESS]: {
description: 'Indicates if the user has no access to Pro models.',
schema: z.boolean(),
defaultValue: false,
},
[ExperimentFlags.GEMINI_3_1_FLASH_LITE_LAUNCHED]: {
description: 'Indicates if Gemini 3.1 Flash Lite has been launched.',
schema: z.boolean(),
defaultValue: false,
},
[ExperimentFlags.DEFAULT_REQUEST_TIMEOUT]: {
description: 'The default request timeout in seconds.',
schema: z.number(),
defaultValue: 0,
},
[ExperimentFlags.ENABLE_AWESOME]: {
description: "When enabled, the ASCII art says 'matt'.",
schema: z.boolean(),
defaultValue: false,
},
};
/**
* Gets the name of an experiment flag from its ID.
*/
export function getExperimentFlagName(flagId: number): string | undefined {
const metadata = ExperimentMetadata[flagId];
if (metadata?.settingKey) {
return metadata.settingKey;
}
for (const [name, id] of Object.entries(ExperimentFlags)) {
if (id === flagId) {
return name.toLowerCase().replace(/_/g, '-');
}
}
return undefined;
}
/**
* Gets the ID of an experiment flag from its name (supports kebab-case or camelCase).
*/
export function getExperimentFlagIdFromName(name: string): number | undefined {
// Check metadata for explicit settingKey matches first
for (const [idStr, metadata] of Object.entries(ExperimentMetadata)) {
if (metadata.settingKey === name) {
return parseInt(idStr, 10);
}
}
// Convert enableNumericalRouting or enable-numerical-routing to ENABLE_NUMERICAL_ROUTING
const constantName = name
.replace(/([a-z])([A-Z])/g, '$1_$2') // camelCase to snake_case
.toUpperCase()
.replace(/-/g, '_'); // kebab-case to snake_case
return (ExperimentFlags as Record<string, number>)[constantName];
}
@@ -1,49 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { Config } from './config.js';
import { ExperimentFlags } from '../code_assist/experiments/flagNames.js';
describe('Config CLI Override', () => {
const sessionId = 'test-session';
const targetDir = process.cwd();
const cwd = process.cwd();
const model = 'gemini-pro';
it('should prioritize CLI argument over local setting', async () => {
const config = new Config({
sessionId,
targetDir,
cwd,
model,
debugMode: false,
experimentalCliArgs: { 'enable-numerical-routing': true },
experimentalSettings: { 'enable-numerical-routing': false },
});
expect(await config.getNumericalRoutingEnabled()).toBe(true);
});
it('should prioritize CLI argument over remote experiment', async () => {
const config = new Config({
sessionId,
targetDir,
cwd,
model,
debugMode: false,
experimentalCliArgs: { 'enable-numerical-routing': true },
experiments: {
flags: {
[ExperimentFlags.ENABLE_NUMERICAL_ROUTING]: { boolValue: false },
},
experimentIds: [],
},
});
expect(await config.getNumericalRoutingEnabled()).toBe(true);
});
});
@@ -1,142 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { Config } from './config.js';
import { ExperimentFlags } from '../code_assist/experiments/flagNames.js';
describe('Config getExperimentValue', () => {
const sessionId = 'test-session';
const targetDir = process.cwd();
const cwd = process.cwd();
const model = 'gemini-pro';
it('should prioritize CLI argument over others', () => {
const config = new Config({
sessionId,
targetDir,
cwd,
model,
debugMode: false,
experimentalCliArgs: { 'enable-preview': true },
experimentalSettings: { 'enable-preview': false },
experiments: {
flags: {
[ExperimentFlags.ENABLE_PREVIEW]: { boolValue: false },
},
experimentIds: [],
},
});
expect(config.getExperimentValue(ExperimentFlags.ENABLE_PREVIEW)).toBe(
true,
);
});
it('should prioritize local setting over remote experiment', () => {
const config = new Config({
sessionId,
targetDir: process.cwd(),
cwd: process.cwd(),
model,
debugMode: false,
experimentalSettings: { 'enable-preview': true },
experiments: {
flags: {
[ExperimentFlags.ENABLE_PREVIEW]: { boolValue: false },
},
experimentIds: [],
},
});
expect(config.getExperimentValue(ExperimentFlags.ENABLE_PREVIEW)).toBe(
true,
);
});
it('should use remote experiment if no local override', () => {
const config = new Config({
sessionId,
targetDir: process.cwd(),
cwd: process.cwd(),
model,
debugMode: false,
experiments: {
flags: {
[ExperimentFlags.ENABLE_PREVIEW]: { boolValue: true },
},
experimentIds: [],
},
});
expect(config.getExperimentValue(ExperimentFlags.ENABLE_PREVIEW)).toBe(
true,
);
});
it('should use default value if nothing else is set', () => {
const config = new Config({
sessionId,
targetDir: process.cwd(),
cwd: process.cwd(),
model,
debugMode: false,
});
// Default for ENABLE_PREVIEW is false
expect(config.getExperimentValue(ExperimentFlags.ENABLE_PREVIEW)).toBe(
false,
);
});
it('should handle numeric values correctly', () => {
const config = new Config({
sessionId,
targetDir: process.cwd(),
cwd: process.cwd(),
model,
debugMode: false,
experimentalCliArgs: { 'classifier-threshold': 0.8 },
});
expect(
config.getExperimentValue<number>(ExperimentFlags.CLASSIFIER_THRESHOLD),
).toBe(0.8);
});
it('should handle string representation of numbers from remote', () => {
const config = new Config({
sessionId,
targetDir: process.cwd(),
cwd: process.cwd(),
model,
debugMode: false,
experiments: {
flags: {
[ExperimentFlags.CLASSIFIER_THRESHOLD]: { stringValue: '0.7' },
},
experimentIds: [],
},
});
expect(
config.getExperimentValue<number>(ExperimentFlags.CLASSIFIER_THRESHOLD),
).toBe(0.7);
});
it('should return true for isAwesomeEnabled when flag is set', () => {
const config = new Config({
sessionId,
targetDir,
cwd,
model,
debugMode: false,
experimentalCliArgs: { 'enable-awesome': true },
});
expect(config.isAwesomeEnabled()).toBe(true);
});
});
+2 -2
View File
@@ -545,9 +545,9 @@ describe('Server Config (config.ts)', () => {
expect(await config.getUserCaching()).toBe(false);
});
it('should return the default value if there are no experiments', async () => {
it('should return undefined if there are no experiments', async () => {
const config = new Config(baseParams);
expect(await config.getUserCaching()).toBe(false);
expect(await config.getUserCaching()).toBeUndefined();
});
});
+99 -179
View File
@@ -128,7 +128,6 @@ import { DEFAULT_MODEL_CONFIGS } from './defaultModelConfigs.js';
import { MemoryContextManager } from '../context/memoryContextManager.js';
import { TrackerService } from '../services/trackerService.js';
import type { GenerateContentParameters } from '@google/genai';
import { ExperimentManager } from './experimentManager.js';
// Re-export OAuth config type
export type { MCPOAuthConfig, AnyToolInvocation, AnyDeclarativeTool };
@@ -156,19 +155,14 @@ import type {
} from '../code_assist/types.js';
import type { HierarchicalMemory } from './memory.js';
import { getCodeAssistServer } from '../code_assist/codeAssist.js';
import { AgentRegistry } from '../agents/registry.js';
import { AcknowledgedAgentsService } from '../agents/acknowledgedAgents.js';
import { setGlobalProxy, updateGlobalFetchTimeouts } from '../utils/fetch.js';
import { SubagentTool } from '../agents/subagent-tool.js';
import {
ExperimentFlags,
ExperimentMetadata,
getExperimentFlagName,
} from '../code_assist/experiments/flagNames.js';
import {
getExperiments,
type Experiments,
} from '../code_assist/experiments/experiments.js';
import { AgentRegistry } from '../agents/registry.js';
import { AcknowledgedAgentsService } from '../agents/acknowledgedAgents.js';
import { setGlobalProxy, updateGlobalFetchTimeouts } from '../utils/fetch.js';
import { ExperimentFlags } from '../code_assist/experiments/flagNames.js';
import { debugLogger } from '../utils/debugLogger.js';
import { SkillManager, type SkillDefinition } from '../skills/skillManager.js';
import { startupProfiler } from '../telemetry/startupProfiler.js';
@@ -448,6 +442,7 @@ import {
DEFAULT_MIN_PRUNABLE_TOKENS_THRESHOLD,
DEFAULT_PROTECT_LATEST_TURN,
} from '../context/toolOutputMaskingService.js';
import {
type ExtensionLoader,
SimpleExtensionLoader,
@@ -697,8 +692,6 @@ export interface ConfigParameters {
disabledHooks?: string[];
projectHooks?: { [K in HookEventName]?: HookDefinition[] };
enableAgents?: boolean;
experimentalSettings?: Record<string, unknown>;
experimentalCliArgs?: Record<string, unknown>;
enableEventDrivenScheduler?: boolean;
skillsSupport?: boolean;
disabledSkills?: string[];
@@ -824,6 +817,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly listExtensions: boolean;
private readonly _extensionLoader: ExtensionLoader;
private readonly _enabledExtensions: string[];
private readonly enableExtensionReloading: boolean;
fallbackModelHandler?: FallbackModelHandler;
validationHandler?: ValidationHandler;
private quotaErrorOccurred: boolean = false;
@@ -881,7 +875,6 @@ export class Config implements McpContext, AgentLoopContext {
private shellExecutionConfig: ShellExecutionConfig;
private readonly extensionManagement: boolean = true;
private readonly extensionRegistryURI: string | undefined;
private readonly enablePromptCompletion: boolean = false;
private readonly truncateToolOutputThreshold: number;
private compressionTruncationCounter = 0;
private initialized = false;
@@ -894,7 +887,6 @@ export class Config implements McpContext, AgentLoopContext {
private readonly workspacePoliciesDir: string | undefined;
private readonly _messageBus: MessageBus;
private readonly policyEngine: PolicyEngine;
private readonly experimentManager: ExperimentManager;
private policyUpdateConfirmationRequest:
| PolicyUpdateConfirmationRequest
| undefined;
@@ -919,7 +911,14 @@ export class Config implements McpContext, AgentLoopContext {
private pendingIncludeDirectories: string[];
private readonly enableHooksUI: boolean;
private readonly enableHooks: boolean;
private experimentsPromise: Promise<void> | undefined;
private hooks: { [K in HookEventName]?: HookDefinition[] } | undefined;
private projectHooks:
| ({ [K in HookEventName]?: HookDefinition[] } & { disabled?: string[] })
| undefined;
private disabledHooks: string[];
private experiments: Experiments | undefined;
private experimentsPromise: Promise<Experiments | undefined> | undefined;
private hookSystem?: HookSystem;
private readonly onModelChange: ((model: string) => void) | undefined;
private readonly onReload:
@@ -951,7 +950,6 @@ export class Config implements McpContext, AgentLoopContext {
private readonly modelSteering: boolean;
private memoryContextManager?: MemoryContextManager;
private readonly contextManagement: ContextManagementConfig;
private contextManager?: ContextManager;
private terminalBackground: string | undefined = undefined;
private remoteAdminSettings: AdminControlsSettings | undefined;
private latestApiRequest: GenerateContentParameters | undefined;
@@ -1094,51 +1092,15 @@ export class Config implements McpContext, AgentLoopContext {
this._activeModel = params.model;
this.enableAgents = params.enableAgents ?? true;
this.agents = params.agents ?? {};
this.experimentalSettings = params.experimentalSettings ?? {};
this.experimentalCliArgs = params.experimentalCliArgs ?? {};
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
this.planEnabled = params.plan ?? true;
this.trackerEnabled = params.tracker ?? false;
this.planModeRoutingEnabled = params.planSettings?.modelRouting ?? true;
// Merge legacy experimental parameters into a single object for the manager
const experimentalSettings = {
...(params.experimentalSettings ?? {}),
};
if (params.plan !== undefined) experimentalSettings['plan'] = params.plan;
if (params.experimentalJitContext !== undefined)
experimentalSettings['jitContext'] = params.experimentalJitContext;
if (params.enableAgents !== undefined)
experimentalSettings['enableAgents'] = params.enableAgents;
if (params.modelSteering !== undefined)
experimentalSettings['modelSteering'] = params.modelSteering;
if (params.enableExtensionReloading !== undefined)
experimentalSettings['extensionReloading'] =
params.enableExtensionReloading;
if (params.extensionManagement !== undefined)
experimentalSettings['extensionManagement'] = params.extensionManagement;
if (params.disableLLMCorrection !== undefined)
experimentalSettings['disableLLMCorrection'] =
params.disableLLMCorrection;
if (params.toolOutputMasking?.enabled !== undefined) {
experimentalSettings['toolOutputMasking'] = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
...((experimentalSettings['toolOutputMasking'] as object) ?? {}),
enabled: params.toolOutputMasking.enabled,
};
}
this.experimentManager = new ExperimentManager({
experimentalSettings,
experimentalCliArgs: params.experimentalCliArgs,
experiments: params.experiments,
});
this.enableEventDrivenScheduler = params.enableEventDrivenScheduler ?? true;
this.skillsSupport = params.skillsSupport ?? true;
this.disabledSkills = params.disabledSkills ?? [];
this.adminSkillsEnabled = params.adminSkillsEnabled ?? true;
this.modelAvailabilityService = new ModelAvailabilityService();
<<<<<<< HEAD
this.dynamicModelConfiguration = params.dynamicModelConfiguration ?? false;
// HACK: The settings loading logic doesn't currently merge the default
@@ -1382,6 +1344,7 @@ export class Config implements McpContext, AgentLoopContext {
this.projectHooks = params.projectHooks;
}
this.experiments = params.experiments;
this.onModelChange = params.onModelChange;
this.onReload = params.onReload;
@@ -1446,7 +1409,7 @@ export class Config implements McpContext, AgentLoopContext {
}
// Add plans directory to workspace context for plan file storage
if (this.isPlanEnabled()) {
if (this.planEnabled) {
const plansDir = this.storage.getPlansDir();
try {
await fs.promises.access(plansDir);
@@ -1528,15 +1491,9 @@ export class Config implements McpContext, AgentLoopContext {
await this.hookSystem.initialize();
}
<<<<<<< HEAD
if (this.experimentalJitContext) {
this.memoryContextManager = new MemoryContextManager(this);
await this.memoryContextManager.refresh();
=======
if (this.isJitContextEnabled()) {
this.contextManager = new ContextManager(this);
await this.contextManager.refresh();
>>>>>>> 49ab62d87 (refactor: move experiment logic to ExperimentManager with Zod validation)
}
await this._geminiClient.initialize();
@@ -1635,11 +1592,9 @@ export class Config implements McpContext, AgentLoopContext {
this.setModel(DEFAULT_GEMINI_MODEL_AUTO);
}
// Fetch admin controls
await this.ensureExperimentsLoaded();
const adminControlsEnabled = !!this.getExperimentValue<boolean>(
ExperimentFlags.ENABLE_ADMIN_CONTROLS,
);
const adminControlsEnabled =
experiments?.flags[ExperimentFlags.ENABLE_ADMIN_CONTROLS]?.boolValue ??
false;
const adminControls = await fetchAdminControls(
codeAssistServer,
this.getRemoteAdminSettings(),
@@ -1657,8 +1612,8 @@ export class Config implements McpContext, AgentLoopContext {
}
async getExperimentsAsync(): Promise<Experiments | undefined> {
if (this.experimentManager.getExperiments()) {
return this.experimentManager.getExperiments();
if (this.experiments) {
return this.experiments;
}
const codeAssistServer = getCodeAssistServer(this);
return getExperiments(codeAssistServer);
@@ -2365,12 +2320,12 @@ export class Config implements McpContext, AgentLoopContext {
}
getUserMemory(): string | HierarchicalMemory {
if (this.isJitContextEnabled() && this.contextManager) {
if (this.experimentalJitContext && this.memoryContextManager) {
return {
global: this.contextManager.getGlobalMemory(),
extension: this.contextManager.getExtensionMemory(),
project: this.contextManager.getEnvironmentMemory(),
userProjectMemory: this.contextManager.getUserProjectMemory(),
global: this.memoryContextManager.getGlobalMemory(),
extension: this.memoryContextManager.getExtensionMemory(),
project: this.memoryContextManager.getEnvironmentMemory(),
userProjectMemory: this.memoryContextManager.getUserProjectMemory(),
};
}
return this.userMemory;
@@ -2380,8 +2335,8 @@ export class Config implements McpContext, AgentLoopContext {
* Refreshes the MCP context, including memory, tools, and system instructions.
*/
async refreshMcpContext(): Promise<void> {
if (this.isJitContextEnabled() && this.contextManager) {
await this.contextManager.refresh();
if (this.experimentalJitContext && this.memoryContextManager) {
await this.memoryContextManager.refresh();
} else {
const { refreshServerHierarchicalMemory } = await import(
'../utils/memoryDiscovery.js'
@@ -2456,7 +2411,7 @@ export class Config implements McpContext, AgentLoopContext {
}
isJitContextEnabled(): boolean {
return this.experimentManager.isJitContextEnabled();
return this.experimentalJitContext;
}
isContextManagementEnabled(): boolean {
@@ -2492,37 +2447,49 @@ export class Config implements McpContext, AgentLoopContext {
}
isModelSteeringEnabled(): boolean {
return this.experimentManager.isModelSteeringEnabled();
}
getToolOutputMaskingEnabled(): boolean {
return this.getExperimentValue<boolean>(
ExperimentFlags.ENABLE_TOOL_OUTPUT_MASKING,
)!;
return this.modelSteering;
}
async getToolOutputMaskingConfig(): Promise<ToolOutputMaskingConfig> {
await this.ensureExperimentsLoaded();
const remoteProtection =
this.experiments?.flags[ExperimentFlags.MASKING_PROTECTION_THRESHOLD]
?.intValue;
const remotePrunable =
this.experiments?.flags[ExperimentFlags.MASKING_PRUNABLE_THRESHOLD]
?.intValue;
const remoteProtectLatest =
this.experiments?.flags[ExperimentFlags.MASKING_PROTECT_LATEST_TURN]
?.boolValue;
const parsedProtection = remoteProtection
? parseInt(remoteProtection, 10)
: undefined;
const parsedPrunable = remotePrunable
? parseInt(remotePrunable, 10)
: undefined;
return {
enabled: this.getExperimentValue<boolean>(
ExperimentFlags.ENABLE_TOOL_OUTPUT_MASKING,
)!,
toolProtectionThreshold: this.getExperimentValue<number>(
ExperimentFlags.MASKING_PROTECTION_THRESHOLD,
)!,
minPrunableTokensThreshold: this.getExperimentValue<number>(
ExperimentFlags.MASKING_PRUNABLE_THRESHOLD,
)!,
protectLatestTurn: this.getExperimentValue<boolean>(
ExperimentFlags.MASKING_PROTECT_LATEST_TURN,
)!,
protectionThresholdTokens:
parsedProtection !== undefined && !isNaN(parsedProtection)
? parsedProtection
: this.contextManagement.tools.outputMasking
.protectionThresholdTokens,
minPrunableThresholdTokens:
parsedPrunable !== undefined && !isNaN(parsedPrunable)
? parsedPrunable
: this.contextManagement.tools.outputMasking
.minPrunableThresholdTokens,
protectLatestTurn:
remoteProtectLatest ??
this.contextManagement.tools.outputMasking.protectLatestTurn,
};
}
getGeminiMdFileCount(): number {
if (this.isJitContextEnabled() && this.contextManager) {
return this.contextManager.getLoadedPaths().size;
if (this.experimentalJitContext && this.memoryContextManager) {
return this.memoryContextManager.getLoadedPaths().size;
}
return this.geminiMdFileCount;
}
@@ -2532,8 +2499,8 @@ export class Config implements McpContext, AgentLoopContext {
}
getGeminiMdFilePaths(): string[] {
if (this.isJitContextEnabled() && this.contextManager) {
return Array.from(this.contextManager.getLoadedPaths());
if (this.experimentalJitContext && this.memoryContextManager) {
return Array.from(this.memoryContextManager.getLoadedPaths());
}
return this.geminiMdFilePaths;
}
@@ -2631,45 +2598,6 @@ export class Config implements McpContext, AgentLoopContext {
}
/**
* Synchronizes enter/exit plan mode tools based on current mode.
*/
syncPlanModeTools(): void {
const registry = this.getToolRegistry();
if (!registry) {
return;
}
const approvalMode = this.getApprovalMode();
const isPlanMode = approvalMode === ApprovalMode.PLAN;
const isYoloMode = approvalMode === ApprovalMode.YOLO;
if (isPlanMode) {
if (registry.getTool(ENTER_PLAN_MODE_TOOL_NAME)) {
registry.unregisterTool(ENTER_PLAN_MODE_TOOL_NAME);
}
if (!registry.getTool(EXIT_PLAN_MODE_TOOL_NAME)) {
registry.registerTool(new ExitPlanModeTool(this, this.messageBus));
}
} else {
if (registry.getTool(EXIT_PLAN_MODE_TOOL_NAME)) {
registry.unregisterTool(EXIT_PLAN_MODE_TOOL_NAME);
}
if (this.isPlanEnabled() && !isYoloMode) {
if (!registry.getTool(ENTER_PLAN_MODE_TOOL_NAME)) {
registry.registerTool(new EnterPlanModeTool(this, this.messageBus));
}
} else {
if (registry.getTool(ENTER_PLAN_MODE_TOOL_NAME)) {
registry.unregisterTool(ENTER_PLAN_MODE_TOOL_NAME);
}
}
}
if (this.geminiClient?.isInitialized()) {
this.geminiClient.setTools().catch((err) => {
debugLogger.error('Failed to update tools', err);
});
}
}
* Logs the duration of the current approval mode.
*/
logCurrentModeDuration(mode: ApprovalMode): void {
@@ -2901,9 +2829,7 @@ export class Config implements McpContext, AgentLoopContext {
}
getExtensionManagement(): boolean {
return this.experimentManager.getExperimentValue<boolean>(
ExperimentFlags.EXTENSION_MANAGEMENT,
);
return this.extensionManagement;
}
getExtensions(): GeminiCLIExtension[] {
@@ -2921,15 +2847,15 @@ export class Config implements McpContext, AgentLoopContext {
}
getEnableExtensionReloading(): boolean {
return this.experimentManager.getEnableExtensionReloading();
return this.enableExtensionReloading;
}
getDisableLLMCorrection(): boolean {
return this.experimentManager.getDisableLLMCorrection();
return this.disableLLMCorrection;
}
isPlanEnabled(): boolean {
return this.experimentManager.isPlanEnabled();
return this.planEnabled;
}
isTrackerEnabled(): boolean {
@@ -2949,7 +2875,7 @@ export class Config implements McpContext, AgentLoopContext {
}
isAgentsEnabled(): boolean {
return this.experimentManager.isAgentsEnabled();
return this.enableAgents;
}
isEventDrivenSchedulerEnabled(): boolean {
@@ -3074,9 +3000,9 @@ export class Config implements McpContext, AgentLoopContext {
await this.ensureExperimentsLoaded();
const remoteThreshold = this.getExperimentValue<number>(
ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD,
);
const remoteThreshold =
this.experiments?.flags[ExperimentFlags.CONTEXT_COMPRESSION_THRESHOLD]
?.floatValue;
if (remoteThreshold === 0) {
return undefined;
}
@@ -3084,7 +3010,9 @@ export class Config implements McpContext, AgentLoopContext {
}
async getUserCaching(): Promise<boolean | undefined> {
return this.experimentManager.getUserCaching();
await this.ensureExperimentsLoaded();
return this.experiments?.flags[ExperimentFlags.USER_CACHING]?.boolValue;
}
async getPlanModeRoutingEnabled(): Promise<boolean> {
@@ -3092,7 +3020,11 @@ export class Config implements McpContext, AgentLoopContext {
}
async getNumericalRoutingEnabled(): Promise<boolean> {
return this.experimentManager.isNumericalRoutingEnabled();
await this.ensureExperimentsLoaded();
const flag =
this.experiments?.flags[ExperimentFlags.ENABLE_NUMERICAL_ROUTING];
return flag?.boolValue ?? true;
}
/**
@@ -3117,15 +3049,29 @@ export class Config implements McpContext, AgentLoopContext {
}
async getClassifierThreshold(): Promise<number | undefined> {
return this.experimentManager.getClassifierThreshold();
await this.ensureExperimentsLoaded();
const flag = this.experiments?.flags[ExperimentFlags.CLASSIFIER_THRESHOLD];
if (flag?.intValue !== undefined) {
return parseInt(flag.intValue, 10);
}
return flag?.floatValue;
}
async getBannerTextNoCapacityIssues(): Promise<string> {
return this.experimentManager.getBannerTextNoCapacityIssues();
await this.ensureExperimentsLoaded();
return (
this.experiments?.flags[ExperimentFlags.BANNER_TEXT_NO_CAPACITY_ISSUES]
?.stringValue ?? ''
);
}
async getBannerTextCapacityIssues(): Promise<string> {
return this.experimentManager.getBannerTextCapacityIssues();
await this.ensureExperimentsLoaded();
return (
this.experiments?.flags[ExperimentFlags.BANNER_TEXT_CAPACITY_ISSUES]
?.stringValue ?? ''
);
}
/**
@@ -3249,12 +3195,6 @@ export class Config implements McpContext, AgentLoopContext {
);
}
isAwesomeEnabled(): boolean {
return (
this.getExperimentValue<boolean>(ExperimentFlags.ENABLE_AWESOME) ?? false
);
}
private async ensureExperimentsLoaded(): Promise<void> {
if (!this.experimentsPromise) {
return;
@@ -3734,34 +3674,14 @@ export class Config implements McpContext, AgentLoopContext {
* Get experiments configuration
*/
getExperiments(): Experiments | undefined {
return this.experimentManager.getExperiments();
}
/**
* Resolves the value of an experiment flag based on priority:
* 1. Command-line argument (--experiment-<flag-name>)
* 2. Local setting (experimental.<flag-name>)
* 3. Remote experiment
* 4. Default value
*/
getExperimentValue<T extends boolean | number | string>(
flagId: number,
): T | undefined {
return this.experimentManager.getExperimentValue<T>(flagId);
}
/**
* Updates experimental settings.
*/
updateExperimentalSettings(settings: Record<string, unknown>): void {
this.experimentManager.updateExperimentalSettings(settings);
return this.experiments;
}
/**
* Set experiments configuration
*/
setExperiments(experiments: Experiments): void {
this.experimentManager.setExperiments(experiments);
this.experiments = experiments;
const flagSummaries = Object.entries(experiments.flags ?? {})
.sort(([a], [b]) => a.localeCompare(b))
.map(([flagId, flag]) => {
@@ -1,134 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { ExperimentManager } from './experimentManager.js';
import { ExperimentFlags } from '../code_assist/experiments/flagNames.js';
describe('ExperimentManager', () => {
const baseOptions = {
experimentalSettings: {},
experimentalCliArgs: {},
};
describe('getExperimentValue', () => {
it('should return default value when no overrides are present', () => {
const manager = new ExperimentManager(baseOptions);
// USER_CACHING default is false
expect(manager.getExperimentValue(ExperimentFlags.USER_CACHING)).toBe(
false,
);
});
it('should prioritize CLI arguments over all else', () => {
const manager = new ExperimentManager({
...baseOptions,
experimentalCliArgs: { 'user-caching': true },
experimentalSettings: { 'user-caching': false },
experiments: {
flags: {
[ExperimentFlags.USER_CACHING]: { boolValue: false },
},
experimentIds: [],
},
});
expect(manager.getExperimentValue(ExperimentFlags.USER_CACHING)).toBe(
true,
);
});
it('should prioritize local settings over remote experiments', () => {
const manager = new ExperimentManager({
...baseOptions,
experimentalSettings: { 'user-caching': true },
experiments: {
flags: {
[ExperimentFlags.USER_CACHING]: { boolValue: false },
},
experimentIds: [],
},
});
expect(manager.getExperimentValue(ExperimentFlags.USER_CACHING)).toBe(
true,
);
});
it('should use remote experiment if no local override', () => {
const manager = new ExperimentManager({
...baseOptions,
experiments: {
flags: {
[ExperimentFlags.USER_CACHING]: { boolValue: true },
},
experimentIds: [],
},
});
expect(manager.getExperimentValue(ExperimentFlags.USER_CACHING)).toBe(
true,
);
});
it('should handle nested settings correctly', () => {
const manager = new ExperimentManager({
...baseOptions,
experimentalSettings: {
toolOutputMasking: {
enabled: false,
},
},
});
expect(
manager.getExperimentValue(ExperimentFlags.ENABLE_TOOL_OUTPUT_MASKING),
).toBe(false);
});
it('should validate values using Zod schema', () => {
const manager = new ExperimentManager({
...baseOptions,
experimentalSettings: {
'classifier-threshold': 'not-a-number',
},
});
// CLASSIFIER_THRESHOLD default is 0.5
expect(
manager.getExperimentValue(ExperimentFlags.CLASSIFIER_THRESHOLD),
).toBe(0.5);
});
it('should parse numeric strings from remote flags if metadata type is number', () => {
const manager = new ExperimentManager({
...baseOptions,
experiments: {
flags: {
[ExperimentFlags.CLASSIFIER_THRESHOLD]: { stringValue: '0.8' },
},
experimentIds: [],
},
});
expect(
manager.getExperimentValue(ExperimentFlags.CLASSIFIER_THRESHOLD),
).toBe(0.8);
});
});
describe('convenience getters', () => {
it('isPlanEnabled should resolve correctly', () => {
const manager = new ExperimentManager({
...baseOptions,
experimentalSettings: { plan: true },
});
expect(manager.isPlanEnabled()).toBe(true);
});
it('isAgentsEnabled should resolve correctly', () => {
const manager = new ExperimentManager({
...baseOptions,
experimentalSettings: { enableAgents: true },
});
expect(manager.isAgentsEnabled()).toBe(true);
});
});
});
@@ -1,224 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
ExperimentFlags,
ExperimentMetadata,
getExperimentFlagName,
type ExperimentMetadataEntry,
} from '../code_assist/experiments/flagNames.js';
import type { Experiments } from '../code_assist/experiments/experiments.js';
import { debugLogger } from '../utils/debugLogger.js';
import { z } from 'zod';
export interface ExperimentManagerOptions {
experimentalSettings?: Record<string, unknown>;
experimentalCliArgs?: Record<string, unknown>;
experiments?: Experiments;
}
/**
* Manages resolution and validation of experimental flags.
*/
export class ExperimentManager {
private experimentalSettings: Record<string, unknown>;
private readonly experimentalCliArgs: Record<string, unknown>;
private experiments?: Experiments;
constructor(options: ExperimentManagerOptions) {
this.experimentalSettings = options.experimentalSettings ?? {};
this.experimentalCliArgs = options.experimentalCliArgs ?? {};
this.experiments = options.experiments;
}
/**
* Resolves the value of an experiment flag, applying layering logic:
* 1. Command-line argument
* 2. Local setting (settings.json)
* 3. Remote experiment (server-side)
* 4. Default value (from metadata)
*/
getExperimentValue<T>(flagId: number): T {
const metadata = ExperimentMetadata[flagId];
if (!metadata) {
debugLogger.warn(`Unknown experiment flag ID: ${flagId}`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return undefined as unknown as T;
}
const flagName = getExperimentFlagName(flagId);
// 1. CLI Argument
if (flagName && this.experimentalCliArgs[flagName] !== undefined) {
let val: unknown = this.experimentalCliArgs[flagName];
// Type coercion for CLI args
val = this.coerceValue(val, metadata);
const result = metadata.schema.safeParse(val);
if (result.success) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return result.data as T;
}
debugLogger.warn(
`Invalid CLI value for ${flagName}: ${val}. Error: ${result.error.message}`,
);
}
// 2. Local Setting (settings.json)
const settingKey = metadata.settingKey || flagName;
if (settingKey) {
const val = this.getNestedValue(this.experimentalSettings, settingKey);
if (val !== undefined) {
const result = metadata.schema.safeParse(val);
if (result.success) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return result.data as T;
}
debugLogger.warn(
`Invalid local setting for ${settingKey}: ${val}. Error: ${result.error.message}`,
);
}
}
// 3. Remote Experiment
const remoteFlag = this.experiments?.flags[flagId];
if (remoteFlag) {
let val: unknown =
remoteFlag.boolValue ??
remoteFlag.floatValue ??
(remoteFlag.intValue ? Number(remoteFlag.intValue) : undefined) ??
remoteFlag.stringValue;
if (val !== undefined) {
val = this.coerceValue(val, metadata);
const result = metadata.schema.safeParse(val);
if (result.success) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return result.data as T;
}
debugLogger.warn(
`Invalid remote value for flag ${flagId}: ${val}. Error: ${result.error.message}`,
);
}
}
// 4. Default Value
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return metadata.defaultValue as T;
}
private coerceValue(
val: unknown,
metadata: ExperimentMetadataEntry,
): unknown {
if (metadata.schema instanceof z.ZodNumber && typeof val === 'string') {
const num = Number(val);
if (!isNaN(num)) return num;
}
if (metadata.schema instanceof z.ZodBoolean && typeof val === 'string') {
if (val === 'true' || val === 'on') return true;
if (val === 'false' || val === 'off') return false;
}
return val;
}
private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
const parts = path.split('.');
let current: unknown = obj;
for (const part of parts) {
if (current === null || typeof current !== 'object') return undefined;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
current = (current as Record<string, unknown>)[part];
}
return current;
}
/**
* Updates the local experimental settings.
*/
updateExperimentalSettings(settings: Record<string, unknown>): void {
this.experimentalSettings = {
...this.experimentalSettings,
...settings,
};
}
/**
* Updates remote experiments.
*/
setExperiments(experiments: Experiments): void {
this.experiments = experiments;
}
/**
* Gets all experimental settings (for serialization/display).
*/
getExperimentalSettings(): Record<string, unknown> {
return this.experimentalSettings;
}
getExperiments(): Experiments | undefined {
return this.experiments;
}
// Convenience getters for commonly used flags
isPlanEnabled(): boolean {
return this.getExperimentValue<boolean>(ExperimentFlags.PLAN);
}
isAgentsEnabled(): boolean {
return this.getExperimentValue<boolean>(ExperimentFlags.ENABLE_AGENTS);
}
getEnableExtensionReloading(): boolean {
return this.getExperimentValue<boolean>(
ExperimentFlags.EXTENSION_RELOADING,
);
}
getDisableLLMCorrection(): boolean {
return this.getExperimentValue<boolean>(
ExperimentFlags.DISABLE_LLM_CORRECTION,
);
}
isModelSteeringEnabled(): boolean {
return this.getExperimentValue<boolean>(ExperimentFlags.MODEL_STEERING);
}
isJitContextEnabled(): boolean {
return this.getExperimentValue<boolean>(ExperimentFlags.JIT_CONTEXT);
}
isNumericalRoutingEnabled(): boolean {
return this.getExperimentValue<boolean>(
ExperimentFlags.ENABLE_NUMERICAL_ROUTING,
);
}
getClassifierThreshold(): number | undefined {
return this.getExperimentValue<number>(
ExperimentFlags.CLASSIFIER_THRESHOLD,
);
}
getUserCaching(): boolean {
return this.getExperimentValue<boolean>(ExperimentFlags.USER_CACHING);
}
getBannerTextNoCapacityIssues(): string {
return this.getExperimentValue<string>(
ExperimentFlags.BANNER_TEXT_NO_CAPACITY_ISSUES,
);
}
getBannerTextCapacityIssues(): string {
return this.getExperimentValue<string>(
ExperimentFlags.BANNER_TEXT_CAPACITY_ISSUES,
);
}
}
@@ -211,12 +211,9 @@ describe('NumericalClassifierStrategy', () => {
describe('Remote Threshold Logic', () => {
it('should use the remote CLASSIFIER_THRESHOLD if provided (int value)', async () => {
vi.mocked(mockConfig.getClassifierThreshold).mockResolvedValue(70);
<<<<<<< HEAD
vi.mocked(mockConfig.getResolvedClassifierThreshold).mockResolvedValue(
70,
);
=======
>>>>>>> b9035a18d (refactor(config): revert experiment getter names and async signatures)
const mockApiResponse = {
complexity_reasoning: 'Test task',
complexity_score: 60,
@@ -244,12 +241,9 @@ describe('NumericalClassifierStrategy', () => {
it('should use the remote CLASSIFIER_THRESHOLD if provided (float value)', async () => {
vi.mocked(mockConfig.getClassifierThreshold).mockResolvedValue(45.5);
<<<<<<< HEAD
vi.mocked(mockConfig.getResolvedClassifierThreshold).mockResolvedValue(
45.5,
);
=======
>>>>>>> b9035a18d (refactor(config): revert experiment getter names and async signatures)
const mockApiResponse = {
complexity_reasoning: 'Test task',
complexity_score: 40,
@@ -277,12 +271,9 @@ describe('NumericalClassifierStrategy', () => {
it('should use PRO model if score >= remote CLASSIFIER_THRESHOLD', async () => {
vi.mocked(mockConfig.getClassifierThreshold).mockResolvedValue(30);
<<<<<<< HEAD
vi.mocked(mockConfig.getResolvedClassifierThreshold).mockResolvedValue(
30,
);
=======
>>>>>>> b9035a18d (refactor(config): revert experiment getter names and async signatures)
const mockApiResponse = {
complexity_reasoning: 'Test task',
complexity_score: 35,
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import 'vitest';
import {
vi,
describe,
-8
View File
@@ -2877,7 +2877,6 @@
"default": false,
"type": "boolean"
},
<<<<<<< HEAD
"directWebFetch": {
"title": "Direct Web Fetch",
"description": "Enable web fetch behavior that bypasses LLM summarization.",
@@ -2960,13 +2959,6 @@
"markdownDescription": "Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"enable-awesome": {
"title": "Enable Awesome",
"description": "When enabled, the ASCII art says 'matt'.",
"markdownDescription": "When enabled, the ASCII art says 'matt'.\n\n- Category: `Experimental`\n- Requires restart: `no`\n- Default: `false`",
"default": false,
"type": "boolean"
}
},
"additionalProperties": false