syncable_cli/agent/compact/
config.rs

1//! Compaction configuration
2//!
3//! Defines when and how compaction should be triggered.
4
5use serde::{Deserialize, Serialize};
6
7/// Default values for compaction
8pub mod defaults {
9    /// Default retention window - messages to keep after compaction
10    pub const RETENTION_WINDOW: usize = 10;
11
12    /// Default eviction window - percentage of context to summarize
13    pub const EVICTION_WINDOW: f64 = 0.6;
14
15    /// Default token threshold - trigger compaction at this token count
16    pub const TOKEN_THRESHOLD: usize = 80_000;
17
18    /// Default turn threshold - trigger compaction after this many turns
19    pub const TURN_THRESHOLD: usize = 20;
20
21    /// Default message threshold - trigger compaction after this many messages
22    pub const MESSAGE_THRESHOLD: usize = 50;
23}
24
25/// Thresholds that trigger compaction
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CompactThresholds {
28    /// Token count threshold (triggers when exceeded)
29    pub token_threshold: Option<usize>,
30
31    /// User turn count threshold (triggers when exceeded)
32    pub turn_threshold: Option<usize>,
33
34    /// Total message count threshold (triggers when exceeded)
35    pub message_threshold: Option<usize>,
36
37    /// Trigger compaction when last message is from user
38    /// (useful for compacting before sending new request)
39    pub on_turn_end: Option<bool>,
40}
41
42impl Default for CompactThresholds {
43    fn default() -> Self {
44        Self {
45            token_threshold: Some(defaults::TOKEN_THRESHOLD),
46            turn_threshold: Some(defaults::TURN_THRESHOLD),
47            message_threshold: Some(defaults::MESSAGE_THRESHOLD),
48            on_turn_end: None,
49        }
50    }
51}
52
53impl CompactThresholds {
54    /// Create minimal thresholds (for aggressive compaction)
55    pub fn aggressive() -> Self {
56        Self {
57            token_threshold: Some(40_000),
58            turn_threshold: Some(10),
59            message_threshold: Some(25),
60            on_turn_end: Some(true),
61        }
62    }
63
64    /// Create relaxed thresholds (for large context windows)
65    pub fn relaxed() -> Self {
66        Self {
67            token_threshold: Some(150_000),
68            turn_threshold: Some(50),
69            message_threshold: Some(100),
70            on_turn_end: None,
71        }
72    }
73
74    /// Disable all thresholds (manual compaction only)
75    pub fn disabled() -> Self {
76        Self {
77            token_threshold: None,
78            turn_threshold: None,
79            message_threshold: None,
80            on_turn_end: None,
81        }
82    }
83}
84
85/// Complete compaction configuration
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct CompactConfig {
88    /// Number of most recent messages to always preserve
89    pub retention_window: usize,
90
91    /// Percentage of context eligible for summarization (0.0-1.0)
92    /// Higher = more aggressive compaction
93    pub eviction_window: f64,
94
95    /// Thresholds that trigger automatic compaction
96    pub thresholds: CompactThresholds,
97}
98
99impl Default for CompactConfig {
100    fn default() -> Self {
101        Self {
102            retention_window: defaults::RETENTION_WINDOW,
103            eviction_window: defaults::EVICTION_WINDOW,
104            thresholds: CompactThresholds::default(),
105        }
106    }
107}
108
109impl CompactConfig {
110    /// Create with custom retention window
111    pub fn with_retention(retention: usize) -> Self {
112        Self {
113            retention_window: retention,
114            ..Default::default()
115        }
116    }
117
118    /// Create with custom thresholds
119    pub fn with_thresholds(thresholds: CompactThresholds) -> Self {
120        Self {
121            thresholds,
122            ..Default::default()
123        }
124    }
125
126    /// Check if compaction should be triggered based on current state
127    ///
128    /// # Arguments
129    /// * `token_count` - Current estimated token count
130    /// * `turn_count` - Number of user turns
131    /// * `message_count` - Total number of messages
132    /// * `last_is_user` - Whether the last message is from user
133    pub fn should_compact(
134        &self,
135        token_count: usize,
136        turn_count: usize,
137        message_count: usize,
138        last_is_user: bool,
139    ) -> bool {
140        // Check token threshold
141        if let Some(threshold) = self.thresholds.token_threshold {
142            if token_count >= threshold {
143                return true;
144            }
145        }
146
147        // Check turn threshold
148        if let Some(threshold) = self.thresholds.turn_threshold {
149            if turn_count >= threshold {
150                return true;
151            }
152        }
153
154        // Check message threshold
155        if let Some(threshold) = self.thresholds.message_threshold {
156            if message_count >= threshold {
157                return true;
158            }
159        }
160
161        // Check turn end trigger
162        if let Some(true) = self.thresholds.on_turn_end {
163            if last_is_user {
164                // Only trigger if we're also close to other thresholds
165                let near_token = self
166                    .thresholds
167                    .token_threshold
168                    .map(|t| token_count >= t / 2)
169                    .unwrap_or(false);
170                let near_turn = self
171                    .thresholds
172                    .turn_threshold
173                    .map(|t| turn_count >= t / 2)
174                    .unwrap_or(false);
175
176                if near_token || near_turn {
177                    return true;
178                }
179            }
180        }
181
182        false
183    }
184
185    /// Get the reason why compaction was triggered
186    pub fn compaction_reason(
187        &self,
188        token_count: usize,
189        turn_count: usize,
190        message_count: usize,
191    ) -> Option<String> {
192        if let Some(threshold) = self.thresholds.token_threshold {
193            if token_count >= threshold {
194                return Some(format!(
195                    "token count ({}) >= threshold ({})",
196                    token_count, threshold
197                ));
198            }
199        }
200
201        if let Some(threshold) = self.thresholds.turn_threshold {
202            if turn_count >= threshold {
203                return Some(format!(
204                    "turn count ({}) >= threshold ({})",
205                    turn_count, threshold
206                ));
207            }
208        }
209
210        if let Some(threshold) = self.thresholds.message_threshold {
211            if message_count >= threshold {
212                return Some(format!(
213                    "message count ({}) >= threshold ({})",
214                    message_count, threshold
215                ));
216            }
217        }
218
219        None
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_default_config() {
229        let config = CompactConfig::default();
230        assert_eq!(config.retention_window, defaults::RETENTION_WINDOW);
231        assert!((config.eviction_window - defaults::EVICTION_WINDOW).abs() < f64::EPSILON);
232    }
233
234    #[test]
235    fn test_should_compact_tokens() {
236        let config = CompactConfig::default();
237        assert!(!config.should_compact(50_000, 5, 10, false));
238        assert!(config.should_compact(100_000, 5, 10, false));
239    }
240
241    #[test]
242    fn test_should_compact_turns() {
243        let config = CompactConfig::default();
244        assert!(!config.should_compact(10_000, 10, 20, false));
245        assert!(config.should_compact(10_000, 25, 50, false));
246    }
247
248    #[test]
249    fn test_should_compact_messages() {
250        let config = CompactConfig::default();
251        assert!(!config.should_compact(10_000, 10, 30, false));
252        assert!(config.should_compact(10_000, 10, 60, false));
253    }
254
255    #[test]
256    fn test_aggressive_thresholds() {
257        let thresholds = CompactThresholds::aggressive();
258        assert_eq!(thresholds.token_threshold, Some(40_000));
259        assert_eq!(thresholds.turn_threshold, Some(10));
260    }
261
262    #[test]
263    fn test_disabled_thresholds() {
264        let config = CompactConfig::with_thresholds(CompactThresholds::disabled());
265        assert!(!config.should_compact(1_000_000, 1000, 10000, true));
266    }
267}