Repository navigation
feat(core): consistent oversized-tool-result handling, plus raw-vs-injected size telemetry for every tool #11536
Description
Latest PR verification — 2026-10-07
Formal, open PR #13599 is now at 1e92feee82ed7ad297cac5e65829eeb1761f859c. It preserves the tested production tree and includes the parallel patrol fixes additively. Latest targeted-file receipts cover 1,509 distinct executed passing cases; affected core/CLI builds and typechecks passed. High-pressure native CLI/tmux report and actual JSON MCP discovery/invocation/tmux report verify next-request sizes, complete-output recovery and digests. The reports pin their individual executed commits and controlled-provider boundaries.
MCP deliberately inherits both global character and line limits (defaults 80,000 / 2,000), and explicit values remain independent. This is a proposed default/observability improvement with local verification, not a claim of production billing savings. The PR is unmerged and unreleased; keep this issue open until merge.
中文说明
正式 PR #13599 已推送到 1e92feee82ed,增量纳入并行修正,保留实际验收的 production tree。最新定向文件回执去重后共 1,509 个实际执行通过的用例,相关 core/CLI 构建与类型检查通过;两份真实 tmux 报告分别核验高压力 CLI 与 MCP JSON 的实际请求大小、完整恢复文件及摘要,并明确各自测试 commit 和受控 provider 边界。MCP 同时跟随全局字符/行数预算,默认 80,000 / 2,000,显式值分别有效。PR 未合入、未发布,issue 继续开放,不宣称生产账单收益。
Current main verification — 2026-10-07
Verified at 1aba19c87889 using freshly built core/CLI artifacts.
This issue is partially implemented, not complete. PR #11727 has merged and improves producer-side budget ordering. In the freshly built core, a successful read_file ToolCallEvent still exposes content_length but no raw_content_length or injected_content_length fields. Source inspection also confirms that the scheduler recomputes model-facing length after truncation. This is a telemetry-type/configuration check, not an end-to-end metrics-backend run. Current finalizer diagnostics are useful prior work, but they do not prove the requested per-call raw-versus-injected reporting and consistent handling across tools. The issue remains open.
中文说明
已有预算相关修复合并,但每次调用的原始大小与注入大小统计仍未完整提供,继续保持开放。此次证据是实际类型/配置和源码检查,不是指标后端验收。
What would you like to be added?
Two coupled changes: make oversized tool-result handling consistent and observable across tools, and report raw-vs-injected result size for every tool call.
A. Oversized-result handling
origin/main already has an extensive pipeline, so this is not a greenfield request. Four ordered layers run in coreToolScheduler before a result reaches history:
- Persistence gate —
maybePersistLargeToolResult()(L6353, called at L5423/L5831). Fires abovegetTruncateToolOutputThreshold() + GATE_HEADROOM(3000, so 28k by default) and replaces the result with a<persisted-output>stub pointing attool-results/<callId>.txt. Exempt:GATE_EXEMPT_TOOLS=read_file,read_mcp_resource,enter_plan_mode. - Per-tool / global truncation —
truncateLlmContent()(L5567) →truncateAndSaveToFile(), budget frommaxOutputChars, previewPREVIEW_SIZE_CHARS = 2000(head/tail), sha256 digest (FULL_OUTPUT_DIGEST_LABEL), idempotency sentinel (TOOL_OUTPUT_TRUNCATED_PREFIX). - Combined second pass — L5603, re-bounds at
COMBINED_PASS_TOLERANCE_FACTOR(2x) after hook/skill<system-reminder>metadata is appended. - Per-message batch budget —
applyBatchOutputBudget()(L6403) →finalizeToolResponses()(tools/tool-response-finalizer.ts), reducing the aggregate of parallel calls toDEFAULT_TOOL_OUTPUT_BATCH_BUDGET = 200_000.
Global defaults: DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD = 25_000, DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES = 1000 (config/config.ts L773-780). Per-tool budgets vary by more than an order of magnitude:
| tool | maxOutputChars |
|---|---|
shell |
30,000 default (getShellOutputThreshold, config-overridable); also truncates in-execute() |
grep / ripGrep |
20,000 |
agent |
32,000 (truncateKeep = tail) |
web_search |
100,000 + 2,000 envelope headroom |
MCP tool (mcp-tool.ts) |
500,000; also truncates in-execute() via truncateTextParts() |
read_file, read_mcp_resource, enter_plan_mode |
Infinity (self-managed, and gate-exempt) |
| everything else | undefined → global 25,000 |
Requests:
- Re-examine the 500k MCP budget. An MCP result under 500k chars is injected verbatim: no spill, no preview, no event — in practice the largest uncontrolled contributor to injected context, and it is 20x the global default.
web_fetch(MAX_CONTENT_CHARS = 100_000, head-onlytruncateText()) andweb_searchself-truncate insideexecute()with their own wording and no spill-to-file, whileshelland MCP self-truncate and spill. The recovery story therefore differs per tool for no stated reason; worth consolidating or at least documenting which mechanism wins.- Improve the recovery path when a result does spill. The model sees head+tail only and loses the middle; the ask from the field is to make the stub self-sufficient (total chars/lines, digest, and an explicit "page in with
read_fileoffset/limit" hint) so the model reads the spilled file instead of re-running the tool.truncateKeepis already the per-tool hook. - Consider a middle tier between "inject verbatim" and "character-window slice" for structured results (notably MCP JSON): keys/shape + row counts + a sample. A char window can cut JSON mid-token, which is the accuracy complaint behind this request.
B. Telemetry: raw vs injected size, on all four layers
Current state:
tool_output_truncatedcovers exactly one of the four layers.logToolOutputTruncated()is emitted only fromtruncateToolOutput()(tools/truncation.ts), and only insideif (result.outputFile). The persistence gate (persistAndTruncateToolResult) and the batch budget (tool-response-finalizer.ts,tool-result-retention.ts) contain no telemetry calls at all — verified by grep. So the two layers that offload the most content are unobservable, and even layer 2 silently skips the fallback path where the wrapped form is not smaller than the original.ToolCallEvent.content_lengthis post-truncation only. The scheduler captures the raw length first, then deliberately overwrites it (// Recompute AFTER truncation so it reflects the model-facing length, L5659) and again after the batch pass (L6440). The raw number is lost for every call that did not spill.contentLengthstaysundefinedfor non-string (Part[]) results, so a whole class of MCP results reports no size at all ontool_call.- No metric carries a size dimension.
recordToolCallMetricsattributes arefunction_name,status,success,decision,tool_typeonly;TOOL_CALL_COUNT/TOOL_CALL_LATENCY/TOOL_EXECUTION_*have nothing size-related, andendToolExecutionSpan()sets no content-length attribute. Nothing is aggregatable in the metrics backend. content_lengthdoes not reach the RUM path either —QwenLogger.logToolCallEventhas no size field (it does mirrororiginal_content_length/truncated_content_lengthfor the truncation event).ToolResultPersistedEventis dead code. Declared intelemetry/types.tsL977 withtool_name/bytes_written/output_file/prompt_id, listed in the event union, but never constructed and never logged — there is nologToolResultPersistedand no call site. This is the obvious ready-made hook for layer-1 telemetry.- Everything is measured in characters; the governance target is tokens.
Requests:
- On every completed tool call, report
raw_content_lengthandinjected_content_length, plus atruncatedflag and the applied budget with its source (per-tool vs global fallback vs batch). Compute it forPart[]results too. - Emit size telemetry from all four layers — wiring up the existing
ToolResultPersistedEventfor the persistence gate is the cheapest first step — so offload rate is measurable per layer, not just for layer 2. - Add an estimated token count next to the character count, reusing the estimator the compression pipeline already uses (
estimatePartChars,services/compactionInputSlimming.ts) so both agree about the same history. - Add a size histogram metric (e.g.
qwen_code.tool.result.chars/.tokens) withfunction_name,tool_typeandtruncatedattributes, so per-tool consumption can be charted and alerted on rather than sampled from logs. Respect the existing metric-cardinality controls.
Already done, do not re-request: analyzeToolResultRetention() diagnostics (toolResultCount, totalChars, largestResultChars, oversizedResultCount) are surfaced by /doctor memory (packages/cli/src/ui/commands/doctorCommand.ts L496/L569). Extending that same view with the layer-1/4 blind spots would be a natural follow-on.
Why is this needed?
Tool results are the dominant controllable contributor to injected context, and the two problems reinforce each other:
- Accuracy. When a result is oversized the model gets a head/tail preview and loses the middle. Reported internally as degrading answer quality; the workaround people fall back to is rewriting the underlying script or skill to emit less, which does not generalise.
- Blindness. We cannot rank tools by injected cost. The raw size is discarded for every call that fits under budget,
content_lengthis post-truncation, and the only size-bearing event fires on one of four layers. Without per-tool raw-vs-injected numbers there is no data to tune the budgets in section A, and no way to tell whether a change helped.
Fixing only one side is not enough: tightening budgets without measurement risks silently degrading accuracy, and adding measurement without a consistent spill path leaves the largest results unbounded.
Additional context
- Read the prior art first.
docs/design/2026-08-10-tool-output-offload-preview.mdis the authoritative description of the offload/preview state transition and privacy model (required by #4184, mitigation implemented in feat(core): layered tool-output truncation, per-message budget, per-tool limits #4880). Alsodocs/design/final-tool-response-budget.md— which notes the layers do not share structured state, the root cause of the observability gap in section B — plusdocs/plans/final-tool-response-budget-implementation.mdanddocs/design/tool-use-summary/unified-tool-output.md.FULL_OUTPUT_DIGEST_LABELcame out of #9450 (loop guards fingerprinting persisted output); any new stub format must keeploopDetectionServicerecognising the sentinels. - Origin: feedback on tool-result context costs. The request combines spill-to-file and read-on-demand handling with per-tool raw-versus-injected character/token reporting. Extra read round trips and precision loss from partial reads are the concerns addressed by A.3/A.4.
- Related but distinct: PR #10410 preserves the prompt cache for deferred tools. That addresses tool-declaration context cost (deferred loading currently only kicks in above ~10% of model context because loading invalidates the cache), not tool-result size. Not a substitute for this issue.
- All paths, constants and line numbers above were read from
origin/main(b1ac3e2970). No existing open issue covers this (searched tool-result truncation / oversized output / result-size telemetry).
Activity
yiliang114 commentedon Oct 7, 2026
Partial implementation status: #11727 merged on 2026-09-13. It lets a producer's own size decision govern the same result body instead of applying the generic persistence gate again, preserving the shell preview's trailing exit/error information.
That PR addresses the budget-ordering subset of section A. It does not cover the full cross-tool handling policy or section B's every-call raw-versus-injected telemetry. Keeping this umbrella open for those remaining acceptance criteria.
yiliang114 commentedon Oct 7, 2026
Implemented in #13599, ready for review at
38d5debb6062e74c371a9d9865ef9306783fb205. The result pipeline now carries pre-reduction raw size when known, measures processed and generator-submitted size, adds numeric events across shortening layers and size histograms/RUM fields, moves MCP to the global budget, and provides complete-output paging hints plus bounded structured JSON samples. Restored/pre-bounded results lacking original-size metadata keep raw size unknown. Producer-specific semantic reductions remain documented rather than treated as network-byte measurements.Fresh core/CLI builds and typechecks, targeted regression checks and changed-file static checks passed. Native CLI in actual tmux sessions used an anonymous controlled provider: low/high/explicit and real MCP discovery/invocation all exited 0; injection sizes matched actual provider bodies and full spill files/digests were verified. High/explicit profiles also performed automatic compression. These are local controlled-provider results, not production-model savings or deployed telemetry-backend acceptance. The separate PR comment contains the sanitized report and terminal excerpts.
Keeping this issue open until the PR merges; main/release availability is not yet implied.
中文:已提交正式 PR #13599,本地构建、类型检查、针对性回归和静态检查通过,四组原生 CLI 测试保存了真实 tmux capture。新增 raw/processed/injected 数值观测、MCP 全局预算、完整输出恢复信息及 JSON 结构样本;没有原始尺寸元数据时保留未知,不把预览冒充完整原始数据。受控 provider 的实际正文与注入统计一致,完整文件和摘要可核对;高压力/显式组还进行了自动压缩。公开报告已脱敏,远端指标与真实模型收益未验收。PR 未合并,因此 issue 继续开放,也不宣称 main 或发布版本已修复。
yiliang114 commentedon Oct 7, 2026
Latest PR verification — 2026-10-07
Formal, open PR #13599 is now at
1e92feee82ed7ad297cac5e65829eeb1761f859c. It preserves the tested production tree and includes the parallel patrol fixes additively. Latest targeted-file receipts cover 1,509 distinct executed passing cases; affected core/CLI builds and typechecks passed. High-pressure native CLI/tmux report and actual JSON MCP discovery/invocation/tmux report verify next-request sizes, complete-output recovery and digests. The reports pin their individual executed commits and controlled-provider boundaries.MCP deliberately inherits both global character and line limits (defaults 80,000 / 2,000), and explicit values remain independent. This is a proposed default/observability improvement with local verification, not a claim of production billing savings. The PR is unmerged and unreleased; keep this issue open until merge.
中文说明
正式 PR #13599 已推送到
1e92feee82ed,增量纳入并行修正,保留实际验收的 production tree。最新定向文件回执去重后共 1,509 个实际执行通过的用例,相关 core/CLI 构建与类型检查通过;两份真实 tmux 报告分别核验高压力 CLI 与 MCP JSON 的实际请求大小、完整恢复文件及摘要,并明确各自测试 commit 和受控 provider 边界。MCP 同时跟随全局字符/行数预算,默认 80,000 / 2,000,显式值分别有效。PR 未合入、未发布,issue 继续开放,不宣称生产账单收益。refactor(core): narrow to pressure-aware tool budgets (QwenLM#2566)
feat(core): shrink tool results to the headroom left before auto-comp…