feat(core): centralize line truncation and apply to all tools

This commit is contained in:
Christian Gunderman
2026-03-02 18:48:56 -08:00
parent 0d69f9f7fa
commit 9850f01894
3 changed files with 104 additions and 20 deletions

View File

@@ -40,23 +40,17 @@ vi.mock('../core/coreToolHookTriggers.js', () => ({
executeToolWithHooks: vi.fn(),
}));
// Mock runInDevTraceSpan
const runInDevTraceSpan = vi.hoisted(() =>
vi.fn(async (opts, fn) => {
vi.mock('../telemetry/trace.js', () => ({
runInDevTraceSpan: vi.fn(async (opts, fn) => {
const metadata = { attributes: opts.attributes || {} };
return fn({
metadata,
endSpan: vi.fn(),
});
}),
);
}));
vi.mock('../index.js', async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>();
return {
...actual,
runInDevTraceSpan,
};
});
import { runInDevTraceSpan } from '../telemetry/trace.js';
describe('ToolExecutor', () => {
let config: Config;

View File

@@ -5,19 +5,17 @@
*/
import type {
ToolCallRequestInfo,
ToolCallResponseInfo,
ToolResult,
Config,
ToolResultDisplay,
ToolLiveOutput,
} from '../index.js';
import {
ToolErrorType,
ToolOutputTruncatedEvent,
logToolOutputTruncated,
runInDevTraceSpan,
} from '../index.js';
} from '../tools/tools.js';
import type { Config } from '../config/config.js';
import { ToolErrorType } from '../tools/tool-error.js';
import { logToolOutputTruncated } from '../telemetry/loggers.js';
import { ToolOutputTruncatedEvent } from '../telemetry/types.js';
import { runInDevTraceSpan } from '../telemetry/trace.js';
import { truncateLongLines } from '../utils/textUtils.js';
import { DEFAULT_MAX_LINE_LENGTH } from '../utils/constants.js';
import { SHELL_TOOL_NAME } from '../tools/tool-names.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import { ShellToolInvocation } from '../tools/shell.js';
@@ -34,6 +32,8 @@ import type {
ErroredToolCall,
SuccessfulToolCall,
CancelledToolCall,
ToolCallRequestInfo,
ToolCallResponseInfo,
} from './types.js';
import { CoreToolCallStatus } from './types.js';
import {
@@ -237,6 +237,10 @@ export class ToolExecutor {
const toolName = call.request.originalRequestName || call.request.name;
const callId = call.request.callId;
if (typeof content === 'string') {
content = truncateLongLines(content, DEFAULT_MAX_LINE_LENGTH);
}
if (typeof content === 'string' && toolName === SHELL_TOOL_NAME) {
const threshold = this.config.getTruncateToolOutputThreshold();

View File

@@ -83,6 +83,92 @@ export function truncateString(
return str.slice(0, maxLength) + suffix;
}
/**
* Options for line truncation.
*/
export interface TruncateLineOptions {
maxLength: number;
centerIndex?: number;
includeStats?: boolean;
}
/**
* Truncates a single line, optionally centering around a specific index.
*/
export function truncateLine(
line: string,
options: TruncateLineOptions,
): string {
const { maxLength, centerIndex, includeStats } = options;
const originalLength = line.length;
if (originalLength <= maxLength) {
return line;
}
let truncated: string;
let start = 0;
let end = maxLength;
if (centerIndex !== undefined) {
const halfLength = Math.floor(maxLength / 2);
start = Math.max(0, centerIndex - halfLength);
end = start + maxLength;
if (end > originalLength) {
end = originalLength;
start = Math.max(0, end - maxLength);
}
const prefix = start > 0 ? '... ' : '';
const suffix = end < originalLength ? ' ...' : '';
truncated = prefix + line.substring(start, end) + suffix;
} else {
truncated = line.substring(0, maxLength) + ' ...';
}
if (includeStats) {
const stats =
centerIndex !== undefined
? `[Truncated: showing characters ${start} to ${end} of ${originalLength}]`
: `[Truncated to ${maxLength} characters (total length: ${originalLength})]`;
if (centerIndex !== undefined) {
// For centered, we put stats at both ends if they are truncated
const prefix = start > 0 ? `${stats} ... ` : '';
const suffix = end < originalLength ? ` ... ${stats}` : '';
truncated = prefix + line.substring(start, end) + suffix;
} else {
truncated = line.substring(0, maxLength) + ` ${stats}`;
}
}
return truncated;
}
/**
* Truncates all lines in a string that exceed the maximum length.
*/
export function truncateLongLines(
text: string,
maxLength: number,
includeStats = true,
): string {
if (!text) return text;
const lines = text.split('\n');
let modified = false;
const processed = lines.map((line) => {
if (line.length > maxLength) {
modified = true;
return truncateLine(line, { maxLength, includeStats });
}
return line;
});
return modified ? processed.join('\n') : text;
}
/**
* Safely replaces placeholders in a template string with values from a replacements object.
* This performs a single-pass replacement to prevent double-interpolation attacks.