diff --git a/docs/cli/settings.md b/docs/cli/settings.md index e7741249f7..9a60f89a53 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -101,9 +101,7 @@ they appear in the UI. | Show Color | `tools.shell.showColor` | Show color in shell output. | `false` | | Approval Mode | `tools.approvalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` | | Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` | -| Enable Tool Output Truncation | `tools.enableToolOutputTruncation` | Enable truncation of large tool outputs. | `true` | -| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Truncate tool output if it is larger than this many characters. Set to -1 to disable. | `4000000` | -| Tool Output Truncation Lines | `tools.truncateToolOutputLines` | The number of lines to keep when truncating tool output. | `1000` | +| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation. | `40000` | | Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `true` | ### Security diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 066d866986..3b1d3899ae 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -716,20 +716,10 @@ their corresponding top-level category object in your `settings.json` file. implementation. Provides faster search performance. - **Default:** `true` -- **`tools.enableToolOutputTruncation`** (boolean): - - **Description:** Enable truncation of large tool outputs. - - **Default:** `true` - - **Requires restart:** Yes - - **`tools.truncateToolOutputThreshold`** (number): - - **Description:** Truncate tool output if it is larger than this many - characters. Set to -1 to disable. - - **Default:** `4000000` - - **Requires restart:** Yes - -- **`tools.truncateToolOutputLines`** (number): - - **Description:** The number of lines to keep when truncating tool output. - - **Default:** `1000` + - **Description:** Maximum characters to show when truncating large tool + outputs. Set to 0 or negative to disable truncation. + - **Default:** `40000` - **Requires restart:** Yes - **`tools.disableLLMCorrection`** (boolean): diff --git a/packages/a2a-server/src/utils/testing_utils.ts b/packages/a2a-server/src/utils/testing_utils.ts index 87c7315f82..36880fda79 100644 --- a/packages/a2a-server/src/utils/testing_utils.ts +++ b/packages/a2a-server/src/utils/testing_utils.ts @@ -12,7 +12,6 @@ import type { import { ApprovalMode, DEFAULT_GEMINI_MODEL, - DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, GeminiClient, HookSystem, @@ -47,7 +46,6 @@ export function createMockConfig( } as Storage, getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, - getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, getActiveModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL), getDebugMode: vi.fn().mockReturnValue(false), getContentGeneratorConfig: vi.fn().mockReturnValue({ model: 'gemini-pro' }), diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index dec86e980c..45bec5d41e 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -800,8 +800,6 @@ export async function loadCliConfig( skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck, enablePromptCompletion: settings.general?.enablePromptCompletion, truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold, - truncateToolOutputLines: settings.tools?.truncateToolOutputLines, - enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation, eventEmitter: coreEvents, useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos, output: { diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index c4224f2846..4cac04caf1 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -10,7 +10,6 @@ // -------------------------------------------------------------------------- import { - DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_MODEL_CONFIGS, type MCPServerConfig, @@ -1149,15 +1148,6 @@ const SETTINGS_SCHEMA = { 'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.', showInDialog: true, }, - enableToolOutputTruncation: { - type: 'boolean', - label: 'Enable Tool Output Truncation', - category: 'General', - requiresRestart: true, - default: true, - description: 'Enable truncation of large tool outputs.', - showInDialog: true, - }, truncateToolOutputThreshold: { type: 'number', label: 'Tool Output Truncation Threshold', @@ -1165,16 +1155,7 @@ const SETTINGS_SCHEMA = { requiresRestart: true, default: DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, description: - 'Truncate tool output if it is larger than this many characters. Set to -1 to disable.', - showInDialog: true, - }, - truncateToolOutputLines: { - type: 'number', - label: 'Tool Output Truncation Lines', - category: 'General', - requiresRestart: true, - default: DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, - description: 'The number of lines to keep when truncating tool output.', + 'Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation.', showInDialog: true, }, disableLLMCorrection: { diff --git a/packages/cli/src/config/settings_repro.test.ts b/packages/cli/src/config/settings_repro.test.ts index 846aea374c..a93450de35 100644 --- a/packages/cli/src/config/settings_repro.test.ts +++ b/packages/cli/src/config/settings_repro.test.ts @@ -149,7 +149,6 @@ describe('Settings Repro', () => { showColor: true, enableInteractiveShell: true, }, - truncateToolOutputLines: 100, }, experimental: { useModelRouter: false, diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index 025b275ffe..4c424941d1 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -1396,7 +1396,6 @@ describe('SettingsDialog', () => { }, tools: { truncateToolOutputThreshold: 50000, - truncateToolOutputLines: 1000, }, context: { discoveryMaxDirs: 500, @@ -1465,7 +1464,6 @@ describe('SettingsDialog', () => { enableInteractiveShell: true, useRipgrep: true, truncateToolOutputThreshold: 25000, - truncateToolOutputLines: 500, }, security: { folderTrust: { diff --git a/packages/cli/src/ui/hooks/useToolScheduler.test.ts b/packages/cli/src/ui/hooks/useToolScheduler.test.ts index 051d0e057f..81cafb4f34 100644 --- a/packages/cli/src/ui/hooks/useToolScheduler.test.ts +++ b/packages/cli/src/ui/hooks/useToolScheduler.test.ts @@ -25,7 +25,6 @@ import type { AnyToolInvocation, } from '@google/gemini-cli-core'; import { - DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, ToolConfirmationOutcome, ApprovalMode, @@ -70,7 +69,6 @@ const mockConfig = { getProjectTempDir: () => '/tmp', }, getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, - getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, getAllowedTools: vi.fn(() => []), getActiveModel: () => PREVIEW_GEMINI_MODEL, getContentGeneratorConfig: () => ({ diff --git a/packages/core/index.ts b/packages/core/index.ts index dfbf08336c..1d5dce60d3 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -19,10 +19,7 @@ export { type AnsiLine, type AnsiToken, } from './src/utils/terminalSerializer.js'; -export { - DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, - DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, -} from './src/config/config.js'; +export { DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD } from './src/config/config.js'; export { detectIdeFromEnv } from './src/ide/detect-ide.js'; export { logExtensionEnable, diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index ce67c53e74..312c1b5b0a 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -1104,8 +1104,8 @@ describe('Server Config (config.ts)', () => { 1000, ); // 4 * (32000 - 1000) = 4 * 31000 = 124000 - // default is 4_000_000 - expect(config.getTruncateToolOutputThreshold()).toBe(124000); + // default is 40_000, so min(124000, 40000) = 40000 + expect(config.getTruncateToolOutputThreshold()).toBe(40_000); }); it('should return the default threshold when the calculated value is larger', () => { @@ -1115,8 +1115,8 @@ describe('Server Config (config.ts)', () => { 500_000, ); // 4 * (2_000_000 - 500_000) = 4 * 1_500_000 = 6_000_000 - // default is 4_000_000 - expect(config.getTruncateToolOutputThreshold()).toBe(4_000_000); + // default is 40_000 + expect(config.getTruncateToolOutputThreshold()).toBe(40_000); }); it('should use a custom truncateToolOutputThreshold if provided', () => { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 17997e587d..48f81d081f 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -303,8 +303,7 @@ export { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, }; -export const DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 4_000_000; -export const DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES = 1000; +export const DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 40_000; export class MCPServerConfig { constructor( @@ -442,8 +441,6 @@ export interface ConfigParameters { extensionManagement?: boolean; enablePromptCompletion?: boolean; truncateToolOutputThreshold?: number; - truncateToolOutputLines?: number; - enableToolOutputTruncation?: boolean; eventEmitter?: EventEmitter; useWriteTodos?: boolean; policyEngineConfig?: PolicyEngineConfig; @@ -586,9 +583,7 @@ export class Config { private readonly extensionManagement: boolean = true; private readonly enablePromptCompletion: boolean = false; private readonly truncateToolOutputThreshold: number; - private readonly truncateToolOutputLines: number; private compressionTruncationCounter = 0; - private readonly enableToolOutputTruncation: boolean; private initialized: boolean = false; readonly storage: Storage; private readonly fileExclusions: FileExclusions; @@ -778,9 +773,6 @@ export class Config { this.truncateToolOutputThreshold = params.truncateToolOutputThreshold ?? DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD; - this.truncateToolOutputLines = - params.truncateToolOutputLines ?? DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES; - this.enableToolOutputTruncation = params.enableToolOutputTruncation ?? true; // // TODO(joshualitt): Re-evaluate the todo tool for 3 family. this.useWriteTodos = isPreviewModel(this.model) ? false @@ -2063,10 +2055,6 @@ export class Config { return this.enablePromptCompletion; } - getEnableToolOutputTruncation(): boolean { - return this.enableToolOutputTruncation; - } - getTruncateToolOutputThreshold(): number { return Math.min( // Estimate remaining context window in characters (1 token ~= 4 chars). @@ -2076,10 +2064,6 @@ export class Config { ); } - getTruncateToolOutputLines(): number { - return this.truncateToolOutputLines; - } - getNextCompressionTruncationId(): number { return ++this.compressionTruncationCounter; } diff --git a/packages/core/src/core/coreToolScheduler.test.ts b/packages/core/src/core/coreToolScheduler.test.ts index 6a5e3524a0..2755303c80 100644 --- a/packages/core/src/core/coreToolScheduler.test.ts +++ b/packages/core/src/core/coreToolScheduler.test.ts @@ -23,7 +23,6 @@ import type { MessageBus, } from '../index.js'; import { - DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, BaseDeclarativeTool, BaseToolInvocation, @@ -271,7 +270,6 @@ function createMockConfig(overrides: Partial = {}): Config { }, getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, - getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, getToolRegistry: () => defaultToolRegistry, getActiveModel: () => DEFAULT_GEMINI_MODEL, getGeminiClient: () => null, diff --git a/packages/core/src/scheduler/tool-executor.test.ts b/packages/core/src/scheduler/tool-executor.test.ts index 2470a39dcd..d5e8ac0a26 100644 --- a/packages/core/src/scheduler/tool-executor.test.ts +++ b/packages/core/src/scheduler/tool-executor.test.ts @@ -44,7 +44,6 @@ describe('ToolExecutor', () => { // Default mock implementation vi.mocked(fileUtils.saveTruncatedToolOutput).mockResolvedValue({ outputFile: '/tmp/truncated_output.txt', - totalLines: 100, }); vi.mocked(fileUtils.formatTruncatedToolOutput).mockReturnValue( 'TruncatedContent...', @@ -180,9 +179,7 @@ describe('ToolExecutor', () => { it('should truncate large shell output', async () => { // 1. Setup Config for Truncation - vi.spyOn(config, 'getEnableToolOutputTruncation').mockReturnValue(true); vi.spyOn(config, 'getTruncateToolOutputThreshold').mockReturnValue(10); - vi.spyOn(config, 'getTruncateToolOutputLines').mockReturnValue(5); const mockTool = new MockTool({ name: SHELL_TOOL_NAME }); const invocation = mockTool.build({}); @@ -227,7 +224,7 @@ describe('ToolExecutor', () => { expect(fileUtils.formatTruncatedToolOutput).toHaveBeenCalledWith( longOutput, '/tmp/truncated_output.txt', - 5, // lines + 10, // threshold (maxChars) ); expect(result.status).toBe('success'); diff --git a/packages/core/src/scheduler/tool-executor.ts b/packages/core/src/scheduler/tool-executor.ts index ec02d25953..76b25f7c67 100644 --- a/packages/core/src/scheduler/tool-executor.ts +++ b/packages/core/src/scheduler/tool-executor.ts @@ -204,18 +204,11 @@ export class ToolExecutor { const toolName = call.request.name; const callId = call.request.callId; - if ( - typeof content === 'string' && - toolName === SHELL_TOOL_NAME && - this.config.getEnableToolOutputTruncation() && - this.config.getTruncateToolOutputThreshold() > 0 && - this.config.getTruncateToolOutputLines() > 0 - ) { - const originalContentLength = content.length; + if (typeof content === 'string' && toolName === SHELL_TOOL_NAME) { const threshold = this.config.getTruncateToolOutputThreshold(); - const lines = this.config.getTruncateToolOutputLines(); - if (content.length > threshold) { + if (threshold > 0 && content.length > threshold) { + const originalContentLength = content.length; const { outputFile: savedPath } = await saveTruncatedToolOutput( content, toolName, @@ -224,7 +217,7 @@ export class ToolExecutor { this.config.getSessionId(), ); outputFile = savedPath; - content = formatTruncatedToolOutput(content, outputFile, lines); + content = formatTruncatedToolOutput(content, outputFile, threshold); logToolOutputTruncated( this.config, @@ -233,7 +226,6 @@ export class ToolExecutor { originalContentLength, truncatedContentLength: content.length, threshold, - lines, }), ); } diff --git a/packages/core/src/services/chatCompressionService.test.ts b/packages/core/src/services/chatCompressionService.test.ts index 8b3ff2cb16..4f5a712f2d 100644 --- a/packages/core/src/services/chatCompressionService.test.ts +++ b/packages/core/src/services/chatCompressionService.test.ts @@ -183,6 +183,7 @@ describe('ChatCompressionService', () => { getMessageBus: vi.fn().mockReturnValue(undefined), getHookSystem: () => undefined, getNextCompressionTruncationId: vi.fn().mockReturnValue(1), + getTruncateToolOutputThreshold: vi.fn().mockReturnValue(40000), storage: { getProjectTempDir: vi.fn().mockReturnValue(testTempDir), }, @@ -581,10 +582,10 @@ describe('ChatCompressionService', () => { const truncatedPart = shellResponse!.parts![0].functionResponse; const content = truncatedPart?.response?.['output'] as string; + // DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 40000 -> head=8000 (20%), tail=32000 (80%) expect(content).toContain( - 'Output too large. Showing the last 4,000 characters of the output.', + 'Showing first 8,000 and last 32,000 characters', ); - // It's a single line, so NO [LINE WIDTH TRUNCATED] }); it('should use character-based truncation for massive single-line raw strings', async () => { @@ -645,8 +646,9 @@ describe('ChatCompressionService', () => { const truncatedPart = rawResponse!.parts![0].functionResponse; const content = truncatedPart?.response?.['output'] as string; + // DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 40000 -> head=8000 (20%), tail=32000 (80%) expect(content).toContain( - 'Output too large. Showing the last 4,000 characters of the output.', + 'Showing first 8,000 and last 32,000 characters', ); }); diff --git a/packages/core/src/services/chatCompressionService.ts b/packages/core/src/services/chatCompressionService.ts index 6cbaf4f4a1..00e58bb2db 100644 --- a/packages/core/src/services/chatCompressionService.ts +++ b/packages/core/src/services/chatCompressionService.ts @@ -49,11 +49,6 @@ export const COMPRESSION_PRESERVE_THRESHOLD = 0.3; */ export const COMPRESSION_FUNCTION_RESPONSE_TOKEN_BUDGET = 50_000; -/** - * The number of lines to keep when truncating a function response during compression. - */ -export const COMPRESSION_TRUNCATE_LINES = 30; - /** * Returns the index of the oldest item to keep when compressing. May return * contents.length which indicates that everything should be compressed. @@ -189,11 +184,10 @@ async function truncateHistoryToBudget( config.storage.getProjectTempDir(), ); - // Prepare a honest, readable snippet of the tail. const truncatedMessage = formatTruncatedToolOutput( contentStr, outputFile, - COMPRESSION_TRUNCATE_LINES, + config.getTruncateToolOutputThreshold(), ); newParts.unshift({ diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 2afe9cf356..4a7f1db8d0 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -1213,10 +1213,6 @@ export class ClearcutLogger { EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_TRUNCATED_THRESHOLD, value: JSON.stringify(event.threshold), }, - { - gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_TRUNCATED_LINES, - value: JSON.stringify(event.lines), - }, ]; const logEvent = this.createLogEvent( diff --git a/packages/core/src/telemetry/loggers.test.ts b/packages/core/src/telemetry/loggers.test.ts index 0fe51a7120..246bed694d 100644 --- a/packages/core/src/telemetry/loggers.test.ts +++ b/packages/core/src/telemetry/loggers.test.ts @@ -1663,7 +1663,6 @@ describe('loggers', () => { originalContentLength: 1000, truncatedContentLength: 100, threshold: 500, - lines: 10, }); logToolOutputTruncated(mockConfig, event); @@ -1683,7 +1682,6 @@ describe('loggers', () => { original_content_length: 1000, truncated_content_length: 100, threshold: 500, - lines: 10, }, }); }); diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 0271aa4344..7a7399fd74 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -1334,7 +1334,6 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent { original_content_length: number; truncated_content_length: number; threshold: number; - lines: number; prompt_id: string; constructor( @@ -1344,7 +1343,6 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent { originalContentLength: number; truncatedContentLength: number; threshold: number; - lines: number; }, ) { this['event.name'] = this.eventName; @@ -1353,7 +1351,6 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent { this.original_content_length = details.originalContentLength; this.truncated_content_length = details.truncatedContentLength; this.threshold = details.threshold; - this.lines = details.lines; } toOpenTelemetryAttributes(config: Config): LogAttributes { @@ -1366,7 +1363,6 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent { original_content_length: this.original_content_length, truncated_content_length: this.truncated_content_length, threshold: this.threshold, - lines: this.lines, prompt_id: this.prompt_id, }; } diff --git a/packages/core/src/utils/fileUtils.test.ts b/packages/core/src/utils/fileUtils.test.ts index 95b10ced69..79ac66d24c 100644 --- a/packages/core/src/utils/fileUtils.test.ts +++ b/packages/core/src/utils/fileUtils.test.ts @@ -1125,7 +1125,6 @@ describe('fileUtils', () => { 'shell_123.txt', ); expect(result.outputFile).toBe(expectedOutputFile); - expect(result.totalLines).toBe(1); const savedContent = await fsPromises.readFile( expectedOutputFile, @@ -1200,43 +1199,32 @@ describe('fileUtils', () => { expect(result.outputFile).toBe(expectedOutputFile); }); - it('should format multi-line output correctly', () => { - const lines = Array.from({ length: 50 }, (_, i) => `line ${i}`); - const content = lines.join('\n'); + it('should truncate showing first 20% and last 80%', () => { + const content = 'abcdefghijklmnopqrstuvwxyz'; // 26 chars const outputFile = '/tmp/out.txt'; + // maxChars=10 -> head=2 (20%), tail=8 (80%) const formatted = formatTruncatedToolOutput(content, outputFile, 10); - expect(formatted).toContain( - 'Output too large. Showing the last 10 of 50 lines.', - ); + expect(formatted).toContain('Showing first 2 and last 8 characters'); expect(formatted).toContain('For full output see: /tmp/out.txt'); - expect(formatted).toContain('line 49'); - expect(formatted).not.toContain('line 0'); + expect(formatted).toContain('ab'); // first 2 chars + expect(formatted).toContain('stuvwxyz'); // last 8 chars + expect(formatted).toContain('[16 characters omitted]'); // 26 - 2 - 8 = 16 }); - it('should truncate "elephant lines" (long single line in multi-line output)', () => { - const longLine = 'a'.repeat(2000); - const content = `line 1\n${longLine}\nline 3`; - const outputFile = '/tmp/out.txt'; - - const formatted = formatTruncatedToolOutput(content, outputFile, 3); - - expect(formatted).toContain('(some long lines truncated)'); - expect(formatted).toContain('... [LINE WIDTH TRUNCATED]'); - expect(formatted.length).toBeLessThan(longLine.length); - }); - - it('should handle massive single-line string with character-based truncation', () => { + it('should format large content with head/tail truncation', () => { const content = 'a'.repeat(50000); const outputFile = '/tmp/out.txt'; - const formatted = formatTruncatedToolOutput(content, outputFile); + // maxChars=4000 -> head=800 (20%), tail=3200 (80%) + const formatted = formatTruncatedToolOutput(content, outputFile, 4000); expect(formatted).toContain( - 'Output too large. Showing the last 4,000 characters', + 'Showing first 800 and last 3,200 characters', ); - expect(formatted.endsWith(content.slice(-4000))).toBe(true); + expect(formatted).toContain('For full output see: /tmp/out.txt'); + expect(formatted).toContain('[46,000 characters omitted]'); // 50000 - 800 - 3200 }); }); }); diff --git a/packages/core/src/utils/fileUtils.ts b/packages/core/src/utils/fileUtils.ts index bac694d6d9..d9c01ae36a 100644 --- a/packages/core/src/utils/fileUtils.ts +++ b/packages/core/src/utils/fileUtils.ts @@ -569,9 +569,6 @@ export async function fileExists(filePath: string): Promise { } } -const MAX_TRUNCATED_LINE_WIDTH = 1000; -const MAX_TRUNCATED_CHARS = 4000; - /** * Sanitizes a string for use as a filename part by removing path traversal * characters and other non-alphanumeric characters. @@ -581,43 +578,29 @@ export function sanitizeFilenamePart(part: string): string { } /** - * Formats a truncated message for tool output, handling multi-line and single-line (elephant) cases. + * Formats a truncated message for tool output. + * Shows the first 20% and last 80% of the allowed characters with a marker in between. */ export function formatTruncatedToolOutput( contentStr: string, outputFile: string, - truncateLines: number = 30, + maxChars: number, ): string { - const physicalLines = contentStr.split('\n'); - const totalPhysicalLines = physicalLines.length; + if (contentStr.length <= maxChars) return contentStr; - if (totalPhysicalLines > 1) { - // Multi-line case: show last N lines, but protect against "elephant" lines. - const lastLines = physicalLines.slice(-truncateLines); - let someLinesTruncatedInWidth = false; - const processedLines = lastLines.map((line) => { - if (line.length > MAX_TRUNCATED_LINE_WIDTH) { - someLinesTruncatedInWidth = true; - return ( - line.substring(0, MAX_TRUNCATED_LINE_WIDTH) + - '... [LINE WIDTH TRUNCATED]' - ); - } - return line; - }); + const headChars = Math.floor(maxChars * 0.2); + const tailChars = maxChars - headChars; - const widthWarning = someLinesTruncatedInWidth - ? ' (some long lines truncated)' - : ''; - return `Output too large. Showing the last ${processedLines.length} of ${totalPhysicalLines} lines${widthWarning}. For full output see: ${outputFile} -... -${processedLines.join('\n')}`; - } else { - // Single massive line case: use character-based truncation description. - const snippet = contentStr.slice(-MAX_TRUNCATED_CHARS); - return `Output too large. Showing the last ${MAX_TRUNCATED_CHARS.toLocaleString()} characters of the output. For full output see: ${outputFile} -...${snippet}`; - } + const head = contentStr.slice(0, headChars); + const tail = contentStr.slice(-tailChars); + const omittedChars = contentStr.length - headChars - tailChars; + + return `Output too large. Showing first ${headChars.toLocaleString()} and last ${tailChars.toLocaleString()} characters. For full output see: ${outputFile} +${head} + +... [${omittedChars.toLocaleString()} characters omitted] ... + +${tail}`; } /** @@ -631,7 +614,7 @@ export async function saveTruncatedToolOutput( id: string | number, // Accept string (callId) or number (truncationId) projectTempDir: string, sessionId?: string, -): Promise<{ outputFile: string; totalLines: number }> { +): Promise<{ outputFile: string }> { const safeToolName = sanitizeFilenamePart(toolName).toLowerCase(); const safeId = sanitizeFilenamePart(id.toString()).toLowerCase(); const fileName = `${safeToolName}_${safeId}.txt`; @@ -646,9 +629,5 @@ export async function saveTruncatedToolOutput( await fsPromises.mkdir(toolOutputDir, { recursive: true }); await fsPromises.writeFile(outputFile, content); - const lines = content.split('\n'); - return { - outputFile, - totalLines: lines.length, - }; + return { outputFile }; } diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 2cac0ed760..0e9a9cce9b 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -1180,25 +1180,11 @@ "default": true, "type": "boolean" }, - "enableToolOutputTruncation": { - "title": "Enable Tool Output Truncation", - "description": "Enable truncation of large tool outputs.", - "markdownDescription": "Enable truncation of large tool outputs.\n\n- Category: `General`\n- Requires restart: `yes`\n- Default: `true`", - "default": true, - "type": "boolean" - }, "truncateToolOutputThreshold": { "title": "Tool Output Truncation Threshold", - "description": "Truncate tool output if it is larger than this many characters. Set to -1 to disable.", - "markdownDescription": "Truncate tool output if it is larger than this many characters. Set to -1 to disable.\n\n- Category: `General`\n- Requires restart: `yes`\n- Default: `4000000`", - "default": 4000000, - "type": "number" - }, - "truncateToolOutputLines": { - "title": "Tool Output Truncation Lines", - "description": "The number of lines to keep when truncating tool output.", - "markdownDescription": "The number of lines to keep when truncating tool output.\n\n- Category: `General`\n- Requires restart: `yes`\n- Default: `1000`", - "default": 1000, + "description": "Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation.", + "markdownDescription": "Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation.\n\n- Category: `General`\n- Requires restart: `yes`\n- Default: `40000`", + "default": 40000, "type": "number" }, "disableLLMCorrection": {