Skip to main content

matrixcode_core/compress/
config.rs

1//! Compression configuration and bias settings.
2
3use anyhow::Result;
4
5// ============================================================================
6// Constants
7// ============================================================================
8
9/// Compression trigger threshold (percentage of context window).
10/// Lowered to 0.5 to compress earlier for long conversations (128K context -> 64K threshold)
11pub const DEFAULT_COMPRESSION_THRESHOLD: f64 = 0.5;
12
13/// Minimum messages to keep after compression.
14/// Increased to preserve more recent context for continuity
15pub const MIN_MESSAGES_TO_KEEP: usize = 20;
16
17/// Target ratio after compression (keep this fraction of tokens).
18pub const DEFAULT_TARGET_RATIO: f64 = 0.4;
19
20/// Default model for summarization.
21pub const DEFAULT_COMPRESSOR_MODEL: &str = "claude-3-5-haiku-20241022";
22
23// ============================================================================
24// Helper Functions
25// ============================================================================
26
27/// Format token count for display.
28pub fn format_tokens(n: u32) -> String {
29    if n < 1_000 {
30        n.to_string()
31    } else if n < 10_000 {
32        format!("{:.1}K", n as f64 / 1_000.0)
33    } else {
34        format!("{:.0}K", n as f64 / 1_000.0)
35    }
36}
37
38// ============================================================================
39// Compression Bias
40// ============================================================================
41
42/// Compression bias - controls what to prioritize during compression.
43#[derive(Debug, Clone, Default)]
44pub struct CompressionBias {
45    /// Preserve tool calls and their results.
46    pub preserve_tools: bool,
47    /// Preserve thinking blocks.
48    pub preserve_thinking: bool,
49    /// Preserve user questions.
50    pub preserve_user_questions: bool,
51    /// Compact long outputs instead of removing.
52    pub compact_long_outputs: bool,
53    /// Aggressive mode - remove more content.
54    pub aggressive: bool,
55    /// Custom keywords to preserve.
56    pub preserve_keywords: Vec<String>,
57}
58
59impl CompressionBias {
60    /// Default bias - balanced preservation.
61    pub fn balanced() -> Self {
62        Self {
63            preserve_tools: true,
64            preserve_thinking: false,
65            preserve_user_questions: true,
66            compact_long_outputs: false,
67            aggressive: false,
68            preserve_keywords: vec![
69                "决定".to_string(),
70                "decision".to_string(),
71                "重要".to_string(),
72                "important".to_string(),
73                "关键".to_string(),
74                "key".to_string(),
75            ],
76        }
77    }
78
79    /// Preserve all important content.
80    pub fn preserve_important() -> Self {
81        Self {
82            preserve_tools: true,
83            preserve_thinking: true,
84            preserve_user_questions: true,
85            compact_long_outputs: true,
86            aggressive: false,
87            preserve_keywords: vec![
88                "决定".to_string(),
89                "decision".to_string(),
90                "重要".to_string(),
91                "important".to_string(),
92                "关键".to_string(),
93                "key".to_string(),
94                "完成".to_string(),
95                "done".to_string(),
96                "成功".to_string(),
97                "success".to_string(),
98            ],
99        }
100    }
101
102    /// Aggressive compression.
103    pub fn aggressive() -> Self {
104        Self {
105            preserve_tools: false,
106            preserve_thinking: false,
107            preserve_user_questions: false,
108            compact_long_outputs: false,
109            aggressive: true,
110            preserve_keywords: vec![],
111        }
112    }
113
114    /// Focus on preserving tool operations.
115    pub fn tool_focused() -> Self {
116        Self {
117            preserve_tools: true,
118            preserve_thinking: false,
119            preserve_user_questions: false,
120            compact_long_outputs: false,
121            aggressive: false,
122            preserve_keywords: vec![
123                "工具".to_string(),
124                "tool".to_string(),
125                "执行".to_string(),
126                "execute".to_string(),
127                "文件".to_string(),
128                "file".to_string(),
129            ],
130        }
131    }
132
133    /// Parse bias from a string specification.
134    pub fn parse(spec: &str) -> Result<Self> {
135        let spec = spec.trim().to_lowercase();
136
137        if spec == "balanced" || spec == "default" || spec.is_empty() {
138            return Ok(Self::balanced());
139        }
140        if spec == "aggressive" {
141            return Ok(Self::aggressive());
142        }
143        if spec == "preserve_important" || spec == "important" {
144            return Ok(Self::preserve_important());
145        }
146        if spec == "tool_focused" || spec == "tools" {
147            return Ok(Self::tool_focused());
148        }
149
150        // Parse custom specification
151        let mut bias = Self::default();
152
153        for part in spec.split_whitespace() {
154            if let Some(preserve_list) = part.strip_prefix("preserve:") {
155                for item in preserve_list.split(',') {
156                    match item.trim() {
157                        "tools" | "tool" => bias.preserve_tools = true,
158                        "thinking" | "think" => bias.preserve_thinking = true,
159                        "user" | "questions" => bias.preserve_user_questions = true,
160                        "compact" | "long" => bias.compact_long_outputs = true,
161                        _ => {}
162                    }
163                }
164            } else if let Some(keyword_list) = part.strip_prefix("keywords:") {
165                bias.preserve_keywords = keyword_list
166                    .split(',')
167                    .map(|k| k.trim().to_string())
168                    .filter(|k| !k.is_empty())
169                    .collect();
170            } else if part == "aggressive" {
171                bias.aggressive = true;
172            }
173        }
174
175        Ok(bias)
176    }
177
178    /// Format bias for display.
179    pub fn format(&self) -> String {
180        let mut parts: Vec<String> = Vec::new();
181
182        if self.preserve_tools {
183            parts.push("tools".to_string());
184        }
185        if self.preserve_thinking {
186            parts.push("thinking".to_string());
187        }
188        if self.preserve_user_questions {
189            parts.push("user".to_string());
190        }
191        if self.compact_long_outputs {
192            parts.push("compact".to_string());
193        }
194        if self.aggressive {
195            parts.push("aggressive".to_string());
196        }
197
198        if !self.preserve_keywords.is_empty() {
199            parts.push(format!("keywords:{}", self.preserve_keywords.join(",")));
200        }
201
202        if parts.is_empty() {
203            "default".to_string()
204        } else {
205            parts.join(", ")
206        }
207    }
208}
209
210// ============================================================================
211// Compression Configuration
212// ============================================================================
213
214/// Configuration for context compression.
215#[derive(Debug, Clone)]
216pub struct CompressionConfig {
217    /// Threshold (0.0-1.0) at which to trigger compression.
218    pub threshold: f64,
219    /// Maximum tokens to target after compression.
220    pub target_ratio: f64,
221    /// Minimum recent messages to always preserve.
222    pub min_preserve_messages: usize,
223    /// Whether to use AI summarization.
224    pub use_summarization: bool,
225    /// Optional model name for summarization.
226    pub compressor_model: Option<String>,
227    /// Compression bias.
228    pub bias: CompressionBias,
229}
230
231impl Default for CompressionConfig {
232    fn default() -> Self {
233        Self {
234            threshold: DEFAULT_COMPRESSION_THRESHOLD,
235            target_ratio: DEFAULT_TARGET_RATIO,
236            min_preserve_messages: MIN_MESSAGES_TO_KEEP,
237            use_summarization: true,
238            compressor_model: None,
239            bias: CompressionBias::balanced(),
240        }
241    }
242}
243
244impl CompressionConfig {
245    /// Get the compressor model name.
246    pub fn compressor_model_name(&self) -> &str {
247        self.compressor_model
248            .as_deref()
249            .unwrap_or(DEFAULT_COMPRESSOR_MODEL)
250    }
251}