syncable_cli/agent/tools/
truncation.rs1pub struct TruncationLimits {
8 pub max_file_lines: usize,
10 pub shell_prefix_lines: usize,
12 pub shell_suffix_lines: usize,
14 pub max_line_length: usize,
16 pub max_dir_entries: usize,
18}
19
20impl Default for TruncationLimits {
21 fn default() -> Self {
22 Self {
23 max_file_lines: 2000,
24 shell_prefix_lines: 200,
25 shell_suffix_lines: 200,
26 max_line_length: 2000,
27 max_dir_entries: 500,
28 }
29 }
30}
31
32pub struct TruncatedFileContent {
34 pub content: String,
36 pub total_lines: usize,
38 pub returned_lines: usize,
40 pub was_truncated: bool,
42 #[allow(dead_code)]
44 pub lines_char_truncated: usize,
45}
46
47pub fn truncate_file_content(content: &str, limits: &TruncationLimits) -> TruncatedFileContent {
49 let lines: Vec<&str> = content.lines().collect();
50 let total_lines = lines.len();
51
52 let (selected_lines, was_truncated) = if total_lines <= limits.max_file_lines {
53 (lines.clone(), false)
54 } else {
55 (lines[..limits.max_file_lines].to_vec(), true)
57 };
58
59 let mut lines_char_truncated = 0;
60 let processed: Vec<String> = selected_lines
61 .iter()
62 .map(|line| {
63 if line.chars().count() > limits.max_line_length {
64 lines_char_truncated += 1;
65 let truncated: String = line.chars().take(limits.max_line_length).collect();
66 let extra = line.chars().count() - limits.max_line_length;
67 format!("{}...[{} chars truncated]", truncated, extra)
68 } else {
69 line.to_string()
70 }
71 })
72 .collect();
73
74 let returned_lines = processed.len();
75 let mut result = processed.join("\n");
76
77 if was_truncated {
79 result.push_str(&format!(
80 "\n\n[OUTPUT TRUNCATED: Showing first {} of {} lines. Use start_line/end_line to read specific sections.]",
81 returned_lines, total_lines
82 ));
83 }
84
85 TruncatedFileContent {
86 content: result,
87 total_lines,
88 returned_lines,
89 was_truncated,
90 lines_char_truncated,
91 }
92}
93
94pub struct TruncatedShellOutput {
96 pub stdout: String,
98 pub stderr: String,
100 pub stdout_total_lines: usize,
102 pub stderr_total_lines: usize,
104 pub stdout_truncated: bool,
106 pub stderr_truncated: bool,
108}
109
110pub fn truncate_shell_output(
113 stdout: &str,
114 stderr: &str,
115 limits: &TruncationLimits,
116) -> TruncatedShellOutput {
117 let stdout_result = truncate_stream(
118 stdout,
119 limits.shell_prefix_lines,
120 limits.shell_suffix_lines,
121 limits.max_line_length,
122 );
123
124 let stderr_result = truncate_stream(
125 stderr,
126 limits.shell_prefix_lines,
127 limits.shell_suffix_lines,
128 limits.max_line_length,
129 );
130
131 TruncatedShellOutput {
132 stdout: stdout_result.0,
133 stderr: stderr_result.0,
134 stdout_total_lines: stdout_result.1,
135 stderr_total_lines: stderr_result.1,
136 stdout_truncated: stdout_result.2,
137 stderr_truncated: stderr_result.2,
138 }
139}
140
141fn truncate_stream(
143 content: &str,
144 prefix_lines: usize,
145 suffix_lines: usize,
146 max_line_length: usize,
147) -> (String, usize, bool) {
148 let lines: Vec<&str> = content.lines().collect();
149 let total_lines = lines.len();
150 let max_total = prefix_lines + suffix_lines;
151
152 if total_lines <= max_total {
153 let processed: Vec<String> = lines
155 .iter()
156 .map(|line| truncate_line(line, max_line_length))
157 .collect();
158 return (processed.join("\n"), total_lines, false);
159 }
160
161 let mut result = Vec::new();
163
164 for line in lines.iter().take(prefix_lines) {
166 result.push(truncate_line(line, max_line_length));
167 }
168
169 let hidden = total_lines - prefix_lines - suffix_lines;
171 result.push(format!(
172 "\n... [{} lines hidden, showing first {} and last {} of {} total] ...\n",
173 hidden, prefix_lines, suffix_lines, total_lines
174 ));
175
176 for line in lines.iter().skip(total_lines - suffix_lines) {
178 result.push(truncate_line(line, max_line_length));
179 }
180
181 (result.join("\n"), total_lines, true)
182}
183
184fn truncate_line(line: &str, max_length: usize) -> String {
186 if line.chars().count() <= max_length {
187 line.to_string()
188 } else {
189 let truncated: String = line.chars().take(max_length).collect();
190 let extra = line.chars().count() - max_length;
191 format!("{}...[{} chars]", truncated, extra)
192 }
193}
194
195pub struct TruncatedDirListing {
197 pub entries: Vec<serde_json::Value>,
199 pub total_entries: usize,
201 pub was_truncated: bool,
203}
204
205pub fn truncate_dir_listing(
207 entries: Vec<serde_json::Value>,
208 max_entries: usize,
209) -> TruncatedDirListing {
210 let total_entries = entries.len();
211
212 if total_entries <= max_entries {
213 TruncatedDirListing {
214 entries,
215 total_entries,
216 was_truncated: false,
217 }
218 } else {
219 TruncatedDirListing {
220 entries: entries.into_iter().take(max_entries).collect(),
221 total_entries,
222 was_truncated: true,
223 }
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_truncate_file_no_truncation_needed() {
233 let content = "line1\nline2\nline3";
234 let limits = TruncationLimits::default();
235 let result = truncate_file_content(content, &limits);
236
237 assert_eq!(result.total_lines, 3);
238 assert_eq!(result.returned_lines, 3);
239 assert!(!result.was_truncated);
240 assert_eq!(result.content, content);
241 }
242
243 #[test]
244 fn test_truncate_file_exceeds_limit() {
245 let lines: Vec<String> = (0..100).map(|i| format!("line {}", i)).collect();
246 let content = lines.join("\n");
247 let limits = TruncationLimits {
248 max_file_lines: 10,
249 ..Default::default()
250 };
251 let result = truncate_file_content(&content, &limits);
252
253 assert_eq!(result.total_lines, 100);
254 assert_eq!(result.returned_lines, 10);
255 assert!(result.was_truncated);
256 assert!(result.content.contains("[OUTPUT TRUNCATED"));
257 }
258
259 #[test]
260 fn test_truncate_shell_prefix_suffix() {
261 let lines: Vec<String> = (0..500).map(|i| format!("output line {}", i)).collect();
262 let stdout = lines.join("\n");
263 let limits = TruncationLimits {
264 shell_prefix_lines: 5,
265 shell_suffix_lines: 5,
266 ..Default::default()
267 };
268 let result = truncate_shell_output(&stdout, "", &limits);
269
270 assert_eq!(result.stdout_total_lines, 500);
271 assert!(result.stdout_truncated);
272 assert!(result.stdout.contains("output line 0"));
273 assert!(result.stdout.contains("output line 499"));
274 assert!(result.stdout.contains("lines hidden"));
275 }
276
277 #[test]
278 fn test_truncate_long_line() {
279 let long_line = "x".repeat(3000);
280 let result = truncate_line(&long_line, 100);
281
282 assert!(result.len() < 200); assert!(result.contains("chars]"));
284 }
285
286 #[test]
287 fn test_truncate_dir_listing() {
288 let entries: Vec<serde_json::Value> = (0..100)
289 .map(|i| serde_json::json!({"name": format!("file{}", i)}))
290 .collect();
291
292 let result = truncate_dir_listing(entries, 10);
293
294 assert_eq!(result.total_entries, 100);
295 assert_eq!(result.entries.len(), 10);
296 assert!(result.was_truncated);
297 }
298}