1use crate::budget::{BudgetHealth, ContextBudget};
8#[cfg(test)]
9use crate::compactor::CompactionStrategy;
10use crate::compactor::{CompactionResult, CompactionStrategyType, Compactor};
11use crate::segment::{ContextSegment, ContextSegmentType};
12use crate::token_counter::TokenCounter;
13use chrono::{DateTime, Utc};
14use enact_core::kernel::ExecutionId;
15use serde::{Deserialize, Serialize};
16use std::time::Instant;
17use thiserror::Error;
18
19#[derive(Debug, Error)]
21pub enum ContextWindowError {
22 #[error("Token counter error: {0}")]
23 TokenCounter(String),
24
25 #[error("Budget exceeded: need {needed} tokens, only {available} available")]
26 BudgetExceeded { needed: usize, available: usize },
27
28 #[error("Segment budget exceeded for {segment_type:?}: need {needed}, max {max}")]
29 SegmentBudgetExceeded {
30 segment_type: ContextSegmentType,
31 needed: usize,
32 max: usize,
33 },
34
35 #[error("Compaction failed: {0}")]
36 CompactionFailed(String),
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct ContextWindowState {
45 pub execution_id: ExecutionId,
47
48 pub segments: Vec<ContextSegment>,
50
51 pub budget: ContextBudget,
53
54 pub compaction_history: Vec<CompactionResult>,
56
57 pub compaction_count: u32,
59
60 pub total_tokens_saved: usize,
62
63 pub health: BudgetHealth,
65
66 pub updated_at: DateTime<Utc>,
68}
69
70pub struct ContextWindow {
72 execution_id: ExecutionId,
74
75 segments: Vec<ContextSegment>,
77
78 budget: ContextBudget,
80
81 token_counter: TokenCounter,
83
84 compaction_history: Vec<CompactionResult>,
86
87 next_sequence: u64,
89}
90
91impl ContextWindow {
92 pub fn new(budget: ContextBudget) -> Result<Self, ContextWindowError> {
94 let token_counter =
95 TokenCounter::new().map_err(|e| ContextWindowError::TokenCounter(e.to_string()))?;
96
97 Ok(Self {
98 execution_id: budget.execution_id.clone(),
99 segments: Vec::new(),
100 budget,
101 token_counter,
102 compaction_history: Vec::new(),
103 next_sequence: 0,
104 })
105 }
106
107 pub fn with_preset_gpt4_128k(execution_id: ExecutionId) -> Result<Self, ContextWindowError> {
109 Self::new(ContextBudget::preset_gpt4_128k(execution_id))
110 }
111
112 pub fn with_preset_claude_200k(execution_id: ExecutionId) -> Result<Self, ContextWindowError> {
114 Self::new(ContextBudget::preset_claude_200k(execution_id))
115 }
116
117 pub fn with_preset_default(execution_id: ExecutionId) -> Result<Self, ContextWindowError> {
119 Self::new(ContextBudget::preset_default(execution_id))
120 }
121
122 pub fn execution_id(&self) -> &ExecutionId {
124 &self.execution_id
125 }
126
127 pub fn segments(&self) -> &[ContextSegment] {
129 &self.segments
130 }
131
132 pub fn budget(&self) -> &ContextBudget {
134 &self.budget
135 }
136
137 pub fn budget_mut(&mut self) -> &mut ContextBudget {
139 &mut self.budget
140 }
141
142 pub fn count_tokens(&self, text: &str) -> usize {
144 self.token_counter.count(text)
145 }
146
147 pub fn add_segment_auto(
149 &mut self,
150 mut segment: ContextSegment,
151 ) -> Result<(), ContextWindowError> {
152 if segment.token_count == 0 {
154 segment.token_count = self.token_counter.count(&segment.content);
155 }
156
157 self.add_segment(segment)
158 }
159
160 pub fn add_segment(&mut self, mut segment: ContextSegment) -> Result<(), ContextWindowError> {
162 if let Some(seg_budget) = self.budget.get_segment(segment.segment_type) {
164 let new_usage = seg_budget.current_tokens + segment.token_count;
165 if new_usage > seg_budget.max_tokens {
166 return Err(ContextWindowError::SegmentBudgetExceeded {
167 segment_type: segment.segment_type,
168 needed: segment.token_count,
169 max: seg_budget.max_tokens - seg_budget.current_tokens,
170 });
171 }
172 }
173
174 let new_total = self.budget.used_tokens + segment.token_count;
176 if new_total > self.budget.available_tokens {
177 return Err(ContextWindowError::BudgetExceeded {
178 needed: segment.token_count,
179 available: self.budget.remaining(),
180 });
181 }
182
183 segment.sequence = self.next_sequence;
185 self.next_sequence += 1;
186
187 self.budget
189 .add_tokens(segment.segment_type, segment.token_count);
190
191 self.segments.push(segment);
193
194 Ok(())
195 }
196
197 pub fn remove_segment(&mut self, segment_id: &str) -> bool {
199 if let Some(pos) = self.segments.iter().position(|s| s.id == segment_id) {
200 let segment = self.segments.remove(pos);
201 self.budget
202 .remove_tokens(segment.segment_type, segment.token_count);
203 true
204 } else {
205 false
206 }
207 }
208
209 pub fn segments_of_type(&self, segment_type: ContextSegmentType) -> Vec<&ContextSegment> {
211 self.segments
212 .iter()
213 .filter(|s| s.segment_type == segment_type)
214 .collect()
215 }
216
217 pub fn used_tokens(&self) -> usize {
219 self.budget.used_tokens
220 }
221
222 pub fn remaining_tokens(&self) -> usize {
224 self.budget.remaining()
225 }
226
227 pub fn needs_compaction(&self) -> bool {
229 self.budget.is_warning()
230 }
231
232 pub fn is_critical(&self) -> bool {
234 self.budget.is_critical()
235 }
236
237 pub fn health(&self) -> BudgetHealth {
239 self.budget.health()
240 }
241
242 pub fn compact(
244 &mut self,
245 compactor: &Compactor,
246 ) -> Result<CompactionResult, ContextWindowError> {
247 let start = Instant::now();
248 let tokens_before = self.budget.used_tokens;
249
250 let result = match compactor.strategy().strategy_type {
251 CompactionStrategyType::Truncate => {
252 compactor.compact_truncate(&mut self.segments, tokens_before)
253 }
254 CompactionStrategyType::SlidingWindow => {
255 compactor.compact_sliding_window(&mut self.segments)
256 }
257 CompactionStrategyType::Summarize => {
258 compactor.compact_summarize(&mut self.segments, tokens_before)
259 }
260 CompactionStrategyType::ExtractKeyPoints => {
261 compactor.compact_extract_key_points(&mut self.segments, tokens_before)
262 }
263 CompactionStrategyType::ImportanceWeighted => {
264 compactor.compact_importance_weighted(&mut self.segments, tokens_before)
265 }
266 CompactionStrategyType::Hybrid => {
267 compactor.compact_hybrid(&mut self.segments, tokens_before)
268 }
269 };
270
271 let duration_ms = start.elapsed().as_millis() as u64;
272
273 match result {
274 Ok(tokens_removed) => {
275 self.recalculate_budget();
277
278 let tokens_after = self.budget.used_tokens;
279 let segments_compacted = (tokens_removed > 0) as usize;
280
281 let compaction_result = CompactionResult::success(
282 self.execution_id.clone(),
283 compactor.strategy().strategy_type,
284 tokens_before,
285 tokens_after,
286 segments_compacted,
287 duration_ms,
288 );
289
290 self.compaction_history.push(compaction_result.clone());
291 Ok(compaction_result)
292 }
293 Err(e) => {
294 let compaction_result = CompactionResult::failure(
295 self.execution_id.clone(),
296 compactor.strategy().strategy_type,
297 tokens_before,
298 e.to_string(),
299 duration_ms,
300 );
301
302 self.compaction_history.push(compaction_result.clone());
303 Err(ContextWindowError::CompactionFailed(e.to_string()))
304 }
305 }
306 }
307
308 fn recalculate_budget(&mut self) {
310 for seg_budget in &mut self.budget.segments {
312 seg_budget.current_tokens = 0;
313 }
314
315 for segment in &self.segments {
317 self.budget
318 .add_tokens(segment.segment_type, segment.token_count);
319 }
320 }
321
322 pub fn build_context(&self) -> String {
324 let mut parts: Vec<&str> = Vec::new();
325
326 let mut sorted: Vec<&ContextSegment> = self.segments.iter().collect();
328 sorted.sort_by_key(|s| s.sequence);
329
330 for segment in sorted {
331 parts.push(&segment.content);
332 }
333
334 parts.join("\n\n")
335 }
336
337 pub fn state(&self) -> ContextWindowState {
339 ContextWindowState {
340 execution_id: self.execution_id.clone(),
341 segments: self.segments.clone(),
342 budget: self.budget.clone(),
343 compaction_history: self.compaction_history.clone(),
344 compaction_count: self.compaction_history.len() as u32,
345 total_tokens_saved: self.compaction_history.iter().map(|r| r.tokens_saved).sum(),
346 health: self.budget.health(),
347 updated_at: Utc::now(),
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 fn test_execution_id() -> ExecutionId {
357 ExecutionId::new()
358 }
359
360 #[test]
361 fn test_create_window() {
362 let budget = ContextBudget::preset_default(test_execution_id());
363 let window = ContextWindow::new(budget).unwrap();
364
365 assert_eq!(window.used_tokens(), 0);
366 assert!(window.remaining_tokens() > 0);
367 }
368
369 #[test]
370 fn test_add_segment() {
371 let budget = ContextBudget::preset_default(test_execution_id());
372 let mut window = ContextWindow::new(budget).unwrap();
373
374 let segment = ContextSegment::system("You are a helpful assistant.", 10);
375 window.add_segment(segment).unwrap();
376
377 assert_eq!(window.segments().len(), 1);
378 assert_eq!(window.used_tokens(), 10);
379 }
380
381 #[test]
382 fn test_health_tracking() {
383 let budget = ContextBudget::preset_default(test_execution_id());
384 let window = ContextWindow::new(budget).unwrap();
385
386 assert_eq!(window.health(), BudgetHealth::Healthy);
387 assert!(!window.needs_compaction());
388 }
389
390 #[test]
391 fn test_compact_with_summarize_strategy() {
392 let budget = ContextBudget::preset_default(test_execution_id());
393 let mut window = ContextWindow::new(budget).unwrap();
394
395 let system = ContextSegment::system("You are helpful.", 10);
397 window.add_segment(system).unwrap();
398
399 let history1 = ContextSegment::history(
401 "The user asked about the weather. The result was sunny. Important note about temperature.",
402 500,
403 1,
404 );
405 let history2 = ContextSegment::history(
406 "Then we discussed travel plans. The conclusion was to visit Paris. The output showed flight options.",
407 600,
408 2,
409 );
410 window.add_segment(history1).unwrap();
411 window.add_segment(history2).unwrap();
412
413 let compactor = Compactor::summarize(200, 100);
415 let result = window.compact(&compactor);
416
417 assert!(result.is_ok());
418 let compaction_result = result.unwrap();
419 assert!(compaction_result.success);
420 assert!(compaction_result.tokens_saved > 0);
421
422 assert!(window
424 .segments()
425 .iter()
426 .any(|s| s.segment_type == ContextSegmentType::System));
427 }
428
429 #[test]
430 fn test_compact_with_extract_key_points_returns_error() {
431 let budget = ContextBudget::preset_default(test_execution_id());
432 let mut window = ContextWindow::new(budget).unwrap();
433
434 let history = ContextSegment::history("Some content", 100, 1);
435 window.add_segment(history).unwrap();
436
437 let strategy = CompactionStrategy {
439 strategy_type: CompactionStrategyType::ExtractKeyPoints,
440 target_tokens: 50,
441 min_preserve_percent: 20,
442 segments_to_compact: None,
443 protected_segments: None,
444 summary_max_tokens: None,
445 window_size: None,
446 min_importance_score: None,
447 };
448 let compactor = Compactor::new(strategy);
449 let result = window.compact(&compactor);
450
451 assert!(result.is_err());
452 match result {
453 Err(ContextWindowError::CompactionFailed(msg)) => {
454 assert!(msg.contains("ExtractKeyPoints"));
455 }
456 _ => panic!("Expected CompactionFailed error"),
457 }
458 }
459
460 #[test]
461 fn test_compact_with_importance_weighted_returns_error() {
462 let budget = ContextBudget::preset_default(test_execution_id());
463 let mut window = ContextWindow::new(budget).unwrap();
464
465 let history = ContextSegment::history("Some content", 100, 1);
466 window.add_segment(history).unwrap();
467
468 let strategy = CompactionStrategy {
469 strategy_type: CompactionStrategyType::ImportanceWeighted,
470 target_tokens: 50,
471 min_preserve_percent: 20,
472 segments_to_compact: None,
473 protected_segments: None,
474 summary_max_tokens: None,
475 window_size: None,
476 min_importance_score: Some(0.5),
477 };
478 let compactor = Compactor::new(strategy);
479 let result = window.compact(&compactor);
480
481 assert!(result.is_err());
482 match result {
483 Err(ContextWindowError::CompactionFailed(msg)) => {
484 assert!(msg.contains("ImportanceWeighted"));
485 }
486 _ => panic!("Expected CompactionFailed error"),
487 }
488 }
489
490 #[test]
491 fn test_compact_with_hybrid_returns_error() {
492 let budget = ContextBudget::preset_default(test_execution_id());
493 let mut window = ContextWindow::new(budget).unwrap();
494
495 let history = ContextSegment::history("Some content", 100, 1);
496 window.add_segment(history).unwrap();
497
498 let strategy = CompactionStrategy {
499 strategy_type: CompactionStrategyType::Hybrid,
500 target_tokens: 50,
501 min_preserve_percent: 20,
502 segments_to_compact: None,
503 protected_segments: None,
504 summary_max_tokens: None,
505 window_size: None,
506 min_importance_score: None,
507 };
508 let compactor = Compactor::new(strategy);
509 let result = window.compact(&compactor);
510
511 assert!(result.is_err());
512 match result {
513 Err(ContextWindowError::CompactionFailed(msg)) => {
514 assert!(msg.contains("Hybrid"));
515 }
516 _ => panic!("Expected CompactionFailed error"),
517 }
518 }
519}