use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextEntry {
pub id: String,
pub content: String,
pub entry_type: EntryType,
pub tokens: usize,
pub priority: u8, pub importance: f64, pub created_at: f64,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum EntryType {
System,
User,
Assistant,
Tool,
Tile,
Instruction,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum EvictionPolicy {
FIFO, LRU, Priority, Importance, SlidingWindow, Hybrid, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextConfig {
pub max_tokens: usize,
pub reserved_system_tokens: usize,
pub eviction_policy: EvictionPolicy,
pub chars_per_token: f64,
pub min_entries: usize, pub importance_decay: f64, }
impl Default for ContextConfig {
fn default() -> Self {
Self { max_tokens: 4096, reserved_system_tokens: 256,
eviction_policy: EvictionPolicy::Hybrid, chars_per_token: 4.0,
min_entries: 2, importance_decay: 0.99 }
}
}
pub struct RoomContext {
config: ContextConfig,
entries: VecDeque<ContextEntry>,
system_tokens: usize,
user_tokens: usize,
total_added: usize,
total_evicted: usize,
resize_count: usize,
}
impl RoomContext {
pub fn new(config: ContextConfig) -> Self {
Self { config, entries: VecDeque::new(), system_tokens: 0,
user_tokens: 0, total_added: 0, total_evicted: 0, resize_count: 0 }
}
pub fn add(&mut self, id: &str, content: &str, entry_type: EntryType,
priority: u8, importance: f64) -> usize {
let tokens = self.estimate_tokens(content);
let entry = ContextEntry {
id: id.to_string(), content: content.to_string(),
entry_type: entry_type.clone(), tokens, priority, importance,
created_at: now(), metadata: HashMap::new(),
};
match entry_type {
EntryType::System => self.system_tokens += tokens,
_ => self.user_tokens += tokens,
}
self.entries.push_back(entry);
self.total_added += 1;
self.maybe_evict();
tokens
}
pub fn add_system(&mut self, content: &str) -> usize {
self.add("system", content, EntryType::System, 0, 1.0)
}
pub fn add_user(&mut self, id: &str, content: &str) -> usize {
self.add(id, content, EntryType::User, 2, 0.5)
}
pub fn add_assistant(&mut self, id: &str, content: &str) -> usize {
self.add(id, content, EntryType::Assistant, 1, 0.7)
}
pub fn add_tile(&mut self, tile_id: &str, content: &str, importance: f64) -> usize {
self.add(tile_id, content, EntryType::Tile, 3, importance)
}
pub fn entries(&self) -> Vec<&ContextEntry> {
self.entries.iter().collect()
}
pub fn format(&self) -> String {
self.entries.iter().map(|e| {
let prefix = match e.entry_type {
EntryType::System => "[SYSTEM]",
EntryType::User => "[USER]",
EntryType::Assistant => "[ASSISTANT]",
EntryType::Tool => "[TOOL]",
EntryType::Tile => "[TILE]",
EntryType::Instruction => "[INSTRUCTION]",
};
format!("{} {}", prefix, e.content)
}).collect::<Vec<_>>().join("\n")
}
pub fn entries_by_type(&self, entry_type: &EntryType) -> Vec<&ContextEntry> {
self.entries.iter().filter(|e| &e.entry_type == entry_type).collect()
}
pub fn token_usage(&self) -> TokenUsage {
TokenUsage {
system: self.system_tokens, user: self.user_tokens,
total: self.system_tokens + self.user_tokens,
max: self.config.max_tokens,
available: self.config.max_tokens.saturating_sub(self.system_tokens + self.user_tokens),
utilization: (self.system_tokens + self.user_tokens) as f64 / self.config.max_tokens as f64,
}
}
pub fn trim_to(&mut self, target_tokens: usize) -> usize {
let target = target_tokens.max(self.config.min_entries * 10);
while self.system_tokens + self.user_tokens > target && self.entries.len() > self.config.min_entries {
let idx = self.entries.iter().position(|e| e.entry_type != EntryType::System);
if let Some(idx) = idx {
if let Some(removed) = self.entries.remove(idx) {
match removed.entry_type {
EntryType::System => self.system_tokens -= removed.tokens,
_ => self.user_tokens -= removed.tokens,
}
self.total_evicted += 1;
}
} else { break; }
}
self.system_tokens + self.user_tokens
}
pub fn clear(&mut self) {
self.entries.retain(|e| e.entry_type == EntryType::System);
self.user_tokens = 0;
self.total_evicted += self.entries.len();
}
pub fn reset(&mut self) {
self.entries.clear();
self.system_tokens = 0;
self.user_tokens = 0;
}
pub fn resize(&mut self, new_max_tokens: usize) {
self.config.max_tokens = new_max_tokens;
self.resize_count += 1;
self.maybe_evict();
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn boost(&mut self, id: &str, boost: f64) -> bool {
if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
entry.importance = (entry.importance + boost).min(1.0);
return true;
}
false
}
fn maybe_evict(&mut self) {
let budget = self.config.max_tokens;
let min_entries = self.config.min_entries;
while self.system_tokens + self.user_tokens > budget && self.entries.len() > min_entries {
let non_system: Vec<usize> = self.entries.iter().enumerate()
.filter(|(_, e)| e.entry_type != EntryType::System)
.map(|(i, _)| i).collect();
if non_system.is_empty() { break; }
let evict_idx = match self.config.eviction_policy {
EvictionPolicy::FIFO => non_system.first().cloned().unwrap_or(0),
EvictionPolicy::LRU => non_system.first().cloned().unwrap_or(0), EvictionPolicy::Priority => {
non_system.iter().cloned().max_by(|&a, &b| {
self.entries[a].priority.cmp(&self.entries[b].priority)
}).unwrap_or(0)
}
EvictionPolicy::Importance => {
non_system.iter().cloned().min_by(|&a, &b| {
self.entries[a].importance.partial_cmp(&self.entries[b].importance).unwrap_or(std::cmp::Ordering::Equal)
}).unwrap_or(0)
}
EvictionPolicy::SlidingWindow => non_system.first().cloned().unwrap_or(0),
EvictionPolicy::Hybrid => {
non_system.iter().cloned().min_by(|&a, &b| {
let score_a = self.entries[a].created_at * 0.3
+ self.entries[a].priority as f64 * 10.0
+ self.entries[a].importance * 50.0;
let score_b = self.entries[b].created_at * 0.3
+ self.entries[b].priority as f64 * 10.0
+ self.entries[b].importance * 50.0;
score_a.partial_cmp(&score_b).unwrap_or(std::cmp::Ordering::Equal)
}).unwrap_or(0)
}
};
if let Some(removed) = self.entries.remove(evict_idx) {
self.user_tokens = self.user_tokens.saturating_sub(removed.tokens);
self.total_evicted += 1;
}
}
}
fn estimate_tokens(&self, text: &str) -> usize {
(text.len() as f64 / self.config.chars_per_token).ceil() as usize
}
pub fn stats(&self) -> ContextStats {
let types: HashMap<String, usize> = self.entries.iter()
.map(|e| (format!("{:?}", e.entry_type), 1))
.fold(HashMap::new(), |mut acc, (k, v)| { *acc.entry(k).or_insert(0) += v; acc });
ContextStats { entries: self.entries.len(), total_added: self.total_added,
total_evicted: self.total_evicted, resizes: self.resize_count,
token_usage: self.token_usage(), entry_types: types }
}
}
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenUsage {
pub system: usize,
pub user: usize,
pub total: usize,
pub max: usize,
pub available: usize,
pub utilization: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextStats {
pub entries: usize,
pub total_added: usize,
pub total_evicted: usize,
pub resizes: usize,
pub token_usage: TokenUsage,
pub entry_types: HashMap<String, usize>,
}
fn now() -> f64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64()).unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_and_evict() {
let config = ContextConfig { max_tokens: 100, ..Default::default() };
let mut ctx = RoomContext::new(config);
ctx.add_system("You are helpful.");
for i in 0..50 {
ctx.add_user(&format!("msg-{}", i), &"x".repeat(40));
}
assert!(ctx.len() <= 50); let usage = ctx.token_usage();
assert!(usage.total <= 120); }
#[test]
fn test_system_protected() {
let config = ContextConfig { max_tokens: 20, min_entries: 1, ..Default::default() };
let mut ctx = RoomContext::new(config);
ctx.add_system("System prompt that is quite long to exceed the budget");
assert!(ctx.entries().iter().any(|e| e.entry_type == EntryType::System));
}
#[test]
fn test_format() {
let mut ctx = RoomContext::new(ContextConfig::default());
ctx.add_system("System");
ctx.add_user("u1", "Hello");
ctx.add_assistant("a1", "Hi there");
let formatted = ctx.format();
assert!(formatted.contains("[SYSTEM]"));
assert!(formatted.contains("[USER]"));
assert!(formatted.contains("[ASSISTANT]"));
}
#[test]
fn test_trim() {
let mut ctx = RoomContext::new(ContextConfig::default());
for i in 0..20 {
ctx.add_user(&format!("{}", i), &"hello world ".repeat(10));
}
let trimmed = ctx.trim_to(50);
assert!(trimmed <= 60);
}
#[test]
fn test_boost() {
let mut ctx = RoomContext::new(ContextConfig::default());
ctx.add_user("important", "critical info");
ctx.boost("important", 0.5);
assert_eq!(ctx.entries().iter().find(|e| e.id == "important").unwrap().importance, 1.0);
}
#[test]
fn test_priority_eviction() {
let mut config = ContextConfig::default();
config.max_tokens = 30;
config.eviction_policy = EvictionPolicy::Priority;
let mut ctx = RoomContext::new(config);
ctx.add_user("low", &"x".repeat(100)); ctx.add("high", &"y".repeat(100), EntryType::Tile, 3, 0.9); }
#[test]
fn test_clear_keeps_system() {
let mut ctx = RoomContext::new(ContextConfig::default());
ctx.add_system("System");
ctx.add_user("u1", "Hello");
ctx.clear();
assert_eq!(ctx.len(), 1);
assert_eq!(ctx.entries()[0].entry_type, EntryType::System);
}
}