pub const DEFAULT_MAX_LINES: usize = 2_000;
pub const DEFAULT_MAX_BYTES: usize = 50_000;
pub fn truncate_tool_output(output: &str, max_lines: usize, max_bytes: usize) -> String {
if output.is_empty() {
return String::new();
}
let mut result = String::new();
let mut byte_count: usize = 0;
for (line_index, segment) in output.split_inclusive('\n').enumerate() {
let segment_bytes = segment.len();
if line_index >= max_lines {
let remaining_lines = output.split_inclusive('\n').count() - line_index;
result.push_str(&format!(
"... [output truncated at {max_lines} lines, {remaining_lines} lines omitted]"
));
return result;
}
if byte_count + segment_bytes > max_bytes {
let remaining_budget = max_bytes.saturating_sub(byte_count);
let mut kept_from_segment = 0;
if remaining_budget > 0 {
let mut end = remaining_budget;
while end > 0 && !segment.is_char_boundary(end) {
end -= 1;
}
kept_from_segment = end;
if end > 0 {
result.push_str(&segment[..end]);
}
}
let total_bytes = output.len();
let kept_bytes = byte_count + kept_from_segment;
if !result.ends_with('\n') {
result.push('\n');
}
result.push_str(&format!(
"... [output truncated at {max_bytes} bytes, {} bytes omitted]",
total_bytes.saturating_sub(kept_bytes)
));
return result;
}
result.push_str(segment);
byte_count += segment_bytes;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_truncation_small_input() {
let input = "line 1\nline 2\nline 3\n";
let result = truncate_tool_output(input, DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES);
assert_eq!(result, input);
}
#[test]
fn truncate_by_lines() {
let input: String = (1..=10).map(|i| format!("line {i}\n")).collect();
let result = truncate_tool_output(&input, 3, DEFAULT_MAX_BYTES);
assert!(result.starts_with("line 1\nline 2\nline 3\n"));
assert!(result.contains("[output truncated at 3 lines"));
assert!(result.contains("7 lines omitted"));
assert!(!result.contains("line 4\n"));
}
#[test]
fn truncate_by_bytes() {
let input = "x\n".repeat(100);
let result = truncate_tool_output(&input, DEFAULT_MAX_LINES, 10);
assert!(result.contains("[output truncated at 10 bytes"));
let before_marker = result.split("... [output truncated").next().unwrap();
assert!(before_marker.len() <= 10);
}
#[test]
fn empty_input() {
let result = truncate_tool_output("", 100, 100);
assert_eq!(result, "");
}
#[test]
fn char_boundary_safety() {
let input = "\u{1F600}\u{1F600}\u{1F600}\u{1F600}";
assert_eq!(input.len(), 16);
let result = truncate_tool_output(input, DEFAULT_MAX_LINES, 6);
assert!(result.contains("[output truncated"));
let before_trailer = result.split("\n... [output truncated").next().unwrap();
assert_eq!(before_trailer, "\u{1F600}");
}
#[test]
fn both_limits_hit() {
let input: String = (1..=5).map(|_| "abcdefghij\n").collect();
assert_eq!(input.len(), 55);
let result = truncate_tool_output(&input, 3, 25);
assert!(result.contains("[output truncated at 25 bytes"));
let result2 = truncate_tool_output(&input, 2, 100);
assert!(result2.contains("[output truncated at 2 lines"));
}
#[test]
fn byte_trailer_accurate_after_backtrack() {
let input = "\u{1F600}\u{1F600}\u{1F600}\u{1F600}";
assert_eq!(input.len(), 16);
let result = truncate_tool_output(input, DEFAULT_MAX_LINES, 6);
assert!(
result.contains("12 bytes omitted"),
"Expected '12 bytes omitted' in: {}",
result
);
}
}