Skip to main content

lean_ctx/proxy/
compress.rs

1use crate::core::tokens::count_tokens;
2use crate::core::web::distill;
3
4/// Char budget for the research-prose squeeze (~6k tokens). Only oversized prose
5/// is truncated; the squeeze's main job is dedup + blank-collapse, not cutting.
6const RESEARCH_PROSE_CAP: usize = 24_000;
7
8/// Proxy compression funnel: routes a tool result to the right compressor.
9///
10/// 1. Already-cited research output (from `ctx_url_read` / the web layer) is kept
11///    verbatim — it is distilled and citation-stamped, so the shell pipeline must
12///    not touch its footer or claim markers.
13/// 2. Prose results (web fetches, doc reads, research MCP bridges) are squeezed
14///    by the prose-aware research compressor instead of the log/code-tuned shell
15///    engine.
16/// 3. Everything else (shell/build/search output) flows through the unified
17///    `compress_if_beneficial` pipeline. A `$ ...` command hint is extracted so
18///    the pattern engine gets the same routing as the CLI and MCP paths.
19pub 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    // #709: honour explicit <lc_safe>…</lc_safe> spans on the proxy path too.
25    // Protected spans pass through verbatim; each unprotected segment flows back
26    // through the normal funnel (markers are stripped, so this never recurses).
27    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    // Proxy fidelity guard. A foreign shell tool gives us at most a generic
47    // command (`"shell"`) or none, so the engine's command-gated build/test
48    // verbatim guards never fire. When the *output* is unmistakably a build or
49    // test run, preserve it verbatim (bounded by safety-line-preserving
50    // truncation) so compiler errors, panics and test summaries reach the model
51    // intact — the exact signal a bug-fix task depends on.
52    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
62/// Strong, ecosystem-spanning signals that an output is a *test run* (passing or
63/// failing). Conservative — matches the summary/result lines a bug-fix task must
64/// never lose. Only consulted on the proxy path when the real command is unknown.
65fn output_looks_like_test_run(content: &str) -> bool {
66    const NEEDLES: &[&str] = &[
67        "test result:",            // rust
68        "short test summary info", // pytest
69        " passed in ",             // pytest summary
70        " failed in ",             // pytest summary
71        "=== RUN",                 // go
72        "--- FAIL:",               // go
73        "--- PASS:",               // go
74        "Test Suites:",            // jest
75        " examples, ",             // rspec ("5 examples, 0 failures")
76        "FAILED",                  // generic test failure marker
77    ];
78    NEEDLES.iter().any(|n| content.contains(n))
79}
80
81/// Strong, specific signals of a build / compile / runtime failure across the
82/// major toolchains. Used only on the proxy path for generically-named tools so
83/// the failing diagnostics (paths, lines, messages) survive intact.
84fn output_looks_like_build_failure(content: &str) -> bool {
85    const NEEDLES: &[&str] = &[
86        "error[",                            // rustc (E0277 …)
87        ": error:",                          // gcc / clang "file.c:12:5: error:"
88        "fatal error:",                      // gcc / clang
89        "undefined reference to",            // linker
90        "panicked at",                       // rust runtime
91        "could not compile",                 // cargo
92        "Traceback (most recent call last)", // python
93        "AssertionError",                    // python / junit
94        "make: ***",                         // make
95        "Build FAILED",
96        "BUILD FAILED",
97        "Segmentation fault",
98    ];
99    NEEDLES.iter().any(|n| content.contains(n))
100}
101
102/// True when `content` is a lean-ctx web read: distilled body + citation footer
103/// (`Source: …\nSite: … · Retrieved: …`). Such output is re-compression-hostile.
104fn is_cited_research_output(content: &str) -> bool {
105    content.contains("· Retrieved: ") && content.contains("\nSource: ")
106}
107
108/// Code/shell symbols whose density cleanly separates source/logs from prose.
109const CODE_SYMBOLS: &str = "{}<>;=|\\$`";
110
111/// Conservative prose detector: substantial, letter-dense, low code-symbol, with
112/// real sentences and long lines. Code, logs, tables and JSON all fail this.
113fn 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
140/// Apply the prose squeeze, returning a footer-stamped result only when it
141/// actually saves tokens; otherwise `None` so the normal pipeline can try.
142fn 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        // Repeated paragraph (well over the 600-char prose floor) → dedup keeps one.
240        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        // A forge/pi-style shell tool: the name says "shell" and the output has
264        // no `$ cmd` hint, so the engine's command-gated guards cannot fire. The
265        // compiler error must still reach the model intact for a bug-fix task.
266        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}