Skip to main content

lean_ctx/core/
protocol.rs

1use std::path::Path;
2
3pub fn detect_project_root(file_path: &str) -> Option<String> {
4    let mut dir = Path::new(file_path).parent()?;
5    loop {
6        if dir.join(".git").exists() {
7            return Some(dir.to_string_lossy().to_string());
8        }
9        dir = dir.parent()?;
10    }
11}
12
13pub fn detect_project_root_or_cwd(file_path: &str) -> String {
14    detect_project_root(file_path).unwrap_or_else(|| {
15        std::env::current_dir()
16            .map(|p| p.to_string_lossy().to_string())
17            .unwrap_or_else(|_| ".".to_string())
18    })
19}
20
21pub fn shorten_path(path: &str) -> String {
22    let p = Path::new(path);
23    if let Some(name) = p.file_name() {
24        return name.to_string_lossy().to_string();
25    }
26    path.to_string()
27}
28
29pub fn format_savings(original: usize, compressed: usize) -> String {
30    let saved = original.saturating_sub(compressed);
31    if original == 0 {
32        return "0 tok saved".to_string();
33    }
34    let pct = (saved as f64 / original as f64 * 100.0).round() as usize;
35    format!("[{saved} tok saved ({pct}%)]")
36}
37
38pub struct InstructionTemplate {
39    pub code: &'static str,
40    pub full: &'static str,
41}
42
43const TEMPLATES: &[InstructionTemplate] = &[
44    InstructionTemplate {
45        code: "ACT1",
46        full: "Act immediately, report result in one line",
47    },
48    InstructionTemplate {
49        code: "BRIEF",
50        full: "Summarize approach in 1-2 lines, then act",
51    },
52    InstructionTemplate {
53        code: "FULL",
54        full: "Outline approach, consider edge cases, then act",
55    },
56    InstructionTemplate {
57        code: "DELTA",
58        full: "Only show changed lines, not full files",
59    },
60    InstructionTemplate {
61        code: "NOREPEAT",
62        full: "Never repeat known context. Reference cached files by Fn ID",
63    },
64    InstructionTemplate {
65        code: "STRUCT",
66        full: "Use notation, not sentences. Changes: +line/-line/~line",
67    },
68    InstructionTemplate {
69        code: "1LINE",
70        full: "One line per action. Summarize, don't explain",
71    },
72    InstructionTemplate {
73        code: "NODOC",
74        full: "Don't add comments that narrate what code does",
75    },
76    InstructionTemplate {
77        code: "ACTFIRST",
78        full: "Execute tool calls immediately. Never narrate before acting",
79    },
80    InstructionTemplate {
81        code: "QUALITY",
82        full: "Never skip edge case analysis or error handling to save tokens",
83    },
84    InstructionTemplate {
85        code: "NOMOCK",
86        full: "Never use mock data, fake values, or placeholder code",
87    },
88    InstructionTemplate {
89        code: "FREF",
90        full: "Reference files by Fn refs only, never full paths",
91    },
92    InstructionTemplate {
93        code: "DIFF",
94        full: "For code changes: show only diff lines, not full files",
95    },
96    InstructionTemplate {
97        code: "ABBREV",
98        full: "Use abbreviations: fn, cfg, impl, deps, req, res, ctx, err",
99    },
100    InstructionTemplate {
101        code: "SYMBOLS",
102        full: "Use TDD notation: +=add -=remove ~=modify ->=returns ok/fail for status",
103    },
104];
105
106/// Build the decoder block that explains all instruction codes (sent once per session).
107pub fn instruction_decoder_block() -> String {
108    let mut lines = vec!["INSTRUCTION CODES:".to_string()];
109    for t in TEMPLATES {
110        lines.push(format!("  {} = {}", t.code, t.full));
111    }
112    lines.join("\n")
113}
114
115/// Encode an instruction suffix using short codes with budget hints.
116/// Response budget is dynamic based on task complexity to shape LLM output length.
117pub fn encode_instructions(complexity: &str) -> String {
118    match complexity {
119        "mechanical" => "MODE: ACT1 DELTA 1LINE | BUDGET: <=50 tokens, 1 line answer".to_string(),
120        "simple" => "MODE: BRIEF DELTA 1LINE | BUDGET: <=100 tokens, structured".to_string(),
121        "standard" => "MODE: BRIEF DELTA NOREPEAT STRUCT | BUDGET: <=200 tokens".to_string(),
122        "complex" => {
123            "MODE: FULL QUALITY NOREPEAT STRUCT FREF DIFF | BUDGET: <=500 tokens".to_string()
124        }
125        "architectural" => {
126            "MODE: FULL QUALITY NOREPEAT STRUCT FREF | BUDGET: unlimited".to_string()
127        }
128        _ => "MODE: BRIEF | BUDGET: <=200 tokens".to_string(),
129    }
130}
131
132/// Encode instructions with SNR metric for context quality awareness.
133pub fn encode_instructions_with_snr(complexity: &str, compression_pct: f64) -> String {
134    let snr = if compression_pct > 0.0 {
135        1.0 - (compression_pct / 100.0)
136    } else {
137        1.0
138    };
139    let base = encode_instructions(complexity);
140    format!("{base} | SNR: {snr:.2}")
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn decoder_block_contains_all_codes() {
149        let block = instruction_decoder_block();
150        for t in TEMPLATES {
151            assert!(
152                block.contains(t.code),
153                "decoder should contain code {}",
154                t.code
155            );
156        }
157    }
158
159    #[test]
160    fn encoded_instructions_are_compact() {
161        use super::super::tokens::count_tokens;
162        let full = "TASK COMPLEXITY: mechanical\nMinimal reasoning needed. Act immediately, report result in one line. Show only changed lines, not full files.";
163        let encoded = encode_instructions("mechanical");
164        assert!(
165            count_tokens(&encoded) <= count_tokens(full),
166            "encoded ({}) should be <= full ({})",
167            count_tokens(&encoded),
168            count_tokens(full)
169        );
170    }
171
172    #[test]
173    fn all_complexity_levels_encode() {
174        for level in &["mechanical", "standard", "architectural"] {
175            let encoded = encode_instructions(level);
176            assert!(encoded.starts_with("MODE:"), "should start with MODE:");
177        }
178    }
179}