Skip to main content

lean_ctx/proxy/
compress.rs

1use 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}