fix(shell): bound command output sent to the model#28401
Conversation
The shell tool forwarded the full command output to the provider in its tool result. The only mediation was summarizeToolOutput, which is disabled by default, so a command printing a large payload sent it back unbounded (issue google-gemini#28090 observed a 40319-byte tool result). Add truncateLlmOutput: a deterministic, codepoint-safe head+tail byte bound (MAX_LLM_OUTPUT_BYTES, 32 KiB) applied to the output segment of the tool result on normal completion and on cancellation-with-output. Error/exit-code lines are unaffected since they are appended after the output. Output within the bound is returned unchanged; enabling tool-output summarization remains the smarter, model-driven alternative.
|
📊 PR Size: size/M
|
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a safety mechanism to limit the amount of shell command output forwarded to the model. By enforcing a 32 KiB limit, it prevents excessively large outputs from overwhelming the model's context window, while ensuring that critical diagnostic information at the beginning and end of the output remains available. The user-facing display remains unaffected, preserving the full output for the user. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
🛑 Action Required: Evaluation ApprovalSteering changes have been detected in this PR. To prevent regressions, a maintainer must approve the evaluation run before this PR can be merged. Maintainers:
Once approved, the evaluation results will be posted here automatically. |
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to truncate shell command output sent to the LLM to prevent sending arbitrarily large payloads. It defines a maximum byte limit of 32KB and implements a truncation utility that preserves the head and tail of the output. Feedback was provided regarding potential performance and memory issues when calling Array.from() on large command outputs, suggesting the use of Intl.Segmenter to safely process grapheme clusters instead.
| function takeBytes(output: string, maxBytes: number, fromEnd: boolean): string { | ||
| if (maxBytes <= 0) { | ||
| return ''; | ||
| } | ||
| const chars = Array.from(output); |
There was a problem hiding this comment.
Calling Array.from(output) on the entire, potentially huge command output can cause severe performance degradation or Out-Of-Memory (OOM) crashes. However, using string.slice() directly to truncate the string before operating on grapheme clusters can split multi-byte Unicode characters (such as emojis or surrogate pairs) at the boundary. To prevent character splitting while maintaining performance, we should use Intl.Segmenter to safely iterate over grapheme clusters up to the limit without loading the entire array into memory.
function takeBytes(output: string, maxBytes: number, fromEnd: boolean): string {
if (maxBytes <= 0) {
return '';
}
const segmenter = new Intl.Segmenter();
if (fromEnd) {
const safeSlice = output.slice(-maxBytes * 2);
const segments = Array.from(segmenter.segment(safeSlice));
return segments.slice(-maxBytes).map(s => s.segment).join('');
} else {
const segments = segmenter.segment(output);
let result = '';
let count = 0;
for (const { segment } of segments) {
if (count >= maxBytes) break;
result += segment;
count++;
}
return result;
}
}References
- When truncating strings that may contain multi-byte Unicode characters (e.g., emojis), use methods that operate on grapheme clusters (like
Intl.SegmenterorArray.from()) instead of UTF-16 code units (string.length,string.slice()) to prevent character splitting.
Summary
The shell tool forwards the entire command output to the model with no upper bound. A single command that prints a lot (e.g.
find /, a verbose build, a largegit log) can inject hundreds of KB into the model context, degrading responses and burning tokens. This PR bounds the output sent to the model at 32 KiB while leaving the full output available to the user-facing display path.Details
truncateLlmOutput()inpackages/core/src/tools/shell.ts: keeps the head and tail of the output (head-biased split) and inserts a marker line stating how many bytes were elided, so the model still sees how the command started and finished.MAX_LLM_OUTPUT_BYTES = 32 KiB) and codepoint-safe — it never splits a multi-byte UTF-8 character.llmContentfrom the fullresult.output.llmContentis affected;returnDisplay(what the user sees) is unchanged.summarizeToolOutputmediation still applies; this is a hard backstop when summarization is not configured.Related Issues
Fixes #28090
How to Validate
cd packages/core && npx vitest run src/tools/shell.test.ts— 93 tests pass, including 4 new ones covering: output under the limit passes through untouched, oversized output is bounded with the truncation marker, multi-byte characters at the cut boundary are not split, and the aborted-command path is also bounded.node -e "console.log('x'.repeat(40319))") through the shell tool and inspectllmContent— it is capped at 32768 bytes with head and tail preserved (the reported 40319-byte case truncates to 32768).Pre-Merge Checklist