use crate::budget::{BudgetHealth, ContextBudget};
#[cfg(test)]
use crate::compactor::CompactionStrategy;
use crate::compactor::{CompactionResult, CompactionStrategyType, Compactor};
use crate::segment::{ContextSegment, ContextSegmentType};
use crate::token_counter::TokenCounter;
use chrono::{DateTime, Utc};
use enact_core::kernel::ExecutionId;
use serde::{Deserialize, Serialize};
use std::time::Instant;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ContextWindowError {
#[error("Token counter error: {0}")]
TokenCounter(String),
#[error("Budget exceeded: need {needed} tokens, only {available} available")]
BudgetExceeded { needed: usize, available: usize },
#[error("Segment budget exceeded for {segment_type:?}: need {needed}, max {max}")]
SegmentBudgetExceeded {
segment_type: ContextSegmentType,
needed: usize,
max: usize,
},
#[error("Compaction failed: {0}")]
CompactionFailed(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContextWindowState {
pub execution_id: ExecutionId,
pub segments: Vec<ContextSegment>,
pub budget: ContextBudget,
pub compaction_history: Vec<CompactionResult>,
pub compaction_count: u32,
pub total_tokens_saved: usize,
pub health: BudgetHealth,
pub updated_at: DateTime<Utc>,
}
pub struct ContextWindow {
execution_id: ExecutionId,
segments: Vec<ContextSegment>,
budget: ContextBudget,
token_counter: TokenCounter,
compaction_history: Vec<CompactionResult>,
next_sequence: u64,
}
impl ContextWindow {
pub fn new(budget: ContextBudget) -> Result<Self, ContextWindowError> {
let token_counter =
TokenCounter::new().map_err(|e| ContextWindowError::TokenCounter(e.to_string()))?;
Ok(Self {
execution_id: budget.execution_id.clone(),
segments: Vec::new(),
budget,
token_counter,
compaction_history: Vec::new(),
next_sequence: 0,
})
}
pub fn with_preset_gpt4_128k(execution_id: ExecutionId) -> Result<Self, ContextWindowError> {
Self::new(ContextBudget::preset_gpt4_128k(execution_id))
}
pub fn with_preset_claude_200k(execution_id: ExecutionId) -> Result<Self, ContextWindowError> {
Self::new(ContextBudget::preset_claude_200k(execution_id))
}
pub fn with_preset_default(execution_id: ExecutionId) -> Result<Self, ContextWindowError> {
Self::new(ContextBudget::preset_default(execution_id))
}
pub fn execution_id(&self) -> &ExecutionId {
&self.execution_id
}
pub fn segments(&self) -> &[ContextSegment] {
&self.segments
}
pub fn budget(&self) -> &ContextBudget {
&self.budget
}
pub fn budget_mut(&mut self) -> &mut ContextBudget {
&mut self.budget
}
pub fn count_tokens(&self, text: &str) -> usize {
self.token_counter.count(text)
}
pub fn add_segment_auto(
&mut self,
mut segment: ContextSegment,
) -> Result<(), ContextWindowError> {
if segment.token_count == 0 {
segment.token_count = self.token_counter.count(&segment.content);
}
self.add_segment(segment)
}
pub fn add_segment(&mut self, mut segment: ContextSegment) -> Result<(), ContextWindowError> {
if let Some(seg_budget) = self.budget.get_segment(segment.segment_type) {
let new_usage = seg_budget.current_tokens + segment.token_count;
if new_usage > seg_budget.max_tokens {
return Err(ContextWindowError::SegmentBudgetExceeded {
segment_type: segment.segment_type,
needed: segment.token_count,
max: seg_budget.max_tokens - seg_budget.current_tokens,
});
}
}
let new_total = self.budget.used_tokens + segment.token_count;
if new_total > self.budget.available_tokens {
return Err(ContextWindowError::BudgetExceeded {
needed: segment.token_count,
available: self.budget.remaining(),
});
}
segment.sequence = self.next_sequence;
self.next_sequence += 1;
self.budget
.add_tokens(segment.segment_type, segment.token_count);
self.segments.push(segment);
Ok(())
}
pub fn remove_segment(&mut self, segment_id: &str) -> bool {
if let Some(pos) = self.segments.iter().position(|s| s.id == segment_id) {
let segment = self.segments.remove(pos);
self.budget
.remove_tokens(segment.segment_type, segment.token_count);
true
} else {
false
}
}
pub fn segments_of_type(&self, segment_type: ContextSegmentType) -> Vec<&ContextSegment> {
self.segments
.iter()
.filter(|s| s.segment_type == segment_type)
.collect()
}
pub fn used_tokens(&self) -> usize {
self.budget.used_tokens
}
pub fn remaining_tokens(&self) -> usize {
self.budget.remaining()
}
pub fn needs_compaction(&self) -> bool {
self.budget.is_warning()
}
pub fn is_critical(&self) -> bool {
self.budget.is_critical()
}
pub fn health(&self) -> BudgetHealth {
self.budget.health()
}
pub fn compact(
&mut self,
compactor: &Compactor,
) -> Result<CompactionResult, ContextWindowError> {
let start = Instant::now();
let tokens_before = self.budget.used_tokens;
let result = match compactor.strategy().strategy_type {
CompactionStrategyType::Truncate => {
compactor.compact_truncate(&mut self.segments, tokens_before)
}
CompactionStrategyType::SlidingWindow => {
compactor.compact_sliding_window(&mut self.segments)
}
CompactionStrategyType::Summarize => {
compactor.compact_summarize(&mut self.segments, tokens_before)
}
CompactionStrategyType::ExtractKeyPoints => {
compactor.compact_extract_key_points(&mut self.segments, tokens_before)
}
CompactionStrategyType::ImportanceWeighted => {
compactor.compact_importance_weighted(&mut self.segments, tokens_before)
}
CompactionStrategyType::Hybrid => {
compactor.compact_hybrid(&mut self.segments, tokens_before)
}
};
let duration_ms = start.elapsed().as_millis() as u64;
match result {
Ok(tokens_removed) => {
self.recalculate_budget();
let tokens_after = self.budget.used_tokens;
let segments_compacted = (tokens_removed > 0) as usize;
let compaction_result = CompactionResult::success(
self.execution_id.clone(),
compactor.strategy().strategy_type,
tokens_before,
tokens_after,
segments_compacted,
duration_ms,
);
self.compaction_history.push(compaction_result.clone());
Ok(compaction_result)
}
Err(e) => {
let compaction_result = CompactionResult::failure(
self.execution_id.clone(),
compactor.strategy().strategy_type,
tokens_before,
e.to_string(),
duration_ms,
);
self.compaction_history.push(compaction_result.clone());
Err(ContextWindowError::CompactionFailed(e.to_string()))
}
}
}
fn recalculate_budget(&mut self) {
for seg_budget in &mut self.budget.segments {
seg_budget.current_tokens = 0;
}
for segment in &self.segments {
self.budget
.add_tokens(segment.segment_type, segment.token_count);
}
}
pub fn build_context(&self) -> String {
let mut parts: Vec<&str> = Vec::new();
let mut sorted: Vec<&ContextSegment> = self.segments.iter().collect();
sorted.sort_by_key(|s| s.sequence);
for segment in sorted {
parts.push(&segment.content);
}
parts.join("\n\n")
}
pub fn state(&self) -> ContextWindowState {
ContextWindowState {
execution_id: self.execution_id.clone(),
segments: self.segments.clone(),
budget: self.budget.clone(),
compaction_history: self.compaction_history.clone(),
compaction_count: self.compaction_history.len() as u32,
total_tokens_saved: self.compaction_history.iter().map(|r| r.tokens_saved).sum(),
health: self.budget.health(),
updated_at: Utc::now(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_execution_id() -> ExecutionId {
ExecutionId::new()
}
#[test]
fn test_create_window() {
let budget = ContextBudget::preset_default(test_execution_id());
let window = ContextWindow::new(budget).unwrap();
assert_eq!(window.used_tokens(), 0);
assert!(window.remaining_tokens() > 0);
}
#[test]
fn test_add_segment() {
let budget = ContextBudget::preset_default(test_execution_id());
let mut window = ContextWindow::new(budget).unwrap();
let segment = ContextSegment::system("You are a helpful assistant.", 10);
window.add_segment(segment).unwrap();
assert_eq!(window.segments().len(), 1);
assert_eq!(window.used_tokens(), 10);
}
#[test]
fn test_health_tracking() {
let budget = ContextBudget::preset_default(test_execution_id());
let window = ContextWindow::new(budget).unwrap();
assert_eq!(window.health(), BudgetHealth::Healthy);
assert!(!window.needs_compaction());
}
#[test]
fn test_compact_with_summarize_strategy() {
let budget = ContextBudget::preset_default(test_execution_id());
let mut window = ContextWindow::new(budget).unwrap();
let system = ContextSegment::system("You are helpful.", 10);
window.add_segment(system).unwrap();
let history1 = ContextSegment::history(
"The user asked about the weather. The result was sunny. Important note about temperature.",
500,
1,
);
let history2 = ContextSegment::history(
"Then we discussed travel plans. The conclusion was to visit Paris. The output showed flight options.",
600,
2,
);
window.add_segment(history1).unwrap();
window.add_segment(history2).unwrap();
let compactor = Compactor::summarize(200, 100);
let result = window.compact(&compactor);
assert!(result.is_ok());
let compaction_result = result.unwrap();
assert!(compaction_result.success);
assert!(compaction_result.tokens_saved > 0);
assert!(window
.segments()
.iter()
.any(|s| s.segment_type == ContextSegmentType::System));
}
#[test]
fn test_compact_with_extract_key_points_returns_error() {
let budget = ContextBudget::preset_default(test_execution_id());
let mut window = ContextWindow::new(budget).unwrap();
let history = ContextSegment::history("Some content", 100, 1);
window.add_segment(history).unwrap();
let strategy = CompactionStrategy {
strategy_type: CompactionStrategyType::ExtractKeyPoints,
target_tokens: 50,
min_preserve_percent: 20,
segments_to_compact: None,
protected_segments: None,
summary_max_tokens: None,
window_size: None,
min_importance_score: None,
};
let compactor = Compactor::new(strategy);
let result = window.compact(&compactor);
assert!(result.is_err());
match result {
Err(ContextWindowError::CompactionFailed(msg)) => {
assert!(msg.contains("ExtractKeyPoints"));
}
_ => panic!("Expected CompactionFailed error"),
}
}
#[test]
fn test_compact_with_importance_weighted_returns_error() {
let budget = ContextBudget::preset_default(test_execution_id());
let mut window = ContextWindow::new(budget).unwrap();
let history = ContextSegment::history("Some content", 100, 1);
window.add_segment(history).unwrap();
let strategy = CompactionStrategy {
strategy_type: CompactionStrategyType::ImportanceWeighted,
target_tokens: 50,
min_preserve_percent: 20,
segments_to_compact: None,
protected_segments: None,
summary_max_tokens: None,
window_size: None,
min_importance_score: Some(0.5),
};
let compactor = Compactor::new(strategy);
let result = window.compact(&compactor);
assert!(result.is_err());
match result {
Err(ContextWindowError::CompactionFailed(msg)) => {
assert!(msg.contains("ImportanceWeighted"));
}
_ => panic!("Expected CompactionFailed error"),
}
}
#[test]
fn test_compact_with_hybrid_returns_error() {
let budget = ContextBudget::preset_default(test_execution_id());
let mut window = ContextWindow::new(budget).unwrap();
let history = ContextSegment::history("Some content", 100, 1);
window.add_segment(history).unwrap();
let strategy = CompactionStrategy {
strategy_type: CompactionStrategyType::Hybrid,
target_tokens: 50,
min_preserve_percent: 20,
segments_to_compact: None,
protected_segments: None,
summary_max_tokens: None,
window_size: None,
min_importance_score: None,
};
let compactor = Compactor::new(strategy);
let result = window.compact(&compactor);
assert!(result.is_err());
match result {
Err(ContextWindowError::CompactionFailed(msg)) => {
assert!(msg.contains("Hybrid"));
}
_ => panic!("Expected CompactionFailed error"),
}
}
}