1use serde::{Deserialize, Serialize};
20use std::collections::VecDeque;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ContextEntry {
25 pub id: String,
26 pub content: String,
27 pub entry_type: EntryType,
28 pub tokens: usize,
29 pub priority: u8, pub importance: f64, pub created_at: f64,
32 pub metadata: HashMap<String, String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36pub enum EntryType {
37 System,
38 User,
39 Assistant,
40 Tool,
41 Tile,
42 Instruction,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub enum EvictionPolicy {
48 FIFO, LRU, Priority, Importance, SlidingWindow,Hybrid, }
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ContextConfig {
59 pub max_tokens: usize,
60 pub reserved_system_tokens: usize,
61 pub eviction_policy: EvictionPolicy,
62 pub chars_per_token: f64,
63 pub min_entries: usize, pub importance_decay: f64, }
66
67impl Default for ContextConfig {
68 fn default() -> Self {
69 Self { max_tokens: 4096, reserved_system_tokens: 256,
70 eviction_policy: EvictionPolicy::Hybrid, chars_per_token: 4.0,
71 min_entries: 2, importance_decay: 0.99 }
72 }
73}
74
75pub struct RoomContext {
77 config: ContextConfig,
78 entries: VecDeque<ContextEntry>,
79 system_tokens: usize,
80 user_tokens: usize,
81 total_added: usize,
82 total_evicted: usize,
83 resize_count: usize,
84}
85
86impl RoomContext {
87 pub fn new(config: ContextConfig) -> Self {
88 Self { config, entries: VecDeque::new(), system_tokens: 0,
89 user_tokens: 0, total_added: 0, total_evicted: 0, resize_count: 0 }
90 }
91
92 pub fn add(&mut self, id: &str, content: &str, entry_type: EntryType,
94 priority: u8, importance: f64) -> usize {
95 let tokens = self.estimate_tokens(content);
96 let entry = ContextEntry {
97 id: id.to_string(), content: content.to_string(),
98 entry_type: entry_type.clone(), tokens, priority, importance,
99 created_at: now(), metadata: HashMap::new(),
100 };
101
102 match entry_type {
103 EntryType::System => self.system_tokens += tokens,
104 _ => self.user_tokens += tokens,
105 }
106
107 self.entries.push_back(entry);
108 self.total_added += 1;
109
110 self.maybe_evict();
112
113 tokens
114 }
115
116 pub fn add_system(&mut self, content: &str) -> usize {
118 self.add("system", content, EntryType::System, 0, 1.0)
119 }
120
121 pub fn add_user(&mut self, id: &str, content: &str) -> usize {
123 self.add(id, content, EntryType::User, 2, 0.5)
124 }
125
126 pub fn add_assistant(&mut self, id: &str, content: &str) -> usize {
128 self.add(id, content, EntryType::Assistant, 1, 0.7)
129 }
130
131 pub fn add_tile(&mut self, tile_id: &str, content: &str, importance: f64) -> usize {
133 self.add(tile_id, content, EntryType::Tile, 3, importance)
134 }
135
136 pub fn entries(&self) -> Vec<&ContextEntry> {
138 self.entries.iter().collect()
139 }
140
141 pub fn format(&self) -> String {
143 self.entries.iter().map(|e| {
144 let prefix = match e.entry_type {
145 EntryType::System => "[SYSTEM]",
146 EntryType::User => "[USER]",
147 EntryType::Assistant => "[ASSISTANT]",
148 EntryType::Tool => "[TOOL]",
149 EntryType::Tile => "[TILE]",
150 EntryType::Instruction => "[INSTRUCTION]",
151 };
152 format!("{} {}", prefix, e.content)
153 }).collect::<Vec<_>>().join("\n")
154 }
155
156 pub fn entries_by_type(&self, entry_type: &EntryType) -> Vec<&ContextEntry> {
158 self.entries.iter().filter(|e| &e.entry_type == entry_type).collect()
159 }
160
161 pub fn token_usage(&self) -> TokenUsage {
163 TokenUsage {
164 system: self.system_tokens, user: self.user_tokens,
165 total: self.system_tokens + self.user_tokens,
166 max: self.config.max_tokens,
167 available: self.config.max_tokens.saturating_sub(self.system_tokens + self.user_tokens),
168 utilization: (self.system_tokens + self.user_tokens) as f64 / self.config.max_tokens as f64,
169 }
170 }
171
172 pub fn trim_to(&mut self, target_tokens: usize) -> usize {
174 let target = target_tokens.max(self.config.min_entries * 10);
175 while self.system_tokens + self.user_tokens > target && self.entries.len() > self.config.min_entries {
176 let idx = self.entries.iter().position(|e| e.entry_type != EntryType::System);
178 if let Some(idx) = idx {
179 if let Some(removed) = self.entries.remove(idx) {
180 match removed.entry_type {
181 EntryType::System => self.system_tokens -= removed.tokens,
182 _ => self.user_tokens -= removed.tokens,
183 }
184 self.total_evicted += 1;
185 }
186 } else { break; }
187 }
188 self.system_tokens + self.user_tokens
189 }
190
191 pub fn clear(&mut self) {
193 self.entries.retain(|e| e.entry_type == EntryType::System);
194 self.user_tokens = 0;
195 self.total_evicted += self.entries.len();
196 }
197
198 pub fn reset(&mut self) {
200 self.entries.clear();
201 self.system_tokens = 0;
202 self.user_tokens = 0;
203 }
204
205 pub fn resize(&mut self, new_max_tokens: usize) {
207 self.config.max_tokens = new_max_tokens;
208 self.resize_count += 1;
209 self.maybe_evict();
210 }
211
212 pub fn len(&self) -> usize {
214 self.entries.len()
215 }
216
217 pub fn is_empty(&self) -> bool {
218 self.entries.is_empty()
219 }
220
221 pub fn boost(&mut self, id: &str, boost: f64) -> bool {
223 if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
224 entry.importance = (entry.importance + boost).min(1.0);
225 return true;
226 }
227 false
228 }
229
230 fn maybe_evict(&mut self) {
231 let budget = self.config.max_tokens;
232 let min_entries = self.config.min_entries;
233 while self.system_tokens + self.user_tokens > budget && self.entries.len() > min_entries {
234 let non_system: Vec<usize> = self.entries.iter().enumerate()
236 .filter(|(_, e)| e.entry_type != EntryType::System)
237 .map(|(i, _)| i).collect();
238
239 if non_system.is_empty() { break; }
240
241 let evict_idx = match self.config.eviction_policy {
242 EvictionPolicy::FIFO => non_system.first().cloned().unwrap_or(0),
243 EvictionPolicy::LRU => non_system.first().cloned().unwrap_or(0), EvictionPolicy::Priority => {
245 non_system.iter().cloned().max_by(|&a, &b| {
246 self.entries[a].priority.cmp(&self.entries[b].priority)
247 }).unwrap_or(0)
248 }
249 EvictionPolicy::Importance => {
250 non_system.iter().cloned().min_by(|&a, &b| {
251 self.entries[a].importance.partial_cmp(&self.entries[b].importance).unwrap_or(std::cmp::Ordering::Equal)
252 }).unwrap_or(0)
253 }
254 EvictionPolicy::SlidingWindow => non_system.first().cloned().unwrap_or(0),
255 EvictionPolicy::Hybrid => {
256 non_system.iter().cloned().min_by(|&a, &b| {
258 let score_a = self.entries[a].created_at * 0.3
259 + self.entries[a].priority as f64 * 10.0
260 + self.entries[a].importance * 50.0;
261 let score_b = self.entries[b].created_at * 0.3
262 + self.entries[b].priority as f64 * 10.0
263 + self.entries[b].importance * 50.0;
264 score_a.partial_cmp(&score_b).unwrap_or(std::cmp::Ordering::Equal)
265 }).unwrap_or(0)
266 }
267 };
268
269 if let Some(removed) = self.entries.remove(evict_idx) {
270 self.user_tokens = self.user_tokens.saturating_sub(removed.tokens);
271 self.total_evicted += 1;
272 }
273 }
274 }
275
276 fn estimate_tokens(&self, text: &str) -> usize {
277 (text.len() as f64 / self.config.chars_per_token).ceil() as usize
278 }
279
280 pub fn stats(&self) -> ContextStats {
281 let types: HashMap<String, usize> = self.entries.iter()
282 .map(|e| (format!("{:?}", e.entry_type), 1))
283 .fold(HashMap::new(), |mut acc, (k, v)| { *acc.entry(k).or_insert(0) += v; acc });
284 ContextStats { entries: self.entries.len(), total_added: self.total_added,
285 total_evicted: self.total_evicted, resizes: self.resize_count,
286 token_usage: self.token_usage(), entry_types: types }
287 }
288}
289
290use std::collections::HashMap;
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct TokenUsage {
294 pub system: usize,
295 pub user: usize,
296 pub total: usize,
297 pub max: usize,
298 pub available: usize,
299 pub utilization: f64,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
303pub struct ContextStats {
304 pub entries: usize,
305 pub total_added: usize,
306 pub total_evicted: usize,
307 pub resizes: usize,
308 pub token_usage: TokenUsage,
309 pub entry_types: HashMap<String, usize>,
310}
311
312fn now() -> f64 {
313 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
314 .map(|d| d.as_secs_f64()).unwrap_or(0.0)
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_add_and_evict() {
323 let config = ContextConfig { max_tokens: 100, ..Default::default() };
324 let mut ctx = RoomContext::new(config);
325 ctx.add_system("You are helpful.");
326 for i in 0..50 {
327 ctx.add_user(&format!("msg-{}", i), &"x".repeat(40));
328 }
329 assert!(ctx.len() <= 50); let usage = ctx.token_usage();
331 assert!(usage.total <= 120); }
333
334 #[test]
335 fn test_system_protected() {
336 let config = ContextConfig { max_tokens: 20, min_entries: 1, ..Default::default() };
337 let mut ctx = RoomContext::new(config);
338 ctx.add_system("System prompt that is quite long to exceed the budget");
339 assert!(ctx.entries().iter().any(|e| e.entry_type == EntryType::System));
341 }
342
343 #[test]
344 fn test_format() {
345 let mut ctx = RoomContext::new(ContextConfig::default());
346 ctx.add_system("System");
347 ctx.add_user("u1", "Hello");
348 ctx.add_assistant("a1", "Hi there");
349 let formatted = ctx.format();
350 assert!(formatted.contains("[SYSTEM]"));
351 assert!(formatted.contains("[USER]"));
352 assert!(formatted.contains("[ASSISTANT]"));
353 }
354
355 #[test]
356 fn test_trim() {
357 let mut ctx = RoomContext::new(ContextConfig::default());
358 for i in 0..20 {
359 ctx.add_user(&format!("{}", i), &"hello world ".repeat(10));
360 }
361 let trimmed = ctx.trim_to(50);
362 assert!(trimmed <= 60);
363 }
364
365 #[test]
366 fn test_boost() {
367 let mut ctx = RoomContext::new(ContextConfig::default());
368 ctx.add_user("important", "critical info");
369 ctx.boost("important", 0.5);
370 assert_eq!(ctx.entries().iter().find(|e| e.id == "important").unwrap().importance, 1.0);
371 }
372
373 #[test]
374 fn test_priority_eviction() {
375 let mut config = ContextConfig::default();
376 config.max_tokens = 30;
377 config.eviction_policy = EvictionPolicy::Priority;
378 let mut ctx = RoomContext::new(config);
379 ctx.add_user("low", &"x".repeat(100)); ctx.add("high", &"y".repeat(100), EntryType::Tile, 3, 0.9); }
384
385 #[test]
386 fn test_clear_keeps_system() {
387 let mut ctx = RoomContext::new(ContextConfig::default());
388 ctx.add_system("System");
389 ctx.add_user("u1", "Hello");
390 ctx.clear();
391 assert_eq!(ctx.len(), 1);
392 assert_eq!(ctx.entries()[0].entry_type, EntryType::System);
393 }
394}