From a84d4d876e50db6e39d8bdede9b6cb359af13584 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Fri, 27 Mar 2026 11:40:26 -0700 Subject: [PATCH 01/23] Increase memory limited for eslint. (#24022) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73ebef63fd..8bb5f25e20 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "test:integration:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests", "test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && cross-env GEMINI_SANDBOX=docker vitest run --root ./integration-tests", "test:integration:sandbox:podman": "cross-env GEMINI_SANDBOX=podman vitest run --root ./integration-tests", - "lint": "eslint . --cache --max-warnings 0", + "lint": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" eslint . --cache --max-warnings 0", "lint:fix": "eslint . --fix --ext .ts,.tsx && eslint integration-tests --fix && eslint scripts --fix && npm run format", "lint:ci": "npm run lint:all", "lint:all": "node scripts/lint.js", From e7dccabf1477fb9301dce9a2154f161bb3ed2da1 Mon Sep 17 00:00:00 2001 From: Sri Pasumarthi <111310667+sripasg@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:49:13 -0700 Subject: [PATCH 02/23] fix(acp): prevent crash on empty response in ACP mode (#23952) --- packages/cli/src/acp/acpClient.test.ts | 27 ++++++++++++++++++++++ packages/cli/src/acp/acpClient.ts | 32 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/packages/cli/src/acp/acpClient.test.ts b/packages/cli/src/acp/acpClient.test.ts index 9e4b89ea20..14295954dd 100644 --- a/packages/cli/src/acp/acpClient.test.ts +++ b/packages/cli/src/acp/acpClient.test.ts @@ -28,6 +28,7 @@ import { LlmRole, type GitService, processSingleFileContent, + InvalidStreamError, } from '@google/gemini-cli-core'; import { SettingScope, @@ -785,6 +786,32 @@ describe('Session', () => { expect(result).toMatchObject({ stopReason: 'end_turn' }); }); + it('should handle prompt with empty response (InvalidStreamError)', async () => { + mockChat.sendMessageStream.mockRejectedValue( + new InvalidStreamError('Empty response', 'NO_RESPONSE_TEXT'), + ); + + const result = await session.prompt({ + sessionId: 'session-1', + prompt: [{ type: 'text', text: 'Hi' }], + }); + + expect(mockChat.sendMessageStream).toHaveBeenCalled(); + expect(result).toMatchObject({ stopReason: 'end_turn' }); + }); + + it('should handle prompt with empty response (NO_RESPONSE_TEXT anomaly)', async () => { + mockChat.sendMessageStream.mockRejectedValue({ type: 'NO_RESPONSE_TEXT' }); + + const result = await session.prompt({ + sessionId: 'session-1', + prompt: [{ type: 'text', text: 'Hi' }], + }); + + expect(mockChat.sendMessageStream).toHaveBeenCalled(); + expect(result).toMatchObject({ stopReason: 'end_turn' }); + }); + it('should handle /memory command', async () => { const handleCommandSpy = vi .spyOn( diff --git a/packages/cli/src/acp/acpClient.ts b/packages/cli/src/acp/acpClient.ts index 59c6cb2b3f..6b76ffdc7a 100644 --- a/packages/cli/src/acp/acpClient.ts +++ b/packages/cli/src/acp/acpClient.ts @@ -48,6 +48,7 @@ import { PREVIEW_GEMINI_MODEL_AUTO, getDisplayString, processSingleFileContent, + InvalidStreamError, type AgentLoopContext, updatePolicy, } from '@google/gemini-cli-core'; @@ -851,6 +852,37 @@ export class Session { return { stopReason: CoreToolCallStatus.Cancelled }; } + if ( + error instanceof InvalidStreamError || + (error && + typeof error === 'object' && + 'type' in error && + error.type === 'NO_RESPONSE_TEXT') + ) { + // The stream ended with an empty response or malformed tool call. + // Treat this as a graceful end to the model's turn rather than a crash. + return { + stopReason: 'end_turn', + _meta: { + quota: { + token_count: { + input_tokens: totalInputTokens, + output_tokens: totalOutputTokens, + }, + model_usage: Array.from(modelUsageMap.entries()).map( + ([modelName, counts]) => ({ + model: modelName, + token_count: { + input_tokens: counts.input, + output_tokens: counts.output, + }, + }), + ), + }, + }, + }; + } + throw new acp.RequestError( getErrorStatus(error) || 500, getAcpErrorMessage(error), From 320c8aba4ce19d448657f9f564dace10e3b6fb49 Mon Sep 17 00:00:00 2001 From: joshualitt Date: Fri, 27 Mar 2026 12:22:35 -0700 Subject: [PATCH 03/23] feat(core): Land `AgentHistoryProvider`. (#23978) --- docs/cli/settings.md | 26 +-- docs/reference/configuration.md | 27 +++ .../a2a-server/src/utils/testing_utils.ts | 6 + packages/cli/src/config/config.ts | 8 + packages/cli/src/config/settingsSchema.ts | 40 ++++ packages/core/src/config/config.ts | 32 +++ .../core/src/config/defaultModelConfigs.ts | 5 + packages/core/src/core/client.test.ts | 47 +++++ packages/core/src/core/client.ts | 24 ++- .../agentHistoryProvider.test.ts.snap | 17 ++ .../src/services/agentHistoryProvider.test.ts | 138 +++++++++++++ .../core/src/services/agentHistoryProvider.ts | 185 ++++++++++++++++++ .../resolved-aliases-retry.golden.json | 4 + .../test-data/resolved-aliases.golden.json | 4 + packages/core/src/telemetry/loggers.test.ts | 5 +- schemas/settings.schema.json | 42 +++- 16 files changed, 593 insertions(+), 17 deletions(-) create mode 100644 packages/core/src/services/__snapshots__/agentHistoryProvider.test.ts.snap create mode 100644 packages/core/src/services/agentHistoryProvider.test.ts create mode 100644 packages/core/src/services/agentHistoryProvider.ts diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 5f432b8c8d..ac1fdc98fc 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -155,17 +155,21 @@ they appear in the UI. ### Experimental -| UI Label | Setting | Description | Default | -| -------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` | -| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` | -| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` | -| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` | -| Plan | `experimental.plan` | Enable Plan Mode. | `true` | -| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` | -| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` | -| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` | -| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` | +| UI Label | Setting | Description | Default | +| ---------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` | +| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` | +| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` | +| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` | +| Plan | `experimental.plan` | Enable Plan Mode. | `true` | +| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` | +| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` | +| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` | +| Agent History Truncation | `experimental.agentHistoryTruncation` | Enable truncation window logic for the Agent History Provider. | `false` | +| Agent History Truncation Threshold | `experimental.agentHistoryTruncationThreshold` | The maximum number of messages before history is truncated. | `30` | +| Agent History Retained Messages | `experimental.agentHistoryRetainedMessages` | The number of recent messages to retain after truncation. | `15` | +| Agent History Summarization | `experimental.agentHistorySummarization` | Enable summarization of truncated content via a small model for the Agent History Provider. | `false` | +| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` | ### Skills diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 8be2ede444..786691882c 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -670,6 +670,11 @@ their corresponding top-level category object in your `settings.json` file. "modelConfig": { "model": "gemini-3-pro-preview" } + }, + "agent-history-provider-summarizer": { + "modelConfig": { + "model": "gemini-3-flash-preview" + } } } ``` @@ -1677,6 +1682,28 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `false` - **Requires restart:** Yes +- **`experimental.agentHistoryTruncation`** (boolean): + - **Description:** Enable truncation window logic for the Agent History + Provider. + - **Default:** `false` + - **Requires restart:** Yes + +- **`experimental.agentHistoryTruncationThreshold`** (number): + - **Description:** The maximum number of messages before history is truncated. + - **Default:** `30` + - **Requires restart:** Yes + +- **`experimental.agentHistoryRetainedMessages`** (number): + - **Description:** The number of recent messages to retain after truncation. + - **Default:** `15` + - **Requires restart:** Yes + +- **`experimental.agentHistorySummarization`** (boolean): + - **Description:** Enable summarization of truncated content via a small model + for the Agent History Provider. + - **Default:** `false` + - **Requires restart:** Yes + - **`experimental.topicUpdateNarration`** (boolean): - **Description:** Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. diff --git a/packages/a2a-server/src/utils/testing_utils.ts b/packages/a2a-server/src/utils/testing_utils.ts index 8181f702f1..f7f1645f8c 100644 --- a/packages/a2a-server/src/utils/testing_utils.ts +++ b/packages/a2a-server/src/utils/testing_utils.ts @@ -109,6 +109,12 @@ export function createMockConfig( enableEnvironmentVariableRedaction: false, }, }), + isExperimentalAgentHistoryTruncationEnabled: vi.fn().mockReturnValue(false), + getExperimentalAgentHistoryTruncationThreshold: vi.fn().mockReturnValue(50), + getExperimentalAgentHistoryRetainedMessages: vi.fn().mockReturnValue(30), + isExperimentalAgentHistorySummarizationEnabled: vi + .fn() + .mockReturnValue(false), ...overrides, } as unknown as Config; diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index af8c1ae0ac..ec14eced75 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -975,6 +975,14 @@ export async function loadCliConfig( disabledSkills: settings.skills?.disabled, experimentalJitContext: settings.experimental?.jitContext, experimentalMemoryManager: settings.experimental?.memoryManager, + experimentalAgentHistoryTruncation: + settings.experimental?.agentHistoryTruncation, + experimentalAgentHistoryTruncationThreshold: + settings.experimental?.agentHistoryTruncationThreshold, + experimentalAgentHistoryRetainedMessages: + settings.experimental?.agentHistoryRetainedMessages, + experimentalAgentHistorySummarization: + settings.experimental?.agentHistorySummarization, modelSteering: settings.experimental?.modelSteering, topicUpdateNarration: settings.experimental?.topicUpdateNarration, toolOutputMasking: settings.experimental?.toolOutputMasking, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index aec521317c..db38cf598c 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -2141,6 +2141,46 @@ const SETTINGS_SCHEMA = { 'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.', showInDialog: true, }, + agentHistoryTruncation: { + type: 'boolean', + label: 'Agent History Truncation', + category: 'Experimental', + requiresRestart: true, + default: false, + description: + 'Enable truncation window logic for the Agent History Provider.', + showInDialog: true, + }, + agentHistoryTruncationThreshold: { + type: 'number', + label: 'Agent History Truncation Threshold', + category: 'Experimental', + requiresRestart: true, + default: 30, + description: + 'The maximum number of messages before history is truncated.', + showInDialog: true, + }, + agentHistoryRetainedMessages: { + type: 'number', + label: 'Agent History Retained Messages', + category: 'Experimental', + requiresRestart: true, + default: 15, + description: + 'The number of recent messages to retain after truncation.', + showInDialog: true, + }, + agentHistorySummarization: { + type: 'boolean', + label: 'Agent History Summarization', + category: 'Experimental', + requiresRestart: true, + default: false, + description: + 'Enable summarization of truncated content via a small model for the Agent History Provider.', + showInDialog: true, + }, topicUpdateNarration: { type: 'boolean', label: 'Topic & Update Narration', diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 16e7cbf59e..69ffc2c507 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -681,6 +681,10 @@ export interface ConfigParameters { adminSkillsEnabled?: boolean; experimentalJitContext?: boolean; experimentalMemoryManager?: boolean; + experimentalAgentHistoryTruncation?: boolean; + experimentalAgentHistoryTruncationThreshold?: number; + experimentalAgentHistoryRetainedMessages?: number; + experimentalAgentHistorySummarization?: boolean; topicUpdateNarration?: boolean; toolOutputMasking?: Partial; disableLLMCorrection?: boolean; @@ -909,6 +913,10 @@ export class Config implements McpContext, AgentLoopContext { private readonly experimentalJitContext: boolean; private readonly experimentalMemoryManager: boolean; + private readonly experimentalAgentHistoryTruncation: boolean; + private readonly experimentalAgentHistoryTruncationThreshold: number; + private readonly experimentalAgentHistoryRetainedMessages: number; + private readonly experimentalAgentHistorySummarization: boolean; private readonly topicUpdateNarration: boolean; private readonly disableLLMCorrection: boolean; private readonly planEnabled: boolean; @@ -1118,6 +1126,14 @@ export class Config implements McpContext, AgentLoopContext { this.experimentalJitContext = params.experimentalJitContext ?? true; this.experimentalMemoryManager = params.experimentalMemoryManager ?? false; + this.experimentalAgentHistoryTruncation = + params.experimentalAgentHistoryTruncation ?? false; + this.experimentalAgentHistoryTruncationThreshold = + params.experimentalAgentHistoryTruncationThreshold ?? 30; + this.experimentalAgentHistoryRetainedMessages = + params.experimentalAgentHistoryRetainedMessages ?? 15; + this.experimentalAgentHistorySummarization = + params.experimentalAgentHistorySummarization ?? false; this.topicUpdateNarration = params.topicUpdateNarration ?? false; this.modelSteering = params.modelSteering ?? false; this.injectionService = new InjectionService(() => @@ -2298,6 +2314,22 @@ export class Config implements McpContext, AgentLoopContext { return this.experimentalMemoryManager; } + isExperimentalAgentHistoryTruncationEnabled(): boolean { + return this.experimentalAgentHistoryTruncation; + } + + getExperimentalAgentHistoryTruncationThreshold(): number { + return this.experimentalAgentHistoryTruncationThreshold; + } + + getExperimentalAgentHistoryRetainedMessages(): number { + return this.experimentalAgentHistoryRetainedMessages; + } + + isExperimentalAgentHistorySummarizationEnabled(): boolean { + return this.experimentalAgentHistorySummarization; + } + isTopicUpdateNarrationEnabled(): boolean { return this.topicUpdateNarration; } diff --git a/packages/core/src/config/defaultModelConfigs.ts b/packages/core/src/config/defaultModelConfigs.ts index 62357aa733..84c2478a5f 100644 --- a/packages/core/src/config/defaultModelConfigs.ts +++ b/packages/core/src/config/defaultModelConfigs.ts @@ -243,6 +243,11 @@ export const DEFAULT_MODEL_CONFIGS: ModelConfigServiceConfig = { model: 'gemini-3-pro-preview', }, }, + 'agent-history-provider-summarizer': { + modelConfig: { + model: 'gemini-3-flash-preview', + }, + }, }, overrides: [ { diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index e93eedf055..e741092ce9 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -279,6 +279,16 @@ describe('Gemini Client (client.ts)', () => { getActiveModel: vi.fn().mockReturnValue('test-model'), setActiveModel: vi.fn(), resetTurn: vi.fn(), + isExperimentalAgentHistoryTruncationEnabled: vi + .fn() + .mockReturnValue(false), + getExperimentalAgentHistoryTruncationThreshold: vi + .fn() + .mockReturnValue(30), + getExperimentalAgentHistoryRetainedMessages: vi.fn().mockReturnValue(15), + isExperimentalAgentHistorySummarizationEnabled: vi + .fn() + .mockReturnValue(false), getModelAvailabilityService: vi .fn() .mockReturnValue(createAvailabilityServiceMock()), @@ -704,6 +714,43 @@ describe('Gemini Client (client.ts)', () => { }); describe('sendMessageStream', () => { + it('calls AgentHistoryProvider.manageHistory when history truncation is enabled', async () => { + // Arrange + mockConfig.isExperimentalAgentHistoryTruncationEnabled = vi + .fn() + .mockReturnValue(true); + const manageHistorySpy = vi + .spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (client as any).agentHistoryProvider, + 'manageHistory', + ) + .mockResolvedValue([ + { role: 'user', parts: [{ text: 'preserved message' }] }, + ]); + + mockTurnRunFn.mockReturnValue( + (async function* () { + yield { type: 'content', value: 'Hello' }; + })(), + ); + + // Act + const stream = client.sendMessageStream( + [{ text: 'Hi' }], + new AbortController().signal, + 'prompt-id-1', + ); + + await fromAsync(stream); + + // Assert + expect(manageHistorySpy).toHaveBeenCalledWith( + expect.any(Array), + expect.any(AbortSignal), + ); + }); + it('emits a compression event when the context was automatically compressed', async () => { // Arrange mockTurnRunFn.mockReturnValue( diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 8922c977f2..42adab3a05 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -44,6 +44,7 @@ import type { import type { ContentGenerator } from './contentGenerator.js'; import { LoopDetectionService } from '../services/loopDetectionService.js'; import { ChatCompressionService } from '../services/chatCompressionService.js'; +import { AgentHistoryProvider } from '../services/agentHistoryProvider.js'; import { ideContextStore } from '../ide/ideContext.js'; import { logContentRetryFailure, @@ -98,6 +99,7 @@ export class GeminiClient { private readonly loopDetector: LoopDetectionService; private readonly compressionService: ChatCompressionService; + private readonly agentHistoryProvider: AgentHistoryProvider; private readonly toolOutputMaskingService: ToolOutputMaskingService; private lastPromptId: string; private currentSequenceModel: string | null = null; @@ -113,6 +115,12 @@ export class GeminiClient { constructor(private readonly context: AgentLoopContext) { this.loopDetector = new LoopDetectionService(this.config); this.compressionService = new ChatCompressionService(); + this.agentHistoryProvider = new AgentHistoryProvider(this.config, { + truncationThreshold: + this.config.getExperimentalAgentHistoryTruncationThreshold(), + retainedMessages: + this.config.getExperimentalAgentHistoryRetainedMessages(), + }); this.toolOutputMaskingService = new ToolOutputMaskingService(); this.lastPromptId = this.config.getSessionId(); @@ -613,10 +621,20 @@ export class GeminiClient { // Check for context window overflow const modelForLimitCheck = this._getActiveModelForCurrentTurn(); - const compressed = await this.tryCompressChat(prompt_id, false, signal); + if (this.config.isExperimentalAgentHistoryTruncationEnabled()) { + const newHistory = await this.agentHistoryProvider.manageHistory( + this.getHistory(), + signal, + ); + if (newHistory.length !== this.getHistory().length) { + this.getChat().setHistory(newHistory); + } + } else { + const compressed = await this.tryCompressChat(prompt_id, false, signal); - if (compressed.compressionStatus === CompressionStatus.COMPRESSED) { - yield { type: GeminiEventType.ChatCompressed, value: compressed }; + if (compressed.compressionStatus === CompressionStatus.COMPRESSED) { + yield { type: GeminiEventType.ChatCompressed, value: compressed }; + } } const remainingTokenCount = diff --git a/packages/core/src/services/__snapshots__/agentHistoryProvider.test.ts.snap b/packages/core/src/services/__snapshots__/agentHistoryProvider.test.ts.snap new file mode 100644 index 0000000000..af7990ad52 --- /dev/null +++ b/packages/core/src/services/__snapshots__/agentHistoryProvider.test.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`AgentHistoryProvider > should handle summarizer failures gracefully 1`] = ` +{ + "parts": [ + { + "text": "[System Note: Prior conversation history was truncated. The most recent user message before truncation was:] + +Message 18", + }, + { + "text": "Message 20", + }, + ], + "role": "user", +} +`; diff --git a/packages/core/src/services/agentHistoryProvider.test.ts b/packages/core/src/services/agentHistoryProvider.test.ts new file mode 100644 index 0000000000..7906398bb9 --- /dev/null +++ b/packages/core/src/services/agentHistoryProvider.test.ts @@ -0,0 +1,138 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { AgentHistoryProvider } from './agentHistoryProvider.js'; +import type { Content, GenerateContentResponse } from '@google/genai'; +import type { Config } from '../config/config.js'; +import type { BaseLlmClient } from '../core/baseLlmClient.js'; + +describe('AgentHistoryProvider', () => { + let config: Config; + let provider: AgentHistoryProvider; + let generateContentMock: ReturnType; + + beforeEach(() => { + config = { + isExperimentalAgentHistoryTruncationEnabled: vi + .fn() + .mockReturnValue(false), + isExperimentalAgentHistorySummarizationEnabled: vi + .fn() + .mockReturnValue(false), + getBaseLlmClient: vi.fn(), + } as unknown as Config; + + generateContentMock = vi.fn().mockResolvedValue({ + candidates: [{ content: { parts: [{ text: 'Mock intent summary' }] } }], + } as unknown as GenerateContentResponse); + + config.getBaseLlmClient = vi.fn().mockReturnValue({ + generateContent: generateContentMock, + } as unknown as BaseLlmClient); + + provider = new AgentHistoryProvider(config, { + truncationThreshold: 30, + retainedMessages: 15, + }); + }); + + const createMockHistory = (count: number): Content[] => + Array.from({ length: count }).map((_, i) => ({ + role: i % 2 === 0 ? 'user' : 'model', + parts: [{ text: `Message ${i}` }], + })); + + it('should return history unchanged if truncation is disabled', async () => { + vi.spyOn( + config, + 'isExperimentalAgentHistoryTruncationEnabled', + ).mockReturnValue(false); + + const history = createMockHistory(40); + const result = await provider.manageHistory(history); + + expect(result).toBe(history); + expect(result.length).toBe(40); + }); + + it('should return history unchanged if length is under threshold', async () => { + vi.spyOn( + config, + 'isExperimentalAgentHistoryTruncationEnabled', + ).mockReturnValue(true); + + const history = createMockHistory(20); // Threshold is 30 + const result = await provider.manageHistory(history); + + expect(result).toBe(history); + expect(result.length).toBe(20); + }); + + it('should truncate mechanically to RETAINED_MESSAGES without summarization when sum flag is off', async () => { + vi.spyOn( + config, + 'isExperimentalAgentHistoryTruncationEnabled', + ).mockReturnValue(true); + vi.spyOn( + config, + 'isExperimentalAgentHistorySummarizationEnabled', + ).mockReturnValue(false); + + const history = createMockHistory(35); // Above 30 threshold, should truncate to 15 + const result = await provider.manageHistory(history); + + expect(result.length).toBe(15); + expect(generateContentMock).not.toHaveBeenCalled(); + + // Check fallback message logic + // Messages 20 to 34 are retained. Message 20 is 'user'. + expect(result[0].role).toBe('user'); + expect(result[0].parts![0].text).toContain( + 'System Note: Prior conversation history was truncated', + ); + }); + + it('should call summarizer and prepend summary when summarization is enabled', async () => { + vi.spyOn( + config, + 'isExperimentalAgentHistoryTruncationEnabled', + ).mockReturnValue(true); + vi.spyOn( + config, + 'isExperimentalAgentHistorySummarizationEnabled', + ).mockReturnValue(true); + + const history = createMockHistory(35); + const result = await provider.manageHistory(history); + + expect(generateContentMock).toHaveBeenCalled(); + expect(result.length).toBe(15); // retained messages + expect(result[0].role).toBe('user'); + expect(result[0].parts![0].text).toContain(''); + expect(result[0].parts![0].text).toContain('Mock intent summary'); + }); + + it('should handle summarizer failures gracefully', async () => { + vi.spyOn( + config, + 'isExperimentalAgentHistoryTruncationEnabled', + ).mockReturnValue(true); + vi.spyOn( + config, + 'isExperimentalAgentHistorySummarizationEnabled', + ).mockReturnValue(true); + + generateContentMock.mockRejectedValue(new Error('API Error')); + + const history = createMockHistory(35); + const result = await provider.manageHistory(history); + + expect(generateContentMock).toHaveBeenCalled(); + expect(result.length).toBe(15); + expect(result[0]).toMatchSnapshot(); + }); +}); diff --git a/packages/core/src/services/agentHistoryProvider.ts b/packages/core/src/services/agentHistoryProvider.ts new file mode 100644 index 0000000000..fa9f23d437 --- /dev/null +++ b/packages/core/src/services/agentHistoryProvider.ts @@ -0,0 +1,185 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Content } from '@google/genai'; +import type { Config } from '../config/config.js'; +import { getResponseText } from '../utils/partUtils.js'; +import { LlmRole } from '../telemetry/llmRole.js'; +import { debugLogger } from '../utils/debugLogger.js'; + +export interface AgentHistoryProviderConfig { + truncationThreshold: number; + retainedMessages: number; +} + +export class AgentHistoryProvider { + constructor( + private readonly config: Config, + private readonly providerConfig: AgentHistoryProviderConfig, + ) {} + + /** + * Evaluates the chat history and performs truncation and summarization if necessary. + * Returns a new array of Content if truncation occurred, otherwise returns the original array. + */ + async manageHistory( + history: readonly Content[], + abortSignal?: AbortSignal, + ): Promise { + if (!this.shouldTruncate(history)) { + return history; + } + + const { messagesToKeep, messagesToTruncate } = + this.splitHistoryForTruncation(history); + + debugLogger.log( + `AgentHistoryProvider: Truncating ${messagesToTruncate.length} messages, retaining ${messagesToKeep.length} messages.`, + ); + + const summaryText = await this.getSummaryText( + messagesToTruncate, + abortSignal, + ); + + return this.mergeSummaryWithHistory(summaryText, messagesToKeep); + } + + private shouldTruncate(history: readonly Content[]): boolean { + if (!this.config.isExperimentalAgentHistoryTruncationEnabled()) { + return false; + } + return history.length > this.providerConfig.truncationThreshold; + } + + private splitHistoryForTruncation(history: readonly Content[]): { + messagesToKeep: readonly Content[]; + messagesToTruncate: readonly Content[]; + } { + return { + messagesToKeep: history.slice(-this.providerConfig.retainedMessages), + messagesToTruncate: history.slice( + 0, + history.length - this.providerConfig.retainedMessages, + ), + }; + } + + private getFallbackSummaryText( + messagesToTruncate: readonly Content[], + ): string { + const defaultNote = + 'System Note: Prior conversation history was truncated to maintain performance and focus. Important context should have been saved to memory.'; + + let lastUserText = ''; + for (let i = messagesToTruncate.length - 1; i >= 0; i--) { + const msg = messagesToTruncate[i]; + if (msg.role === 'user') { + lastUserText = + msg.parts + ?.map((p) => p.text || '') + .join('') + .trim() || ''; + if (lastUserText) { + break; + } + } + } + + if (lastUserText) { + return `[System Note: Prior conversation history was truncated. The most recent user message before truncation was:]\n\n${lastUserText}`; + } + + return defaultNote; + } + + private async getSummaryText( + messagesToTruncate: readonly Content[], + abortSignal?: AbortSignal, + ): Promise { + if (!this.config.isExperimentalAgentHistorySummarizationEnabled()) { + debugLogger.log( + 'AgentHistoryProvider: Summarization disabled, using fallback note.', + ); + return this.getFallbackSummaryText(messagesToTruncate); + } + + try { + const summary = await this.generateIntentSummary( + messagesToTruncate, + abortSignal, + ); + debugLogger.log('AgentHistoryProvider: Summarization successful.'); + return summary; + } catch (error) { + debugLogger.log('AgentHistoryProvider: Summarization failed.', error); + return this.getFallbackSummaryText(messagesToTruncate); + } + } + + private mergeSummaryWithHistory( + summaryText: string, + messagesToKeep: readonly Content[], + ): readonly Content[] { + if (messagesToKeep.length === 0) { + return [{ role: 'user', parts: [{ text: summaryText }] }]; + } + + // To ensure strict user/model alternating roles required by the Gemini API, + // we merge the summary into the first retained message if it's from the 'user'. + const firstRetainedMessage = messagesToKeep[0]; + if (firstRetainedMessage.role === 'user') { + const mergedParts = [ + { text: summaryText }, + ...(firstRetainedMessage.parts || []), + ]; + const mergedMessage: Content = { + role: 'user', + parts: mergedParts, + }; + return [mergedMessage, ...messagesToKeep.slice(1)]; + } else { + const summaryMessage: Content = { + role: 'user', + parts: [{ text: summaryText }], + }; + return [summaryMessage, ...messagesToKeep]; + } + } + + private async generateIntentSummary( + messagesToTruncate: readonly Content[], + abortSignal?: AbortSignal, + ): Promise { + const prompt = `Create a succinct, agent-continuity focused intent summary of the truncated conversation history. +Distill the essence of the ongoing work by capturing: +- The Original Mandate: What the user (or calling agent) originally requested and why. +- The Agent's Strategy: How you (the agent) are approaching the task and where the work is taking place (e.g., specific files, directories, or architectural layers). +- Evolving Context: Any significant shifts in the user's intent or the agent's technical approach over the course of the truncated history. + +Write this summary to orient the active agent. Do NOT predict next steps or summarize the current task state, as those are covered by the active history. Focus purely on foundational context and strategic continuity.`; + + const summaryResponse = await this.config + .getBaseLlmClient() + .generateContent({ + modelConfigKey: { model: 'agent-history-provider-summarizer' }, + contents: [ + ...messagesToTruncate, + { + role: 'user', + parts: [{ text: prompt }], + }, + ], + promptId: 'agent-history-provider', + abortSignal: abortSignal ?? new AbortController().signal, + role: LlmRole.UTILITY_COMPRESSOR, + }); + + let summary = getResponseText(summaryResponse) ?? ''; + summary = summary.replace(/<\/?intent_summary>/g, '').trim(); + return `\n${summary}\n`; + } +} diff --git a/packages/core/src/services/test-data/resolved-aliases-retry.golden.json b/packages/core/src/services/test-data/resolved-aliases-retry.golden.json index 52e2eb7722..33e9ce684b 100644 --- a/packages/core/src/services/test-data/resolved-aliases-retry.golden.json +++ b/packages/core/src/services/test-data/resolved-aliases-retry.golden.json @@ -256,5 +256,9 @@ "chat-compression-default": { "model": "gemini-3-pro-preview", "generateContentConfig": {} + }, + "agent-history-provider-summarizer": { + "model": "gemini-3-flash-preview", + "generateContentConfig": {} } } diff --git a/packages/core/src/services/test-data/resolved-aliases.golden.json b/packages/core/src/services/test-data/resolved-aliases.golden.json index 52e2eb7722..33e9ce684b 100644 --- a/packages/core/src/services/test-data/resolved-aliases.golden.json +++ b/packages/core/src/services/test-data/resolved-aliases.golden.json @@ -256,5 +256,9 @@ "chat-compression-default": { "model": "gemini-3-pro-preview", "generateContentConfig": {} + }, + "agent-history-provider-summarizer": { + "model": "gemini-3-flash-preview", + "generateContentConfig": {} } } diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index 48b7792168..b21fc606e2 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -1151,8 +1151,11 @@ describe('loggers', () => { getQuestion: () => 'test-question', getToolRegistry: () => new ToolRegistry(cfg1, {} as unknown as MessageBus), - getUserMemory: () => 'user-memory', + isExperimentalAgentHistoryTruncationEnabled: () => false, + getExperimentalAgentHistoryTruncationThreshold: () => 30, + getExperimentalAgentHistoryRetainedMessages: () => 15, + isExperimentalAgentHistorySummarizationEnabled: () => false, } as unknown as Config; (cfg2 as unknown as { config: Config; promptId: string }).config = cfg2; diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 74988cb240..f805d243cc 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -636,7 +636,7 @@ "modelConfigs": { "title": "Model Configs", "description": "Model configurations.", - "markdownDescription": "Model configurations.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `{\n \"aliases\": {\n \"base\": {\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"temperature\": 0,\n \"topP\": 1\n }\n }\n },\n \"chat-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"includeThoughts\": true\n },\n \"temperature\": 1,\n \"topP\": 0.95,\n \"topK\": 64\n }\n }\n },\n \"chat-base-2.5\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 8192\n }\n }\n }\n },\n \"chat-base-3\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingLevel\": \"HIGH\"\n }\n }\n }\n },\n \"gemini-3-pro-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"gemini-3-flash-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"gemini-2.5-pro\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"gemini-2.5-flash\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-2.5-flash-lite\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"gemini-2.5-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-3-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"classifier\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 1024,\n \"thinkingConfig\": {\n \"thinkingBudget\": 512\n }\n }\n }\n },\n \"prompt-completion\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.3,\n \"maxOutputTokens\": 16000,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"fast-ack-helper\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.2,\n \"maxOutputTokens\": 120,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"edit-corrector\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"summarizer-default\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"summarizer-shell\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"web-search\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"googleSearch\": {}\n }\n ]\n }\n }\n },\n \"web-fetch\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"urlContext\": {}\n }\n ]\n }\n }\n },\n \"web-fetch-fallback\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection-double-check\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"llm-edit-fixer\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"next-speaker-checker\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"chat-compression-3-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"chat-compression-3-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"chat-compression-3.1-flash-lite\": {\n \"modelConfig\": {\n \"model\": \"gemini-3.1-flash-lite-preview\"\n }\n },\n \"chat-compression-2.5-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"chat-compression-2.5-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"chat-compression-2.5-flash-lite\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"chat-compression-default\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n }\n },\n \"overrides\": [\n {\n \"match\": {\n \"model\": \"chat-base\",\n \"isRetry\": true\n },\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"temperature\": 1\n }\n }\n }\n ],\n \"modelDefinitions\": {\n \"gemini-3.1-flash-lite-preview\": {\n \"tier\": \"flash-lite\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3.1-pro-preview\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3.1-pro-preview-customtools\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"isVisible\": false,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3-pro-preview\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3-flash-preview\": {\n \"tier\": \"flash\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-2.5-pro\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"gemini-2.5-flash\": {\n \"tier\": \"flash\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"gemini-2.5-flash-lite\": {\n \"tier\": \"flash-lite\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"auto\": {\n \"tier\": \"auto\",\n \"isPreview\": true,\n \"isVisible\": false,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"pro\": {\n \"tier\": \"pro\",\n \"isPreview\": false,\n \"isVisible\": false,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"flash\": {\n \"tier\": \"flash\",\n \"isPreview\": false,\n \"isVisible\": false,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"flash-lite\": {\n \"tier\": \"flash-lite\",\n \"isPreview\": false,\n \"isVisible\": false,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"auto-gemini-3\": {\n \"displayName\": \"Auto (Gemini 3)\",\n \"tier\": \"auto\",\n \"isPreview\": true,\n \"isVisible\": true,\n \"dialogDescription\": \"Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash\",\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"auto-gemini-2.5\": {\n \"displayName\": \"Auto (Gemini 2.5)\",\n \"tier\": \"auto\",\n \"isPreview\": false,\n \"isVisible\": true,\n \"dialogDescription\": \"Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n }\n },\n \"modelIdResolutions\": {\n \"gemini-3.1-pro-preview\": {\n \"default\": \"gemini-3.1-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n }\n ]\n },\n \"gemini-3.1-pro-preview-customtools\": {\n \"default\": \"gemini-3.1-pro-preview-customtools\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n }\n ]\n },\n \"gemini-3-flash-preview\": {\n \"default\": \"gemini-3-flash-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-flash\"\n }\n ]\n },\n \"gemini-3-pro-preview\": {\n \"default\": \"gemini-3-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true,\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true\n },\n \"target\": \"gemini-3.1-pro-preview\"\n }\n ]\n },\n \"auto-gemini-3\": {\n \"default\": \"gemini-3-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true,\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true\n },\n \"target\": \"gemini-3.1-pro-preview\"\n }\n ]\n },\n \"auto\": {\n \"default\": \"gemini-3-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true,\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true\n },\n \"target\": \"gemini-3.1-pro-preview\"\n }\n ]\n },\n \"pro\": {\n \"default\": \"gemini-3-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true,\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true\n },\n \"target\": \"gemini-3.1-pro-preview\"\n }\n ]\n },\n \"auto-gemini-2.5\": {\n \"default\": \"gemini-2.5-pro\"\n },\n \"gemini-3.1-flash-lite-preview\": {\n \"default\": \"gemini-3.1-flash-lite-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"useGemini3_1FlashLite\": false\n },\n \"target\": \"gemini-2.5-flash-lite\"\n }\n ]\n },\n \"flash\": {\n \"default\": \"gemini-3-flash-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-flash\"\n }\n ]\n },\n \"flash-lite\": {\n \"default\": \"gemini-2.5-flash-lite\",\n \"contexts\": [\n {\n \"condition\": {\n \"useGemini3_1FlashLite\": true\n },\n \"target\": \"gemini-3.1-flash-lite-preview\"\n }\n ]\n }\n },\n \"classifierIdResolutions\": {\n \"flash\": {\n \"default\": \"gemini-3-flash-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"requestedModels\": [\n \"auto-gemini-2.5\",\n \"gemini-2.5-pro\"\n ]\n },\n \"target\": \"gemini-2.5-flash\"\n },\n {\n \"condition\": {\n \"requestedModels\": [\n \"auto-gemini-3\",\n \"gemini-3-pro-preview\"\n ]\n },\n \"target\": \"gemini-3-flash-preview\"\n }\n ]\n },\n \"pro\": {\n \"default\": \"gemini-3-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"requestedModels\": [\n \"auto-gemini-2.5\",\n \"gemini-2.5-pro\"\n ]\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true,\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true\n },\n \"target\": \"gemini-3.1-pro-preview\"\n }\n ]\n }\n },\n \"modelChains\": {\n \"preview\": [\n {\n \"model\": \"gemini-3-pro-preview\",\n \"actions\": {\n \"terminal\": \"prompt\",\n \"transient\": \"prompt\",\n \"not_found\": \"prompt\",\n \"unknown\": \"prompt\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n },\n {\n \"model\": \"gemini-3-flash-preview\",\n \"isLastResort\": true,\n \"actions\": {\n \"terminal\": \"prompt\",\n \"transient\": \"prompt\",\n \"not_found\": \"prompt\",\n \"unknown\": \"prompt\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n }\n ],\n \"default\": [\n {\n \"model\": \"gemini-2.5-pro\",\n \"actions\": {\n \"terminal\": \"prompt\",\n \"transient\": \"prompt\",\n \"not_found\": \"prompt\",\n \"unknown\": \"prompt\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n },\n {\n \"model\": \"gemini-2.5-flash\",\n \"isLastResort\": true,\n \"actions\": {\n \"terminal\": \"prompt\",\n \"transient\": \"prompt\",\n \"not_found\": \"prompt\",\n \"unknown\": \"prompt\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n }\n ],\n \"lite\": [\n {\n \"model\": \"gemini-2.5-flash-lite\",\n \"actions\": {\n \"terminal\": \"silent\",\n \"transient\": \"silent\",\n \"not_found\": \"silent\",\n \"unknown\": \"silent\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n },\n {\n \"model\": \"gemini-2.5-flash\",\n \"actions\": {\n \"terminal\": \"silent\",\n \"transient\": \"silent\",\n \"not_found\": \"silent\",\n \"unknown\": \"silent\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n },\n {\n \"model\": \"gemini-2.5-pro\",\n \"isLastResort\": true,\n \"actions\": {\n \"terminal\": \"silent\",\n \"transient\": \"silent\",\n \"not_found\": \"silent\",\n \"unknown\": \"silent\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n }\n ]\n }\n}`", + "markdownDescription": "Model configurations.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `{\n \"aliases\": {\n \"base\": {\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"temperature\": 0,\n \"topP\": 1\n }\n }\n },\n \"chat-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"includeThoughts\": true\n },\n \"temperature\": 1,\n \"topP\": 0.95,\n \"topK\": 64\n }\n }\n },\n \"chat-base-2.5\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 8192\n }\n }\n }\n },\n \"chat-base-3\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingLevel\": \"HIGH\"\n }\n }\n }\n },\n \"gemini-3-pro-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"gemini-3-flash-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"gemini-2.5-pro\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"gemini-2.5-flash\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-2.5-flash-lite\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"gemini-2.5-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-3-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"classifier\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 1024,\n \"thinkingConfig\": {\n \"thinkingBudget\": 512\n }\n }\n }\n },\n \"prompt-completion\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.3,\n \"maxOutputTokens\": 16000,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"fast-ack-helper\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.2,\n \"maxOutputTokens\": 120,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"edit-corrector\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"summarizer-default\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"summarizer-shell\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"web-search\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"googleSearch\": {}\n }\n ]\n }\n }\n },\n \"web-fetch\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"urlContext\": {}\n }\n ]\n }\n }\n },\n \"web-fetch-fallback\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection-double-check\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"llm-edit-fixer\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"next-speaker-checker\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"chat-compression-3-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"chat-compression-3-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"chat-compression-3.1-flash-lite\": {\n \"modelConfig\": {\n \"model\": \"gemini-3.1-flash-lite-preview\"\n }\n },\n \"chat-compression-2.5-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"chat-compression-2.5-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"chat-compression-2.5-flash-lite\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"chat-compression-default\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"agent-history-provider-summarizer\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n }\n },\n \"overrides\": [\n {\n \"match\": {\n \"model\": \"chat-base\",\n \"isRetry\": true\n },\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"temperature\": 1\n }\n }\n }\n ],\n \"modelDefinitions\": {\n \"gemini-3.1-flash-lite-preview\": {\n \"tier\": \"flash-lite\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3.1-pro-preview\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3.1-pro-preview-customtools\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"isVisible\": false,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3-pro-preview\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-3-flash-preview\": {\n \"tier\": \"flash\",\n \"family\": \"gemini-3\",\n \"isPreview\": true,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": true\n }\n },\n \"gemini-2.5-pro\": {\n \"tier\": \"pro\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"gemini-2.5-flash\": {\n \"tier\": \"flash\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"gemini-2.5-flash-lite\": {\n \"tier\": \"flash-lite\",\n \"family\": \"gemini-2.5\",\n \"isPreview\": false,\n \"isVisible\": true,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"auto\": {\n \"tier\": \"auto\",\n \"isPreview\": true,\n \"isVisible\": false,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"pro\": {\n \"tier\": \"pro\",\n \"isPreview\": false,\n \"isVisible\": false,\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"flash\": {\n \"tier\": \"flash\",\n \"isPreview\": false,\n \"isVisible\": false,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"flash-lite\": {\n \"tier\": \"flash-lite\",\n \"isPreview\": false,\n \"isVisible\": false,\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n },\n \"auto-gemini-3\": {\n \"displayName\": \"Auto (Gemini 3)\",\n \"tier\": \"auto\",\n \"isPreview\": true,\n \"isVisible\": true,\n \"dialogDescription\": \"Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash\",\n \"features\": {\n \"thinking\": true,\n \"multimodalToolUse\": false\n }\n },\n \"auto-gemini-2.5\": {\n \"displayName\": \"Auto (Gemini 2.5)\",\n \"tier\": \"auto\",\n \"isPreview\": false,\n \"isVisible\": true,\n \"dialogDescription\": \"Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash\",\n \"features\": {\n \"thinking\": false,\n \"multimodalToolUse\": false\n }\n }\n },\n \"modelIdResolutions\": {\n \"gemini-3.1-pro-preview\": {\n \"default\": \"gemini-3.1-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n }\n ]\n },\n \"gemini-3.1-pro-preview-customtools\": {\n \"default\": \"gemini-3.1-pro-preview-customtools\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n }\n ]\n },\n \"gemini-3-flash-preview\": {\n \"default\": \"gemini-3-flash-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-flash\"\n }\n ]\n },\n \"gemini-3-pro-preview\": {\n \"default\": \"gemini-3-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true,\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true\n },\n \"target\": \"gemini-3.1-pro-preview\"\n }\n ]\n },\n \"auto-gemini-3\": {\n \"default\": \"gemini-3-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true,\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true\n },\n \"target\": \"gemini-3.1-pro-preview\"\n }\n ]\n },\n \"auto\": {\n \"default\": \"gemini-3-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true,\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true\n },\n \"target\": \"gemini-3.1-pro-preview\"\n }\n ]\n },\n \"pro\": {\n \"default\": \"gemini-3-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true,\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true\n },\n \"target\": \"gemini-3.1-pro-preview\"\n }\n ]\n },\n \"auto-gemini-2.5\": {\n \"default\": \"gemini-2.5-pro\"\n },\n \"gemini-3.1-flash-lite-preview\": {\n \"default\": \"gemini-3.1-flash-lite-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"useGemini3_1FlashLite\": false\n },\n \"target\": \"gemini-2.5-flash-lite\"\n }\n ]\n },\n \"flash\": {\n \"default\": \"gemini-3-flash-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"hasAccessToPreview\": false\n },\n \"target\": \"gemini-2.5-flash\"\n }\n ]\n },\n \"flash-lite\": {\n \"default\": \"gemini-2.5-flash-lite\",\n \"contexts\": [\n {\n \"condition\": {\n \"useGemini3_1FlashLite\": true\n },\n \"target\": \"gemini-3.1-flash-lite-preview\"\n }\n ]\n }\n },\n \"classifierIdResolutions\": {\n \"flash\": {\n \"default\": \"gemini-3-flash-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"requestedModels\": [\n \"auto-gemini-2.5\",\n \"gemini-2.5-pro\"\n ]\n },\n \"target\": \"gemini-2.5-flash\"\n },\n {\n \"condition\": {\n \"requestedModels\": [\n \"auto-gemini-3\",\n \"gemini-3-pro-preview\"\n ]\n },\n \"target\": \"gemini-3-flash-preview\"\n }\n ]\n },\n \"pro\": {\n \"default\": \"gemini-3-pro-preview\",\n \"contexts\": [\n {\n \"condition\": {\n \"requestedModels\": [\n \"auto-gemini-2.5\",\n \"gemini-2.5-pro\"\n ]\n },\n \"target\": \"gemini-2.5-pro\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true,\n \"useCustomTools\": true\n },\n \"target\": \"gemini-3.1-pro-preview-customtools\"\n },\n {\n \"condition\": {\n \"useGemini3_1\": true\n },\n \"target\": \"gemini-3.1-pro-preview\"\n }\n ]\n }\n },\n \"modelChains\": {\n \"preview\": [\n {\n \"model\": \"gemini-3-pro-preview\",\n \"actions\": {\n \"terminal\": \"prompt\",\n \"transient\": \"prompt\",\n \"not_found\": \"prompt\",\n \"unknown\": \"prompt\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n },\n {\n \"model\": \"gemini-3-flash-preview\",\n \"isLastResort\": true,\n \"actions\": {\n \"terminal\": \"prompt\",\n \"transient\": \"prompt\",\n \"not_found\": \"prompt\",\n \"unknown\": \"prompt\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n }\n ],\n \"default\": [\n {\n \"model\": \"gemini-2.5-pro\",\n \"actions\": {\n \"terminal\": \"prompt\",\n \"transient\": \"prompt\",\n \"not_found\": \"prompt\",\n \"unknown\": \"prompt\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n },\n {\n \"model\": \"gemini-2.5-flash\",\n \"isLastResort\": true,\n \"actions\": {\n \"terminal\": \"prompt\",\n \"transient\": \"prompt\",\n \"not_found\": \"prompt\",\n \"unknown\": \"prompt\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n }\n ],\n \"lite\": [\n {\n \"model\": \"gemini-2.5-flash-lite\",\n \"actions\": {\n \"terminal\": \"silent\",\n \"transient\": \"silent\",\n \"not_found\": \"silent\",\n \"unknown\": \"silent\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n },\n {\n \"model\": \"gemini-2.5-flash\",\n \"actions\": {\n \"terminal\": \"silent\",\n \"transient\": \"silent\",\n \"not_found\": \"silent\",\n \"unknown\": \"silent\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n },\n {\n \"model\": \"gemini-2.5-pro\",\n \"isLastResort\": true,\n \"actions\": {\n \"terminal\": \"silent\",\n \"transient\": \"silent\",\n \"not_found\": \"silent\",\n \"unknown\": \"silent\"\n },\n \"stateTransitions\": {\n \"terminal\": \"terminal\",\n \"transient\": \"terminal\",\n \"not_found\": \"terminal\",\n \"unknown\": \"terminal\"\n }\n }\n ]\n }\n}`", "default": { "aliases": { "base": { @@ -869,6 +869,11 @@ "modelConfig": { "model": "gemini-3-pro-preview" } + }, + "agent-history-provider-summarizer": { + "modelConfig": { + "model": "gemini-3-flash-preview" + } } }, "overrides": [ @@ -1362,7 +1367,7 @@ "aliases": { "title": "Model Config Aliases", "description": "Named presets for model configs. Can be used in place of a model name and can inherit from other aliases using an `extends` property.", - "markdownDescription": "Named presets for model configs. Can be used in place of a model name and can inherit from other aliases using an `extends` property.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `{\n \"base\": {\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"temperature\": 0,\n \"topP\": 1\n }\n }\n },\n \"chat-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"includeThoughts\": true\n },\n \"temperature\": 1,\n \"topP\": 0.95,\n \"topK\": 64\n }\n }\n },\n \"chat-base-2.5\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 8192\n }\n }\n }\n },\n \"chat-base-3\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingLevel\": \"HIGH\"\n }\n }\n }\n },\n \"gemini-3-pro-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"gemini-3-flash-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"gemini-2.5-pro\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"gemini-2.5-flash\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-2.5-flash-lite\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"gemini-2.5-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-3-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"classifier\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 1024,\n \"thinkingConfig\": {\n \"thinkingBudget\": 512\n }\n }\n }\n },\n \"prompt-completion\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.3,\n \"maxOutputTokens\": 16000,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"fast-ack-helper\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.2,\n \"maxOutputTokens\": 120,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"edit-corrector\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"summarizer-default\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"summarizer-shell\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"web-search\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"googleSearch\": {}\n }\n ]\n }\n }\n },\n \"web-fetch\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"urlContext\": {}\n }\n ]\n }\n }\n },\n \"web-fetch-fallback\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection-double-check\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"llm-edit-fixer\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"next-speaker-checker\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"chat-compression-3-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"chat-compression-3-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"chat-compression-3.1-flash-lite\": {\n \"modelConfig\": {\n \"model\": \"gemini-3.1-flash-lite-preview\"\n }\n },\n \"chat-compression-2.5-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"chat-compression-2.5-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"chat-compression-2.5-flash-lite\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"chat-compression-default\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n }\n}`", + "markdownDescription": "Named presets for model configs. Can be used in place of a model name and can inherit from other aliases using an `extends` property.\n\n- Category: `Model`\n- Requires restart: `no`\n- Default: `{\n \"base\": {\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"temperature\": 0,\n \"topP\": 1\n }\n }\n },\n \"chat-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"includeThoughts\": true\n },\n \"temperature\": 1,\n \"topP\": 0.95,\n \"topK\": 64\n }\n }\n },\n \"chat-base-2.5\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 8192\n }\n }\n }\n },\n \"chat-base-3\": {\n \"extends\": \"chat-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingLevel\": \"HIGH\"\n }\n }\n }\n },\n \"gemini-3-pro-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"gemini-3-flash-preview\": {\n \"extends\": \"chat-base-3\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"gemini-2.5-pro\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"gemini-2.5-flash\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-2.5-flash-lite\": {\n \"extends\": \"chat-base-2.5\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"gemini-2.5-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"gemini-3-flash-base\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"classifier\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 1024,\n \"thinkingConfig\": {\n \"thinkingBudget\": 512\n }\n }\n }\n },\n \"prompt-completion\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.3,\n \"maxOutputTokens\": 16000,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"fast-ack-helper\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"temperature\": 0.2,\n \"maxOutputTokens\": 120,\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"edit-corrector\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"thinkingConfig\": {\n \"thinkingBudget\": 0\n }\n }\n }\n },\n \"summarizer-default\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"summarizer-shell\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\",\n \"generateContentConfig\": {\n \"maxOutputTokens\": 2000\n }\n }\n },\n \"web-search\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"googleSearch\": {}\n }\n ]\n }\n }\n },\n \"web-fetch\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {\n \"generateContentConfig\": {\n \"tools\": [\n {\n \"urlContext\": {}\n }\n ]\n }\n }\n },\n \"web-fetch-fallback\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"loop-detection-double-check\": {\n \"extends\": \"base\",\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"llm-edit-fixer\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"next-speaker-checker\": {\n \"extends\": \"gemini-3-flash-base\",\n \"modelConfig\": {}\n },\n \"chat-compression-3-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"chat-compression-3-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n },\n \"chat-compression-3.1-flash-lite\": {\n \"modelConfig\": {\n \"model\": \"gemini-3.1-flash-lite-preview\"\n }\n },\n \"chat-compression-2.5-pro\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-pro\"\n }\n },\n \"chat-compression-2.5-flash\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash\"\n }\n },\n \"chat-compression-2.5-flash-lite\": {\n \"modelConfig\": {\n \"model\": \"gemini-2.5-flash-lite\"\n }\n },\n \"chat-compression-default\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-pro-preview\"\n }\n },\n \"agent-history-provider-summarizer\": {\n \"modelConfig\": {\n \"model\": \"gemini-3-flash-preview\"\n }\n }\n}`", "default": { "base": { "modelConfig": { @@ -1594,6 +1599,11 @@ "modelConfig": { "model": "gemini-3-pro-preview" } + }, + "agent-history-provider-summarizer": { + "modelConfig": { + "model": "gemini-3-flash-preview" + } } }, "type": "object", @@ -2897,6 +2907,34 @@ "default": false, "type": "boolean" }, + "agentHistoryTruncation": { + "title": "Agent History Truncation", + "description": "Enable truncation window logic for the Agent History Provider.", + "markdownDescription": "Enable truncation window logic for the Agent History Provider.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`", + "default": false, + "type": "boolean" + }, + "agentHistoryTruncationThreshold": { + "title": "Agent History Truncation Threshold", + "description": "The maximum number of messages before history is truncated.", + "markdownDescription": "The maximum number of messages before history is truncated.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `30`", + "default": 30, + "type": "number" + }, + "agentHistoryRetainedMessages": { + "title": "Agent History Retained Messages", + "description": "The number of recent messages to retain after truncation.", + "markdownDescription": "The number of recent messages to retain after truncation.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `15`", + "default": 15, + "type": "number" + }, + "agentHistorySummarization": { + "title": "Agent History Summarization", + "description": "Enable summarization of truncated content via a small model for the Agent History Provider.", + "markdownDescription": "Enable summarization of truncated content via a small model for the Agent History Provider.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`", + "default": false, + "type": "boolean" + }, "topicUpdateNarration": { "title": "Topic & Update Narration", "description": "Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting.", From ba71ffa7367e7dab3bc55269241491ceb7cc70c1 Mon Sep 17 00:00:00 2001 From: Abhi <43648792+abhipatel12@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:34:39 -0400 Subject: [PATCH 04/23] fix(core): switch to subshells for shell tool wrapping to fix heredocs and edge cases (#24024) --- packages/core/src/tools/shell.test.ts | 42 +++++++++++++++++++++++++-- packages/core/src/tools/shell.ts | 40 ++++++++++++++++++++----- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index d1dfc415b7..78b54fe297 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -277,7 +277,7 @@ describe('ShellTool', () => { const result = await promise; - const wrappedCommand = `{ my-command & }; __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`; + const wrappedCommand = `(\n${'my-command &'}\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`; expect(mockShellExecutionService).toHaveBeenCalledWith( wrappedCommand, tempRootDir, @@ -295,6 +295,42 @@ describe('ShellTool', () => { expect(fs.existsSync(tmpFile)).toBe(false); }); + it('should add a space when command ends with a backslash to prevent escaping newline', async () => { + const invocation = shellTool.build({ command: 'ls\\' }); + const promise = invocation.execute(mockAbortSignal); + resolveShellExecution(); + await promise; + + const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp'); + const wrappedCommand = `(\nls\\ \n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`; + expect(mockShellExecutionService).toHaveBeenCalledWith( + wrappedCommand, + tempRootDir, + expect.any(Function), + expect.any(AbortSignal), + false, + expect.any(Object), + ); + }); + + it('should handle trailing comments correctly by placing them on their own line', async () => { + const invocation = shellTool.build({ command: 'ls # comment' }); + const promise = invocation.execute(mockAbortSignal); + resolveShellExecution(); + await promise; + + const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp'); + const wrappedCommand = `(\nls # comment\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`; + expect(mockShellExecutionService).toHaveBeenCalledWith( + wrappedCommand, + tempRootDir, + expect.any(Function), + expect.any(AbortSignal), + false, + expect.any(Object), + ); + }); + it('should use the provided absolute directory as cwd', async () => { const subdir = path.join(tempRootDir, 'subdir'); const invocation = shellTool.build({ @@ -306,7 +342,7 @@ describe('ShellTool', () => { await promise; const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp'); - const wrappedCommand = `{ ls; }; __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`; + const wrappedCommand = `(\n${'ls'}\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`; expect(mockShellExecutionService).toHaveBeenCalledWith( wrappedCommand, subdir, @@ -331,7 +367,7 @@ describe('ShellTool', () => { await promise; const tmpFile = path.join(os.tmpdir(), 'shell_pgrep_abcdef.tmp'); - const wrappedCommand = `{ ls; }; __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`; + const wrappedCommand = `(\n${'ls'}\n); __code=$?; pgrep -g 0 >${tmpFile} 2>&1; exit $__code;`; expect(mockShellExecutionService).toHaveBeenCalledWith( wrappedCommand, path.join(tempRootDir, 'subdir'), diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 0b4760ccc7..3a70de3ea4 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -76,6 +76,33 @@ export class ShellToolInvocation extends BaseToolInvocation< super(params, messageBus, _toolName, _toolDisplayName); } + /** + * Wraps a command in a subshell `()` to capture background process IDs (PIDs) using pgrep. + * Uses newlines to prevent breaking heredocs or trailing comments. + * + * @param command The raw command string to execute. + * @param tempFilePath Path to the temporary file where PIDs will be written. + * @param isWindows Whether the current platform is Windows (if true, the command is returned as-is). + * @returns The wrapped command string. + */ + private wrapCommandForPgrep( + command: string, + tempFilePath: string, + isWindows: boolean, + ): string { + if (isWindows) { + return command; + } + let trimmed = command.trim(); + if (!trimmed) { + return ''; + } + if (trimmed.endsWith('\\')) { + trimmed += ' '; + } + return `(\n${trimmed}\n); __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`; + } + private getContextualDetails(): string { let details = ''; // append optional [in directory] @@ -232,14 +259,11 @@ export class ShellToolInvocation extends BaseToolInvocation< try { // pgrep is not available on Windows, so we can't get background PIDs - const commandToExecute = isWindows - ? strippedCommand - : (() => { - // wrap command to append subprocess pids (via pgrep) to temporary file - let command = strippedCommand.trim(); - if (!command.endsWith('&')) command += ';'; - return `{ ${command} }; __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`; - })(); + const commandToExecute = this.wrapCommandForPgrep( + strippedCommand, + tempFilePath, + isWindows, + ); const cwd = this.params.dir_path ? path.resolve(this.context.config.getTargetDir(), this.params.dir_path) From ebe98fdee99354697dbcaafe1c176c762d9f9c84 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Fri, 27 Mar 2026 14:05:22 -0700 Subject: [PATCH 05/23] Debug command. (#23851) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/cli/src/utils/activityLogger.ts | 21 ++++- packages/devtools/GEMINI.md | 9 +- packages/devtools/client/src/App.tsx | 108 +++++++++++++++++++---- packages/devtools/src/index.ts | 36 +++++++- 4 files changed, 153 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/utils/activityLogger.ts b/packages/cli/src/utils/activityLogger.ts index 14cef88a54..8118ccdde9 100644 --- a/packages/cli/src/utils/activityLogger.ts +++ b/packages/cli/src/utils/activityLogger.ts @@ -803,7 +803,26 @@ function setupNetworkLogging( // Flush buffered logs flushBuffer(); break; - + case 'trigger-debugger': { + import('node:inspector') + .then((inspector) => { + inspector.open(); + debugLogger.log( + 'Node debugger attached. Open chrome://inspect in Chrome to start debugging.', + ); + return import('./events.js'); + }) + .then(({ appEvents, AppEvent, TransientMessageType }) => { + appEvents.emit(AppEvent.TransientMessage, { + message: 'Debugger attached from DevTools.', + type: TransientMessageType.Hint, + }); + }) + .catch((err) => + debugLogger.debug('Failed to trigger debugger:', err), + ); + break; + } case 'ping': sendMessage({ type: 'pong', timestamp: Date.now() }); break; diff --git a/packages/devtools/GEMINI.md b/packages/devtools/GEMINI.md index 9da1828a25..7397cedf84 100644 --- a/packages/devtools/GEMINI.md +++ b/packages/devtools/GEMINI.md @@ -51,10 +51,11 @@ gemini.tsx / nonInteractiveCli.ts ## API Endpoints -| Endpoint | Method | Description | -| --------- | --------- | --------------------------------------------------------------------------- | -| `/ws` | WebSocket | Log ingestion from CLI sessions (register, network, console) | -| `/events` | SSE | Pushes snapshot on connect, then incremental network/console/session events | +| Endpoint | Method | Description | +| ----------------------- | --------- | --------------------------------------------------------------------------- | +| `/ws` | WebSocket | Log ingestion from CLI sessions (register, network, console) | +| `/events` | SSE | Pushes snapshot on connect, then incremental network/console/session events | +| `/api/trigger-debugger` | POST | Triggers the Node.js debugger for a specific CLI session via WebSocket | ## Development diff --git a/packages/devtools/client/src/App.tsx b/packages/devtools/client/src/App.tsx index 9c531435b4..7869b93c3c 100644 --- a/packages/devtools/client/src/App.tsx +++ b/packages/devtools/client/src/App.tsx @@ -39,6 +39,21 @@ export default function App() { null, ); + // --- Toast Logic --- + const [toastMessage, setToastMessage] = useState(null); + const toastTimeoutRef = useRef | null>(null); + + const showToast = (msg: string) => { + setToastMessage(msg); + if (toastTimeoutRef.current) { + clearTimeout(toastTimeoutRef.current); + } + toastTimeoutRef.current = setTimeout(() => { + setToastMessage(null); + toastTimeoutRef.current = null; + }, 5000); + }; + // --- Theme Logic --- const [themeMode, setThemeMode] = useState(() => { const saved = localStorage.getItem('devtools-theme'); @@ -306,21 +321,52 @@ export default function App() { > {selectedSessionId && connectedSessions.includes(selectedSessionId) && ( - + <> + + + )}