Skip to main content

enact_context/
compactor.rs

1//! Context Compaction
2//!
3//! Strategies for reducing context size when approaching limits.
4//!
5//! @see packages/enact-schemas/src/context.schemas.ts
6
7use crate::segment::{ContextPriority, ContextSegment, ContextSegmentType};
8use chrono::{DateTime, Utc};
9use enact_core::kernel::ExecutionId;
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13/// Compaction errors
14#[derive(Debug, Error)]
15pub enum CompactionError {
16    #[error("Nothing to compact")]
17    NothingToCompact,
18
19    #[error("Target token count too low: {0}")]
20    TargetTooLow(usize),
21
22    #[error("Summarization failed: {0}")]
23    SummarizationFailed(String),
24}
25
26/// Available compaction strategies
27///
28/// Matches `compactionStrategyTypeSchema` in @enact/schemas
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum CompactionStrategyType {
32    /// Simple truncation (remove oldest)
33    Truncate,
34    /// LLM summarization
35    Summarize,
36    /// Extract key points only
37    ExtractKeyPoints,
38    /// Keep only recent N messages
39    SlidingWindow,
40    /// Keep based on importance scores
41    ImportanceWeighted,
42    /// Combination of strategies
43    Hybrid,
44}
45
46/// Configuration for context compaction
47///
48/// Matches `compactionStrategySchema` in @enact/schemas
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct CompactionStrategy {
52    /// Strategy type
53    #[serde(rename = "type")]
54    pub strategy_type: CompactionStrategyType,
55
56    /// Target token count after compaction
57    pub target_tokens: usize,
58
59    /// Minimum content to preserve (percentage, 0-100)
60    pub min_preserve_percent: u8,
61
62    /// Segments to compact (in priority order)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub segments_to_compact: Option<Vec<ContextSegmentType>>,
65
66    /// Segments to never compact
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub protected_segments: Option<Vec<ContextSegmentType>>,
69
70    /// For summarize strategy: max summary tokens
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub summary_max_tokens: Option<usize>,
73
74    /// For sliding_window strategy: window size
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub window_size: Option<usize>,
77
78    /// For importance_weighted: minimum importance score to keep
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub min_importance_score: Option<f64>,
81}
82
83impl CompactionStrategy {
84    /// Create a truncation strategy
85    pub fn truncate(target_tokens: usize) -> Self {
86        Self {
87            strategy_type: CompactionStrategyType::Truncate,
88            target_tokens,
89            min_preserve_percent: 20,
90            segments_to_compact: None,
91            protected_segments: Some(vec![
92                ContextSegmentType::System,
93                ContextSegmentType::UserInput,
94            ]),
95            summary_max_tokens: None,
96            window_size: None,
97            min_importance_score: None,
98        }
99    }
100
101    /// Create a sliding window strategy
102    pub fn sliding_window(target_tokens: usize, window_size: usize) -> Self {
103        Self {
104            strategy_type: CompactionStrategyType::SlidingWindow,
105            target_tokens,
106            min_preserve_percent: 20,
107            segments_to_compact: Some(vec![ContextSegmentType::History]),
108            protected_segments: Some(vec![
109                ContextSegmentType::System,
110                ContextSegmentType::UserInput,
111            ]),
112            summary_max_tokens: None,
113            window_size: Some(window_size),
114            min_importance_score: None,
115        }
116    }
117
118    /// Create a summarization strategy
119    pub fn summarize(target_tokens: usize, summary_max_tokens: usize) -> Self {
120        Self {
121            strategy_type: CompactionStrategyType::Summarize,
122            target_tokens,
123            min_preserve_percent: 30,
124            segments_to_compact: Some(vec![
125                ContextSegmentType::History,
126                ContextSegmentType::ToolResults,
127            ]),
128            protected_segments: Some(vec![
129                ContextSegmentType::System,
130                ContextSegmentType::UserInput,
131                ContextSegmentType::Guidance,
132            ]),
133            summary_max_tokens: Some(summary_max_tokens),
134            window_size: None,
135            min_importance_score: None,
136        }
137    }
138
139    /// Check if a segment type is protected
140    pub fn is_protected(&self, segment_type: ContextSegmentType) -> bool {
141        self.protected_segments
142            .as_ref()
143            .map(|p| p.contains(&segment_type))
144            .unwrap_or(false)
145    }
146
147    /// Check if a segment type should be compacted
148    pub fn should_compact(&self, segment_type: ContextSegmentType) -> bool {
149        if self.is_protected(segment_type) {
150            return false;
151        }
152
153        self.segments_to_compact
154            .as_ref()
155            .map(|s| s.contains(&segment_type))
156            .unwrap_or(true) // If not specified, compact all non-protected
157    }
158}
159
160/// Result of a compaction operation
161///
162/// Matches `compactionResultSchema` in @enact/schemas
163#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct CompactionResult {
166    /// Execution ID
167    pub execution_id: ExecutionId,
168
169    /// Strategy used
170    pub strategy: CompactionStrategyType,
171
172    /// Tokens before compaction
173    pub tokens_before: usize,
174
175    /// Tokens after compaction
176    pub tokens_after: usize,
177
178    /// Tokens saved
179    pub tokens_saved: usize,
180
181    /// Compression ratio (tokensAfter / tokensBefore)
182    pub compression_ratio: f64,
183
184    /// Number of segments compacted
185    pub segments_compacted: usize,
186
187    /// Duration in milliseconds
188    pub duration_ms: u64,
189
190    /// Whether compaction was successful
191    pub success: bool,
192
193    /// Error message if failed
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub error: Option<String>,
196
197    /// Timestamp
198    pub compacted_at: DateTime<Utc>,
199}
200
201impl CompactionResult {
202    /// Create a successful result
203    pub fn success(
204        execution_id: ExecutionId,
205        strategy: CompactionStrategyType,
206        tokens_before: usize,
207        tokens_after: usize,
208        segments_compacted: usize,
209        duration_ms: u64,
210    ) -> Self {
211        let tokens_saved = tokens_before.saturating_sub(tokens_after);
212        let compression_ratio = if tokens_before > 0 {
213            tokens_after as f64 / tokens_before as f64
214        } else {
215            1.0
216        };
217
218        Self {
219            execution_id,
220            strategy,
221            tokens_before,
222            tokens_after,
223            tokens_saved,
224            compression_ratio,
225            segments_compacted,
226            duration_ms,
227            success: true,
228            error: None,
229            compacted_at: Utc::now(),
230        }
231    }
232
233    /// Create a failed result
234    pub fn failure(
235        execution_id: ExecutionId,
236        strategy: CompactionStrategyType,
237        tokens_before: usize,
238        error: String,
239        duration_ms: u64,
240    ) -> Self {
241        Self {
242            execution_id,
243            strategy,
244            tokens_before,
245            tokens_after: tokens_before,
246            tokens_saved: 0,
247            compression_ratio: 1.0,
248            segments_compacted: 0,
249            duration_ms,
250            success: false,
251            error: Some(error),
252            compacted_at: Utc::now(),
253        }
254    }
255}
256
257/// Compactor - applies compaction strategies to segments
258pub struct Compactor {
259    strategy: CompactionStrategy,
260}
261
262impl Compactor {
263    /// Create a new compactor with the given strategy
264    pub fn new(strategy: CompactionStrategy) -> Self {
265        Self { strategy }
266    }
267
268    /// Create a truncation compactor
269    pub fn truncate(target_tokens: usize) -> Self {
270        Self::new(CompactionStrategy::truncate(target_tokens))
271    }
272
273    /// Create a sliding window compactor
274    pub fn sliding_window(target_tokens: usize, window_size: usize) -> Self {
275        Self::new(CompactionStrategy::sliding_window(
276            target_tokens,
277            window_size,
278        ))
279    }
280
281    /// Get the strategy
282    pub fn strategy(&self) -> &CompactionStrategy {
283        &self.strategy
284    }
285
286    /// Compact segments using truncation strategy
287    ///
288    /// Removes oldest segments (lowest priority first) until target is reached.
289    pub fn compact_truncate(
290        &self,
291        segments: &mut Vec<ContextSegment>,
292        current_tokens: usize,
293    ) -> Result<usize, CompactionError> {
294        if current_tokens <= self.strategy.target_tokens {
295            return Ok(0);
296        }
297
298        let tokens_to_remove = current_tokens - self.strategy.target_tokens;
299        let mut removed = 0;
300
301        // Sort by priority (ascending) then by sequence (ascending = oldest first)
302        segments.sort_by(|a, b| {
303            a.priority
304                .cmp(&b.priority)
305                .then(a.sequence.cmp(&b.sequence))
306        });
307
308        // Remove lowest priority, oldest segments first
309        let mut i = 0;
310        while i < segments.len() && removed < tokens_to_remove {
311            let segment = &segments[i];
312
313            // Skip protected segments
314            if !segment.compressible || self.strategy.is_protected(segment.segment_type) {
315                i += 1;
316                continue;
317            }
318
319            // Skip critical priority
320            if segment.priority == ContextPriority::Critical {
321                i += 1;
322                continue;
323            }
324
325            removed += segment.token_count;
326            segments.remove(i);
327        }
328
329        Ok(removed)
330    }
331
332    /// Compact using sliding window strategy
333    ///
334    /// Keeps only the most recent N messages in the history.
335    pub fn compact_sliding_window(
336        &self,
337        segments: &mut Vec<ContextSegment>,
338    ) -> Result<usize, CompactionError> {
339        let window_size = self.strategy.window_size.unwrap_or(10);
340
341        // Find history segments
342        let history_indices: Vec<usize> = segments
343            .iter()
344            .enumerate()
345            .filter(|(_, s)| s.segment_type == ContextSegmentType::History)
346            .map(|(i, _)| i)
347            .collect();
348
349        if history_indices.len() <= window_size {
350            return Ok(0);
351        }
352
353        // Remove oldest history segments (keep window_size most recent)
354        let to_remove = history_indices.len() - window_size;
355        let mut removed_tokens = 0;
356
357        // Remove from oldest first (indices are in ascending order)
358        for &idx in history_indices.iter().take(to_remove).rev() {
359            removed_tokens += segments[idx].token_count;
360            segments.remove(idx);
361        }
362
363        Ok(removed_tokens)
364    }
365
366    /// Create a summarize compactor
367    pub fn summarize(target_tokens: usize, summary_max_tokens: usize) -> Self {
368        Self::new(CompactionStrategy::summarize(
369            target_tokens,
370            summary_max_tokens,
371        ))
372    }
373
374    /// Compact using summarization strategy
375    ///
376    /// This performs extractive summarization by:
377    /// 1. Identifying compactible segments (History, ToolResults)
378    /// 2. Extracting key sentences from each segment
379    /// 3. Truncating to fit within the summary token budget
380    ///
381    /// Note: For true abstractive summarization (using an LLM), integrate with
382    /// an external summarization service. This implementation provides a local
383    /// extractive approach that doesn't require external API calls.
384    pub fn compact_summarize(
385        &self,
386        segments: &mut Vec<ContextSegment>,
387        current_tokens: usize,
388    ) -> Result<usize, CompactionError> {
389        if current_tokens <= self.strategy.target_tokens {
390            return Ok(0);
391        }
392
393        let summary_max = self.strategy.summary_max_tokens.unwrap_or(500);
394        let mut removed_tokens = 0;
395
396        // Get segments that should be compacted
397        let segments_to_compact = self
398            .strategy
399            .segments_to_compact
400            .clone()
401            .unwrap_or_else(|| vec![ContextSegmentType::History, ContextSegmentType::ToolResults]);
402
403        // Process each segment type
404        for segment_type in segments_to_compact {
405            // Find segments of this type that can be compacted
406            let mut indices_to_summarize: Vec<usize> = Vec::new();
407            let mut combined_content = String::new();
408            let mut total_tokens_in_group = 0;
409
410            for (i, segment) in segments.iter().enumerate() {
411                if segment.segment_type == segment_type
412                    && segment.compressible
413                    && !self.strategy.is_protected(segment.segment_type)
414                    && segment.priority != ContextPriority::Critical
415                {
416                    indices_to_summarize.push(i);
417                    if !combined_content.is_empty() {
418                        combined_content.push_str("\n---\n");
419                    }
420                    combined_content.push_str(&segment.content);
421                    total_tokens_in_group += segment.token_count;
422                }
423            }
424
425            // Skip if nothing to summarize or if summarizing would save no tokens
426            if indices_to_summarize.is_empty() || total_tokens_in_group <= summary_max {
427                continue;
428            }
429
430            // Extract key sentences (extractive summarization)
431            let summary = self.extract_key_content(&combined_content, summary_max);
432            let summary_tokens = summary.len() / 4; // Rough token estimate (will be recounted)
433
434            // Remove old segments (in reverse order to preserve indices)
435            for &idx in indices_to_summarize.iter().rev() {
436                removed_tokens += segments[idx].token_count;
437                segments.remove(idx);
438            }
439
440            // Add summarized segment if there's content
441            if !summary.is_empty() {
442                let summarized_segment = ContextSegment::new(
443                    segment_type,
444                    format!(
445                        "[Summarized {}]\n{}",
446                        segment_type_display(segment_type),
447                        summary
448                    ),
449                    summary_tokens,
450                    0, // Sequence will be updated by ContextWindow
451                )
452                .with_priority(ContextPriority::Low);
453
454                segments.push(summarized_segment);
455                removed_tokens = removed_tokens.saturating_sub(summary_tokens);
456            }
457        }
458
459        if removed_tokens == 0 {
460            return Err(CompactionError::NothingToCompact);
461        }
462
463        Ok(removed_tokens)
464    }
465
466    /// Extract key content from text using extractive summarization
467    ///
468    /// Uses heuristics to identify important sentences:
469    /// - First and last sentences (often contain key info)
470    /// - Sentences with important keywords
471    /// - Sentences with certain patterns (results, conclusions, errors)
472    fn extract_key_content(&self, text: &str, max_tokens: usize) -> String {
473        let sentences: Vec<&str> = text
474            .split(&['.', '!', '?', '\n'][..])
475            .map(|s| s.trim())
476            .filter(|s| !s.is_empty() && s.len() > 10)
477            .collect();
478
479        if sentences.is_empty() {
480            return String::new();
481        }
482
483        // Score sentences by importance
484        let mut scored_sentences: Vec<(usize, &str, i32)> = sentences
485            .iter()
486            .enumerate()
487            .map(|(i, &s)| (i, s, self.score_sentence(s, i, sentences.len())))
488            .collect();
489
490        // Sort by score (descending)
491        scored_sentences.sort_by(|a, b| b.2.cmp(&a.2));
492
493        // Build summary within token budget
494        let max_chars = max_tokens * 4; // Rough estimate: ~4 chars per token
495        let mut summary_parts: Vec<(usize, &str)> = Vec::new();
496        let mut current_len = 0;
497
498        for (idx, sentence, _score) in scored_sentences {
499            if current_len + sentence.len() + 2 > max_chars {
500                break;
501            }
502            summary_parts.push((idx, sentence));
503            current_len += sentence.len() + 2;
504        }
505
506        // Sort by original position to maintain coherence
507        summary_parts.sort_by_key(|(idx, _)| *idx);
508
509        // Join sentences
510        summary_parts
511            .iter()
512            .map(|(_, s)| *s)
513            .collect::<Vec<_>>()
514            .join(". ")
515            + "."
516    }
517
518    /// Score a sentence for importance (higher = more important)
519    fn score_sentence(&self, sentence: &str, position: usize, total: usize) -> i32 {
520        let mut score = 0i32;
521        let lower = sentence.to_lowercase();
522
523        // Position-based scoring
524        if position == 0 {
525            score += 10; // First sentence often important
526        }
527        if position == total - 1 {
528            score += 8; // Last sentence often contains conclusion
529        }
530
531        // Keyword-based scoring
532        let important_keywords = [
533            ("result", 5),
534            ("output", 4),
535            ("error", 6),
536            ("success", 5),
537            ("fail", 6),
538            ("complete", 4),
539            ("return", 3),
540            ("created", 3),
541            ("found", 3),
542            ("important", 4),
543            ("note", 3),
544            ("warning", 5),
545            ("summary", 4),
546            ("conclusion", 5),
547            ("decision", 4),
548            ("because", 3),
549            ("therefore", 3),
550        ];
551
552        for (keyword, keyword_score) in important_keywords {
553            if lower.contains(keyword) {
554                score += keyword_score;
555            }
556        }
557
558        // Length penalty for very short or very long sentences
559        let len = sentence.len();
560        if len < 20 {
561            score -= 2;
562        } else if len > 200 {
563            score -= 1;
564        }
565
566        // Bonus for sentences with code/technical content
567        if sentence.contains('`') || sentence.contains("()") || sentence.contains("::") {
568            score += 2;
569        }
570
571        score
572    }
573
574    /// Compact using key points extraction strategy (not yet implemented)
575    ///
576    /// # Future Implementation
577    ///
578    /// This strategy would:
579    /// 1. Identify key decision points, outcomes, and learnings
580    /// 2. Extract structured bullet points from conversation
581    /// 3. Preserve causal chains and reasoning
582    ///
583    /// Requires integration with an LLM for semantic understanding.
584    pub fn compact_extract_key_points(
585        &self,
586        _segments: &mut Vec<ContextSegment>,
587        _current_tokens: usize,
588    ) -> Result<usize, CompactionError> {
589        Err(CompactionError::SummarizationFailed(
590            "ExtractKeyPoints strategy is not yet implemented. \
591            This strategy requires LLM integration for semantic key point extraction. \
592            Consider using 'Summarize' for extractive summarization or \
593            'SlidingWindow' for recency-based compaction."
594                .to_string(),
595        ))
596    }
597
598    /// Compact using importance-weighted strategy (not yet implemented)
599    ///
600    /// # Future Implementation
601    ///
602    /// This strategy would:
603    /// 1. Score each segment based on semantic importance
604    /// 2. Use embedding similarity to current task
605    /// 3. Weight by recency, reference count, and explicit importance markers
606    /// 4. Remove lowest-scored segments until target is reached
607    ///
608    /// Requires embedding model integration for semantic scoring.
609    pub fn compact_importance_weighted(
610        &self,
611        _segments: &mut Vec<ContextSegment>,
612        _current_tokens: usize,
613    ) -> Result<usize, CompactionError> {
614        Err(CompactionError::SummarizationFailed(
615            "ImportanceWeighted strategy is not yet implemented. \
616            This strategy requires embedding model integration for semantic importance scoring. \
617            Consider using 'Truncate' for priority-based removal or \
618            'SlidingWindow' for recency-based compaction."
619                .to_string(),
620        ))
621    }
622
623    /// Compact using hybrid strategy (not yet implemented)
624    ///
625    /// # Future Implementation
626    ///
627    /// This strategy would combine multiple approaches:
628    /// 1. First pass: Remove lowest-importance segments (truncate)
629    /// 2. Second pass: Summarize remaining compressible content
630    /// 3. Third pass: Apply sliding window to history if needed
631    ///
632    /// Requires all component strategies to be implemented.
633    pub fn compact_hybrid(
634        &self,
635        _segments: &mut Vec<ContextSegment>,
636        _current_tokens: usize,
637    ) -> Result<usize, CompactionError> {
638        Err(CompactionError::SummarizationFailed(
639            "Hybrid strategy is not yet implemented. \
640            This strategy combines multiple compaction approaches for optimal results. \
641            Consider using individual strategies: 'Summarize', 'Truncate', or 'SlidingWindow'."
642                .to_string(),
643        ))
644    }
645}
646
647/// Display name for segment types in summaries
648fn segment_type_display(segment_type: ContextSegmentType) -> &'static str {
649    match segment_type {
650        ContextSegmentType::System => "System",
651        ContextSegmentType::History => "Conversation History",
652        ContextSegmentType::WorkingMemory => "Working Memory",
653        ContextSegmentType::ToolResults => "Tool Results",
654        ContextSegmentType::RagContext => "Retrieved Context",
655        ContextSegmentType::UserInput => "User Input",
656        ContextSegmentType::AgentScratchpad => "Agent Notes",
657        ContextSegmentType::ChildSummary => "Child Execution",
658        ContextSegmentType::Guidance => "Guidance",
659    }
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665
666    #[test]
667    fn test_truncation_strategy() {
668        let strategy = CompactionStrategy::truncate(1000);
669        assert_eq!(strategy.strategy_type, CompactionStrategyType::Truncate);
670        assert!(strategy.is_protected(ContextSegmentType::System));
671        assert!(!strategy.is_protected(ContextSegmentType::History));
672    }
673
674    #[test]
675    fn test_compaction_result() {
676        let exec_id = ExecutionId::new();
677        let result = CompactionResult::success(
678            exec_id,
679            CompactionStrategyType::Truncate,
680            10000,
681            5000,
682            5,
683            100,
684        );
685
686        assert!(result.success);
687        assert_eq!(result.tokens_saved, 5000);
688        assert!((result.compression_ratio - 0.5).abs() < 0.01);
689    }
690
691    #[test]
692    fn test_summarize_strategy_creation() {
693        let strategy = CompactionStrategy::summarize(5000, 500);
694        assert_eq!(strategy.strategy_type, CompactionStrategyType::Summarize);
695        assert_eq!(strategy.target_tokens, 5000);
696        assert_eq!(strategy.summary_max_tokens, Some(500));
697        assert!(strategy.is_protected(ContextSegmentType::System));
698        assert!(strategy.is_protected(ContextSegmentType::UserInput));
699        assert!(!strategy.is_protected(ContextSegmentType::History));
700    }
701
702    #[test]
703    fn test_summarize_compactor_creation() {
704        let compactor = Compactor::summarize(5000, 500);
705        assert_eq!(
706            compactor.strategy().strategy_type,
707            CompactionStrategyType::Summarize
708        );
709    }
710
711    #[test]
712    fn test_compact_summarize_no_change_under_target() {
713        let compactor = Compactor::summarize(10000, 500);
714        let mut segments = vec![ContextSegment::history(
715            "Some history content here.",
716            100,
717            1,
718        )];
719
720        // Current tokens (100) is under target (10000), so no compaction
721        let result = compactor.compact_summarize(&mut segments, 100);
722        assert_eq!(result.unwrap(), 0);
723        assert_eq!(segments.len(), 1);
724    }
725
726    #[test]
727    fn test_compact_summarize_with_history() {
728        let compactor = Compactor::summarize(500, 100);
729        let mut segments = vec![
730            ContextSegment::system("You are a helpful assistant.", 10),
731            ContextSegment::history(
732                "The user asked about Rust programming. We discussed memory safety and ownership. \
733                The result was a successful explanation. The conclusion is that Rust is great.",
734                800,
735                1,
736            ),
737            ContextSegment::history(
738                "Then we talked about error handling. The important point is that Result types are used. \
739                This is a note about the discussion. The output showed various patterns.",
740                700,
741                2,
742            ),
743        ];
744
745        let current_tokens = 10 + 800 + 700;
746        let result = compactor.compact_summarize(&mut segments, current_tokens);
747
748        assert!(result.is_ok());
749        let removed = result.unwrap();
750        assert!(removed > 0, "Should have removed some tokens");
751
752        // System segment should be preserved
753        assert!(segments
754            .iter()
755            .any(|s| s.segment_type == ContextSegmentType::System));
756
757        // Should have a summarized history segment
758        let summarized = segments
759            .iter()
760            .find(|s| s.segment_type == ContextSegmentType::History);
761        assert!(summarized.is_some());
762        assert!(summarized.unwrap().content.contains("[Summarized"));
763    }
764
765    #[test]
766    fn test_compact_summarize_preserves_protected() {
767        let compactor = Compactor::summarize(100, 50);
768        let mut segments = vec![
769            ContextSegment::system("System prompt", 20),
770            ContextSegment::user_input("User question", 15, 1),
771            ContextSegment::guidance("Important guidance", 25, 2),
772            ContextSegment::history("Some history that can be compressed.", 500, 3),
773        ];
774
775        let current_tokens = 20 + 15 + 25 + 500;
776        let _ = compactor.compact_summarize(&mut segments, current_tokens);
777
778        // Protected segments should still exist
779        assert!(segments
780            .iter()
781            .any(|s| s.segment_type == ContextSegmentType::System));
782        assert!(segments
783            .iter()
784            .any(|s| s.segment_type == ContextSegmentType::UserInput));
785        assert!(segments
786            .iter()
787            .any(|s| s.segment_type == ContextSegmentType::Guidance));
788    }
789
790    #[test]
791    fn test_extract_key_content_prioritizes_important_sentences() {
792        let compactor = Compactor::summarize(5000, 100);
793
794        // Create a longer text where summarization will actually reduce content
795        let text = "This is the first sentence and sets context for the discussion. \
796            Some filler information here that is not particularly important. \
797            Another sentence with no real significance to the outcome. \
798            The result of the operation was successful and completed without errors. \
799            More random content follows that could be removed. \
800            Yet another sentence that adds little value to understanding. \
801            Some additional padding content here. \
802            In conclusion, this is the summary of our findings.";
803
804        // Use a small token limit to force extraction
805        let summary = compactor.extract_key_content(text, 20);
806
807        // Should prioritize first sentence, result sentence, and conclusion
808        assert!(!summary.is_empty());
809        // With only 20 tokens (~80 chars), summary should be shorter than original
810        assert!(
811            summary.len() < text.len(),
812            "Summary ({} chars) should be shorter than original ({} chars)",
813            summary.len(),
814            text.len()
815        );
816    }
817
818    #[test]
819    fn test_extract_key_content_handles_empty() {
820        let compactor = Compactor::summarize(5000, 100);
821        let summary = compactor.extract_key_content("", 50);
822        assert!(summary.is_empty() || summary == ".");
823    }
824
825    #[test]
826    fn test_score_sentence_keywords() {
827        let compactor = Compactor::summarize(5000, 100);
828
829        let error_sentence = "There was an error in the process";
830        let normal_sentence = "The weather is nice today";
831
832        let error_score = compactor.score_sentence(error_sentence, 1, 5);
833        let normal_score = compactor.score_sentence(normal_sentence, 1, 5);
834
835        assert!(
836            error_score > normal_score,
837            "Error sentence should score higher"
838        );
839    }
840
841    #[test]
842    fn test_score_sentence_position() {
843        let compactor = Compactor::summarize(5000, 100);
844        let sentence = "This is a test sentence";
845
846        let first_score = compactor.score_sentence(sentence, 0, 5);
847        let middle_score = compactor.score_sentence(sentence, 2, 5);
848        let last_score = compactor.score_sentence(sentence, 4, 5);
849
850        assert!(
851            first_score > middle_score,
852            "First sentence should score higher"
853        );
854        assert!(
855            last_score > middle_score,
856            "Last sentence should score higher"
857        );
858    }
859
860    #[test]
861    fn test_extract_key_points_not_implemented() {
862        let strategy = CompactionStrategy {
863            strategy_type: CompactionStrategyType::ExtractKeyPoints,
864            target_tokens: 5000,
865            min_preserve_percent: 20,
866            segments_to_compact: None,
867            protected_segments: None,
868            summary_max_tokens: None,
869            window_size: None,
870            min_importance_score: None,
871        };
872        let compactor = Compactor::new(strategy);
873        let mut segments = vec![];
874
875        let result = compactor.compact_extract_key_points(&mut segments, 1000);
876        assert!(result.is_err());
877
878        let err = result.unwrap_err();
879        match err {
880            CompactionError::SummarizationFailed(msg) => {
881                assert!(msg.contains("ExtractKeyPoints"));
882                assert!(msg.contains("not yet implemented"));
883            }
884            _ => panic!("Expected SummarizationFailed error"),
885        }
886    }
887
888    #[test]
889    fn test_importance_weighted_not_implemented() {
890        let strategy = CompactionStrategy {
891            strategy_type: CompactionStrategyType::ImportanceWeighted,
892            target_tokens: 5000,
893            min_preserve_percent: 20,
894            segments_to_compact: None,
895            protected_segments: None,
896            summary_max_tokens: None,
897            window_size: None,
898            min_importance_score: Some(0.5),
899        };
900        let compactor = Compactor::new(strategy);
901        let mut segments = vec![];
902
903        let result = compactor.compact_importance_weighted(&mut segments, 1000);
904        assert!(result.is_err());
905
906        let err = result.unwrap_err();
907        match err {
908            CompactionError::SummarizationFailed(msg) => {
909                assert!(msg.contains("ImportanceWeighted"));
910                assert!(msg.contains("not yet implemented"));
911                assert!(msg.contains("embedding model"));
912            }
913            _ => panic!("Expected SummarizationFailed error"),
914        }
915    }
916
917    #[test]
918    fn test_hybrid_not_implemented() {
919        let strategy = CompactionStrategy {
920            strategy_type: CompactionStrategyType::Hybrid,
921            target_tokens: 5000,
922            min_preserve_percent: 20,
923            segments_to_compact: None,
924            protected_segments: None,
925            summary_max_tokens: None,
926            window_size: None,
927            min_importance_score: None,
928        };
929        let compactor = Compactor::new(strategy);
930        let mut segments = vec![];
931
932        let result = compactor.compact_hybrid(&mut segments, 1000);
933        assert!(result.is_err());
934
935        let err = result.unwrap_err();
936        match err {
937            CompactionError::SummarizationFailed(msg) => {
938                assert!(msg.contains("Hybrid"));
939                assert!(msg.contains("not yet implemented"));
940            }
941            _ => panic!("Expected SummarizationFailed error"),
942        }
943    }
944
945    #[test]
946    fn test_segment_type_display() {
947        assert_eq!(
948            segment_type_display(ContextSegmentType::History),
949            "Conversation History"
950        );
951        assert_eq!(
952            segment_type_display(ContextSegmentType::ToolResults),
953            "Tool Results"
954        );
955        assert_eq!(segment_type_display(ContextSegmentType::System), "System");
956    }
957}