docs/design/final-tool-response-budget.md
Tool output is currently shortened at several independent layers. Shell output is shortened near 30K characters and marked as truncated, generic tool output is shortened near 2K characters, and a Core scheduler batch can offload output when the aggregate exceeds the configured batch budget. These layers do not share structured state.
The scheduler treats an existing truncation marker as proof that no more work is needed. Consequently, several individually shortened Shell results can still exceed the aggregate budget. Headless mode makes the gap larger because it creates one scheduler per tool call and concatenates their responses outside those schedulers. Interactive mode similarly appends duplicate and synthetic responses after scheduler finalization. ACP, agent, and speculative execution have their own aggregation boundaries.
The model request, resumable transcript, and tool-result recording must contain the same bounded response. Rich user-facing tool display is intentionally out of scope and can continue to use the existing result display.
enter_plan_mode lifecycle reminder is policy input, not tool output, and remains inline outside this budget.functionResponse.response.output, functionResponse.response.error, and top-level text parts that belong to the tool-response batch.ToolResult and ToolCallResponseInfo carry an internal optional persistedOutputFiles field.
undefined: no persistence decision was made by the producer.[]: a decision was made and there is no reusable file.The field is not included in hook serialization, ACP payloads, JSON output, telemetry attributes, or persisted UI metadata. A response reconstructed by a hook does not inherit metadata unless it is explicitly copied by the runtime.
Producer truncation controls the normal model preview and persists complete output once.
These previews are not aggregate enforcement. An already shortened response can be shortened again by finalization.
One shared finalizer accepts responses in original order plus the configured aggregate budget. It measures all bounded text fields, then reduces text until the aggregate fits. Existing persisted paths are reused. A response without a reusable path is persisted at most once before a path reference replaces or accompanies its shortened preview.
Reduction is deterministic. A max-min water-fill allocation shares the budget across model-facing text fields while allowing small fields to keep their complete content. Reduced fields retain a small head-and-tail preview and list the available persisted artifact paths when the allocation permits. Unicode surrogate pairs are never split. The final hard-cap pass shortens text without I/O so persistence failure cannot violate the request-size invariant.
The finalizer recomputes contentLength from the returned parts. Infinite or disabled budgets are a no-op.
enter_plan_mode is the sole semantic exception. Its successful function-response output installs the active planning policy, so truncating it would change execution rules rather than shorten diagnostic output. The finalizer and last-chance send guard identify that output by tool name and exclude it from allocation; failure text and all ordinary output in the same batch remain bounded.
PostToolBatch hooks to bound hook input and again after the hook to bound hook output.Persistence failure is reported through existing logging and never prevents final truncation. The returned model response still fits the budget, but may omit a file reference if no complete output was successfully persisted. Media parts remain untouched and are not counted in this character budget.
Cancellation and hook-stop responses are finalized exactly like successful and failed tool responses. Empty output and error fields remain valid. A single response larger than the whole batch budget is reduced on its own; multiple large responses share the remaining preview capacity deterministically.
The public model-facing function response schema does not change. Existing truncation text remains readable, but aggregate finalization no longer depends on it. Existing sessions can still be replayed; only newly recorded tool results gain the stricter invariant.
This change does not add wire-byte hashes, exact token accounting, media budgeting, storage lifecycle changes, transcript migration, or a new temporary-file layout. Those are independent follow-ups.