1use super::ccr;
2use crate::core::tokens::count_tokens;
3use crate::core::web::distill;
4
5const RESEARCH_PROSE_CAP: usize = 24_000;
8
9pub fn compress_tool_result(content: &str, tool_name: Option<&str>) -> String {
21 let compressed = compress_inner(content, tool_name);
22 attach_ccr(content, compressed)
23}
24
25fn attach_ccr(original: &str, result: String) -> String {
32 if original.len() < ccr::MIN_TEE_BYTES
33 || original.len().saturating_sub(result.len()) < ccr::MIN_TEE_BYTES
34 {
35 return result;
36 }
37 match ccr::persist(original) {
38 Some(handle) => match ccr::inband_locator(&handle) {
39 Some(marker) => format!(
43 "{result}\n[lean-ctx: full original elided to save tokens — echo {marker} \
44 on your next turn to get the verbatim original spliced back inline]"
45 ),
46 None => format!(
48 "{result}\n[lean-ctx: full original at {handle} — read it, or \
49 ctx_expand(id=\"{handle}\", head=N|search=\"…\"|json_path=\"…\") for a slice]"
50 ),
51 },
52 None => result,
53 }
54}
55
56fn compress_inner(content: &str, tool_name: Option<&str>) -> String {
57 if content.trim().is_empty() || content.len() < 200 {
58 return content.to_string();
59 }
60
61 if tool_name.is_some_and(is_lean_ctx_tool) {
68 return content.to_string();
69 }
70
71 if crate::core::protect::has_markers(content) {
75 return crate::core::protect::compress_preserving(content, |seg| {
76 compress_inner(seg, tool_name)
77 });
78 }
79
80 if is_cited_research_output(content) {
81 return content.to_string();
82 }
83
84 if extract_command_hint(content).is_none()
85 && looks_like_prose(content)
86 && let Some(out) = squeeze_research_prose(content)
87 {
88 return out;
89 }
90
91 let cmd = infer_command(content, tool_name);
92
93 let generic_command = cmd.is_empty() || cmd == "shell";
100 if generic_command
101 && (output_looks_like_test_run(content) || output_looks_like_build_failure(content))
102 {
103 return crate::shell::compress::engine::preserve_verbatim_pub(content);
104 }
105
106 crate::shell::compress::engine::compress_if_beneficial(&cmd, content)
107}
108
109fn output_looks_like_test_run(content: &str) -> bool {
113 const NEEDLES: &[&str] = &[
114 "test result:", "short test summary info", " passed in ", " failed in ", "=== RUN", "--- FAIL:", "--- PASS:", "Test Suites:", " examples, ", "FAILED", ];
125 NEEDLES.iter().any(|n| content.contains(n))
126}
127
128fn output_looks_like_build_failure(content: &str) -> bool {
132 const NEEDLES: &[&str] = &[
133 "error[", ": error:", "fatal error:", "undefined reference to", "panicked at", "could not compile", "Traceback (most recent call last)", "AssertionError", "make: ***", "Build FAILED",
143 "BUILD FAILED",
144 "Segmentation fault",
145 ];
146 NEEDLES.iter().any(|n| content.contains(n))
147}
148
149fn is_cited_research_output(content: &str) -> bool {
152 content.contains("· Retrieved: ") && content.contains("\nSource: ")
153}
154
155const CODE_SYMBOLS: &str = "{}<>;=|\\$`";
157
158fn looks_like_prose(content: &str) -> bool {
161 let sample: String = content.chars().take(4000).collect();
162 let total = sample.chars().count();
163 if total < 600 {
164 return false;
165 }
166 let total_f = total as f32;
167 let alpha = sample.chars().filter(|c| c.is_alphabetic()).count() as f32;
168 let spaces = sample.chars().filter(|c| *c == ' ').count() as f32;
169 let symbols = sample.chars().filter(|c| CODE_SYMBOLS.contains(*c)).count() as f32;
170
171 if alpha / total_f < 0.6 || spaces / total_f < 0.12 || symbols / total_f > 0.06 {
172 return false;
173 }
174 if sample.matches(['.', '!', '?']).count() < 4 {
175 return false;
176 }
177
178 let non_empty: Vec<&str> = sample.lines().filter(|l| !l.trim().is_empty()).collect();
179 if non_empty.is_empty() {
180 return false;
181 }
182 let avg_len =
183 non_empty.iter().map(|l| l.chars().count()).sum::<usize>() as f32 / non_empty.len() as f32;
184 avg_len >= 40.0
185}
186
187fn squeeze_research_prose(content: &str) -> Option<String> {
190 let before = count_tokens(content);
191 let squeezed = distill::squeeze_prose(content, RESEARCH_PROSE_CAP);
192 if squeezed.trim().is_empty() {
193 return None;
194 }
195 let after = count_tokens(&squeezed);
196 if after + 2 >= before {
197 return None;
198 }
199 Some(crate::core::protocol::append_savings_with_info(
200 &squeezed,
201 before,
202 after,
203 Some("research"),
204 None,
205 ))
206}
207
208fn is_lean_ctx_tool(name: &str) -> bool {
217 let bare = name
218 .rsplit("__")
219 .next()
220 .unwrap_or(name)
221 .rsplit([':', '/', '.'])
222 .next()
223 .unwrap_or(name);
224 bare.starts_with("ctx_") || name.starts_with("ctx_")
225}
226
227fn infer_command(content: &str, tool_name: Option<&str>) -> String {
228 if let Some(cmd) = extract_command_hint(content) {
229 return cmd;
230 }
231
232 if let Some(name) = tool_name {
233 let nl = name.to_lowercase();
234 if nl.contains("bash") || nl.contains("shell") || nl.contains("terminal") {
235 return "shell".to_string();
236 }
237 if nl.contains("search") || nl.contains("grep") || nl.contains("find") {
238 return "grep".to_string();
239 }
240 }
241
242 String::new()
243}
244
245fn extract_command_hint(content: &str) -> Option<String> {
246 for line in content.lines().take(3) {
247 let trimmed = line.trim();
248 if let Some(cmd) = trimmed.strip_prefix("$ ") {
249 return Some(cmd.to_string());
250 }
251 if let Some(cmd) = trimmed.strip_prefix("% ") {
252 return Some(cmd.to_string());
253 }
254 }
255 None
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn short_content_unchanged() {
264 let short = "hello world";
265 assert_eq!(compress_tool_result(short, None), short);
266 }
267
268 #[test]
269 fn empty_content_unchanged() {
270 assert_eq!(compress_tool_result("", None), "");
271 assert_eq!(compress_tool_result(" ", None), " ");
272 }
273
274 #[test]
275 fn command_hint_extraction() {
276 assert_eq!(
277 extract_command_hint("$ cargo build\nCompiling foo"),
278 Some("cargo build".to_string())
279 );
280 assert_eq!(extract_command_hint("no prefix here"), None);
281 }
282
283 #[test]
284 fn tool_name_inference() {
285 assert_eq!(infer_command("some text", Some("bash_execute")), "shell");
286 assert_eq!(infer_command("some text", Some("search_files")), "grep");
287 assert_eq!(infer_command("some text", Some("unknown_tool")), "");
288 }
289
290 #[test]
291 fn lean_ctx_tool_results_pass_through_verbatim() {
292 let raw = (1..=120)
296 .map(|i| format!("Line {i:04}: the quick brown fox jumps over the lazy dog"))
297 .collect::<Vec<_>>()
298 .join("\n");
299 assert!(raw.len() > 200);
300 for tool in [
303 "ctx_shell",
304 "ctx_read",
305 "ctx_search",
306 "ctx_grep",
307 "mcp__lean-ctx__ctx_shell",
308 "lean-ctx:ctx_read",
309 ] {
310 assert_eq!(
311 compress_tool_result(&raw, Some(tool)),
312 raw,
313 "{tool} output must pass through the proxy verbatim"
314 );
315 }
316 assert_ne!(
319 compress_tool_result(&raw, Some("bash")),
320 raw,
321 "foreign-tool output should still be compressed by the proxy"
322 );
323 }
324
325 #[test]
326 fn cited_research_output_is_preserved_verbatim() {
327 let cited = format!(
328 "Rust is a language.\n\n---\nSource: Rust — https://x.com/a\n\
329 Site: x.com · Retrieved: 2026-06-06T00:00:00Z\n{}",
330 "Extra body line that would otherwise be touched. ".repeat(20)
331 );
332 assert_eq!(compress_tool_result(&cited, Some("ctx_url_read")), cited);
333 }
334
335 #[test]
336 fn prose_is_squeezed_and_deduped() {
337 let para = "Rust is a multi-paradigm systems programming language that \
338 emphasizes performance, type safety, and fearless concurrency, \
339 achieving memory safety without a garbage collector at runtime.";
340 let input = format!("{}\n", [para; 8].join("\n\n"));
342 assert!(input.len() > 600);
343 let out = compress_tool_result(&input, Some("web_fetch"));
344 assert_eq!(out.matches("fearless concurrency").count(), 1);
345 assert!(out.contains("performance, type safety"));
346 }
347
348 #[test]
349 fn code_output_is_not_treated_as_prose() {
350 let code = "fn main() {\n let x = vec![1, 2, 3];\n \
351 for i in &x { println!(\"{}\", i); }\n}\n"
352 .repeat(20);
353 assert!(!looks_like_prose(&code));
354 }
355
356 #[test]
357 fn shell_log_is_not_treated_as_prose() {
358 let log = "$ cargo build\n Compiling foo v0.1.0\n Finished dev\n".repeat(20);
359 assert!(!looks_like_prose(&log));
360 }
361
362 #[test]
363 fn foreign_shell_build_failure_preserved_verbatim() {
364 let mut log = String::from("gcc -O2 -c src/versioncmp.c -o versioncmp.o\n");
368 log.push_str("src/versioncmp.c: In function 'version_cmp':\n");
369 log.push_str(
370 "src/versioncmp.c:142:17: error: invalid operands to binary < (have 'char *' and 'int')\n",
371 );
372 for i in 0..40 {
373 log.push_str(&format!(" note: expansion context line {i}\n"));
374 }
375 log.push_str("make: *** [Makefile:23: versioncmp.o] Error 1\n");
376
377 let out = compress_tool_result(&log, Some("shell"));
378 assert!(
379 out.contains("versioncmp.c:142:17: error:"),
380 "compiler error must survive the proxy"
381 );
382 assert!(
383 out.contains("make: ***"),
384 "make failure summary must survive"
385 );
386 }
387
388 #[test]
389 fn foreign_shell_test_failure_preserved_verbatim() {
390 let mut log = String::from("running 3 tests\n");
391 log.push_str("test version::tests::sorts_numeric ... FAILED\n");
392 for i in 0..40 {
393 log.push_str(&format!("note line {i} with some filler content here\n"));
394 }
395 log.push_str("test result: FAILED. 2 passed; 1 failed; 0 ignored\n");
396
397 let out = compress_tool_result(&log, Some("bash"));
398 assert!(
399 out.contains("test result: FAILED"),
400 "test summary must survive the proxy"
401 );
402 assert!(out.contains("sorts_numeric ... FAILED"));
403 }
404
405 #[test]
406 fn plain_shell_log_not_forced_verbatim() {
407 let log = "Listening on port 8080\nRequest received from 10.0.0.2\n".repeat(20);
408 assert!(!output_looks_like_test_run(&log));
409 assert!(!output_looks_like_build_failure(&log));
410 }
411
412 fn big_compressible_log() -> String {
413 (1..=400)
414 .map(|i| format!("[info] processed item {i:04} ok"))
415 .collect::<Vec<_>>()
416 .join("\n")
417 }
418
419 #[test]
420 fn live_compression_is_recoverable_via_ccr_handle() {
421 let _lock = crate::core::data_dir::test_env_lock();
422 let log = big_compressible_log();
423 let out = compress_tool_result(&log, Some("bash"));
424 assert!(
425 out.len() < log.len(),
426 "a large foreign log must be compressed"
427 );
428
429 let handle = ccr::persist(&log).expect("same content -> same handle");
433 assert!(out.contains(&handle), "CCR handle must be embedded: {out}");
434 let recovered = std::fs::read_to_string(&handle).expect("tee file readable");
435 assert!(
436 recovered.contains("processed item 0007 ok")
437 && recovered.contains("processed item 0400 ok"),
438 "verbatim original must be fully recoverable"
439 );
440 }
441
442 #[test]
443 fn live_compression_output_is_byte_stable_across_turns() {
444 let _lock = crate::core::data_dir::test_env_lock();
445 let log = big_compressible_log();
446 let a = compress_tool_result(&log, Some("bash"));
447 let b = compress_tool_result(&log, Some("bash"));
448 assert_eq!(
449 a, b,
450 "the CCR handle is content-addressed, so the rewritten result must be \
451 byte-identical across turns (provider cache prefix stays valid, #448)"
452 );
453 }
454
455 #[test]
456 fn small_or_passthrough_output_gets_no_ccr_handle() {
457 let _lock = crate::core::data_dir::test_env_lock();
458 let tiny = "ok\n".repeat(10);
460 assert!(!compress_tool_result(&tiny, Some("bash")).contains("full original at"));
461 let raw = (1..=120)
463 .map(|i| format!("Line {i:04}: lorem ipsum dolor sit amet consectetur"))
464 .collect::<Vec<_>>()
465 .join("\n");
466 let out = compress_tool_result(&raw, Some("ctx_shell"));
467 assert_eq!(out, raw, "lean-ctx tool result must stay verbatim (no CCR)");
468 }
469}