mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
add config: tool output char limit
This commit is contained in:
@@ -605,6 +605,7 @@ export async function loadCliConfig(
|
|||||||
shouldUseNodePtyShell: settings.shouldUseNodePtyShell,
|
shouldUseNodePtyShell: settings.shouldUseNodePtyShell,
|
||||||
skipStartupContext: settings.skipStartupContext,
|
skipStartupContext: settings.skipStartupContext,
|
||||||
skipNextSpeakerCheck: settings.skipNextSpeakerCheck,
|
skipNextSpeakerCheck: settings.skipNextSpeakerCheck,
|
||||||
|
toolOutputCharLimit: settings.toolOutputCharLimit,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -577,6 +577,16 @@ export const SETTINGS_SCHEMA = {
|
|||||||
description: 'The maximum number of tokens allowed in a session.',
|
description: 'The maximum number of tokens allowed in a session.',
|
||||||
showInDialog: false,
|
showInDialog: false,
|
||||||
},
|
},
|
||||||
|
toolOutputCharLimit: {
|
||||||
|
type: 'number',
|
||||||
|
label: 'Tool Output Character Limit',
|
||||||
|
category: 'General',
|
||||||
|
requiresRestart: false,
|
||||||
|
default: undefined as number | undefined,
|
||||||
|
description:
|
||||||
|
'Max characters for tool outputs (read_file, read_many_files, shell). If set, text content is truncated to this limit.',
|
||||||
|
showInDialog: true,
|
||||||
|
},
|
||||||
systemPromptMappings: {
|
systemPromptMappings: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
label: 'System Prompt Mappings',
|
label: 'System Prompt Mappings',
|
||||||
|
|||||||
@@ -239,6 +239,8 @@ export interface ConfigParameters {
|
|||||||
shouldUseNodePtyShell?: boolean;
|
shouldUseNodePtyShell?: boolean;
|
||||||
skipStartupContext?: boolean;
|
skipStartupContext?: boolean;
|
||||||
skipNextSpeakerCheck?: boolean;
|
skipNextSpeakerCheck?: boolean;
|
||||||
|
// Character limit for tool text outputs (files and shell)
|
||||||
|
toolOutputCharLimit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Config {
|
export class Config {
|
||||||
@@ -326,6 +328,7 @@ export class Config {
|
|||||||
private readonly skipStartupContext: boolean;
|
private readonly skipStartupContext: boolean;
|
||||||
private readonly skipNextSpeakerCheck: boolean;
|
private readonly skipNextSpeakerCheck: boolean;
|
||||||
private initialized: boolean = false;
|
private initialized: boolean = false;
|
||||||
|
private readonly toolOutputCharLimit?: number;
|
||||||
|
|
||||||
constructor(params: ConfigParameters) {
|
constructor(params: ConfigParameters) {
|
||||||
this.sessionId = params.sessionId;
|
this.sessionId = params.sessionId;
|
||||||
@@ -407,6 +410,7 @@ export class Config {
|
|||||||
this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false;
|
this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false;
|
||||||
this.skipStartupContext = params.skipStartupContext ?? true;
|
this.skipStartupContext = params.skipStartupContext ?? true;
|
||||||
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? true;
|
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? true;
|
||||||
|
this.toolOutputCharLimit = params.toolOutputCharLimit;
|
||||||
|
|
||||||
// Web search
|
// Web search
|
||||||
this.tavilyApiKey = params.tavilyApiKey;
|
this.tavilyApiKey = params.tavilyApiKey;
|
||||||
@@ -873,6 +877,14 @@ export class Config {
|
|||||||
return this.skipNextSpeakerCheck;
|
return this.skipNextSpeakerCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the configured maximum number of characters for tool outputs.
|
||||||
|
* If undefined, no character-based truncation is applied by tools.
|
||||||
|
*/
|
||||||
|
getToolOutputCharLimit(): number | undefined {
|
||||||
|
return this.toolOutputCharLimit;
|
||||||
|
}
|
||||||
|
|
||||||
async getGitService(): Promise<GitService> {
|
async getGitService(): Promise<GitService> {
|
||||||
if (!this.gitService) {
|
if (!this.gitService) {
|
||||||
this.gitService = new GitService(this.targetDir);
|
this.gitService = new GitService(this.targetDir);
|
||||||
|
|||||||
@@ -122,21 +122,31 @@ class ReadFileToolInvocation extends BaseToolInvocation<
|
|||||||
}
|
}
|
||||||
|
|
||||||
let llmContent: PartUnion;
|
let llmContent: PartUnion;
|
||||||
|
const charLimit = this.config.getToolOutputCharLimit();
|
||||||
if (result.isTruncated) {
|
if (result.isTruncated) {
|
||||||
const [start, end] = result.linesShown!;
|
const [start, end] = result.linesShown!;
|
||||||
const total = result.originalLineCount!;
|
const total = result.originalLineCount!;
|
||||||
const nextOffset = this.params.offset
|
const nextOffset = this.params.offset
|
||||||
? this.params.offset + end - start + 1
|
? this.params.offset + end - start + 1
|
||||||
: end;
|
: end;
|
||||||
llmContent = `
|
const header = `\nIMPORTANT: The file content has been truncated.\nStatus: Showing lines ${start}-${end} of ${total} total lines.\nAction: To read more of the file, you can use the 'offset' and 'limit' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use offset: ${nextOffset}.\n\n--- FILE CONTENT (truncated) ---\n`;
|
||||||
IMPORTANT: The file content has been truncated.
|
const body = typeof result.llmContent === 'string' ? result.llmContent : '';
|
||||||
Status: Showing lines ${start}-${end} of ${total} total lines.
|
let truncatedBody = body;
|
||||||
Action: To read more of the file, you can use the 'offset' and 'limit' parameters in a subsequent 'read_file' call. For example, to read the next section of the file, use offset: ${nextOffset}.
|
if (typeof charLimit === 'number' && charLimit > 0 && body.length > charLimit) {
|
||||||
|
truncatedBody = `${body.slice(0, charLimit)}\n[... File content truncated to ${charLimit} characters ...]`;
|
||||||
--- FILE CONTENT (truncated) ---
|
}
|
||||||
${result.llmContent}`;
|
llmContent = header + truncatedBody;
|
||||||
} else {
|
} else {
|
||||||
llmContent = result.llmContent || '';
|
let body = result.llmContent || '';
|
||||||
|
if (
|
||||||
|
typeof body === 'string' &&
|
||||||
|
typeof charLimit === 'number' &&
|
||||||
|
charLimit > 0 &&
|
||||||
|
body.length > charLimit
|
||||||
|
) {
|
||||||
|
body = `${body.slice(0, charLimit)}\n[... File content truncated to ${charLimit} characters ...]`;
|
||||||
|
}
|
||||||
|
llmContent = body;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines =
|
const lines =
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ ${finalExclusionPatternsForDescription
|
|||||||
const skippedFiles: Array<{ path: string; reason: string }> = [];
|
const skippedFiles: Array<{ path: string; reason: string }> = [];
|
||||||
const processedFilesRelativePaths: string[] = [];
|
const processedFilesRelativePaths: string[] = [];
|
||||||
const contentParts: PartListUnion = [];
|
const contentParts: PartListUnion = [];
|
||||||
|
const charLimit = this.config.getToolOutputCharLimit();
|
||||||
|
|
||||||
const effectiveExcludes = useDefaultExcludes
|
const effectiveExcludes = useDefaultExcludes
|
||||||
? [...DEFAULT_EXCLUDES, ...exclude]
|
? [...DEFAULT_EXCLUDES, ...exclude]
|
||||||
@@ -436,6 +437,9 @@ ${finalExclusionPatternsForDescription
|
|||||||
);
|
);
|
||||||
|
|
||||||
const results = await Promise.allSettled(fileProcessingPromises);
|
const results = await Promise.allSettled(fileProcessingPromises);
|
||||||
|
let remainingContentChars =
|
||||||
|
typeof charLimit === 'number' && charLimit > 0 ? charLimit : Number.POSITIVE_INFINITY;
|
||||||
|
let globalTruncated = false;
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
@@ -449,22 +453,47 @@ ${finalExclusionPatternsForDescription
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Handle successfully processed files
|
// Handle successfully processed files
|
||||||
const { filePath, relativePathForDisplay, fileReadResult } =
|
const { filePath, relativePathForDisplay, fileReadResult } = fileResult;
|
||||||
fileResult;
|
|
||||||
|
|
||||||
if (typeof fileReadResult.llmContent === 'string') {
|
if (typeof fileReadResult.llmContent === 'string') {
|
||||||
|
// Separator does not count toward char budget
|
||||||
const separator = DEFAULT_OUTPUT_SEPARATOR_FORMAT.replace(
|
const separator = DEFAULT_OUTPUT_SEPARATOR_FORMAT.replace(
|
||||||
'{filePath}',
|
'{filePath}',
|
||||||
filePath,
|
filePath,
|
||||||
);
|
);
|
||||||
let fileContentForLlm = '';
|
|
||||||
|
let prefix = `${separator}\n\n`;
|
||||||
|
// Warning header (if any) does not count toward char budget
|
||||||
if (fileReadResult.isTruncated) {
|
if (fileReadResult.isTruncated) {
|
||||||
fileContentForLlm += `[WARNING: This file was truncated. To view the full content, use the 'read_file' tool on this specific file.]\n\n`;
|
prefix += `[WARNING: This file was truncated. To view the full content, use the 'read_file' tool on this specific file.]\n\n`;
|
||||||
}
|
}
|
||||||
fileContentForLlm += fileReadResult.llmContent;
|
contentParts.push(prefix);
|
||||||
contentParts.push(`${separator}\n\n${fileContentForLlm}\n\n`);
|
|
||||||
|
// Apply global char budget to the actual file content only
|
||||||
|
if (remainingContentChars > 0) {
|
||||||
|
const body = fileReadResult.llmContent;
|
||||||
|
if (body.length <= remainingContentChars) {
|
||||||
|
contentParts.push(body + '\n\n');
|
||||||
|
remainingContentChars -= body.length;
|
||||||
} else {
|
} else {
|
||||||
// This is a Part for image/pdf, which we don't add the separator to.
|
contentParts.push(
|
||||||
|
body.slice(0, Math.max(0, remainingContentChars)),
|
||||||
|
);
|
||||||
|
contentParts.push(
|
||||||
|
`\n[... Content truncated to ${charLimit} characters across files ...]\n`,
|
||||||
|
);
|
||||||
|
remainingContentChars = 0;
|
||||||
|
globalTruncated = true;
|
||||||
|
}
|
||||||
|
} else if (!globalTruncated && typeof charLimit === 'number') {
|
||||||
|
// No remaining budget, emit a single global truncation marker after first overflow
|
||||||
|
contentParts.push(
|
||||||
|
`\n[... Content truncated to ${charLimit} characters across files ...]\n`,
|
||||||
|
);
|
||||||
|
globalTruncated = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-text parts (image/pdf) do not count toward char budget
|
||||||
contentParts.push(fileReadResult.llmContent);
|
contentParts.push(fileReadResult.llmContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,6 +567,10 @@ ${finalExclusionPatternsForDescription
|
|||||||
'No files matching the criteria were found or all were skipped.',
|
'No files matching the criteria were found or all were skipped.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (globalTruncated && typeof charLimit === 'number') {
|
||||||
|
displayMessage += `\n\nNote: Output truncated to ${charLimit} characters (text content only).`;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
llmContent: contentParts,
|
llmContent: contentParts,
|
||||||
returnDisplay: displayMessage.trim(),
|
returnDisplay: displayMessage.trim(),
|
||||||
|
|||||||
@@ -279,6 +279,24 @@ class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply character truncation (middle) to both llmContent and returnDisplay if configured
|
||||||
|
const charLimit = this.config.getToolOutputCharLimit();
|
||||||
|
const middleTruncate = (s: string, limit: number): string => {
|
||||||
|
if (!s || s.length <= limit) return s;
|
||||||
|
const marker = '\n[... Output truncated due to length ...]\n';
|
||||||
|
const keep = Math.max(0, Math.floor((limit - marker.length) / 2));
|
||||||
|
if (keep <= 0) {
|
||||||
|
return s.slice(0, limit);
|
||||||
|
}
|
||||||
|
return s.slice(0, keep) + marker + s.slice(s.length - keep);
|
||||||
|
};
|
||||||
|
if (typeof charLimit === 'number' && charLimit > 0) {
|
||||||
|
llmContent = middleTruncate(llmContent, charLimit);
|
||||||
|
if (returnDisplayMessage) {
|
||||||
|
returnDisplayMessage = middleTruncate(returnDisplayMessage, charLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const summarizeConfig = this.config.getSummarizeToolOutputConfig();
|
const summarizeConfig = this.config.getSummarizeToolOutputConfig();
|
||||||
if (summarizeConfig && summarizeConfig[ShellTool.Name]) {
|
if (summarizeConfig && summarizeConfig[ShellTool.Name]) {
|
||||||
const summary = await summarizeToolOutput(
|
const summary = await summarizeToolOutput(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import mime from 'mime-types';
|
|||||||
import { FileSystemService } from '../services/fileSystemService.js';
|
import { FileSystemService } from '../services/fileSystemService.js';
|
||||||
|
|
||||||
// Constants for text file processing
|
// Constants for text file processing
|
||||||
export const DEFAULT_MAX_LINES_TEXT_FILE = 2000;
|
export const DEFAULT_MAX_LINES_TEXT_FILE = 500;
|
||||||
const MAX_LINE_LENGTH_TEXT_FILE = 2000;
|
const MAX_LINE_LENGTH_TEXT_FILE = 2000;
|
||||||
|
|
||||||
// Default values for encoding and separator format
|
// Default values for encoding and separator format
|
||||||
|
|||||||
Reference in New Issue
Block a user