refactor: simplify tool output truncation to single config (#18446)

This commit is contained in:
Sandy Tao
2026-02-06 13:41:19 -08:00
committed by GitHub
parent fd72a8c40f
commit 28805a4b2d
22 changed files with 56 additions and 189 deletions
+1 -3
View File
@@ -101,9 +101,7 @@ they appear in the UI.
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` | | 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"` | | 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` | | 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` | Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation. | `40000` |
| 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` |
| 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` | | 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 ### Security
+3 -13
View File
@@ -716,20 +716,10 @@ their corresponding top-level category object in your `settings.json` file.
implementation. Provides faster search performance. implementation. Provides faster search performance.
- **Default:** `true` - **Default:** `true`
- **`tools.enableToolOutputTruncation`** (boolean):
- **Description:** Enable truncation of large tool outputs.
- **Default:** `true`
- **Requires restart:** Yes
- **`tools.truncateToolOutputThreshold`** (number): - **`tools.truncateToolOutputThreshold`** (number):
- **Description:** Truncate tool output if it is larger than this many - **Description:** Maximum characters to show when truncating large tool
characters. Set to -1 to disable. outputs. Set to 0 or negative to disable truncation.
- **Default:** `4000000` - **Default:** `40000`
- **Requires restart:** Yes
- **`tools.truncateToolOutputLines`** (number):
- **Description:** The number of lines to keep when truncating tool output.
- **Default:** `1000`
- **Requires restart:** Yes - **Requires restart:** Yes
- **`tools.disableLLMCorrection`** (boolean): - **`tools.disableLLMCorrection`** (boolean):
@@ -12,7 +12,6 @@ import type {
import { import {
ApprovalMode, ApprovalMode,
DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_MODEL,
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
GeminiClient, GeminiClient,
HookSystem, HookSystem,
@@ -47,7 +46,6 @@ export function createMockConfig(
} as Storage, } as Storage,
getTruncateToolOutputThreshold: () => getTruncateToolOutputThreshold: () =>
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
getActiveModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL), getActiveModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL),
getDebugMode: vi.fn().mockReturnValue(false), getDebugMode: vi.fn().mockReturnValue(false),
getContentGeneratorConfig: vi.fn().mockReturnValue({ model: 'gemini-pro' }), getContentGeneratorConfig: vi.fn().mockReturnValue({ model: 'gemini-pro' }),
-2
View File
@@ -800,8 +800,6 @@ export async function loadCliConfig(
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck, skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
enablePromptCompletion: settings.general?.enablePromptCompletion, enablePromptCompletion: settings.general?.enablePromptCompletion,
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold, truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
truncateToolOutputLines: settings.tools?.truncateToolOutputLines,
enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation,
eventEmitter: coreEvents, eventEmitter: coreEvents,
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos, useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
output: { output: {
+1 -20
View File
@@ -10,7 +10,6 @@
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
import { import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
DEFAULT_MODEL_CONFIGS, DEFAULT_MODEL_CONFIGS,
type MCPServerConfig, type MCPServerConfig,
@@ -1149,15 +1148,6 @@ const SETTINGS_SCHEMA = {
'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.', 'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.',
showInDialog: true, 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: { truncateToolOutputThreshold: {
type: 'number', type: 'number',
label: 'Tool Output Truncation Threshold', label: 'Tool Output Truncation Threshold',
@@ -1165,16 +1155,7 @@ const SETTINGS_SCHEMA = {
requiresRestart: true, requiresRestart: true,
default: DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, default: DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
description: description:
'Truncate tool output if it is larger than this many characters. Set to -1 to disable.', 'Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation.',
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.',
showInDialog: true, showInDialog: true,
}, },
disableLLMCorrection: { disableLLMCorrection: {
@@ -149,7 +149,6 @@ describe('Settings Repro', () => {
showColor: true, showColor: true,
enableInteractiveShell: true, enableInteractiveShell: true,
}, },
truncateToolOutputLines: 100,
}, },
experimental: { experimental: {
useModelRouter: false, useModelRouter: false,
@@ -1396,7 +1396,6 @@ describe('SettingsDialog', () => {
}, },
tools: { tools: {
truncateToolOutputThreshold: 50000, truncateToolOutputThreshold: 50000,
truncateToolOutputLines: 1000,
}, },
context: { context: {
discoveryMaxDirs: 500, discoveryMaxDirs: 500,
@@ -1465,7 +1464,6 @@ describe('SettingsDialog', () => {
enableInteractiveShell: true, enableInteractiveShell: true,
useRipgrep: true, useRipgrep: true,
truncateToolOutputThreshold: 25000, truncateToolOutputThreshold: 25000,
truncateToolOutputLines: 500,
}, },
security: { security: {
folderTrust: { folderTrust: {
@@ -25,7 +25,6 @@ import type {
AnyToolInvocation, AnyToolInvocation,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
ToolConfirmationOutcome, ToolConfirmationOutcome,
ApprovalMode, ApprovalMode,
@@ -70,7 +69,6 @@ const mockConfig = {
getProjectTempDir: () => '/tmp', getProjectTempDir: () => '/tmp',
}, },
getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, getTruncateToolOutputThreshold: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
getAllowedTools: vi.fn(() => []), getAllowedTools: vi.fn(() => []),
getActiveModel: () => PREVIEW_GEMINI_MODEL, getActiveModel: () => PREVIEW_GEMINI_MODEL,
getContentGeneratorConfig: () => ({ getContentGeneratorConfig: () => ({
+1 -4
View File
@@ -19,10 +19,7 @@ export {
type AnsiLine, type AnsiLine,
type AnsiToken, type AnsiToken,
} from './src/utils/terminalSerializer.js'; } from './src/utils/terminalSerializer.js';
export { export { DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD } from './src/config/config.js';
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
} from './src/config/config.js';
export { detectIdeFromEnv } from './src/ide/detect-ide.js'; export { detectIdeFromEnv } from './src/ide/detect-ide.js';
export { export {
logExtensionEnable, logExtensionEnable,
+4 -4
View File
@@ -1104,8 +1104,8 @@ describe('Server Config (config.ts)', () => {
1000, 1000,
); );
// 4 * (32000 - 1000) = 4 * 31000 = 124000 // 4 * (32000 - 1000) = 4 * 31000 = 124000
// default is 4_000_000 // default is 40_000, so min(124000, 40000) = 40000
expect(config.getTruncateToolOutputThreshold()).toBe(124000); expect(config.getTruncateToolOutputThreshold()).toBe(40_000);
}); });
it('should return the default threshold when the calculated value is larger', () => { it('should return the default threshold when the calculated value is larger', () => {
@@ -1115,8 +1115,8 @@ describe('Server Config (config.ts)', () => {
500_000, 500_000,
); );
// 4 * (2_000_000 - 500_000) = 4 * 1_500_000 = 6_000_000 // 4 * (2_000_000 - 500_000) = 4 * 1_500_000 = 6_000_000
// default is 4_000_000 // default is 40_000
expect(config.getTruncateToolOutputThreshold()).toBe(4_000_000); expect(config.getTruncateToolOutputThreshold()).toBe(40_000);
}); });
it('should use a custom truncateToolOutputThreshold if provided', () => { it('should use a custom truncateToolOutputThreshold if provided', () => {
+1 -17
View File
@@ -303,8 +303,7 @@ export {
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
}; };
export const DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 4_000_000; export const DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 40_000;
export const DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES = 1000;
export class MCPServerConfig { export class MCPServerConfig {
constructor( constructor(
@@ -442,8 +441,6 @@ export interface ConfigParameters {
extensionManagement?: boolean; extensionManagement?: boolean;
enablePromptCompletion?: boolean; enablePromptCompletion?: boolean;
truncateToolOutputThreshold?: number; truncateToolOutputThreshold?: number;
truncateToolOutputLines?: number;
enableToolOutputTruncation?: boolean;
eventEmitter?: EventEmitter; eventEmitter?: EventEmitter;
useWriteTodos?: boolean; useWriteTodos?: boolean;
policyEngineConfig?: PolicyEngineConfig; policyEngineConfig?: PolicyEngineConfig;
@@ -586,9 +583,7 @@ export class Config {
private readonly extensionManagement: boolean = true; private readonly extensionManagement: boolean = true;
private readonly enablePromptCompletion: boolean = false; private readonly enablePromptCompletion: boolean = false;
private readonly truncateToolOutputThreshold: number; private readonly truncateToolOutputThreshold: number;
private readonly truncateToolOutputLines: number;
private compressionTruncationCounter = 0; private compressionTruncationCounter = 0;
private readonly enableToolOutputTruncation: boolean;
private initialized: boolean = false; private initialized: boolean = false;
readonly storage: Storage; readonly storage: Storage;
private readonly fileExclusions: FileExclusions; private readonly fileExclusions: FileExclusions;
@@ -778,9 +773,6 @@ export class Config {
this.truncateToolOutputThreshold = this.truncateToolOutputThreshold =
params.truncateToolOutputThreshold ?? params.truncateToolOutputThreshold ??
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD; 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. // // TODO(joshualitt): Re-evaluate the todo tool for 3 family.
this.useWriteTodos = isPreviewModel(this.model) this.useWriteTodos = isPreviewModel(this.model)
? false ? false
@@ -2063,10 +2055,6 @@ export class Config {
return this.enablePromptCompletion; return this.enablePromptCompletion;
} }
getEnableToolOutputTruncation(): boolean {
return this.enableToolOutputTruncation;
}
getTruncateToolOutputThreshold(): number { getTruncateToolOutputThreshold(): number {
return Math.min( return Math.min(
// Estimate remaining context window in characters (1 token ~= 4 chars). // Estimate remaining context window in characters (1 token ~= 4 chars).
@@ -2076,10 +2064,6 @@ export class Config {
); );
} }
getTruncateToolOutputLines(): number {
return this.truncateToolOutputLines;
}
getNextCompressionTruncationId(): number { getNextCompressionTruncationId(): number {
return ++this.compressionTruncationCounter; return ++this.compressionTruncationCounter;
} }
@@ -23,7 +23,6 @@ import type {
MessageBus, MessageBus,
} from '../index.js'; } from '../index.js';
import { import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
BaseDeclarativeTool, BaseDeclarativeTool,
BaseToolInvocation, BaseToolInvocation,
@@ -271,7 +270,6 @@ function createMockConfig(overrides: Partial<Config> = {}): Config {
}, },
getTruncateToolOutputThreshold: () => getTruncateToolOutputThreshold: () =>
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
getTruncateToolOutputLines: () => DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
getToolRegistry: () => defaultToolRegistry, getToolRegistry: () => defaultToolRegistry,
getActiveModel: () => DEFAULT_GEMINI_MODEL, getActiveModel: () => DEFAULT_GEMINI_MODEL,
getGeminiClient: () => null, getGeminiClient: () => null,
@@ -44,7 +44,6 @@ describe('ToolExecutor', () => {
// Default mock implementation // Default mock implementation
vi.mocked(fileUtils.saveTruncatedToolOutput).mockResolvedValue({ vi.mocked(fileUtils.saveTruncatedToolOutput).mockResolvedValue({
outputFile: '/tmp/truncated_output.txt', outputFile: '/tmp/truncated_output.txt',
totalLines: 100,
}); });
vi.mocked(fileUtils.formatTruncatedToolOutput).mockReturnValue( vi.mocked(fileUtils.formatTruncatedToolOutput).mockReturnValue(
'TruncatedContent...', 'TruncatedContent...',
@@ -180,9 +179,7 @@ describe('ToolExecutor', () => {
it('should truncate large shell output', async () => { it('should truncate large shell output', async () => {
// 1. Setup Config for Truncation // 1. Setup Config for Truncation
vi.spyOn(config, 'getEnableToolOutputTruncation').mockReturnValue(true);
vi.spyOn(config, 'getTruncateToolOutputThreshold').mockReturnValue(10); vi.spyOn(config, 'getTruncateToolOutputThreshold').mockReturnValue(10);
vi.spyOn(config, 'getTruncateToolOutputLines').mockReturnValue(5);
const mockTool = new MockTool({ name: SHELL_TOOL_NAME }); const mockTool = new MockTool({ name: SHELL_TOOL_NAME });
const invocation = mockTool.build({}); const invocation = mockTool.build({});
@@ -227,7 +224,7 @@ describe('ToolExecutor', () => {
expect(fileUtils.formatTruncatedToolOutput).toHaveBeenCalledWith( expect(fileUtils.formatTruncatedToolOutput).toHaveBeenCalledWith(
longOutput, longOutput,
'/tmp/truncated_output.txt', '/tmp/truncated_output.txt',
5, // lines 10, // threshold (maxChars)
); );
expect(result.status).toBe('success'); expect(result.status).toBe('success');
+4 -12
View File
@@ -204,18 +204,11 @@ export class ToolExecutor {
const toolName = call.request.name; const toolName = call.request.name;
const callId = call.request.callId; const callId = call.request.callId;
if ( if (typeof content === 'string' && toolName === SHELL_TOOL_NAME) {
typeof content === 'string' &&
toolName === SHELL_TOOL_NAME &&
this.config.getEnableToolOutputTruncation() &&
this.config.getTruncateToolOutputThreshold() > 0 &&
this.config.getTruncateToolOutputLines() > 0
) {
const originalContentLength = content.length;
const threshold = this.config.getTruncateToolOutputThreshold(); 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( const { outputFile: savedPath } = await saveTruncatedToolOutput(
content, content,
toolName, toolName,
@@ -224,7 +217,7 @@ export class ToolExecutor {
this.config.getSessionId(), this.config.getSessionId(),
); );
outputFile = savedPath; outputFile = savedPath;
content = formatTruncatedToolOutput(content, outputFile, lines); content = formatTruncatedToolOutput(content, outputFile, threshold);
logToolOutputTruncated( logToolOutputTruncated(
this.config, this.config,
@@ -233,7 +226,6 @@ export class ToolExecutor {
originalContentLength, originalContentLength,
truncatedContentLength: content.length, truncatedContentLength: content.length,
threshold, threshold,
lines,
}), }),
); );
} }
@@ -183,6 +183,7 @@ describe('ChatCompressionService', () => {
getMessageBus: vi.fn().mockReturnValue(undefined), getMessageBus: vi.fn().mockReturnValue(undefined),
getHookSystem: () => undefined, getHookSystem: () => undefined,
getNextCompressionTruncationId: vi.fn().mockReturnValue(1), getNextCompressionTruncationId: vi.fn().mockReturnValue(1),
getTruncateToolOutputThreshold: vi.fn().mockReturnValue(40000),
storage: { storage: {
getProjectTempDir: vi.fn().mockReturnValue(testTempDir), getProjectTempDir: vi.fn().mockReturnValue(testTempDir),
}, },
@@ -581,10 +582,10 @@ describe('ChatCompressionService', () => {
const truncatedPart = shellResponse!.parts![0].functionResponse; const truncatedPart = shellResponse!.parts![0].functionResponse;
const content = truncatedPart?.response?.['output'] as string; const content = truncatedPart?.response?.['output'] as string;
// DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 40000 -> head=8000 (20%), tail=32000 (80%)
expect(content).toContain( 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 () => { 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 truncatedPart = rawResponse!.parts![0].functionResponse;
const content = truncatedPart?.response?.['output'] as string; const content = truncatedPart?.response?.['output'] as string;
// DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 40000 -> head=8000 (20%), tail=32000 (80%)
expect(content).toContain( expect(content).toContain(
'Output too large. Showing the last 4,000 characters of the output.', 'Showing first 8,000 and last 32,000 characters',
); );
}); });
@@ -49,11 +49,6 @@ export const COMPRESSION_PRESERVE_THRESHOLD = 0.3;
*/ */
export const COMPRESSION_FUNCTION_RESPONSE_TOKEN_BUDGET = 50_000; 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 * Returns the index of the oldest item to keep when compressing. May return
* contents.length which indicates that everything should be compressed. * contents.length which indicates that everything should be compressed.
@@ -189,11 +184,10 @@ async function truncateHistoryToBudget(
config.storage.getProjectTempDir(), config.storage.getProjectTempDir(),
); );
// Prepare a honest, readable snippet of the tail.
const truncatedMessage = formatTruncatedToolOutput( const truncatedMessage = formatTruncatedToolOutput(
contentStr, contentStr,
outputFile, outputFile,
COMPRESSION_TRUNCATE_LINES, config.getTruncateToolOutputThreshold(),
); );
newParts.unshift({ newParts.unshift({
@@ -1213,10 +1213,6 @@ export class ClearcutLogger {
EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_TRUNCATED_THRESHOLD, EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_TRUNCATED_THRESHOLD,
value: JSON.stringify(event.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( const logEvent = this.createLogEvent(
@@ -1663,7 +1663,6 @@ describe('loggers', () => {
originalContentLength: 1000, originalContentLength: 1000,
truncatedContentLength: 100, truncatedContentLength: 100,
threshold: 500, threshold: 500,
lines: 10,
}); });
logToolOutputTruncated(mockConfig, event); logToolOutputTruncated(mockConfig, event);
@@ -1683,7 +1682,6 @@ describe('loggers', () => {
original_content_length: 1000, original_content_length: 1000,
truncated_content_length: 100, truncated_content_length: 100,
threshold: 500, threshold: 500,
lines: 10,
}, },
}); });
}); });
-4
View File
@@ -1334,7 +1334,6 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent {
original_content_length: number; original_content_length: number;
truncated_content_length: number; truncated_content_length: number;
threshold: number; threshold: number;
lines: number;
prompt_id: string; prompt_id: string;
constructor( constructor(
@@ -1344,7 +1343,6 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent {
originalContentLength: number; originalContentLength: number;
truncatedContentLength: number; truncatedContentLength: number;
threshold: number; threshold: number;
lines: number;
}, },
) { ) {
this['event.name'] = this.eventName; this['event.name'] = this.eventName;
@@ -1353,7 +1351,6 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent {
this.original_content_length = details.originalContentLength; this.original_content_length = details.originalContentLength;
this.truncated_content_length = details.truncatedContentLength; this.truncated_content_length = details.truncatedContentLength;
this.threshold = details.threshold; this.threshold = details.threshold;
this.lines = details.lines;
} }
toOpenTelemetryAttributes(config: Config): LogAttributes { toOpenTelemetryAttributes(config: Config): LogAttributes {
@@ -1366,7 +1363,6 @@ export class ToolOutputTruncatedEvent implements BaseTelemetryEvent {
original_content_length: this.original_content_length, original_content_length: this.original_content_length,
truncated_content_length: this.truncated_content_length, truncated_content_length: this.truncated_content_length,
threshold: this.threshold, threshold: this.threshold,
lines: this.lines,
prompt_id: this.prompt_id, prompt_id: this.prompt_id,
}; };
} }
+13 -25
View File
@@ -1125,7 +1125,6 @@ describe('fileUtils', () => {
'shell_123.txt', 'shell_123.txt',
); );
expect(result.outputFile).toBe(expectedOutputFile); expect(result.outputFile).toBe(expectedOutputFile);
expect(result.totalLines).toBe(1);
const savedContent = await fsPromises.readFile( const savedContent = await fsPromises.readFile(
expectedOutputFile, expectedOutputFile,
@@ -1200,43 +1199,32 @@ describe('fileUtils', () => {
expect(result.outputFile).toBe(expectedOutputFile); expect(result.outputFile).toBe(expectedOutputFile);
}); });
it('should format multi-line output correctly', () => { it('should truncate showing first 20% and last 80%', () => {
const lines = Array.from({ length: 50 }, (_, i) => `line ${i}`); const content = 'abcdefghijklmnopqrstuvwxyz'; // 26 chars
const content = lines.join('\n');
const outputFile = '/tmp/out.txt'; const outputFile = '/tmp/out.txt';
// maxChars=10 -> head=2 (20%), tail=8 (80%)
const formatted = formatTruncatedToolOutput(content, outputFile, 10); const formatted = formatTruncatedToolOutput(content, outputFile, 10);
expect(formatted).toContain( expect(formatted).toContain('Showing first 2 and last 8 characters');
'Output too large. Showing the last 10 of 50 lines.',
);
expect(formatted).toContain('For full output see: /tmp/out.txt'); expect(formatted).toContain('For full output see: /tmp/out.txt');
expect(formatted).toContain('line 49'); expect(formatted).toContain('ab'); // first 2 chars
expect(formatted).not.toContain('line 0'); 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)', () => { it('should format large content with head/tail truncation', () => {
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', () => {
const content = 'a'.repeat(50000); const content = 'a'.repeat(50000);
const outputFile = '/tmp/out.txt'; 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( 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
}); });
}); });
}); });
+18 -39
View File
@@ -569,9 +569,6 @@ export async function fileExists(filePath: string): Promise<boolean> {
} }
} }
const MAX_TRUNCATED_LINE_WIDTH = 1000;
const MAX_TRUNCATED_CHARS = 4000;
/** /**
* Sanitizes a string for use as a filename part by removing path traversal * Sanitizes a string for use as a filename part by removing path traversal
* characters and other non-alphanumeric characters. * 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( export function formatTruncatedToolOutput(
contentStr: string, contentStr: string,
outputFile: string, outputFile: string,
truncateLines: number = 30, maxChars: number,
): string { ): string {
const physicalLines = contentStr.split('\n'); if (contentStr.length <= maxChars) return contentStr;
const totalPhysicalLines = physicalLines.length;
if (totalPhysicalLines > 1) { const headChars = Math.floor(maxChars * 0.2);
// Multi-line case: show last N lines, but protect against "elephant" lines. const tailChars = maxChars - headChars;
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 widthWarning = someLinesTruncatedInWidth const head = contentStr.slice(0, headChars);
? ' (some long lines truncated)' const tail = contentStr.slice(-tailChars);
: ''; const omittedChars = contentStr.length - headChars - tailChars;
return `Output too large. Showing the last ${processedLines.length} of ${totalPhysicalLines} lines${widthWarning}. For full output see: ${outputFile}
... return `Output too large. Showing first ${headChars.toLocaleString()} and last ${tailChars.toLocaleString()} characters. For full output see: ${outputFile}
${processedLines.join('\n')}`; ${head}
} else {
// Single massive line case: use character-based truncation description. ... [${omittedChars.toLocaleString()} characters omitted] ...
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} ${tail}`;
...${snippet}`;
}
} }
/** /**
@@ -631,7 +614,7 @@ export async function saveTruncatedToolOutput(
id: string | number, // Accept string (callId) or number (truncationId) id: string | number, // Accept string (callId) or number (truncationId)
projectTempDir: string, projectTempDir: string,
sessionId?: string, sessionId?: string,
): Promise<{ outputFile: string; totalLines: number }> { ): Promise<{ outputFile: string }> {
const safeToolName = sanitizeFilenamePart(toolName).toLowerCase(); const safeToolName = sanitizeFilenamePart(toolName).toLowerCase();
const safeId = sanitizeFilenamePart(id.toString()).toLowerCase(); const safeId = sanitizeFilenamePart(id.toString()).toLowerCase();
const fileName = `${safeToolName}_${safeId}.txt`; const fileName = `${safeToolName}_${safeId}.txt`;
@@ -646,9 +629,5 @@ export async function saveTruncatedToolOutput(
await fsPromises.mkdir(toolOutputDir, { recursive: true }); await fsPromises.mkdir(toolOutputDir, { recursive: true });
await fsPromises.writeFile(outputFile, content); await fsPromises.writeFile(outputFile, content);
const lines = content.split('\n'); return { outputFile };
return {
outputFile,
totalLines: lines.length,
};
} }
+3 -17
View File
@@ -1180,25 +1180,11 @@
"default": true, "default": true,
"type": "boolean" "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": { "truncateToolOutputThreshold": {
"title": "Tool Output Truncation Threshold", "title": "Tool Output Truncation Threshold",
"description": "Truncate tool output if it is larger than this many characters. Set to -1 to disable.", "description": "Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation.",
"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`", "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": 4000000, "default": 40000,
"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,
"type": "number" "type": "number"
}, },
"disableLLMCorrection": { "disableLLMCorrection": {