Tool output truncation: per-framework limits and what breaks
Tool output truncation cuts a tool's return value before the model reads it. A per-runtime table of the defaults, each read from its own docs or code, plus the downstream failures a silent cut causes.
Part of Agentic AI testing beyond a single eval run
In brief
5 POINTS- Seven agent runtimes cap tool output differently: three character limits, one token limit, one per-model policy, and two with no default cap.
- Claude Code warns above 10,000 tokens and caps MCP tool results at 25,000, adjustable through the MAX_MCP_OUTPUT_TOKENS environment variable.
- Gemini CLI truncates only shell and MCP tool output, at 40,000 characters, and writes the full result to a temporary file it references.
- Our three-model reliability run scored memory-loss detection at 46.7, 44.4 and 23.3 out of 100, every pairwise 90% interval overlapping; cued losses were caught, silent ones missed.
- Assert on each runtime's truncation marker in your harness, so an oversized fixture fails the run instead of passing quietly.
On this page (5)
Consider a query tool that returns every row you asked for. The model answers from the first few hundred of them, the run finishes green, and your trace shows a complete tool return sitting next to a confident answer. The string that actually crossed into the model’s context was shorter than the one your trace recorded, and nothing in the default logging tells you where it stopped.
Each agent runtime picks its own cap, its own unit, and its own subset of tools to apply it to, so a truncation budget you verified on one stack carries to the next one by luck. The caps also move under you. Google’s Gemini CLI once shipped a default tool-output threshold of 4,000,000 characters. A context-management issue on its own tracker asked for something “significantly lower”, partly on the grounds that the newer model is “significantly more effective at reasoning over compressed or truncated context”. Its configuration reference puts the default on main at 40,000 in August 2026, two orders of magnitude down.
Find your runtime’s row in the table below, then log the observation string as the model received it and assert on that runtime’s truncation marker inside your eval harness. Without the assertion, every pass rate you compute quietly includes the runs where the model answered from a fragment.
This page leans on one of our own runs. The three-model reliability benchmark included a dimension that compacted each model’s working notes mid-task and scored whether the model noticed the loss before finalizing. On a 0-to-100 dimension score with 90% intervals: Kimi K3 46.7 [33.3, 63.3], Claude Fable 5 44.4 [29.6, 59.3], GPT-5.6 Sol 23.3 [10.0, 36.7]. Those are pass rates over the dimension’s items: Kimi 14 of 30, Fable 12 of 27 after blocked calls came out of its denominator, Sol 7 of 30. Every pair of intervals overlaps, so the dimension is enough to show that none of the three handled the loss well and not enough to separate them from each other.
That probe cut working notes rather than tool returns, so the mechanism carries over while the scores stay attached to what they measured. The mechanism is the useful half. When the loss came with a cue, the models mostly caught it; when it was silent, they almost never did.
The cut lands in a layer your trace probably skips
Three separate things can shorten a tool result before a model reads it, and they fail in different ways. The tool can shorten it itself, through a LIMIT clause, an API page size, or a log endpoint that only ever returns the last thousand lines. The runtime can shorten it, which is what the rest of this page covers. And the provider can reject the whole request once the accumulated conversation history fills the model’s window, which is a context-budget problem with its own fix and stays out of scope here. A single oversized tool return that blows the window on its own belongs to this page, because the runtime that would have capped it is the one that did not.
The middle layer rarely reaches a runbook, because it lives as a constant in somebody else’s repository.
It is also the layer whose value you can look up for yourself, and on Claude Code change with an environment variable, which is why it’s worth knowing before an incident rather than during one. Reading it off the source takes a few minutes. Recovering it from a wrong aggregate takes an incident review.
Seven runtimes, three units, and no shared idea of too big
Everything below was read out of each project’s own configuration reference or source file in August 2026, and the file behind each row is listed with the sources at the foot of this page. This corner of the ecosystem moves quickly, so treat the table as a pointer to the named constant rather than a value that will hold all year.
TABLEShow full table (7 rows)Showing full table (7 rows)
| Runtime | Default cap | What it applies to | Where the cut lands | Is the model told? |
|---|---|---|---|---|
| Claude Code (MCP) | 25,000 tokens, with a warning above 10,000 | MCP tool results; MAX_MCP_OUTPUT_TOKENS raises it, and a per-tool anthropic/maxResultSizeChars annotation replaces it for text content, up to a ceiling of 500,000 characters | Results over a separate persist-to-disk threshold, whose value the doc does not print, are written to disk and swapped for a file reference | Yes, the reference replaces the content |
| Gemini CLI | 40,000 characters (tools.truncateToolOutputThreshold); 0 or negative disables it. A separate distillation path caps at 10,000 tokens and summarizes above 20,000 | Shell tool output and MCP text content only, so other tools pass through uncapped | Full output saved to a project temp file, shortened text returned with the path | Yes for the character cut; a summarized result carries no marker |
| OpenAI Codex | A per-model policy in bytes or tokens, resolved from each model’s own limits rather than one shared default | Exec and tool output | Middle of the string removed | Yes, prefixed with the original token count and the total line count |
| OpenHands SDK | 30,000 characters (max_message_chars), matched by MAX_CMD_OUTPUT_SIZE for command output | Every event’s content on its way to the model | Head and tail kept, middle replaced by a notice | Yes, and the notice can name the file holding the full output |
| smolagents | 20,000 characters (MAX_LENGTH_TRUNCATE_CONTENT) | Content passed through truncate_content | First and last halves kept, middle replaced by a notice | Yes, inline |
| OpenAI Agents SDK (sandbox shell) | None. max_output_tokens defaults to None | Shell output, only when a caller sets a limit | Token budget, head-weighted | Yes when a limit is set, with a line count prefixed |
LangGraph ToolNode | None | Nothing | No truncation step exists | Not applicable |
Read the unit column before the number column. A payload that sits comfortably under Claude Code’s 25,000-token ceiling can be cut hard by smolagents at 20,000 characters, because a token is worth roughly four bytes. The OpenAI Agents SDK writes that ratio into its source as APPROX_BYTES_PER_TOKEN = 4, and Codex converts between the units with the same kind of approximation. Apply it and Claude Code’s ceiling comes out near 100,000 bytes, five times smolagents’ 20,000-character cap while the payload stays ASCII. The two units part company on anything else, because UTF-8 spends two to four bytes on an accented letter, a box-drawing glyph or a non-Latin script, so a byte budget runs out earlier than a character count of the same size suggests. Character limits also punish verbose formats. A pretty-printed JSON response burns its budget on indentation.
Gemini CLI runs a second shortening path beside that character cap. The same configuration reference sets contextManagement.tools.distillation.maxOutputTokens to 10,000 tokens and summarizationThresholdTokens to 20,000. Above that threshold a large tool result is handed to a model and summarized before the agent reads it. The model.summarizeToolOutput setting decides which tools that covers, and the shell tool is the only one it supports today. A summary is a paraphrase of the return, so it carries no truncation marker, because nothing was cut. An assertion on the marker passes straight over it.
Two rows carry no default at all, and in both cases the framework hands the decision to the caller. LangGraph hands the tool’s return value straight into the ToolMessage, which keeps the framework honest about what it did and moves the whole problem to you. The Agents SDK sandbox exposes max_output_tokens as a field the caller fills, defaulting to None.
The frameworks announce the cut, so the silence starts somewhere else
Every runtime in the table that truncates also says so, in-band, in the exact place the model will read it. OpenHands inserts <response clipped><NOTE>Due to the max output limit, only part of the full response has been shown to you.</NOTE> and, when persistence is on, extends that note with the saved file path and the approximate line where the cut begins. smolagents writes _This content has been truncated to stay below {max_length} characters_ between the head and the tail. Codex prefixes Warning: truncated output (original token count: ...) along with the total line count.
A marker like that is what makes a truncated observation recoverable: the model can see that something was removed and go back for the rest.
The silence enters from two other directions. Upstream, a tool that paginated, applied a LIMIT, or tailed a log before the runtime ever saw the result hands back a payload that looks complete, and no marker exists to insert because no framework noticed anything was removed. Downstream, most instrumentation wraps the tool function and records its return value, which happens before the runtime applies its cap, so the trace preserves the version of events in which nothing was lost.
A runtime that truncates loudly still produces a silent failure when your logging sits on the wrong side of the cut. That’s a failure-attribution problem: the framework marked the cut, and the observability layer then dropped the evidence that would have placed the fault. RAG pipelines hit the same shape often enough that silent context truncation is one of the five modes in our retrieval failure taxonomy.
What breaks after the cut
A cut through a JSON body is the loudest failure and the least dangerous, because a parser usually rejects the fragment and the step fails visibly. The dangerous version is a fragment that still parses, which is the overlap between this page and schema-mode and JSON failures across providers: an array clipped after its third element is valid JSON describing the wrong world.
Head-and-tail truncation makes that worse in a specific way. smolagents, OpenHands and Codex all keep both ends and drop the middle, which preserves the opening bracket and the closing bracket of a list while removing most of its contents. The shape survives. A model asked to count, sum or compare over that list works from whatever items are left at the two ends, and returns an answer with the right structure and the wrong value.
Then there is the file-reference path. Claude Code, Gemini CLI and OpenHands can all persist the full output and hand the model a pointer to it, which preserves the most information of anything in the table and depends entirely on the model making a second call. When it skips that call and reports anyway, the downstream check sees a well-formed answer against a stub, which is exactly the verification step passing work that is wrong. An agent that treats the pointer as the answer will also tend toward stopping before the task is complete, because the visible portion looks like a finished result.
The uncapped runtimes fail from the other end. With no truncation step, an oversized return accumulates in context until the provider rejects the request or the step stops making progress, which reads to an operator as a run that hangs rather than one that is too large.
The two silent failures share an accounting effect. A fragment that still parses and a file reference nobody followed both finish the run, so the truncated observation enters your pass rate as an ordinary trial: a false success in its narrowest form. Any rate computed over them inherits the distortion that answer coverage exists to expose. The other two announce themselves in the result: a parse error fails the step, and an oversized context shows up as a stalled run.
How to catch it before an eval does
Start from a fixture. A config file gives you the cap somebody declared; a fixture gives you the cap that ran. Point a test tool at a payload deliberately larger than the cap you found in the table, run one turn, and compare two strings: what the tool returned and what the model received. This is fault injection with a known input, which is why the result attributes cleanly.
tool_return = fake_tool.last_return # complete, what your APM already logs
tool_msg = next(m for m in reversed(run.messages) if m.role == "tool")
observed = tool_msg.content # post-cap, what the model actually read
assert len(observed) == len(tool_return) # fails first when the runtime cut it
assert "<response clipped>" not in observed # OpenHands; swap in your runtime's markerThen keep the second assertion permanently. This page prints the literal string for three runtimes: OpenHands, smolagents and Codex. Claude Code swaps the content for a file reference, Gemini CLI returns a shortened string carrying the temp-file path, and the Agents SDK prefixes a line count, so for those three read the current wording out of the source your row names before you assert on it. LangGraph gives you nothing to assert on, because it never cuts. A truncation your harness greps for is a truncation that can no longer pass quietly. Record the marker as a per-run field, then report the share of trials that hit the cap. A rising share deserves the response you would give any signal that a downstream consumer cannot keep up. The cap is doing the job of a flow-control mechanism, badly, at the last possible moment.
One limit on all of this. The table tells you what each runtime does to a string it can see, and it says nothing about a tool that trimmed its own output first, which is the version no framework can mark and no assertion on framework markers will catch. For those, the check has to sit inside the tool, where the row count and the returned count are both still in scope. A runtime that summarizes instead of cutting leaves a blank of the same kind, and the thing that closes it is the length comparison above rather than the marker.
Do three things this week. Read your runtime’s constant out of the source and write it down beside your context budget. Add the marker assertion to the harness, alongside the other pre-flight gates in the reliability testing checklist. Then re-run the last eval whose result you would defend in a design review, with the fixture above wired in, and put an interval on whatever rate survives, which is the reporting practice set out in how to measure agent reliability. The wider method these three steps sit inside, testing an agent across runs instead of scoring a single one, is agentic testing beyond a single eval run. If the second run disagrees with the first, the fragment was doing some of your reasoning.
Sources
- Connect Claude Code to tools via MCP
- Gemini CLI configuration reference
- gemini-cli packages/core/src/scheduler/tool-executor.ts
- [Context Mgmt] Lower default tool output truncation thresholds for Gemini 3 (issue 17037)
- smolagents src/smolagents/utils.py
- OpenHands software-agent-sdk openhands-sdk/openhands/sdk/llm/llm.py
- OpenHands software-agent-sdk openhands-tools/openhands/tools/terminal/constants.py
- OpenHands software-agent-sdk openhands-sdk/openhands/sdk/utils/truncate.py
- openai/codex codex-rs/utils/output-truncation/src/lib.rs
- openai-agents-python src/agents/sandbox/capabilities/tools/shell_tool.py
- langgraph libs/prebuilt/langgraph/prebuilt/tool_node.py