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            && token_count >= threshold
143        {
144            return true;
145        }
146
147        // Check turn threshold
148        if let Some(threshold) = self.thresholds.turn_threshold
149            && turn_count >= threshold
150        {
151            return true;
152        }
153
154        // Check message threshold
155        if let Some(threshold) = self.thresholds.message_threshold
156            && message_count >= threshold
157        {
158            return true;
159        }
160
161        // Check turn end trigger
162        if let Some(true) = self.thresholds.on_turn_end
163            && last_is_user
164        {
165            // Only trigger if we're also close to other thresholds
166            let near_token = self
167                .thresholds
168                .token_threshold
169                .map(|t| token_count >= t / 2)
170                .unwrap_or(false);
171            let near_turn = self
172                .thresholds
173                .turn_threshold
174                .map(|t| turn_count >= t / 2)
175                .unwrap_or(false);
176
177            if near_token || near_turn {
178                return true;
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            && token_count >= threshold
194        {
195            return Some(format!(
196                "token count ({}) >= threshold ({})",
197                token_count, threshold
198            ));
199        }
200
201        if let Some(threshold) = self.thresholds.turn_threshold
202            && turn_count >= threshold
203        {
204            return Some(format!(
205                "turn count ({}) >= threshold ({})",
206                turn_count, threshold
207            ));
208        }
209
210        if let Some(threshold) = self.thresholds.message_threshold
211            && message_count >= threshold
212        {
213            return Some(format!(
214                "message count ({}) >= threshold ({})",
215                message_count, threshold
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}