Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat(core): layered tool-output truncation, per-message budget, per-t…
…ool limits

Bound tool output before it enters conversation history, mirroring Claude
Code's three-layer model:

- Single-result: a per-tool/global char threshold spills oversized output to a
  temp file with a recoverable preview + read_file pointer; media parts are
  kept, with a token-aware fallback, 0600 owner-only perms on the spilled file,
  and a sentinel re-entrancy guard.
- Per-message: when one batch of tool results exceeds toolOutputBatchBudget
  (default 200k chars), the largest results are offloaded to disk; a warning is
  logged if it still can't get under budget.
- Per-tool: DeclarativeTool.maxOutputChars/truncateKeep getters (shell 30k,
  grep 20k, read-file self-managed, mcp 500k, agent 32k/tail).

Truncation runs in CoreToolScheduler, deferring PostToolUse/skill metadata
injection until after truncation so reminders are never bisected. Per-tool
budgets are char-only so the global line cap can't undercut a self-managed tool
(read-file) or a declared char budget (grep). The re-entrancy guard matches the
truncation prefix at the start of the output so an injected copy of the phrase
can't bypass the budget. Adds the toolOutputBatchBudget setting (core + CLI
schema) and supporting tests.
  • Loading branch information
LaZzyMan committed Jun 10, 2026
commit cdfef5190e5b5be24b23a36def36b7ea3122fff3
1 change: 1 addition & 0 deletions packages/cli/src/acp-integration/acpAgent.worktree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ vi.mock('@qwen-code/qwen-code-core', () => ({
})),
SessionService: vi.fn(),
SESSION_TITLE_MAX_LENGTH: 200,
DEFAULT_TOOL_OUTPUT_BATCH_BUDGET: 200_000,
tokenLimit: vi.fn(),
SessionStartSource: { Startup: 'startup', Resume: 'resume' },
SessionEndReason: { PromptInputExit: 'prompt_input_exit', Other: 'other' },
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1909,6 +1909,7 @@ export async function loadCliConfig(
skipStartupContext: settings.model?.skipStartupContext ?? false,
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
truncateToolOutputLines: settings.tools?.truncateToolOutputLines,
toolOutputBatchBudget: settings.tools?.toolOutputBatchBudget,
eventEmitter: appEvents,
gitCoAuthor: settings.general?.gitCoAuthor,
output: {
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
import {
ApprovalMode,
DEFAULT_STOP_HOOK_BLOCK_CAP,
DEFAULT_TOOL_OUTPUT_BATCH_BUDGET,
DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
} from '@qwen-code/qwen-code-core';
Expand Down Expand Up @@ -1962,6 +1963,16 @@ const SETTINGS_SCHEMA = {
description: 'The number of lines to keep when truncating tool output.',
showInDialog: false,
},
toolOutputBatchBudget: {
type: 'number',
label: 'Tool Output Batch Budget',
category: 'General',
requiresRestart: true,
default: DEFAULT_TOOL_OUTPUT_BATCH_BUDGET,
description:
'Per-message budget (characters) for the combined output of one batch of tool calls; the largest results are offloaded to disk when exceeded. Set to -1 to disable.',
showInDialog: false,
},
computerUse: {
type: 'object',
label: 'Computer Use',
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,12 @@ export interface ExtensionInstallMetadata {

export const DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 25_000;
export const DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES = 1000;
/**
* Per-message budget (chars) for the combined model-facing output of one
* batch of tool calls. When a batch's total output exceeds this, the largest
* results are offloaded to disk (with a recoverable pointer). `<= 0` disables.
*/
export const DEFAULT_TOOL_OUTPUT_BATCH_BUDGET = 200_000;

export class MCPServerConfig {
constructor(
Expand Down Expand Up @@ -795,6 +801,7 @@ export interface ConfigParameters {
skipLoopDetection?: boolean;
truncateToolOutputThreshold?: number;
truncateToolOutputLines?: number;
toolOutputBatchBudget?: number;
eventEmitter?: EventEmitter;
output?: OutputSettings;
inputFormat?: InputFormat;
Expand Down Expand Up @@ -1182,6 +1189,7 @@ export class Config {
private readonly fileExclusions: FileExclusions;
private readonly truncateToolOutputThreshold: number;
private readonly truncateToolOutputLines: number;
private readonly toolOutputBatchBudget: number;
private readonly eventEmitter?: EventEmitter;
private readonly channel: string | undefined;
private readonly jsonFd: number | undefined;
Expand Down Expand Up @@ -1376,6 +1384,8 @@ export class Config {
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD;
this.truncateToolOutputLines =
params.truncateToolOutputLines ?? DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES;
this.toolOutputBatchBudget =
params.toolOutputBatchBudget ?? DEFAULT_TOOL_OUTPUT_BATCH_BUDGET;
this.channel = params.channel;
this.jsonFd = params.jsonFd;
this.jsonFile = params.jsonFile;
Expand Down Expand Up @@ -3806,6 +3816,14 @@ export class Config {
return this.truncateToolOutputLines;
}

getToolOutputBatchBudget(): number {
if (this.toolOutputBatchBudget <= 0) {
return Number.POSITIVE_INFINITY;
}

return this.toolOutputBatchBudget;
}

getOutputFormat(): OutputFormat {
return this.outputFormat;
}
Expand Down
Loading
Loading