🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

feat: cap tool output to fit the model context window - #26637

Merged
kylecarbs merged 2 commits into
mainfrom
kylecarbs/cap-mcp-tool-output
Jun 24, 2026
Merged

feat: cap tool output to fit the model context window#26637
kylecarbs merged 2 commits into
mainfrom
kylecarbs/cap-mcp-tool-output

Conversation

@kylecarbs

@kylecarbs kylecarbs commented Jun 23, 2026

Copy link
Copy Markdown
Member

Problem

Local tool results were persisted and replayed to the model verbatim, with no size cap. A single oversized result, most often a multi-megabyte response from an MCP tool, overflows the prompt on the next request. Every retry rebuilds the same history and fails the same way, leaving the chat wedged in error. Auto-compaction is reactive (token usage is only known after a response), so it can't catch a single result that blows the very next request.

Fix

Cap every locally-executed tool result at its single choke point, executeSingleTool in chatloop, so the cap covers built-in tools, global (deployment-pinned) MCP, workspace MCP, and provider runners uniformly. Because this runs before the result is published to the live stream and before it is committed, the SSE preview, the persisted message, and the model replay all see the same bounded output.

The budget is token-aware: a single tool result may use at most half the model's context window (~4 bytes/token), with a 16KB floor and a 64KB default when the window is unknown. Truncation keeps the head and tail of the output and replaces the middle with a marker telling the model how much was removed and to narrow its query; it is UTF-8 safe and never exceeds the budget. Binary media Data is passed through untouched (only the text payload is bounded).

A coderd_chatd_tool_result_truncated_total{provider,model,tool_name} counter and a warning log record each truncation.

Out of scope

  • Provider-executed results (e.g. web search) arrive via the stream, not executeSingleTool.
  • Dynamic/external tool results submitted through the /tool-results API are validated as JSON elsewhere.
  • Cumulative growth across many results is still handled by context compaction; this change only bounds any single result.
Implementation notes
  • New coderd/x/chatd/chatloop/tooltruncate.go: toolResultByteBudget(contextLimitTokens) and truncateToolResultText(text, maxBytes) (pure, unit-tested).
  • chatloop.go: added ContextLimit to ExecuteLocalToolsOptions; threaded a computed byte budget through executeTools into executeSingleTool, where resp.Content is capped for the text, media-text, and error branches.
  • generation.go: passes ContextLimit: prepared.ContextLimitFallback (the model's configured context limit).
  • metrics.go: new ToolResultTruncatedTotal counter + RecordToolResultTruncated.
  • Tunable knobs live as constants in tooltruncate.go (toolResultContextDivisor = 2, bytesPerTokenEstimate, minToolResultBytes, defaultToolResultBytes).

Verified: go build ./coderd/x/chatd/..., go test ./coderd/x/chatd/chatloop/..., and the chatd test binary compiles.


Resolves CODAGT-678

Generated by Coder Agents on behalf of @kylecarbs.

Large tool results, most often multi-megabyte MCP responses, were
persisted and replayed to the model verbatim, overflowing the prompt
and wedging the chat. Cap each local tool result in executeSingleTool
to a token-aware byte budget (a fraction of the model context window)
with UTF-8-safe head-and-tail truncation that preserves the start and
end of the output. Covers built-in, global MCP, and workspace MCP
tools; binary media data is left untouched.

@johnstcn johnstcn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think a truncated and incomplete tool call result is less likely to be useful than an explicit error saying e.g. "tool call was successful but output would exceed token budget". In this case, the model will have a signal that it does not need to retry the tool call.

@kylecarbs

Copy link
Copy Markdown
Member Author

@johnstcn I considered that, but I think that might be dangerous. e.g. you run a bash command, the model thinks it never ran because of the tool-output. It tries to run again and state is mutated.

@johnstcn johnstcn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's impossible to know until we try it, I suppose!

make gen adds the new coderd_chatd_tool_result_truncated_total metric to
the scanned metrics fixture and the Prometheus metrics reference.
@github-actions

Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/admin/integrations/prometheus.md

@kylecarbs kylecarbs changed the title feat(coderd/x/chatd): cap tool output to fit the model context window feat: cap tool output to fit the model context window Jun 23, 2026
@johnstcn

johnstcn commented Jun 24, 2026

Copy link
Copy Markdown
Member

CODAGT-678

@linear-code

linear-code Bot commented Jun 24, 2026

Copy link
Copy Markdown

CODAGT-678

@kylecarbs
kylecarbs merged commit 3221725 into main Jun 24, 2026
40 of 42 checks passed
@kylecarbs
kylecarbs deleted the kylecarbs/cap-mcp-tool-output branch June 24, 2026 15:16
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants