Skip to content

fix(core): keep the tail when tool output is truncated - #62719

Open
vvvzhangshuo wants to merge 1 commit into
anomalyco:devfrom
vvvzhangshuo:tool-output-tail
Open

vvvzhangshuo wants to merge 1 commit into
anomalyco:devfrom
vvvzhangshuo:tool-output-tail

Conversation

@vvvzhangshuo

@vvvzhangshuo vvvzhangshuo commented Nov 8, 2026 •

Copy link
Copy Markdown

Issue for this PR

Fixes #62718

Type of change

  • Bug fix

What does this PR do?

Generic tool output keeps the first lines and discards the rest. Shell output already keeps the end (packages/core/src/shell.ts pages from size - maxBytes), so a cut result behaves differently depending on which tool produced it. This change keeps both ends for generic tool results.

  • Head and tail share max_lines: the tail can use the head's unused line budget, and the total never exceeds the limit.
  • When the tail ends early (for example, an oversized last line), the head takes the leftover byte budget instead of leaving it unused.
  • The marker keeps line ranges: [showing lines 1-5 and 96-100 of 100; full output saved to ...]. It stays compatible with the line counts added in fix(core): show line counts when tool output is truncated #51200 and with read's offset.
  • The marker's bytes are reserved when it fits, so normal previews stay within max_bytes. When max_bytes is smaller than the marker itself, content keeps the old budget behavior instead of disappearing.
  • Kept lines go back into the text item they came from, so text never crosses an attachment and the marker lands where the gap starts.
  • The full output still goes to the retained file; metadata.truncated and outputPath are unchanged.

Example with max_lines: 10 and 100 lines of output:

Before: lines 1-10, then [showing lines 1-10 of 100; ...].

After: lines 1-5, the marker, then lines 96-100.

Not in scope: a single line longer than the byte budget is still dropped as before (minified JSON), and line ranges only cover whole lines. I can follow up if that is wanted.

How did you verify your code works?

  • Reproduced with a 100-line tool result and tool_output.max_lines: 10 in the added test; before the change truncate returned lines 1-10 only.
  • cd packages/core && bun test test/tool-output.test.ts (16 pass)
  • cd packages/core && bun run typecheck and bun run typecheck:tsc
  • bun run lint
  • bun run check from the repository root

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main case works. I ran ToolOutput.truncate on the base and with this PR: 100 lines with max_lines: 10 now gives lines 1-5, the marker, then lines 96-100, where the base gave lines 1-10. The tool-output tests and typecheck:tsc pass. One mcp.test.ts failure happens on the base too, so it isn't from this change.

Public API: no API, config or schema change. The only visible change is the model-facing marker text: showing lines A-B and C-D of N, and showing 0 lines when nothing fits.

Two things are worth fixing before merge (details inline):

  • Lines move around attachments. When a text item is split, its lines can move to the other side of an attachment. That contradicts the description's claim that attachments keep their position.
  • Unused tail budget isn't given back to the head. Output whose last line is too big now shows fewer lines than before, and no tail at all.

A smaller point: because the marker's size, including the full file path, is reserved up front, a small max_bytes (under roughly 150 bytes) now shows no content at all, where the base still showed some lines.

Comment thread packages/core/src/tool-output.ts Outdated
const headText = head.join("\n")
const tailText = tail.join("\n")

// Attachments keep their position: the head and marker replace the first text item, the tail the last one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attachments stay in place in the array, but the text around them doesn't. All head lines go into the first text item and all tail lines into the last, whichever item they came from. With ["a1..a6", file, "b1"] and max_lines: 4, I get ["a1\na2\n<marker>", file, "a6\nb1"]: a6 came before the image in the original and now comes after it. With ["a1", file, "b1..b5"], b1 ends up in front of the image. For tools that interleave text and images (MCP, read), the model then sees text next to the wrong attachment. Could the selection track which item each kept line came from (head or tail) and emit the lines back into that item, with the marker going into the item where the gap starts?

headBytes += size
}

const tail: string[] = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The head is capped at half the bytes (headBudget) and half the lines (headLineBudget) before the tail is tried. When the tail can't use its share, nothing goes back to the head. Example: 30 short lines followed by one 2,000-byte line, with max_lines: 10 and max_bytes: 1000. The base shows 10 lines; this PR shows lines 1-5 and no tail. A long final line is common (a JSON blob, a stack trace on one line). Picking the tail first, or extending the head after the tail loop with whatever budget is left, would keep this case at least as good as before.

@vvvzhangshuo

Copy link
Copy Markdown
Author

Addressed the review points in f1fcf3f:

  • Attachments: kept lines now go back into the text item they came from. With ["a1..a6", file, "b1"] and max_lines: 4, the preview is a1, a2, <marker>, a6, then the image, then b1. No line crosses the attachment, and the marker sits where the gap starts. Added a test for both interleavings.
  • Budget: after the tail selection, the head now extends into whatever byte and line budget is left. The 30-short-lines-plus-one-2,000-byte-line case now keeps 10 lines instead of 5. Added a test.
  • Tiny max_bytes: when the marker itself does not fit, the content gets the full byte budget like before instead of disappearing. Added a test.

bun test test/tool-output.test.ts (16 pass), bun run typecheck:tsc, and the repo check all pass.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both earlier points are addressed. Kept lines now go back into the text item they came from, so text no longer moves across attachments. When the tail ends early, the head takes the leftover budget: an oversized last line now shows as many lines as before. Content also no longer disappears when max_bytes is very small. I re-checked these cases by calling ToolOutput.truncate directly, along with the 100-line case from #62718 and items that end in a newline. The tool-output tests and typecheck:tsc pass.

Public API is unchanged; only the model-facing marker text differs.

No new concerns.

@vvvzhangshuo

Copy link
Copy Markdown
Author

Gentle ping — both automated review rounds are clean (No new concerns), and the change stays inside ToolOutput.truncate plus its tests. The required check workflows are still action_required for a first-time contributor, so CI has not run yet; could a maintainer approve them or take a look?

I want to be explicit about why I think this matters: in my sessions the decisive information is often at the end of a long tool result — a skill's final checklist, an MCP result field, or the actual error at the bottom of a build log. Dropping that tail silently costs task success, forces extra turns to recover the missing part, and usually adds more context than the bounded tail would have. Keeping both ends is a small, bounded change that avoids all three.

Happy to narrow the scope (for example, only MCP/skill results) if generic head+tail is not the direction you want. Thanks!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v2: generic tool output truncation drops the tail (skills, MCP results)

1 participant