Skip to main content

lean_ctx/core/config/
enums.rs

1//! Configuration enums and their behavior.
2//!
3//! Extracted from `config::mod` to keep the top-level config module focused on
4//! the `Config` struct and loading logic. These types are re-exported from the
5//! `config` module root, so external paths like `config::CompressionLevel`
6//! continue to work unchanged.
7
8use serde::{Deserialize, Serialize};
9use std::sync::atomic::AtomicU8;
10
11use super::Config;
12
13static SESSION_DEGRADE_LEVEL: AtomicU8 = AtomicU8::new(0);
14
15/// Controls when shell output is tee'd to disk for later retrieval.
16#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
17#[serde(rename_all = "lowercase")]
18pub enum TeeMode {
19    Never,
20    #[default]
21    Failures,
22    HighCompression,
23    Always,
24}
25
26/// Legacy: Controls agent output verbosity level injected into MCP instructions.
27/// Superseded by `CompressionLevel`. Kept for backward compatibility with old config.toml files.
28/// New setups use `compression_level` instead. See `CompressionLevel::effective()`.
29#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
30#[serde(rename_all = "lowercase")]
31pub enum TerseAgent {
32    #[default]
33    Off,
34    Lite,
35    Full,
36    Ultra,
37}
38
39impl TerseAgent {
40    /// Reads the terse-agent level from the `LEAN_CTX_TERSE_AGENT` env var.
41    pub fn from_env() -> Self {
42        match std::env::var("LEAN_CTX_TERSE_AGENT")
43            .unwrap_or_default()
44            .to_lowercase()
45            .as_str()
46        {
47            "lite" => Self::Lite,
48            "full" => Self::Full,
49            "ultra" => Self::Ultra,
50            _ => Self::Off,
51        }
52    }
53}
54
55/// Legacy: Controls how dense/compact MCP tool output is formatted.
56/// Superseded by `CompressionLevel`. Kept for backward compatibility with old config.toml files.
57/// New setups use `compression_level` instead. See `CompressionLevel::effective()`.
58#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
59#[serde(rename_all = "lowercase")]
60pub enum OutputDensity {
61    #[default]
62    Normal,
63    Terse,
64    Ultra,
65}
66
67impl OutputDensity {
68    /// Reads the output density from the `LEAN_CTX_OUTPUT_DENSITY` env var.
69    pub fn from_env() -> Self {
70        match std::env::var("LEAN_CTX_OUTPUT_DENSITY")
71            .unwrap_or_default()
72            .to_lowercase()
73            .as_str()
74        {
75            "terse" => Self::Terse,
76            "ultra" => Self::Ultra,
77            _ => Self::Normal,
78        }
79    }
80}
81
82/// Unified compression level that replaces the 4 separate legacy concepts:
83/// `terse_agent`, `output_density`, `terse_mode`, and `crp_mode`.
84///
85/// Controls how much detail tool responses include.
86#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
87#[serde(rename_all = "snake_case")]
88pub enum ResponseVerbosity {
89    #[default]
90    Full,
91    HeadersOnly,
92}
93
94impl ResponseVerbosity {
95    pub fn effective() -> Self {
96        if let Ok(v) = std::env::var("LEAN_CTX_RESPONSE_VERBOSITY") {
97            match v.trim().to_lowercase().as_str() {
98                "headers_only" | "headers" | "minimal" => return Self::HeadersOnly,
99                "full" | "" => return Self::Full,
100                _ => {}
101            }
102        }
103        Config::load().response_verbosity
104    }
105
106    pub fn is_headers_only(&self) -> bool {
107        matches!(self, Self::HeadersOnly)
108    }
109}
110
111/// Each level maps to specific component settings via `to_components()`.
112#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
113#[serde(rename_all = "lowercase")]
114pub enum CompressionLevel {
115    Off,
116    /// Default: plain-English "concise" guidance (bullets, no filler). Readable
117    /// by humans inspecting their rules files, and still token-saving. The
118    /// denser, symbolic styles (`Standard`/`Max`, which enable CRP and the
119    /// `→ ∵ ∴` vocabulary) are opt-in "power modes" — set `compression_level`
120    /// in config. This only shapes the model's prose; tool-output compression
121    /// is governed separately and is unaffected.
122    #[default]
123    Lite,
124    Standard,
125    Max,
126}
127
128impl CompressionLevel {
129    /// Decomposes the unified level into legacy component settings.
130    /// Returns (TerseAgent, OutputDensity, crp_mode_str, terse_mode_bool).
131    pub fn to_components(&self) -> (TerseAgent, OutputDensity, &'static str, bool) {
132        match self {
133            Self::Off => (TerseAgent::Off, OutputDensity::Normal, "off", false),
134            Self::Lite => (TerseAgent::Lite, OutputDensity::Terse, "off", true),
135            Self::Standard => (TerseAgent::Full, OutputDensity::Terse, "compact", true),
136            Self::Max => (TerseAgent::Ultra, OutputDensity::Ultra, "tdd", true),
137        }
138    }
139
140    /// Infers a `CompressionLevel` from legacy config keys for backward compatibility.
141    /// Priority: terse_agent > output_density (picks the highest implied level).
142    pub fn from_legacy(terse_agent: &TerseAgent, output_density: &OutputDensity) -> Self {
143        match (terse_agent, output_density) {
144            (TerseAgent::Ultra, _) | (_, OutputDensity::Ultra) => Self::Max,
145            (TerseAgent::Full, _) => Self::Standard,
146            (TerseAgent::Lite, _) | (_, OutputDensity::Terse) => Self::Lite,
147            _ => Self::Off,
148        }
149    }
150
151    /// Reads the compression level from the `LEAN_CTX_COMPRESSION` env var.
152    pub fn from_env() -> Option<Self> {
153        std::env::var("LEAN_CTX_COMPRESSION").ok().and_then(|v| {
154            match v.trim().to_lowercase().as_str() {
155                "off" => Some(Self::Off),
156                "lite" => Some(Self::Lite),
157                "standard" => Some(Self::Standard),
158                "max" => Some(Self::Max),
159                _ => None,
160            }
161        })
162    }
163
164    /// Returns the effective compression level with resolution order:
165    /// 0. Session-level degrade override (set by correction-loop feedback)
166    /// 1. `LEAN_CTX_COMPRESSION` env var
167    /// 2. `compression_level` in config
168    /// 3. Legacy `ultra_compact` flag (maps to `Max`)
169    /// 4. Legacy env vars (`LEAN_CTX_TERSE_AGENT`, `LEAN_CTX_OUTPUT_DENSITY`)
170    /// 5. Legacy config fields (`terse_agent`, `output_density`)
171    pub fn effective(config: &Config) -> Self {
172        if let Some(degraded) = Self::session_degrade_level() {
173            return degraded;
174        }
175        if let Some(env_level) = Self::from_env() {
176            return env_level;
177        }
178        if config.compression_level != Self::Off {
179            return config.compression_level.clone();
180        }
181        if config.ultra_compact {
182            return Self::Max;
183        }
184        let ta_env = TerseAgent::from_env();
185        let od_env = OutputDensity::from_env();
186        let ta = if ta_env == TerseAgent::Off {
187            config.terse_agent.clone()
188        } else {
189            ta_env
190        };
191        let od = if od_env == OutputDensity::Normal {
192            config.output_density.clone()
193        } else {
194            od_env
195        };
196        Self::from_legacy(&ta, &od)
197    }
198
199    /// Session-level degrade: correction loop detected, temporarily reduce compression.
200    /// 0 = no override, 1 = Off, 2 = Lite
201    pub fn session_degrade_level() -> Option<Self> {
202        match SESSION_DEGRADE_LEVEL.load(std::sync::atomic::Ordering::Relaxed) {
203            1 => Some(Self::Off),
204            2 => Some(Self::Lite),
205            _ => None,
206        }
207    }
208
209    /// Sets a session-level compression degrade (called by correction loop detection).
210    pub fn set_session_degrade(level: &Self) {
211        let val = match level {
212            Self::Off => 1u8,
213            Self::Lite => 2u8,
214            _ => 0u8,
215        };
216        SESSION_DEGRADE_LEVEL.store(val, std::sync::atomic::Ordering::Relaxed);
217    }
218
219    /// Clears the session-level degrade (recovery after correction rate drops).
220    pub fn clear_session_degrade() {
221        SESSION_DEGRADE_LEVEL.store(0, std::sync::atomic::Ordering::Relaxed);
222    }
223
224    pub fn from_str_label(s: &str) -> Option<Self> {
225        match s.trim().to_lowercase().as_str() {
226            "off" => Some(Self::Off),
227            "lite" => Some(Self::Lite),
228            "standard" | "std" => Some(Self::Standard),
229            "max" => Some(Self::Max),
230            _ => None,
231        }
232    }
233
234    pub fn is_active(&self) -> bool {
235        !matches!(self, Self::Off)
236    }
237
238    pub fn label(&self) -> &'static str {
239        match self {
240            Self::Off => "off",
241            Self::Lite => "lite",
242            Self::Standard => "standard",
243            Self::Max => "max",
244        }
245    }
246
247    pub fn description(&self) -> &'static str {
248        match self {
249            Self::Off => "No compression — full verbose output",
250            Self::Lite => "Light compression — concise output, basic terse filtering",
251            Self::Standard => {
252                "Standard compression — dense output, compact protocol, pattern-aware"
253            }
254            Self::Max => "Maximum compression — expert mode, TDD protocol, all layers active",
255        }
256    }
257}
258
259/// Where agent rule files are installed: global home dir, project-local, or both.
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub enum RulesScope {
262    Both,
263    Global,
264    Project,
265}