use crate::skills::types::{Skill, SkillManifest};
use crate::utils::file_utils::{read_json_file_sync, write_json_file_sync};
use anyhow::{Context, Result, anyhow};
use hashbrown::HashMap;
use lru::LruCache;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextConfig {
pub max_context_tokens: usize,
pub max_cached_skills: usize,
pub metadata_token_cost: usize,
pub instruction_token_factor: f64,
pub resource_token_cost: usize,
pub enable_monitoring: bool,
pub eviction_policy: EvictionPolicy,
pub enable_persistence: bool,
pub cache_path: Option<PathBuf>,
}
impl Default for ContextConfig {
fn default() -> Self {
Self {
max_context_tokens: 50_000, max_cached_skills: 100,
metadata_token_cost: 50,
instruction_token_factor: 0.25, resource_token_cost: 200,
enable_monitoring: true,
eviction_policy: EvictionPolicy::LRU,
enable_persistence: false,
cache_path: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EvictionPolicy {
LRU,
LFU,
TokenCost,
Manual,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextLevel {
Metadata,
Instructions,
Full,
}
#[derive(Debug, Clone)]
pub struct ContextUsage {
pub access_count: u64,
pub last_access: std::time::Instant,
pub total_loaded_duration: std::time::Duration,
pub token_cost: usize,
}
impl Default for ContextUsage {
fn default() -> Self {
Self {
access_count: 0,
last_access: std::time::Instant::now(),
total_loaded_duration: std::time::Duration::ZERO,
token_cost: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct SkillContextEntry {
pub name: String,
pub level: ContextLevel,
pub manifest: SkillManifest,
pub instructions: Option<String>,
pub skill: Option<Skill>,
pub usage: ContextUsage,
pub memory_size: usize,
}
#[derive(Clone)]
pub struct ContextManager {
config: ContextConfig,
inner: Arc<RwLock<ContextManagerInner>>,
}
struct ContextManagerInner {
active_skills: HashMap<String, SkillContextEntry>,
loaded_skills: LruCache<String, SkillContextEntry>,
current_token_usage: usize,
stats: ContextStats,
}
#[derive(Debug, Default, Clone)]
pub struct ContextStats {
pub total_skills_loaded: u64,
pub total_skills_evicted: u64,
pub total_tokens_loaded: u64,
pub total_tokens_evicted: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub peak_token_usage: usize,
pub current_token_usage: usize,
}
impl ContextManager {
pub fn new() -> Self {
Self::with_config(ContextConfig::default())
}
pub fn with_config(config: ContextConfig) -> Self {
let max_cached_skills = config.max_cached_skills.max(1);
if max_cached_skills != config.max_cached_skills {
warn!(
configured = config.max_cached_skills,
effective = max_cached_skills,
"max_cached_skills must be at least 1; using fallback value"
);
}
let loaded_skills = LruCache::new(
std::num::NonZeroUsize::new(max_cached_skills).unwrap_or(std::num::NonZeroUsize::MIN),
);
Self {
config: config.clone(),
inner: Arc::new(RwLock::new(ContextManagerInner {
active_skills: HashMap::new(),
loaded_skills,
current_token_usage: 0,
stats: ContextStats::default(),
})),
}
}
}
impl Default for ContextManager {
fn default() -> Self {
Self::new()
}
}
impl ContextManager {
pub fn register_skill_metadata(&self, manifest: SkillManifest) -> Result<()> {
let name = manifest.name.clone();
let mut inner = self.inner.write().unwrap_or_else(|poisoned| {
warn!(
"ContextManager write lock poisoned while registering skill metadata; recovering"
);
poisoned.into_inner()
});
let entry = SkillContextEntry {
name: name.clone(),
level: ContextLevel::Metadata,
manifest: manifest.clone(),
instructions: None,
skill: None,
usage: ContextUsage {
access_count: 0,
last_access: std::time::Instant::now(),
total_loaded_duration: std::time::Duration::ZERO,
token_cost: self.config.metadata_token_cost,
},
memory_size: size_of::<SkillContextEntry>() + name.len() + manifest.description.len(),
};
inner.current_token_usage += self.config.metadata_token_cost;
inner.stats.current_token_usage = inner.current_token_usage;
inner.stats.peak_token_usage = inner.stats.peak_token_usage.max(inner.current_token_usage);
inner.active_skills.insert(name, entry);
info!("Registered skill metadata: {}", manifest.name);
Ok(())
}
pub fn load_skill_instructions(&self, name: &str, instructions: String) -> Result<()> {
let mut inner = self.inner.write().unwrap_or_else(|poisoned| {
warn!(
"ContextManager write lock poisoned while loading skill instructions; recovering"
);
poisoned.into_inner()
});
let instruction_size = instructions.len();
if inner.current_token_usage + instruction_size > self.config.max_context_tokens {
self.evict_skills_to_make_room_internal(&mut inner, instruction_size)?;
}
let mut entry = match inner.loaded_skills.get_mut(name) {
Some(entry) => entry.clone(),
None => {
match inner.active_skills.get(name) {
Some(active_entry) => active_entry.clone(),
None => return Err(anyhow!("Skill '{}' not found in active skills", name)),
}
}
};
entry.level = ContextLevel::Instructions;
entry.instructions = Some(instructions.clone());
entry.usage.token_cost = instruction_size;
entry.memory_size += instructions.len();
inner.current_token_usage += instruction_size;
inner.stats.current_token_usage = inner.current_token_usage;
inner.stats.peak_token_usage = inner.stats.peak_token_usage.max(inner.current_token_usage);
inner.stats.total_tokens_loaded += instruction_size as u64;
inner.loaded_skills.put(name.to_string(), entry);
info!(
"Loaded instructions for skill: {} ({} chars)",
name, instruction_size
);
Ok(())
}
pub fn load_full_skill(&self, skill: Skill) -> Result<()> {
let name = skill.name().to_string();
let mut inner = self.inner.write().unwrap_or_else(|poisoned| {
warn!("ContextManager write lock poisoned while loading full skill; recovering");
poisoned.into_inner()
});
let instruction_size = skill.instructions.len();
let resource_size = skill.list_resources().len() * self.config.resource_token_cost * 4; let incremental_cost = instruction_size + resource_size;
if inner.current_token_usage + incremental_cost > self.config.max_context_tokens {
self.evict_skills_to_make_room_internal(&mut inner, incremental_cost)?;
}
let entry = SkillContextEntry {
name: name.clone(),
level: ContextLevel::Full,
manifest: skill.manifest.clone(),
instructions: Some(skill.instructions.clone()),
skill: Some(skill),
usage: ContextUsage {
access_count: 0,
last_access: std::time::Instant::now(),
total_loaded_duration: std::time::Duration::ZERO,
token_cost: incremental_cost,
},
memory_size: size_of::<Skill>() + name.len() * 2,
};
inner.current_token_usage += incremental_cost;
inner.stats.current_token_usage = inner.current_token_usage;
inner.stats.peak_token_usage = inner.stats.peak_token_usage.max(inner.current_token_usage);
inner.stats.total_skills_loaded += 1;
inner.stats.total_tokens_loaded += incremental_cost as u64;
let entry_name = entry.name.clone();
inner.loaded_skills.put(name, entry);
info!(
"Loaded full skill: {} ({} tokens)",
entry_name,
incremental_cost + self.config.metadata_token_cost
);
Ok(())
}
pub fn get_skill_context(&self, name: &str) -> Option<SkillContextEntry> {
let mut inner = self.inner.write().unwrap_or_else(|poisoned| {
warn!("ContextManager write lock poisoned while fetching skill context; recovering");
poisoned.into_inner()
});
if let Some(mut entry) = inner.loaded_skills.get_mut(name).cloned() {
entry.usage.access_count += 1;
entry.usage.last_access = std::time::Instant::now();
inner.stats.cache_hits += 1;
return Some(entry);
}
if let Some(mut entry) = inner.active_skills.get(name).cloned() {
entry.usage.access_count += 1;
entry.usage.last_access = std::time::Instant::now();
inner.stats.cache_misses += 1;
return Some(entry);
}
None
}
fn evict_skills_to_make_room_internal(
&self,
inner: &mut ContextManagerInner,
required_tokens: usize,
) -> Result<()> {
let mut freed_tokens = 0;
let mut evicted_skills = Vec::new();
while freed_tokens < required_tokens && !inner.loaded_skills.is_empty() {
if let Some((name, entry)) = inner.loaded_skills.pop_lru() {
freed_tokens += entry.usage.token_cost;
evicted_skills.push(name);
inner.stats.total_skills_evicted += 1;
inner.stats.total_tokens_evicted += entry.usage.token_cost as u64;
} else {
break;
}
}
inner.current_token_usage -= freed_tokens;
inner.stats.current_token_usage = inner.current_token_usage;
info!(
"Evicted {} skills to free {} tokens",
evicted_skills.len(),
freed_tokens
);
debug!("Evicted skills: {:?}", evicted_skills);
if freed_tokens < required_tokens {
return Err(anyhow!(
"Unable to free enough tokens. Required: {}, Freed: {}",
required_tokens,
freed_tokens
));
}
Ok(())
}
pub fn get_stats(&self) -> ContextStats {
self.inner
.read()
.unwrap_or_else(|poisoned| {
warn!("ContextManager read lock poisoned while reading stats; recovering");
poisoned.into_inner()
})
.stats
.clone()
}
pub fn get_token_usage(&self) -> usize {
self.inner
.read()
.unwrap_or_else(|poisoned| {
warn!("ContextManager read lock poisoned while reading token usage; recovering");
poisoned.into_inner()
})
.current_token_usage
}
pub fn clear_loaded_skills(&self) {
let mut inner = self.inner.write().unwrap_or_else(|poisoned| {
warn!("ContextManager write lock poisoned while clearing loaded skills; recovering");
poisoned.into_inner()
});
let evicted_count = inner.loaded_skills.len();
let evicted_tokens = inner.stats.current_token_usage
- (inner.active_skills.len() * self.config.metadata_token_cost);
inner.loaded_skills.clear();
inner.current_token_usage = inner.active_skills.len() * self.config.metadata_token_cost;
inner.stats.current_token_usage = inner.current_token_usage;
inner.stats.total_skills_evicted += evicted_count as u64;
inner.stats.total_tokens_evicted += evicted_tokens as u64;
info!(
"Cleared {} loaded skills ({} tokens)",
evicted_count, evicted_tokens
);
}
pub fn get_active_skills(&self) -> Vec<String> {
self.inner
.read()
.unwrap_or_else(|poisoned| {
warn!("ContextManager read lock poisoned while reading active skills; recovering");
poisoned.into_inner()
})
.active_skills
.keys()
.cloned()
.collect()
}
pub fn get_memory_usage(&self) -> usize {
let inner = self.inner.read().unwrap_or_else(|poisoned| {
warn!("ContextManager read lock poisoned while calculating memory usage; recovering");
poisoned.into_inner()
});
let active_memory: usize = inner
.active_skills
.values()
.map(|entry| entry.memory_size)
.sum();
let loaded_memory: usize = inner
.loaded_skills
.iter()
.map(|(_, entry)| entry.memory_size)
.sum();
active_memory + loaded_memory
}
}
pub struct PersistentContextManager {
inner: ContextManager,
cache_path: PathBuf,
}
impl PersistentContextManager {
pub fn new(cache_path: PathBuf, config: ContextConfig) -> Result<Self> {
let mut manager = Self {
inner: ContextManager::with_config(config),
cache_path,
};
if let Err(e) = manager.load_cache() {
debug!("Failed to load context cache: {}", e);
}
Ok(manager)
}
fn load_cache(&mut self) -> Result<()> {
if !self.cache_path.exists() {
return Ok(());
}
let cache: ContextCache = read_json_file_sync(&self.cache_path)?;
let skill_count = cache.active_skills.len();
for manifest in cache.active_skills {
self.inner.register_skill_metadata(manifest)?;
}
info!("Loaded {} cached skills", skill_count);
Ok(())
}
pub fn save_cache(&self) -> Result<()> {
let inner = self
.inner
.inner
.read()
.map_err(|err| anyhow!("context manager lock poisoned while saving cache: {err}"))
.context("Failed to save context manager cache state")?;
let cache = ContextCache {
version: 1,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs(),
active_skills: inner
.active_skills
.values()
.map(|entry| entry.manifest.clone())
.collect(),
};
write_json_file_sync(&self.cache_path, &cache)?;
info!("Saved {} skills to cache", cache.active_skills.len());
Ok(())
}
pub fn inner(&self) -> &ContextManager {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut ContextManager {
&mut self.inner
}
}
#[derive(Debug, Serialize, Deserialize)]
struct ContextCache {
version: u32,
timestamp: u64,
active_skills: Vec<SkillManifest>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_config_default() {
let config = ContextConfig::default();
assert_eq!(config.max_context_tokens, 50_000);
assert_eq!(config.max_cached_skills, 100);
}
#[test]
fn test_context_manager_creation() {
let manager = ContextManager::new();
assert_eq!(manager.get_token_usage(), 0);
assert_eq!(manager.get_active_skills().len(), 0);
}
#[test]
fn test_skill_metadata_registration() {
let manager = ContextManager::new();
let manifest = SkillManifest {
name: "test-skill".to_string(),
description: "Test skill".to_string(),
version: Some("1.0.0".to_string()),
author: Some("Test".to_string()),
vtcode_native: Some(true),
..Default::default()
};
assert!(manager.register_skill_metadata(manifest).is_ok());
assert_eq!(manager.get_active_skills().len(), 1);
assert_eq!(manager.get_token_usage(), 50); }
#[test]
fn test_skill_context_retrieval() {
let manager = ContextManager::new();
let manifest = SkillManifest {
name: "test-skill".to_string(),
description: "Test skill".to_string(),
..Default::default()
};
manager.register_skill_metadata(manifest.clone()).unwrap();
let context = manager.get_skill_context("test-skill");
assert!(context.is_some());
assert_eq!(context.unwrap().manifest.name, "test-skill");
}
}