diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index b0db2faab..bc56a437e 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -129,7 +129,6 @@ Settings are organized into categories. All settings should be placed within the | -------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | | `model.name` | string | The Qwen model to use for conversations. | `undefined` | | `model.maxSessionTurns` | number | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` | -| `model.summarizeToolOutput` | object | Enables or disables the summarization of tool output. You can specify the token budget for the summarization using the `tokenBudget` setting. Note: Currently only the `run_shell_command` tool is supported. For example `{"run_shell_command": {"tokenBudget": 2000}}` | `undefined` | | `model.generationConfig` | object | Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, `enableCacheControl`, `contextWindowSize` (override model's context window size), `modalities` (override auto-detected input modalities), `customHeaders` (custom HTTP headers for API requests), and `extra_body` (additional body parameters for OpenAI-compatible API requests only), along with fine-tuning knobs under `samplingParams` (for example `temperature`, `top_p`, `max_tokens`). Leave unset to rely on provider defaults. | `undefined` | | `model.chatCompression.contextPercentageThreshold` | number | Sets the threshold for chat history compression as a percentage of the model's total token limit. This is a value between 0 and 1 that applies to both automatic compression and the manual `/compress` command. For example, a value of `0.6` will trigger compression when the chat history exceeds 60% of the token limit. Use `0` to disable compression entirely. | `0.7` | | `model.skipNextSpeakerCheck` | boolean | Skip the next speaker check. | `false` | @@ -349,11 +348,6 @@ Here is an example of a `settings.json` file with the nested structure, new as o "maxSessionTurns": 10, "enableOpenAILogging": false, "openAILoggingDir": "~/qwen-logs", - "summarizeToolOutput": { - "run_shell_command": { - "tokenBudget": 100 - } - } }, "context": { "fileName": ["CONTEXT.md", "QWEN.md"], diff --git a/integration-tests/fixtures/settings-migration/workspaces.json b/integration-tests/fixtures/settings-migration/workspaces.json index af7a48f84..bd9798009 100644 --- a/integration-tests/fixtures/settings-migration/workspaces.json +++ b/integration-tests/fixtures/settings-migration/workspaces.json @@ -43,7 +43,6 @@ "maxSessionTurns": 50, "preferredEditor": "vscode", "sandbox": false, - "summarizeToolOutput": true, "telemetry": { "enabled": false }, diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 6935b3a18..eab0470c6 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -1013,7 +1013,6 @@ export async function loadCliConfig( warnings: resolvedCliConfig.warnings, cliVersion: await getCliVersion(), webSearch: buildWebSearchConfig(argv, settings, selectedAuthType), - summarizeToolOutput: settings.model?.summarizeToolOutput, ideMode, chatCompression: settings.model?.chatCompression, folderTrust, diff --git a/packages/cli/src/config/migration/versions/v1-to-v2-shared.ts b/packages/cli/src/config/migration/versions/v1-to-v2-shared.ts index c87fa4480..c63979f35 100644 --- a/packages/cli/src/config/migration/versions/v1-to-v2-shared.ts +++ b/packages/cli/src/config/migration/versions/v1-to-v2-shared.ts @@ -55,7 +55,6 @@ export const V1_TO_V2_MIGRATION_MAP: Record = { shellPager: 'tools.shell.pager', shellShowColor: 'tools.shell.showColor', skipNextSpeakerCheck: 'model.skipNextSpeakerCheck', - summarizeToolOutput: 'model.summarizeToolOutput', telemetry: 'telemetry', theme: 'ui.theme', toolDiscoveryCommand: 'tools.discoveryCommand', @@ -157,7 +156,6 @@ export const V1_INDICATOR_KEYS = [ 'shellPager', 'shellShowColor', 'skipNextSpeakerCheck', - 'summarizeToolOutput', 'toolDiscoveryCommand', 'toolCallCommand', 'usageStatisticsEnabled', diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 0809cf090..dbd9a20ec 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -103,10 +103,6 @@ export interface CheckpointingSettings { enabled?: boolean; } -export interface SummarizeToolOutputSettings { - tokenBudget?: number; -} - export interface AccessibilitySettings { enableLoadingPhrases?: boolean; screenReader?: boolean; diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 48c609880..e29da7be3 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -546,17 +546,6 @@ const SETTINGS_SCHEMA = { 'Maximum number of user/model/tool turns to keep in a session. -1 means unlimited.', showInDialog: false, }, - summarizeToolOutput: { - type: 'object', - label: 'Summarize Tool Output', - category: 'Model', - requiresRestart: false, - default: undefined as - | Record - | undefined, - description: 'Settings for summarizing tool output.', - showInDialog: false, - }, chatCompression: { type: 'object', label: 'Chat Compression', diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index dc5a9d517..3fcd3b9ca 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -195,10 +195,6 @@ export interface ChatCompressionSettings { contextPercentageThreshold?: number; } -export interface SummarizeToolOutputSettings { - tokenBudget?: number; -} - export interface TelemetrySettings { enabled?: boolean; target?: TelemetryTarget; @@ -339,7 +335,6 @@ export interface ConfigParameters { allowedMcpServers?: string[]; excludedMcpServers?: string[]; noBrowser?: boolean; - summarizeToolOutput?: Record; folderTrustFeature?: boolean; folderTrust?: boolean; ideMode?: boolean; @@ -497,9 +492,6 @@ export class Config { private readonly listExtensions: boolean; private readonly overrideExtensions?: string[]; - private readonly summarizeToolOutput: - | Record - | undefined; private readonly cliVersion?: string; private readonly experimentalZedIntegration: boolean = false; private readonly chatRecordingEnabled: boolean; @@ -612,7 +604,6 @@ export class Config { this.listExtensions = params.listExtensions ?? false; this.overrideExtensions = params.overrideExtensions; this.noBrowser = params.noBrowser ?? false; - this.summarizeToolOutput = params.summarizeToolOutput; this.folderTrustFeature = params.folderTrustFeature ?? false; this.folderTrust = params.folderTrust ?? false; this.ideMode = params.ideMode ?? false; @@ -1596,12 +1587,6 @@ export class Config { return this.getNoBrowser() || !shouldAttemptBrowserLaunch(); } - getSummarizeToolOutputConfig(): - | Record - | undefined { - return this.summarizeToolOutput; - } - // Web search provider configuration getWebSearchConfig() { return this.webSearch; diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index fae07ae91..e9aa4f850 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -21,7 +21,6 @@ vi.mock('../services/shellExecutionService.js', () => ({ vi.mock('fs'); vi.mock('os'); vi.mock('crypto'); -vi.mock('../utils/summarizer.js'); import { isCommandAllowed } from '../utils/shell-utils.js'; import { ShellTool } from './shell.js'; @@ -35,7 +34,6 @@ import * as os from 'node:os'; import { EOL } from 'node:os'; import * as path from 'node:path'; import * as crypto from 'node:crypto'; -import * as summarizer from '../utils/summarizer.js'; import { ToolErrorType } from './tool-error.js'; import { ToolConfirmationOutcome } from './tools.js'; import { OUTPUT_UPDATE_INTERVAL_MS } from './shell.js'; @@ -55,7 +53,6 @@ describe('ShellTool', () => { getExcludeTools: vi.fn().mockReturnValue([]), getDebugMode: vi.fn().mockReturnValue(false), getTargetDir: vi.fn().mockReturnValue('/test/dir'), - getSummarizeToolOutputConfig: vi.fn().mockReturnValue(undefined), getWorkspaceContext: vi .fn() .mockReturnValue(createMockWorkspaceContext('/test/dir')), @@ -479,42 +476,6 @@ describe('ShellTool', () => { ).toThrow('Directory must be an absolute path.'); }); - it('should summarize output when configured', async () => { - (mockConfig.getSummarizeToolOutputConfig as Mock).mockReturnValue({ - [shellTool.name]: { tokenBudget: 1000 }, - }); - vi.mocked(summarizer.summarizeToolOutput).mockResolvedValue( - 'summarized output', - ); - - const invocation = shellTool.build({ - command: 'ls', - is_background: false, - }); - const promise = invocation.execute(mockAbortSignal); - resolveExecutionPromise({ - output: 'long output', - rawOutput: Buffer.from('long output'), - exitCode: 0, - signal: null, - error: null, - aborted: false, - pid: 12345, - executionMethod: 'child_process', - }); - - const result = await promise; - - expect(summarizer.summarizeToolOutput).toHaveBeenCalledWith( - expect.any(String), - mockConfig.getGeminiClient(), - expect.any(AbortSignal), - 1000, - ); - expect(result.llmContent).toBe('summarized output'); - expect(result.returnDisplay).toBe('long output'); - }); - it('should clean up the temp file on synchronous execution error', async () => { const error = new Error('sync spawn error'); mockShellExecutionService.mockImplementation(() => { diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index d8c205d67..1de48b599 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -26,7 +26,6 @@ import { Kind, } from './tools.js'; import { getErrorMessage } from '../utils/errors.js'; -import { summarizeToolOutput } from '../utils/summarizer.js'; import { truncateAndSaveToFile } from '../utils/truncation.js'; import { logToolOutputTruncated } from '../telemetry/loggers.js'; import { ToolOutputTruncatedEvent } from '../telemetry/types.js'; @@ -418,7 +417,6 @@ export class ShellToolInvocation extends BaseToolInvocation< } } - const summarizeConfig = this.config.getSummarizeToolOutputConfig(); const executionError = result.error ? { error: { @@ -428,20 +426,6 @@ export class ShellToolInvocation extends BaseToolInvocation< } : {}; - if (summarizeConfig && summarizeConfig[ShellTool.Name]) { - const summary = await summarizeToolOutput( - llmContent, - this.config.getGeminiClient(), - signal, - summarizeConfig[ShellTool.Name].tokenBudget, - ); - return { - llmContent: summary, - returnDisplay: returnDisplayMessage, - ...executionError, - }; - } - return { llmContent, returnDisplay: returnDisplayMessage, diff --git a/packages/core/src/utils/summarizer.test.ts b/packages/core/src/utils/summarizer.test.ts deleted file mode 100644 index 6098e77b7..000000000 --- a/packages/core/src/utils/summarizer.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { Mock } from 'vitest'; -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { GeminiClient } from '../core/client.js'; -import { Config } from '../config/config.js'; -import { - summarizeToolOutput, - llmSummarizer, - defaultSummarizer, -} from './summarizer.js'; -import type { ToolResult } from '../tools/tools.js'; - -// Mock GeminiClient and Config constructor -vi.mock('../core/client.js'); -vi.mock('../config/config.js'); - -describe('summarizers', () => { - let mockGeminiClient: GeminiClient; - let MockConfig: Mock; - const abortSignal = new AbortController().signal; - - beforeEach(() => { - MockConfig = vi.mocked(Config); - const mockConfigInstance = new MockConfig( - 'test-api-key', - 'gemini-pro', - false, - '.', - false, - undefined, - false, - undefined, - undefined, - undefined, - ); - - mockGeminiClient = new GeminiClient(mockConfigInstance); - (mockGeminiClient.generateContent as Mock) = vi.fn(); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - describe('summarizeToolOutput', () => { - it('should return original text if it is shorter than maxLength', async () => { - const shortText = 'This is a short text.'; - const result = await summarizeToolOutput( - shortText, - mockGeminiClient, - abortSignal, - 2000, - ); - expect(result).toBe(shortText); - expect(mockGeminiClient.generateContent).not.toHaveBeenCalled(); - }); - - it('should return original text if it is empty', async () => { - const emptyText = ''; - const result = await summarizeToolOutput( - emptyText, - mockGeminiClient, - abortSignal, - 2000, - ); - expect(result).toBe(emptyText); - expect(mockGeminiClient.generateContent).not.toHaveBeenCalled(); - }); - - it('should call generateContent if text is longer than maxLength', async () => { - const longText = 'This is a very long text.'.repeat(200); - const summary = 'This is a summary.'; - (mockGeminiClient.generateContent as Mock).mockResolvedValue({ - candidates: [{ content: { parts: [{ text: summary }] } }], - }); - - const result = await summarizeToolOutput( - longText, - mockGeminiClient, - abortSignal, - 2000, - ); - - expect(mockGeminiClient.generateContent).toHaveBeenCalledTimes(1); - expect(result).toBe(summary); - }); - - it('should return original text if generateContent throws an error', async () => { - const longText = 'This is a very long text.'.repeat(200); - const error = new Error('API Error'); - (mockGeminiClient.generateContent as Mock).mockRejectedValue(error); - - const result = await summarizeToolOutput( - longText, - mockGeminiClient, - abortSignal, - 2000, - ); - - expect(mockGeminiClient.generateContent).toHaveBeenCalledTimes(1); - expect(result).toBe(longText); - }); - - it('should construct the correct prompt for summarization', async () => { - const longText = 'This is a very long text.'.repeat(200); - const summary = 'This is a summary.'; - (mockGeminiClient.generateContent as Mock).mockResolvedValue({ - candidates: [{ content: { parts: [{ text: summary }] } }], - }); - - await summarizeToolOutput(longText, mockGeminiClient, abortSignal, 1000); - - const expectedPrompt = `Summarize the following tool output to be a maximum of 1000 tokens. The summary should be concise and capture the main points of the tool output. - -The summarization should be done based on the content that is provided. Here are the basic rules to follow: -1. If the text is a directory listing or any output that is structural, use the history of the conversation to understand the context. Using this context try to understand what information we need from the tool output and return that as a response. -2. If the text is text content and there is nothing structural that we need, summarize the text. -3. If the text is the output of a shell command, use the history of the conversation to understand the context. Using this context try to understand what information we need from the tool output and return a summarization along with the stack trace of any error within the tags. The stack trace should be complete and not truncated. If there are warnings, you should include them in the summary within tags. - - -Text to summarize: -"${longText}" - -Return the summary string which should first contain an overall summarization of text followed by the full stack trace of errors and warnings in the tool output. -`; - const calledWith = (mockGeminiClient.generateContent as Mock).mock - .calls[0]; - const contents = calledWith[0]; - expect(contents[0].parts[0].text).toBe(expectedPrompt); - }); - }); - - describe('llmSummarizer', () => { - it('should summarize tool output using summarizeToolOutput', async () => { - const toolResult: ToolResult = { - llmContent: 'This is a very long text.'.repeat(200), - returnDisplay: '', - }; - const summary = 'This is a summary.'; - (mockGeminiClient.generateContent as Mock).mockResolvedValue({ - candidates: [{ content: { parts: [{ text: summary }] } }], - }); - - const result = await llmSummarizer( - toolResult, - mockGeminiClient, - abortSignal, - ); - - expect(mockGeminiClient.generateContent).toHaveBeenCalledTimes(1); - expect(result).toBe(summary); - }); - - it('should handle different llmContent types', async () => { - const longText = 'This is a very long text.'.repeat(200); - const toolResult: ToolResult = { - llmContent: [{ text: longText }], - returnDisplay: '', - }; - const summary = 'This is a summary.'; - (mockGeminiClient.generateContent as Mock).mockResolvedValue({ - candidates: [{ content: { parts: [{ text: summary }] } }], - }); - - const result = await llmSummarizer( - toolResult, - mockGeminiClient, - abortSignal, - ); - - expect(mockGeminiClient.generateContent).toHaveBeenCalledTimes(1); - const calledWith = (mockGeminiClient.generateContent as Mock).mock - .calls[0]; - const contents = calledWith[0]; - expect(contents[0].parts[0].text).toContain(`"${longText}"`); - expect(result).toBe(summary); - }); - }); - - describe('defaultSummarizer', () => { - it('should stringify the llmContent', async () => { - const toolResult: ToolResult = { - llmContent: { text: 'some data' }, - returnDisplay: '', - }; - - const result = await defaultSummarizer( - toolResult, - mockGeminiClient, - abortSignal, - ); - - expect(result).toBe(JSON.stringify({ text: 'some data' })); - expect(mockGeminiClient.generateContent).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/core/src/utils/summarizer.ts b/packages/core/src/utils/summarizer.ts deleted file mode 100644 index 8c2b391ea..000000000 --- a/packages/core/src/utils/summarizer.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { ToolResult } from '../tools/tools.js'; -import type { - Content, - GenerateContentConfig, - GenerateContentResponse, -} from '@google/genai'; -import type { GeminiClient } from '../core/client.js'; -import { DEFAULT_QWEN_FLASH_MODEL } from '../config/models.js'; -import { getResponseText, partToString } from './partUtils.js'; -import { createDebugLogger } from './debugLogger.js'; - -const debugLogger = createDebugLogger('SUMMARIZER'); - -/** - * A function that summarizes the result of a tool execution. - * - * @param result The result of the tool execution. - * @returns The summary of the result. - */ -export type Summarizer = ( - result: ToolResult, - geminiClient: GeminiClient, - abortSignal: AbortSignal, -) => Promise; - -/** - * The default summarizer for tool results. - * - * @param result The result of the tool execution. - * @param geminiClient The Gemini client to use for summarization. - * @param abortSignal The abort signal to use for summarization. - * @returns The summary of the result. - */ -export const defaultSummarizer: Summarizer = ( - result: ToolResult, - _geminiClient: GeminiClient, - _abortSignal: AbortSignal, -) => Promise.resolve(JSON.stringify(result.llmContent)); - -const SUMMARIZE_TOOL_OUTPUT_PROMPT = `Summarize the following tool output to be a maximum of {maxOutputTokens} tokens. The summary should be concise and capture the main points of the tool output. - -The summarization should be done based on the content that is provided. Here are the basic rules to follow: -1. If the text is a directory listing or any output that is structural, use the history of the conversation to understand the context. Using this context try to understand what information we need from the tool output and return that as a response. -2. If the text is text content and there is nothing structural that we need, summarize the text. -3. If the text is the output of a shell command, use the history of the conversation to understand the context. Using this context try to understand what information we need from the tool output and return a summarization along with the stack trace of any error within the tags. The stack trace should be complete and not truncated. If there are warnings, you should include them in the summary within tags. - - -Text to summarize: -"{textToSummarize}" - -Return the summary string which should first contain an overall summarization of text followed by the full stack trace of errors and warnings in the tool output. -`; - -export const llmSummarizer: Summarizer = (result, geminiClient, abortSignal) => - summarizeToolOutput( - partToString(result.llmContent), - geminiClient, - abortSignal, - ); - -export async function summarizeToolOutput( - textToSummarize: string, - geminiClient: GeminiClient, - abortSignal: AbortSignal, - maxOutputTokens: number = 2000, -): Promise { - // There is going to be a slight difference here since we are comparing length of string with maxOutputTokens. - // This is meant to be a ballpark estimation of if we need to summarize the tool output. - if (!textToSummarize || textToSummarize.length < maxOutputTokens) { - return textToSummarize; - } - const prompt = SUMMARIZE_TOOL_OUTPUT_PROMPT.replace( - '{maxOutputTokens}', - String(maxOutputTokens), - ).replace('{textToSummarize}', textToSummarize); - - const contents: Content[] = [{ role: 'user', parts: [{ text: prompt }] }]; - const toolOutputSummarizerConfig: GenerateContentConfig = { - maxOutputTokens, - }; - try { - const parsedResponse = (await geminiClient.generateContent( - contents, - toolOutputSummarizerConfig, - abortSignal, - DEFAULT_QWEN_FLASH_MODEL, - )) as unknown as GenerateContentResponse; - return getResponseText(parsedResponse) || textToSummarize; - } catch (error) { - debugLogger.error('Failed to summarize tool output.', error); - return textToSummarize; - } -} diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index 8dfbeffa2..b9cfcd332 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -242,11 +242,6 @@ "type": "number", "default": -1 }, - "summarizeToolOutput": { - "description": "Settings for summarizing tool output.", - "type": "object", - "additionalProperties": true - }, "chatCompression": { "description": "Chat compression settings.", "type": "object",