lean_ctx/proxy/
compress.rs1use crate::core::tokens::count_tokens;
2use crate::core::web::distill;
3
4const RESEARCH_PROSE_CAP: usize = 24_000;
7
8pub fn compress_tool_result(content: &str, tool_name: Option<&str>) -> String {
20 if content.trim().is_empty() || content.len() < 200 {
21 return content.to_string();
22 }
23
24 if crate::core::protect::has_markers(content) {
28 return crate::core::protect::compress_preserving(content, |seg| {
29 compress_tool_result(seg, tool_name)
30 });
31 }
32
33 if is_cited_research_output(content) {
34 return content.to_string();
35 }
36
37 if extract_command_hint(content).is_none()
38 && looks_like_prose(content)
39 && let Some(out) = squeeze_research_prose(content)
40 {
41 return out;
42 }
43
44 let cmd = infer_command(content, tool_name);
45
46 let generic_command = cmd.is_empty() || cmd == "shell";
53 if generic_command
54 && (output_looks_like_test_run(content) || output_looks_like_build_failure(content))
55 {
56 return crate::shell::compress::engine::preserve_verbatim_pub(content);
57 }
58
59 crate::shell::compress::engine::compress_if_beneficial(&cmd, content)
60}
61
62fn output_looks_like_test_run(content: &str) -> bool {
66 const NEEDLES: &[&str] = &[
67 "test result:", "short test summary info", " passed in ", " failed in ", "=== RUN", "--- FAIL:", "--- PASS:", "Test Suites:", " examples, ", "FAILED", ];
78 NEEDLES.iter().any(|n| content.contains(n))
79}
80
81fn output_looks_like_build_failure(content: &str) -> bool {
85 const NEEDLES: &[&str] = &[
86 "error[", ": error:", "fatal error:", "undefined reference to", "panicked at", "could not compile", "Traceback (most recent call last)", "AssertionError", "make: ***", "Build FAILED",
96 "BUILD FAILED",
97 "Segmentation fault",
98 ];
99 NEEDLES.iter().any(|n| content.contains(n))
100}
101
102fn is_cited_research_output(content: &str) -> bool {
105 content.contains("· Retrieved: ") && content.contains("\nSource: ")
106}
107
108const CODE_SYMBOLS: &str = "{}<>;=|\\$`";
110
111fn looks_like_prose(content: &str) -> bool {
114 let sample: String = content.chars().take(4000).collect();
115 let total = sample.chars().count();
116 if total < 600 {
117 return false;
118 }
119 let total_f = total as f32;
120 let alpha = sample.chars().filter(|c| c.is_alphabetic()).count() as f32;
121 let spaces = sample.chars().filter(|c| *c == ' ').count() as f32;
122 let symbols = sample.chars().filter(|c| CODE_SYMBOLS.contains(*c)).count() as f32;
123
124 if alpha / total_f < 0.6 || spaces / total_f < 0.12 || symbols / total_f > 0.06 {
125 return false;
126 }
127 if sample.matches(['.', '!', '?']).count() < 4 {
128 return false;
129 }
130
131 let non_empty: Vec<&str> = sample.lines().filter(|l| !l.trim().is_empty()).collect();
132 if non_empty.is_empty() {
133 return false;
134 }
135 let avg_len =
136 non_empty.iter().map(|l| l.chars().count()).sum::<usize>() as f32 / non_empty.len() as f32;
137 avg_len >= 40.0
138}
139
140fn squeeze_research_prose(content: &str) -> Option<String> {
143 let before = count_tokens(content);
144 let squeezed = distill::squeeze_prose(content, RESEARCH_PROSE_CAP);
145 if squeezed.trim().is_empty() {
146 return None;
147 }
148 let after = count_tokens(&squeezed);
149 if after + 2 >= before {
150 return None;
151 }
152 Some(crate::core::protocol::append_savings_with_info(
153 &squeezed,
154 before,
155 after,
156 Some("research"),
157 None,
158 ))
159}
160
161fn infer_command(content: &str, tool_name: Option<&str>) -> String {
162 if let Some(cmd) = extract_command_hint(content) {
163 return cmd;
164 }
165
166 if let Some(name) = tool_name {
167 let nl = name.to_lowercase();
168 if nl.contains("bash") || nl.contains("shell") || nl.contains("terminal") {
169 return "shell".to_string();
170 }
171 if nl.contains("search") || nl.contains("grep") || nl.contains("find") {
172 return "grep".to_string();
173 }
174 }
175
176 String::new()
177}
178
179fn extract_command_hint(content: &str) -> Option<String> {
180 for line in content.lines().take(3) {
181 let trimmed = line.trim();
182 if let Some(cmd) = trimmed.strip_prefix("$ ") {
183 return Some(cmd.to_string());
184 }
185 if let Some(cmd) = trimmed.strip_prefix("% ") {
186 return Some(cmd.to_string());
187 }
188 }
189 None
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn short_content_unchanged() {
198 let short = "hello world";
199 assert_eq!(compress_tool_result(short, None), short);
200 }
201
202 #[test]
203 fn empty_content_unchanged() {
204 assert_eq!(compress_tool_result("", None), "");
205 assert_eq!(compress_tool_result(" ", None), " ");
206 }
207
208 #[test]
209 fn command_hint_extraction() {
210 assert_eq!(
211 extract_command_hint("$ cargo build\nCompiling foo"),
212 Some("cargo build".to_string())
213 );
214 assert_eq!(extract_command_hint("no prefix here"), None);
215 }
216
217 #[test]
218 fn tool_name_inference() {
219 assert_eq!(infer_command("some text", Some("bash_execute")), "shell");
220 assert_eq!(infer_command("some text", Some("search_files")), "grep");
221 assert_eq!(infer_command("some text", Some("unknown_tool")), "");
222 }
223
224 #[test]
225 fn cited_research_output_is_preserved_verbatim() {
226 let cited = format!(
227 "Rust is a language.\n\n---\nSource: Rust — https://x.com/a\n\
228 Site: x.com · Retrieved: 2026-06-06T00:00:00Z\n{}",
229 "Extra body line that would otherwise be touched. ".repeat(20)
230 );
231 assert_eq!(compress_tool_result(&cited, Some("ctx_url_read")), cited);
232 }
233
234 #[test]
235 fn prose_is_squeezed_and_deduped() {
236 let para = "Rust is a multi-paradigm systems programming language that \
237 emphasizes performance, type safety, and fearless concurrency, \
238 achieving memory safety without a garbage collector at runtime.";
239 let input = format!("{}\n", [para; 8].join("\n\n"));
241 assert!(input.len() > 600);
242 let out = compress_tool_result(&input, Some("web_fetch"));
243 assert_eq!(out.matches("fearless concurrency").count(), 1);
244 assert!(out.contains("performance, type safety"));
245 }
246
247 #[test]
248 fn code_output_is_not_treated_as_prose() {
249 let code = "fn main() {\n let x = vec![1, 2, 3];\n \
250 for i in &x { println!(\"{}\", i); }\n}\n"
251 .repeat(20);
252 assert!(!looks_like_prose(&code));
253 }
254
255 #[test]
256 fn shell_log_is_not_treated_as_prose() {
257 let log = "$ cargo build\n Compiling foo v0.1.0\n Finished dev\n".repeat(20);
258 assert!(!looks_like_prose(&log));
259 }
260
261 #[test]
262 fn foreign_shell_build_failure_preserved_verbatim() {
263 let mut log = String::from("gcc -O2 -c src/versioncmp.c -o versioncmp.o\n");
267 log.push_str("src/versioncmp.c: In function 'version_cmp':\n");
268 log.push_str(
269 "src/versioncmp.c:142:17: error: invalid operands to binary < (have 'char *' and 'int')\n",
270 );
271 for i in 0..40 {
272 log.push_str(&format!(" note: expansion context line {i}\n"));
273 }
274 log.push_str("make: *** [Makefile:23: versioncmp.o] Error 1\n");
275
276 let out = compress_tool_result(&log, Some("shell"));
277 assert!(
278 out.contains("versioncmp.c:142:17: error:"),
279 "compiler error must survive the proxy"
280 );
281 assert!(
282 out.contains("make: ***"),
283 "make failure summary must survive"
284 );
285 }
286
287 #[test]
288 fn foreign_shell_test_failure_preserved_verbatim() {
289 let mut log = String::from("running 3 tests\n");
290 log.push_str("test version::tests::sorts_numeric ... FAILED\n");
291 for i in 0..40 {
292 log.push_str(&format!("note line {i} with some filler content here\n"));
293 }
294 log.push_str("test result: FAILED. 2 passed; 1 failed; 0 ignored\n");
295
296 let out = compress_tool_result(&log, Some("bash"));
297 assert!(
298 out.contains("test result: FAILED"),
299 "test summary must survive the proxy"
300 );
301 assert!(out.contains("sorts_numeric ... FAILED"));
302 }
303
304 #[test]
305 fn plain_shell_log_not_forced_verbatim() {
306 let log = "Listening on port 8080\nRequest received from 10.0.0.2\n".repeat(20);
307 assert!(!output_looks_like_test_run(&log));
308 assert!(!output_looks_like_build_failure(&log));
309 }
310}