mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-27 13:34:15 -07:00
feat(core): centralize line truncation and apply to all tools
This commit is contained in:
@@ -40,23 +40,17 @@ vi.mock('../core/coreToolHookTriggers.js', () => ({
|
|||||||
executeToolWithHooks: vi.fn(),
|
executeToolWithHooks: vi.fn(),
|
||||||
}));
|
}));
|
||||||
// Mock runInDevTraceSpan
|
// Mock runInDevTraceSpan
|
||||||
const runInDevTraceSpan = vi.hoisted(() =>
|
vi.mock('../telemetry/trace.js', () => ({
|
||||||
vi.fn(async (opts, fn) => {
|
runInDevTraceSpan: vi.fn(async (opts, fn) => {
|
||||||
const metadata = { attributes: opts.attributes || {} };
|
const metadata = { attributes: opts.attributes || {} };
|
||||||
return fn({
|
return fn({
|
||||||
metadata,
|
metadata,
|
||||||
endSpan: vi.fn(),
|
endSpan: vi.fn(),
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
}));
|
||||||
|
|
||||||
vi.mock('../index.js', async (importOriginal) => {
|
import { runInDevTraceSpan } from '../telemetry/trace.js';
|
||||||
const actual = await importOriginal<Record<string, unknown>>();
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
runInDevTraceSpan,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ToolExecutor', () => {
|
describe('ToolExecutor', () => {
|
||||||
let config: Config;
|
let config: Config;
|
||||||
|
|||||||
@@ -5,19 +5,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ToolCallRequestInfo,
|
|
||||||
ToolCallResponseInfo,
|
|
||||||
ToolResult,
|
ToolResult,
|
||||||
Config,
|
|
||||||
ToolResultDisplay,
|
ToolResultDisplay,
|
||||||
ToolLiveOutput,
|
ToolLiveOutput,
|
||||||
} from '../index.js';
|
} from '../tools/tools.js';
|
||||||
import {
|
import type { Config } from '../config/config.js';
|
||||||
ToolErrorType,
|
import { ToolErrorType } from '../tools/tool-error.js';
|
||||||
ToolOutputTruncatedEvent,
|
import { logToolOutputTruncated } from '../telemetry/loggers.js';
|
||||||
logToolOutputTruncated,
|
import { ToolOutputTruncatedEvent } from '../telemetry/types.js';
|
||||||
runInDevTraceSpan,
|
import { runInDevTraceSpan } from '../telemetry/trace.js';
|
||||||
} from '../index.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 { SHELL_TOOL_NAME } from '../tools/tool-names.js';
|
||||||
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
|
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
|
||||||
import { ShellToolInvocation } from '../tools/shell.js';
|
import { ShellToolInvocation } from '../tools/shell.js';
|
||||||
@@ -34,6 +32,8 @@ import type {
|
|||||||
ErroredToolCall,
|
ErroredToolCall,
|
||||||
SuccessfulToolCall,
|
SuccessfulToolCall,
|
||||||
CancelledToolCall,
|
CancelledToolCall,
|
||||||
|
ToolCallRequestInfo,
|
||||||
|
ToolCallResponseInfo,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { CoreToolCallStatus } from './types.js';
|
import { CoreToolCallStatus } from './types.js';
|
||||||
import {
|
import {
|
||||||
@@ -237,6 +237,10 @@ export class ToolExecutor {
|
|||||||
const toolName = call.request.originalRequestName || call.request.name;
|
const toolName = call.request.originalRequestName || call.request.name;
|
||||||
const callId = call.request.callId;
|
const callId = call.request.callId;
|
||||||
|
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
content = truncateLongLines(content, DEFAULT_MAX_LINE_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof content === 'string' && toolName === SHELL_TOOL_NAME) {
|
if (typeof content === 'string' && toolName === SHELL_TOOL_NAME) {
|
||||||
const threshold = this.config.getTruncateToolOutputThreshold();
|
const threshold = this.config.getTruncateToolOutputThreshold();
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,92 @@ export function truncateString(
|
|||||||
return str.slice(0, maxLength) + suffix;
|
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.
|
* Safely replaces placeholders in a template string with values from a replacements object.
|
||||||
* This performs a single-pass replacement to prevent double-interpolation attacks.
|
* This performs a single-pass replacement to prevent double-interpolation attacks.
|
||||||
|
|||||||
Reference in New Issue
Block a user