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
+13 -25
View File
@@ -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
});
});
});
+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
* 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 };
}