Skip to content

Commit 9606b30

Browse files
committed
fix: preserve tail when truncating bash tool output
Prioritize the tail of bash_tool output when truncation is needed, so final errors and command results remain visible. Add regression tests for truncation and render_result behavior.
1 parent aa12906 commit 9606b30

File tree

1 file changed

+54
-4
lines changed
  • src/crates/core/src/agentic/tools/implementations

1 file changed

+54
-4
lines changed

src/crates/core/src/agentic/tools/implementations/bash_tool.rs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,28 @@ const BANNED_COMMANDS: &[&str] = &[
4545
"safari",
4646
];
4747

48-
fn truncate_string_by_chars(s: &str, max_chars: usize) -> String {
48+
fn truncate_output_preserving_tail(s: &str, max_chars: usize) -> String {
4949
let chars: Vec<char> = s.chars().collect();
50-
chars[..max_chars].into_iter().collect()
50+
if chars.len() <= max_chars {
51+
return s.to_string();
52+
}
53+
54+
let tail_bias = max_chars.saturating_mul(4) / 5;
55+
let separator = "\n... [truncated, middle omitted, tail preserved] ...\n";
56+
let separator_len = separator.chars().count();
57+
58+
if separator_len >= max_chars {
59+
return chars[chars.len() - max_chars..].iter().collect();
60+
}
61+
62+
let content_budget = max_chars - separator_len;
63+
let tail_len = tail_bias.min(content_budget);
64+
let head_len = content_budget.saturating_sub(tail_len);
65+
66+
let head: String = chars[..head_len].iter().collect();
67+
let tail: String = chars[chars.len() - tail_len..].iter().collect();
68+
69+
format!("{head}{separator}{tail}")
5170
}
5271

5372
/// Result of shell resolution for bash tool
@@ -142,7 +161,7 @@ impl BashTool {
142161
let cleaned_output = strip_ansi(output_text);
143162
let output_len = cleaned_output.chars().count();
144163
if output_len > MAX_OUTPUT_LENGTH {
145-
let truncated = truncate_string_by_chars(&cleaned_output, MAX_OUTPUT_LENGTH);
164+
let truncated = truncate_output_preserving_tail(&cleaned_output, MAX_OUTPUT_LENGTH);
146165
result_string.push_str(&format!(
147166
"<output truncated=\"true\">{}</output>",
148167
truncated
@@ -226,7 +245,7 @@ Usage notes:
226245
- DO NOT use multiline commands or HEREDOC syntax (e.g., <<EOF, heredoc with newlines). Only single-line commands are supported.
227246
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).
228247
- It is very helpful if you write a clear, concise description of what this command does. For simple commands, keep it brief (5-10 words). For complex commands (piped commands, obscure flags, or anything hard to understand at a glance), add enough context to clarify what it does.
229-
- If the output exceeds {MAX_OUTPUT_LENGTH} characters, output will be truncated before being returned to you.
248+
- If the output exceeds {MAX_OUTPUT_LENGTH} characters, output will be truncated before being returned to you, with the tail of the output preserved because the ending is usually more important.
230249
- You can use the `run_in_background` parameter to run the command in a new dedicated background terminal session. The tool returns the background session ID immediately without waiting for the command to finish. Only use this for long-running processes (e.g., dev servers, watchers) where you don't need the output right away. You do not need to append '&' to the command. NOTE: `timeout_ms` is ignored when `run_in_background` is true.
231250
- Each result includes a `<terminal_session_id>` tag identifying the terminal session. The persistent shell session ID remains constant throughout the entire conversation; background sessions each have their own unique ID.
232251
- The output may include the command echo and/or the shell prompt (e.g., `PS C:\path>`). Do not treat these as part of the command's actual result.
@@ -916,3 +935,34 @@ impl BashTool {
916935
}])
917936
}
918937
}
938+
939+
#[cfg(test)]
940+
mod tests {
941+
use super::*;
942+
943+
#[test]
944+
fn truncate_output_preserving_tail_keeps_end_of_output() {
945+
let input = "BEGIN-".to_string() + &"x".repeat(120) + "-IMPORTANT-END";
946+
947+
let truncated = truncate_output_preserving_tail(&input, 80);
948+
949+
assert!(truncated.contains("tail preserved"));
950+
assert!(truncated.ends_with("IMPORTANT-END"));
951+
assert!(!truncated.contains("BEGIN-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
952+
assert!(truncated.chars().count() <= 80);
953+
}
954+
955+
#[test]
956+
fn render_result_marks_truncated_output_and_keeps_tail() {
957+
let tool = BashTool::new();
958+
let long_output =
959+
"prefix\n".to_string() + &"y".repeat(MAX_OUTPUT_LENGTH + 100) + "\nfinal-error";
960+
961+
let rendered = tool.render_result("session-1", &long_output, false, false, 1);
962+
963+
assert!(rendered.contains("<output truncated=\"true\">"));
964+
assert!(rendered.contains("tail preserved"));
965+
assert!(rendered.contains("final-error"));
966+
assert!(rendered.contains("<exit_code>1</exit_code>"));
967+
}
968+
}

0 commit comments

Comments
 (0)