lean_ctx/proxy/
compress.rs1use crate::core::patterns;
2
3pub fn compress_tool_result(content: &str, tool_name: Option<&str>) -> String {
4 if content.trim().is_empty() {
5 return content.to_string();
6 }
7
8 let original_len = content.len();
9 if original_len < 200 {
10 return content.to_string();
11 }
12
13 if let Some(compressed) = try_shell_compress(content, tool_name) {
14 return compressed;
15 }
16
17 if let Some(compressed) = try_file_compress(content) {
18 return compressed;
19 }
20
21 if let Some(compressed) = try_search_compress(content) {
22 return compressed;
23 }
24
25 generic_compress(content)
26}
27
28fn try_shell_compress(content: &str, tool_name: Option<&str>) -> Option<String> {
29 let is_shell = tool_name.is_some_and(|n| {
30 let nl = n.to_lowercase();
31 nl.contains("bash")
32 || nl.contains("shell")
33 || nl.contains("terminal")
34 || nl.contains("command")
35 || nl == "execute"
36 });
37
38 if !is_shell && !looks_like_shell_output(content) {
39 return None;
40 }
41
42 let cmd_hint = extract_command_hint(content);
43 let cmd = cmd_hint.as_deref().unwrap_or("");
44
45 patterns::compress_output(cmd, content)
46}
47
48fn try_file_compress(content: &str) -> Option<String> {
49 if !looks_like_file_content(content) {
50 return None;
51 }
52
53 let lines: Vec<&str> = content.lines().collect();
54 if lines.len() <= 50 {
55 return None;
56 }
57
58 let has_numbered_lines = lines
59 .iter()
60 .take(5)
61 .any(|l| l.starts_with(|c: char| c.is_ascii_digit()) && l.contains('|'));
62
63 if has_numbered_lines {
64 let first = &lines[..10.min(lines.len())];
65 let last = &lines[lines.len().saturating_sub(10)..];
66 let omitted = lines.len().saturating_sub(20);
67 if omitted > 0 {
68 return Some(format!(
69 "{}\n[... {omitted} lines omitted ...]\n{}",
70 first.join("\n"),
71 last.join("\n"),
72 ));
73 }
74 }
75
76 None
77}
78
79fn try_search_compress(content: &str) -> Option<String> {
80 if !looks_like_search_results(content) {
81 return None;
82 }
83
84 patterns::compress_output("grep", content)
85}
86
87fn generic_compress(content: &str) -> String {
88 let lines: Vec<&str> = content.lines().collect();
89
90 if lines.len() <= 100 {
91 return content.to_string();
92 }
93
94 let mut deduped: Vec<&str> = Vec::with_capacity(lines.len());
95 let mut last_line = "";
96 let mut dup_count = 0u32;
97
98 for line in &lines {
99 if *line == last_line {
100 dup_count += 1;
101 continue;
102 }
103 if dup_count > 0 {
104 deduped.push(last_line);
105 if dup_count > 1 {
106 deduped.push(" [... repeated ...]");
107 }
108 dup_count = 0;
109 }
110 last_line = line;
111 deduped.push(line);
112 }
113 if dup_count > 0 && !last_line.is_empty() {
114 deduped.push(last_line);
115 }
116
117 if deduped.len() > 200 {
118 let first = &deduped[..30];
119 let last = &deduped[deduped.len() - 30..];
120 let omitted = deduped.len() - 60;
121 format!(
122 "{}\n[... {omitted} lines omitted ...]\n{}",
123 first.join("\n"),
124 last.join("\n"),
125 )
126 } else {
127 deduped.join("\n")
128 }
129}
130
131fn looks_like_shell_output(content: &str) -> bool {
132 let first_lines: Vec<&str> = content.lines().take(5).collect();
133 let indicators = [
134 "$ ",
135 "% ",
136 "# ",
137 "error:",
138 "warning:",
139 "Compiling",
140 "Building",
141 "running",
142 "fatal:",
143 "npm ",
144 "yarn ",
145 "cargo ",
146 "git ",
147 "make",
148 "pip ",
149 ];
150 first_lines
151 .iter()
152 .any(|l| indicators.iter().any(|i| l.contains(i)))
153}
154
155fn looks_like_file_content(content: &str) -> bool {
156 let first_lines: Vec<&str> = content.lines().take(10).collect();
157 let code_indicators = [
158 "import ",
159 "from ",
160 "use ",
161 "pub fn",
162 "fn ",
163 "class ",
164 "def ",
165 "function ",
166 "const ",
167 "let ",
168 "var ",
169 "#include",
170 "package ",
171 "module ",
172 "struct ",
173 "interface ",
174 "enum ",
175 "trait ",
176 ];
177
178 let matches = first_lines
179 .iter()
180 .filter(|l| code_indicators.iter().any(|i| l.contains(i)))
181 .count();
182
183 matches >= 2
184}
185
186fn looks_like_search_results(content: &str) -> bool {
187 let lines: Vec<&str> = content.lines().take(10).collect();
188 let pattern_count = lines
189 .iter()
190 .filter(|l| {
191 l.contains(':') && {
192 let parts: Vec<&str> = l.splitn(3, ':').collect();
193 parts.len() >= 2 && parts[0].contains('.')
194 }
195 })
196 .count();
197
198 pattern_count >= 3
199}
200
201fn extract_command_hint(content: &str) -> Option<String> {
202 for line in content.lines().take(3) {
203 let trimmed = line.trim();
204 if let Some(cmd) = trimmed.strip_prefix("$ ") {
205 return Some(cmd.to_string());
206 }
207 if let Some(cmd) = trimmed.strip_prefix("% ") {
208 return Some(cmd.to_string());
209 }
210 }
211 None
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn short_content_unchanged() {
220 let short = "hello world";
221 assert_eq!(compress_tool_result(short, None), short);
222 }
223
224 #[test]
225 fn shell_output_detected() {
226 assert!(looks_like_shell_output("$ cargo build\nCompiling foo v0.1"));
227 assert!(!looks_like_shell_output("just some text\nnothing special"));
228 }
229
230 #[test]
231 fn file_content_detected() {
232 let code = "use std::io;\nimport os\nfrom pathlib import Path\nclass Foo:\n def bar(self):\n pass\nconst x = 1;\nlet y = 2;\nvar z = 3;\nfn main() {}";
233 assert!(looks_like_file_content(code));
234 }
235
236 #[test]
237 fn search_results_detected() {
238 let grep =
239 "src/main.rs:10:fn main()\nsrc/lib.rs:5:pub mod foo\nsrc/utils.rs:20:fn helper()";
240 assert!(looks_like_search_results(grep));
241 }
242}