1use crate::tools::CrpMode;
3use lean_ctx::core::tokens::count_tokens;
4
5pub fn handle(response: &str, crp_mode: CrpMode) -> String {
6 handle_with_context(response, crp_mode, None)
7}
8
9pub fn handle_with_context(
10 response: &str,
11 crp_mode: CrpMode,
12 input_context: Option<&str>,
13) -> String {
14 let original_tokens = count_tokens(response);
15
16 if original_tokens <= 100 {
17 return response.to_string();
18 }
19
20 let compressed = if crp_mode.is_tdd() {
21 compress_tdd(response, input_context)
22 } else {
23 compress_standard(response, input_context)
24 };
25
26 let compressed_tokens = count_tokens(&compressed);
27 let savings = original_tokens.saturating_sub(compressed_tokens);
28 let pct = if original_tokens > 0 {
29 (savings as f64 / original_tokens as f64 * 100.0).round() as usize
30 } else {
31 0
32 };
33
34 if pct < 3 {
35 return response.to_string();
36 }
37
38 lean_ctx::core::protocol::append_savings(&compressed, original_tokens, compressed_tokens)
39}
40
41fn compress_standard(text: &str, input_context: Option<&str>) -> String {
42 let echo_lines = input_context.map(build_echo_set);
43
44 let mut result = Vec::new();
45 let mut prev_empty = false;
46
47 for line in text.lines() {
48 let trimmed = line.trim();
49
50 if trimmed.is_empty() {
51 if !prev_empty {
52 result.push(String::new());
53 prev_empty = true;
54 }
55 continue;
56 }
57 prev_empty = false;
58
59 if is_filler_line(trimmed) {
60 continue;
61 }
62 if is_boilerplate_code(trimmed) {
63 continue;
64 }
65 if let Some(ref echoes) = echo_lines
66 && is_context_echo(trimmed, echoes)
67 {
68 continue;
69 }
70
71 result.push(line.to_string());
72 }
73
74 strip_edge_pleasantries(&mut result);
75 result.join("\n")
76}
77
78fn compress_tdd(text: &str, input_context: Option<&str>) -> String {
79 let echo_lines = input_context.map(build_echo_set);
80
81 let mut result = Vec::new();
82 let mut in_fence = false;
83
84 for line in text.lines() {
85 let trimmed = line.trim();
86
87 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
88 in_fence = !in_fence;
89 result.push(line.to_string());
90 continue;
91 }
92 if in_fence {
94 if !(trimmed.is_empty() && result.last().is_some_and(String::is_empty)) {
95 result.push(line.to_string());
96 }
97 continue;
98 }
99
100 if trimmed.is_empty() {
101 continue;
102 }
103
104 if is_filler_line(trimmed) {
105 continue;
106 }
107 if is_boilerplate_code(trimmed) {
108 continue;
109 }
110 if let Some(ref echoes) = echo_lines
111 && is_context_echo(trimmed, echoes)
112 {
113 continue;
114 }
115
116 let compressed = apply_tdd_shortcuts(trimmed);
119 if count_tokens(&compressed) < count_tokens(trimmed) {
120 result.push(compressed);
121 } else {
122 result.push(trimmed.to_string());
123 }
124 }
125
126 strip_edge_pleasantries(&mut result);
127 result.join("\n")
128}
129
130fn build_echo_set(context: &str) -> std::collections::HashSet<String> {
131 context
132 .lines()
133 .map(normalize_for_echo)
134 .filter(|l| l.len() > 10)
135 .collect()
136}
137
138fn normalize_for_echo(line: &str) -> String {
139 line.trim().to_lowercase().replace(char::is_whitespace, " ")
140}
141
142fn is_context_echo(line: &str, echo_set: &std::collections::HashSet<String>) -> bool {
143 let normalized = normalize_for_echo(line);
144 if normalized.len() <= 10 {
145 return false;
146 }
147 echo_set.contains(&normalized)
148}
149
150fn is_boilerplate_code(line: &str) -> bool {
151 let trimmed = line.trim();
152
153 if trimmed.starts_with("//")
154 && !trimmed.starts_with("// TODO")
155 && !trimmed.starts_with("// FIXME")
156 && !trimmed.starts_with("// SAFETY")
157 && !trimmed.starts_with("// NOTE")
158 {
159 let comment_body = trimmed.trim_start_matches("//").trim();
160 if is_narration_comment(comment_body) {
161 return true;
162 }
163 }
164
165 if trimmed.starts_with('#') && !trimmed.starts_with("#[") && !trimmed.starts_with("#!") {
166 let comment_body = trimmed.trim_start_matches('#').trim();
167 if is_narration_comment(comment_body) {
168 return true;
169 }
170 }
171
172 false
173}
174
175fn is_narration_comment(body: &str) -> bool {
176 let b = body.to_lowercase();
177
178 let what_prefixes = [
179 "import ",
180 "define ",
181 "create ",
182 "set up ",
183 "initialize ",
184 "declare ",
185 "add ",
186 "get ",
187 "return ",
188 "check ",
189 "handle ",
190 "call ",
191 "update ",
192 "increment ",
193 "decrement ",
194 "loop ",
195 "iterate ",
196 "print ",
197 "log ",
198 "convert ",
199 "parse ",
200 "read ",
201 "write ",
202 "send ",
203 "receive ",
204 "validate ",
205 "set ",
206 "start ",
207 "stop ",
208 "open ",
209 "close ",
210 "fetch ",
211 "load ",
212 "save ",
213 "store ",
214 "delete ",
215 "remove ",
216 "calculate ",
217 "compute ",
218 "render ",
219 "display ",
220 "show ",
221 "this function ",
222 "this method ",
223 "this class ",
224 "the following ",
225 "here we ",
226 "now we ",
227 ];
228 if what_prefixes.iter().any(|p| b.starts_with(p)) {
229 return true;
230 }
231
232 let what_patterns = [" the ", " a ", " an "];
233 if b.len() < 60 && what_patterns.iter().all(|p| !b.contains(p)) {
234 return false;
235 }
236 if b.len() < 40
237 && b.split_whitespace().count() <= 5
238 && b.chars().filter(|c| c.is_uppercase()).count() == 0
239 {
240 return false;
241 }
242
243 false
244}
245
246fn is_filler_line(line: &str) -> bool {
247 let l = line.to_lowercase();
248
249 if l.starts_with("note:")
251 || l.starts_with("hint:")
252 || l.starts_with("warning:")
253 || l.starts_with("error:")
254 || l.starts_with("however,")
255 || l.starts_with("but ")
256 || l.starts_with("caution:")
257 || l.starts_with("important:")
258 {
259 return false;
260 }
261
262 let prefix_fillers = [
264 "here's what i",
266 "here is what i",
267 "let me explain",
268 "let me walk you",
269 "let me break",
270 "i'll now",
271 "i will now",
272 "i'm going to",
273 "first, let me",
274 "allow me to",
275 "i think",
277 "i believe",
278 "i would say",
279 "it seems like",
280 "it looks like",
281 "it appears that",
282 "that's a great question",
284 "that's an interesting",
285 "good question",
286 "great question",
287 "sure thing",
288 "sure,",
289 "of course,",
290 "absolutely,",
291 "now, let's",
293 "now let's",
294 "next, i'll",
295 "moving on",
296 "going forward",
297 "with that said",
298 "with that in mind",
299 "having said that",
300 "that being said",
301 "hope this helps",
303 "i hope this",
304 "let me know if",
305 "feel free to",
306 "don't hesitate",
307 "happy to help",
308 "as you can see",
310 "as we can see",
311 "this is because",
312 "the reason is",
313 "in this case",
314 "in other words",
315 "to summarize",
316 "to sum up",
317 "basically,",
318 "essentially,",
319 "it's worth noting",
320 "it should be noted",
321 "as mentioned",
322 "as i mentioned",
323 "understood.",
325 "got it.",
326 "i understand.",
327 "i see.",
328 "right,",
329 "okay,",
330 "ok,",
331 ];
332
333 prefix_fillers.iter().any(|f| l.starts_with(f))
334}
335
336const TDD_ABBREVIATIONS: &[(&str, &str)] = &[
341 ("function", "fn"),
342 ("functions", "fns"),
343 ("configuration", "cfg"),
344 ("implementation", "impl"),
345 ("dependencies", "deps"),
346 ("dependency", "dep"),
347 ("parameter", "param"),
348 ("parameters", "params"),
349 ("argument", "arg"),
350 ("arguments", "args"),
351 ("variable", "var"),
352 ("variables", "vars"),
353 ("directory", "dir"),
354 ("directories", "dirs"),
355 ("repository", "repo"),
356 ("repositories", "repos"),
357 ("application", "app"),
358 ("environment", "env"),
359 ("description", "desc"),
360 ("information", "info"),
361 ("approximately", "~"),
362 ("package", "pkg"),
363 ("packages", "pkgs"),
364];
365
366fn effective_tdd_abbreviations() -> &'static [(&'static str, &'static str)] {
370 static RULES: std::sync::OnceLock<Vec<(&'static str, &'static str)>> =
371 std::sync::OnceLock::new();
372 RULES.get_or_init(|| {
373 TDD_ABBREVIATIONS
374 .iter()
375 .copied()
376 .filter(|(from, to)| {
377 count_tokens(&format!("the {from} is")) > count_tokens(&format!("the {to} is"))
379 })
380 .collect()
381 })
382}
383
384const LEADING_WRAP: &[char] = &['(', '[', '"', '\''];
388const TRAILING_WRAP: &[char] = &['.', ',', ';', ':', '!', '?', ')', ']', '"', '\''];
389
390fn apply_tdd_shortcuts(line: &str) -> String {
394 apply_tdd_shortcuts_with(line, effective_tdd_abbreviations())
395}
396
397fn apply_tdd_shortcuts_with(line: &str, rules: &[(&str, &str)]) -> String {
398 if rules.is_empty() {
399 return line.to_string();
400 }
401 let mut out = String::with_capacity(line.len());
402 let mut in_code = false;
403 let mut word = String::new();
404 let flush = |word: &mut String, out: &mut String, in_code: bool| {
405 if word.is_empty() {
406 return;
407 }
408 let ticks = word.matches('`').count();
409 if in_code || ticks > 0 {
410 out.push_str(word);
411 } else {
412 out.push_str(&abbreviate_word(word, rules));
413 }
414 word.clear();
415 };
416 for ch in line.chars() {
417 if ch.is_whitespace() {
418 let ticks = word.matches('`').count();
419 flush(&mut word, &mut out, in_code);
420 if ticks % 2 == 1 {
421 in_code = !in_code;
422 }
423 out.push(ch);
424 } else {
425 word.push(ch);
426 }
427 }
428 flush(&mut word, &mut out, in_code);
429 out
430}
431
432fn abbreviate_word(word: &str, rules: &[(&str, &str)]) -> String {
433 let core_start = word.len() - word.trim_start_matches(LEADING_WRAP).len();
434 let core_end = word.trim_end_matches(TRAILING_WRAP).len();
435 if core_start >= core_end {
436 return word.to_string();
437 }
438 let core = &word[core_start..core_end];
439 if !core.bytes().all(|b| b.is_ascii_lowercase()) {
440 return word.to_string();
441 }
442 match rules.iter().find(|(from, _)| *from == core) {
443 Some((_, to)) => format!("{}{to}{}", &word[..core_start], &word[core_end..]),
444 None => word.to_string(),
445 }
446}
447
448const LEADING_PLEASANTRIES: &[&str] = &[
452 "sure",
453 "certainly",
454 "absolutely",
455 "of course",
456 "great question",
457 "good question",
458 "i'd be happy to",
459 "i would be happy to",
460 "i'm happy to",
461 "happy to help",
462 "thanks for",
463 "thank you for",
464];
465const TRAILING_OFFERS: &[&str] = &[
466 "let me know if",
467 "feel free to",
468 "don't hesitate",
469 "hope this helps",
470 "i hope this",
471 "if you have any other questions",
472 "if you have any questions",
473 "happy to help",
474 "is there anything else",
475];
476
477fn split_sentences(line: &str) -> Vec<&str> {
480 let mut out = Vec::new();
481 let mut start = 0;
482 let bytes = line.as_bytes();
483 for (i, b) in bytes.iter().enumerate() {
484 if matches!(b, b'.' | b'!' | b'?') && bytes.get(i + 1).is_some_and(u8::is_ascii_whitespace)
485 {
486 out.push(&line[start..=i]);
487 start = i + 1;
488 }
489 }
490 if start < line.len() {
491 out.push(&line[start..]);
492 }
493 out
494}
495
496fn is_pleasantry(sentence: &str, patterns: &[&str]) -> bool {
497 let s = sentence.trim().to_lowercase();
498 s.split_whitespace().count() <= 12 && patterns.iter().any(|p| s.starts_with(p))
501}
502
503fn strip_edge_pleasantries(lines: &mut Vec<String>) {
504 if let Some(first) = lines.iter().position(|l| !l.trim().is_empty()) {
505 let sentences = split_sentences(&lines[first]);
506 let keep = sentences
507 .iter()
508 .position(|s| !is_pleasantry(s, LEADING_PLEASANTRIES))
509 .unwrap_or(sentences.len());
510 if keep > 0 {
511 let indent_len = lines[first].len() - lines[first].trim_start().len();
512 let indent = lines[first][..indent_len].to_string();
513 let rest = sentences[keep..].concat();
514 lines[first] = format!("{indent}{}", rest.trim_start());
515 }
516 }
517 if let Some(last) = lines.iter().rposition(|l| !l.trim().is_empty()) {
518 let sentences = split_sentences(&lines[last]);
519 let keep = sentences
520 .iter()
521 .rposition(|s| !is_pleasantry(s, TRAILING_OFFERS))
522 .map_or(0, |i| i + 1);
523 if keep < sentences.len() {
524 lines[last] = sentences[..keep].concat().trim_end().to_string();
525 }
526 }
527 while lines.last().is_some_and(|l| l.trim().is_empty()) {
528 lines.pop();
529 }
530 while lines.first().is_some_and(|l| l.trim().is_empty()) {
531 lines.remove(0);
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
540 fn test_filler_detection_original() {
541 assert!(is_filler_line("Here's what I found"));
542 assert!(is_filler_line("Let me explain how this works"));
543 assert!(!is_filler_line("fn main() {}"));
544 assert!(!is_filler_line("Note: important detail"));
545 }
546
547 #[test]
548 fn test_filler_hedging_patterns() {
549 assert!(is_filler_line("I think the issue is here"));
550 assert!(is_filler_line("I believe this is correct"));
551 assert!(is_filler_line("It seems like the problem is"));
552 assert!(is_filler_line("It looks like we need to"));
553 assert!(is_filler_line("It appears that something broke"));
554 }
555
556 #[test]
557 fn test_filler_meta_commentary() {
558 assert!(is_filler_line("That's a great question!"));
559 assert!(is_filler_line("Good question, let me check"));
560 assert!(is_filler_line("Sure thing, I'll do that"));
561 assert!(is_filler_line("Of course, here's the code"));
562 assert!(is_filler_line("Absolutely, that makes sense"));
563 }
564
565 #[test]
566 fn test_filler_closings() {
567 assert!(is_filler_line("Hope this helps!"));
568 assert!(is_filler_line("Let me know if you need more"));
569 assert!(is_filler_line("Feel free to ask questions"));
570 assert!(is_filler_line("Don't hesitate to reach out"));
571 assert!(is_filler_line("Happy to help with anything"));
572 }
573
574 #[test]
575 fn test_filler_transitions() {
576 assert!(is_filler_line("Now, let's move on"));
577 assert!(is_filler_line("Moving on to the next part"));
578 assert!(is_filler_line("Going forward, we should"));
579 assert!(is_filler_line("With that said, here's what"));
580 assert!(is_filler_line("Having said that, let's"));
581 }
582
583 #[test]
584 fn test_filler_acknowledgments() {
585 assert!(is_filler_line("Understood."));
586 assert!(is_filler_line("Got it."));
587 assert!(is_filler_line("I understand."));
588 assert!(is_filler_line("I see."));
589 }
590
591 #[test]
592 fn test_filler_false_positive_protection() {
593 assert!(!is_filler_line("Note: this is critical"));
594 assert!(!is_filler_line("Warning: deprecated API"));
595 assert!(!is_filler_line("Error: connection refused"));
596 assert!(!is_filler_line("However, the edge case fails"));
597 assert!(!is_filler_line("But the second argument is wrong"));
598 assert!(!is_filler_line("Important: do not skip this step"));
599 assert!(!is_filler_line("Caution: this deletes all data"));
600 assert!(!is_filler_line("Hint: use --force flag"));
601 assert!(!is_filler_line("fn validate_token()"));
602 assert!(!is_filler_line(" let result = parse(input);"));
603 assert!(!is_filler_line("The token is expired after 24h"));
604 }
605
606 #[test]
607 fn test_tdd_shortcuts_whole_words_only() {
608 let rules: &[(&str, &str)] = &[("function", "fn")];
611 let apply = |s: &str| apply_tdd_shortcuts_with(s, rules);
612 assert_eq!(apply("the function."), "the fn.");
613 assert_eq!(apply("(function) works"), "(fn) works");
614 for keep in [
616 "function_id",
617 "myfunction",
618 "a.function",
619 "mod::function",
620 "`function`",
621 "functions_total",
622 "FUNCTION",
623 "Function",
624 "src/function/x",
625 "call `the function here` now",
626 ] {
627 assert_eq!(apply(keep), keep, "rewrote `{keep}`");
628 }
629 }
630
631 #[test]
633 fn test_tdd_no_meaning_changing_rewrites() {
634 let line = "The tests completed successfully and there were no warnings. \
635 The value is not being read; it returns the initialized env variable.";
636 let out = apply_tdd_shortcuts(line);
637 for bad in ["ok ok", "WARNs", "initd", "!=", " & ", "->", " val "] {
638 assert!(!out.contains(bad), "artifact `{bad}` in: {out}");
639 }
640 }
641
642 #[test]
643 fn test_every_active_rule_saves_tokens() {
644 for (from, to) in effective_tdd_abbreviations() {
645 assert!(
646 count_tokens(&format!("the {to} is")) < count_tokens(&format!("the {from} is")),
647 "{from}→{to} saves nothing"
648 );
649 }
650 }
651
652 const FIXTURES: &[&str] = &[
653 "Sure! I'd be happy to help with that.\n\n\
654 After reviewing the configuration, I found that the function `calculate_total` \
655 returns an incorrect value because the environment variable for the tax rate is \
656 not being read. The dependency on the configuration module was initialized after \
657 the request handler.\n\n\
658 - I moved the initialization of the configuration module before the request handler.\n\
659 - I added a test for the missing environment variable.\n\
660 - I updated the documentation in the repository.\n\n\
661 The tests completed successfully and there were no warnings.\n\n\
662 Let me know if you have any other questions!",
663 "I've updated the implementation of the parser.\n\n\
664 ```rust\nfn parse(configuration: &str) -> Result<Config, Error> {\n\n\n \
665 let parameters = configuration.split(',');\n todo!()\n}\n```\n\n\
666 The function now validates every parameter and argument before use, and the \
667 description of each package is stored in the application directory. \
668 Hope this helps! Feel free to ask if anything is unclear.",
669 "The build failed on the CI runner because the repository cache directory was \
670 missing. I recreated the directory, re-ran the pipeline, and the application \
671 now starts in approximately two seconds. The information in the environment \
672 file was stale, so I refreshed it from the deployment dependencies manifest \
673 and verified that every variable resolves.",
674 ];
675
676 #[test]
677 fn test_tdd_never_costs_more_than_off() {
678 for fixture in FIXTURES {
679 let off = compress_standard(fixture, None);
680 let tdd = compress_tdd(fixture, None);
681 assert!(
682 count_tokens(&tdd) <= count_tokens(&off),
683 "tdd {} > off {}\n--- off\n{off}\n--- tdd\n{tdd}",
684 count_tokens(&tdd),
685 count_tokens(&off)
686 );
687 }
688 }
689
690 #[test]
691 fn test_tdd_keeps_code_blocks_verbatim() {
692 let tdd = compress_tdd(FIXTURES[1], None);
693 assert!(tdd.contains("fn parse(configuration: &str) -> Result<Config, Error> {"));
694 assert!(tdd.contains(" let parameters = configuration.split(',');"));
695 }
696
697 #[test]
698 fn test_edge_pleasantries_removed_content_kept() {
699 for compress in [compress_standard, compress_tdd] {
700 let out = compress(FIXTURES[0], None);
701 assert!(!out.contains("happy to help"), "{out}");
702 assert!(!out.contains("Let me know"), "{out}");
703 assert!(out.contains("calculate_total"), "{out}");
704 assert!(out.contains("no warnings"), "{out}");
705 let out = compress(FIXTURES[1], None);
706 assert!(!out.contains("Hope this helps"), "{out}");
707 assert!(!out.contains("Feel free"), "{out}");
708 assert!(out.contains("application"), "{out}");
709 }
710 }
711
712 #[test]
713 fn test_contentful_sure_sentence_kept() {
714 let text = "Sure enough, the cache was stale because the watcher dropped the inotify event \
715 after the directory was renamed during the rebuild of the project index.";
716 let mut lines = vec![text.to_string()];
717 strip_edge_pleasantries(&mut lines);
718 assert_eq!(lines, vec![text.to_string()]);
719 }
720
721 #[test]
722 fn test_handle_tdd_saves_at_least_as_much_as_off() {
723 for fixture in FIXTURES {
724 let off = count_tokens(&handle(fixture, CrpMode::Off));
725 let tdd = count_tokens(&handle(fixture, CrpMode::Tdd));
726 assert!(tdd <= off, "tdd {tdd} > off {off}");
727 }
728 }
729
730 #[test]
731 fn test_compress_integration() {
732 let response = "Let me explain how this works.\n\
733 I think this is correct.\n\
734 Hope this helps!\n\
735 \n\
736 The function returns an error when the token is expired.\n\
737 Note: always check the expiry first.";
738
739 let compressed = compress_standard(response, None);
740 assert!(!compressed.contains("Let me explain"));
741 assert!(!compressed.contains("I think"));
742 assert!(!compressed.contains("Hope this helps"));
743 assert!(compressed.contains("error when the token"));
744 assert!(compressed.contains("Note:"));
745 }
746
747 #[test]
748 fn test_echo_detection() {
749 let context = "fn shannon_entropy(text: &str) -> f64 {\n let freq = HashMap::new();\n}";
750 let response = "Here's the code:\nfn shannon_entropy(text: &str) -> f64 {\n let freq = HashMap::new();\n}\nI added the new function below.";
751
752 let compressed = compress_standard(response, Some(context));
753 assert!(!compressed.contains("fn shannon_entropy"));
754 assert!(compressed.contains("added the new function"));
755 }
756
757 #[test]
758 fn test_boilerplate_comment_removal() {
759 let response = "// Import the module\nuse std::io;\n// Define the function\nfn main() {}\n// NOTE: important edge case\nlet x = 1;";
760 let compressed = compress_standard(response, None);
761 assert!(!compressed.contains("Import the module"));
762 assert!(!compressed.contains("Define the function"));
763 assert!(compressed.contains("NOTE: important edge case"));
764 assert!(compressed.contains("use std::io"));
765 assert!(compressed.contains("fn main()"));
766 }
767}