use crate::segment::{ContextPriority, ContextSegment, ContextSegmentType};
use chrono::{DateTime, Utc};
use enact_core::kernel::ExecutionId;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CompactionError {
#[error("Nothing to compact")]
NothingToCompact,
#[error("Target token count too low: {0}")]
TargetTooLow(usize),
#[error("Summarization failed: {0}")]
SummarizationFailed(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CompactionStrategyType {
Truncate,
Summarize,
ExtractKeyPoints,
SlidingWindow,
ImportanceWeighted,
Hybrid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompactionStrategy {
#[serde(rename = "type")]
pub strategy_type: CompactionStrategyType,
pub target_tokens: usize,
pub min_preserve_percent: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub segments_to_compact: Option<Vec<ContextSegmentType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub protected_segments: Option<Vec<ContextSegmentType>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary_max_tokens: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub window_size: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_importance_score: Option<f64>,
}
impl CompactionStrategy {
pub fn truncate(target_tokens: usize) -> Self {
Self {
strategy_type: CompactionStrategyType::Truncate,
target_tokens,
min_preserve_percent: 20,
segments_to_compact: None,
protected_segments: Some(vec![
ContextSegmentType::System,
ContextSegmentType::UserInput,
]),
summary_max_tokens: None,
window_size: None,
min_importance_score: None,
}
}
pub fn sliding_window(target_tokens: usize, window_size: usize) -> Self {
Self {
strategy_type: CompactionStrategyType::SlidingWindow,
target_tokens,
min_preserve_percent: 20,
segments_to_compact: Some(vec![ContextSegmentType::History]),
protected_segments: Some(vec![
ContextSegmentType::System,
ContextSegmentType::UserInput,
]),
summary_max_tokens: None,
window_size: Some(window_size),
min_importance_score: None,
}
}
pub fn summarize(target_tokens: usize, summary_max_tokens: usize) -> Self {
Self {
strategy_type: CompactionStrategyType::Summarize,
target_tokens,
min_preserve_percent: 30,
segments_to_compact: Some(vec![
ContextSegmentType::History,
ContextSegmentType::ToolResults,
]),
protected_segments: Some(vec![
ContextSegmentType::System,
ContextSegmentType::UserInput,
ContextSegmentType::Guidance,
]),
summary_max_tokens: Some(summary_max_tokens),
window_size: None,
min_importance_score: None,
}
}
pub fn is_protected(&self, segment_type: ContextSegmentType) -> bool {
self.protected_segments
.as_ref()
.map(|p| p.contains(&segment_type))
.unwrap_or(false)
}
pub fn should_compact(&self, segment_type: ContextSegmentType) -> bool {
if self.is_protected(segment_type) {
return false;
}
self.segments_to_compact
.as_ref()
.map(|s| s.contains(&segment_type))
.unwrap_or(true) }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompactionResult {
pub execution_id: ExecutionId,
pub strategy: CompactionStrategyType,
pub tokens_before: usize,
pub tokens_after: usize,
pub tokens_saved: usize,
pub compression_ratio: f64,
pub segments_compacted: usize,
pub duration_ms: u64,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub compacted_at: DateTime<Utc>,
}
impl CompactionResult {
pub fn success(
execution_id: ExecutionId,
strategy: CompactionStrategyType,
tokens_before: usize,
tokens_after: usize,
segments_compacted: usize,
duration_ms: u64,
) -> Self {
let tokens_saved = tokens_before.saturating_sub(tokens_after);
let compression_ratio = if tokens_before > 0 {
tokens_after as f64 / tokens_before as f64
} else {
1.0
};
Self {
execution_id,
strategy,
tokens_before,
tokens_after,
tokens_saved,
compression_ratio,
segments_compacted,
duration_ms,
success: true,
error: None,
compacted_at: Utc::now(),
}
}
pub fn failure(
execution_id: ExecutionId,
strategy: CompactionStrategyType,
tokens_before: usize,
error: String,
duration_ms: u64,
) -> Self {
Self {
execution_id,
strategy,
tokens_before,
tokens_after: tokens_before,
tokens_saved: 0,
compression_ratio: 1.0,
segments_compacted: 0,
duration_ms,
success: false,
error: Some(error),
compacted_at: Utc::now(),
}
}
}
pub struct Compactor {
strategy: CompactionStrategy,
}
impl Compactor {
pub fn new(strategy: CompactionStrategy) -> Self {
Self { strategy }
}
pub fn truncate(target_tokens: usize) -> Self {
Self::new(CompactionStrategy::truncate(target_tokens))
}
pub fn sliding_window(target_tokens: usize, window_size: usize) -> Self {
Self::new(CompactionStrategy::sliding_window(
target_tokens,
window_size,
))
}
pub fn strategy(&self) -> &CompactionStrategy {
&self.strategy
}
pub fn compact_truncate(
&self,
segments: &mut Vec<ContextSegment>,
current_tokens: usize,
) -> Result<usize, CompactionError> {
if current_tokens <= self.strategy.target_tokens {
return Ok(0);
}
let tokens_to_remove = current_tokens - self.strategy.target_tokens;
let mut removed = 0;
segments.sort_by(|a, b| {
a.priority
.cmp(&b.priority)
.then(a.sequence.cmp(&b.sequence))
});
let mut i = 0;
while i < segments.len() && removed < tokens_to_remove {
let segment = &segments[i];
if !segment.compressible || self.strategy.is_protected(segment.segment_type) {
i += 1;
continue;
}
if segment.priority == ContextPriority::Critical {
i += 1;
continue;
}
removed += segment.token_count;
segments.remove(i);
}
Ok(removed)
}
pub fn compact_sliding_window(
&self,
segments: &mut Vec<ContextSegment>,
) -> Result<usize, CompactionError> {
let window_size = self.strategy.window_size.unwrap_or(10);
let history_indices: Vec<usize> = segments
.iter()
.enumerate()
.filter(|(_, s)| s.segment_type == ContextSegmentType::History)
.map(|(i, _)| i)
.collect();
if history_indices.len() <= window_size {
return Ok(0);
}
let to_remove = history_indices.len() - window_size;
let mut removed_tokens = 0;
for &idx in history_indices.iter().take(to_remove).rev() {
removed_tokens += segments[idx].token_count;
segments.remove(idx);
}
Ok(removed_tokens)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncation_strategy() {
let strategy = CompactionStrategy::truncate(1000);
assert_eq!(strategy.strategy_type, CompactionStrategyType::Truncate);
assert!(strategy.is_protected(ContextSegmentType::System));
assert!(!strategy.is_protected(ContextSegmentType::History));
}
#[test]
fn test_compaction_result() {
let exec_id = ExecutionId::new();
let result = CompactionResult::success(
exec_id,
CompactionStrategyType::Truncate,
10000,
5000,
5,
100,
);
assert!(result.success);
assert_eq!(result.tokens_saved, 5000);
assert!((result.compression_ratio - 0.5).abs() < 0.01);
}
}