1use crate::skills::types::{Skill, SkillManifest};
11use crate::utils::file_utils::{read_json_file_sync, write_json_file_sync};
12use anyhow::{Context, Result, anyhow};
13use hashbrown::HashMap;
14use lru::LruCache;
15use serde::{Deserialize, Serialize};
16use std::path::PathBuf;
17use std::sync::{Arc, RwLock};
18use tracing::{debug, info, warn};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ContextConfig {
23 pub max_context_tokens: usize,
25
26 pub max_cached_skills: usize,
28
29 pub metadata_token_cost: usize,
31
32 pub instruction_token_factor: f64,
34
35 pub resource_token_cost: usize,
37
38 pub enable_monitoring: bool,
40
41 pub eviction_policy: EvictionPolicy,
43
44 pub enable_persistence: bool,
46
47 pub cache_path: Option<PathBuf>,
49}
50
51impl Default for ContextConfig {
52 fn default() -> Self {
53 Self {
54 max_context_tokens: 50_000, max_cached_skills: 100,
56 metadata_token_cost: 50,
57 instruction_token_factor: 0.25, resource_token_cost: 200,
59 enable_monitoring: true,
60 eviction_policy: EvictionPolicy::LRU,
61 enable_persistence: false,
62 cache_path: None,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub enum EvictionPolicy {
70 LRU,
72 LFU,
74 TokenCost,
76 Manual,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
82pub enum ContextLevel {
83 Metadata,
85 Instructions,
87 Full,
89}
90
91#[derive(Debug, Clone)]
93pub struct ContextUsage {
94 pub access_count: u64,
96
97 pub last_access: std::time::Instant,
99
100 pub total_loaded_duration: std::time::Duration,
102
103 pub token_cost: usize,
105}
106
107impl Default for ContextUsage {
108 fn default() -> Self {
109 Self {
110 access_count: 0,
111 last_access: std::time::Instant::now(),
112 total_loaded_duration: std::time::Duration::ZERO,
113 token_cost: 0,
114 }
115 }
116}
117
118#[derive(Debug, Clone)]
120pub struct SkillContextEntry {
121 pub name: String,
123
124 pub level: ContextLevel,
126
127 pub manifest: SkillManifest,
129
130 pub instructions: Option<String>,
132
133 pub skill: Option<Box<Skill>>,
135
136 pub usage: ContextUsage,
138
139 pub memory_size: usize,
141}
142
143#[derive(Clone)]
145pub struct ContextManager {
146 config: ContextConfig,
147 inner: Arc<RwLock<ContextManagerInner>>,
148}
149
150struct ContextManagerInner {
152 active_skills: HashMap<String, SkillContextEntry>,
154
155 loaded_skills: LruCache<String, SkillContextEntry>,
157
158 current_token_usage: usize,
160
161 stats: ContextStats,
163}
164
165#[derive(Debug, Default, Clone)]
167pub struct ContextStats {
168 pub total_skills_loaded: u64,
169 pub total_skills_evicted: u64,
170 pub total_tokens_loaded: u64,
171 pub total_tokens_evicted: u64,
172 pub cache_hits: u64,
173 pub cache_misses: u64,
174 pub peak_token_usage: usize,
175 pub current_token_usage: usize,
176}
177
178impl ContextManager {
179 pub fn new() -> Self {
181 Self::with_config(ContextConfig::default())
182 }
183
184 pub fn with_config(config: ContextConfig) -> Self {
186 let max_cached_skills = config.max_cached_skills.max(1);
187 if max_cached_skills != config.max_cached_skills {
188 warn!(
189 configured = config.max_cached_skills,
190 effective = max_cached_skills,
191 "max_cached_skills must be at least 1; using fallback value"
192 );
193 }
194 let loaded_skills = LruCache::new(
195 std::num::NonZeroUsize::new(max_cached_skills).unwrap_or(std::num::NonZeroUsize::MIN),
196 );
197
198 Self {
199 config: config.clone(),
200 inner: Arc::new(RwLock::new(ContextManagerInner {
201 active_skills: HashMap::new(),
202 loaded_skills,
203 current_token_usage: 0,
204 stats: ContextStats::default(),
205 })),
206 }
207 }
208}
209
210impl Default for ContextManager {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216impl ContextManager {
217 pub fn register_skill_metadata(&self, manifest: SkillManifest) -> Result<()> {
219 let name = manifest.name.clone();
220
221 let mut inner = self.inner.write().unwrap_or_else(|poisoned| {
222 warn!(
223 "ContextManager write lock poisoned while registering skill metadata; recovering"
224 );
225 poisoned.into_inner()
226 });
227
228 let entry = SkillContextEntry {
229 name: name.clone(),
230 level: ContextLevel::Metadata,
231 manifest: manifest.clone(),
232 instructions: None,
233 skill: None,
234 usage: ContextUsage {
235 access_count: 0,
236 last_access: std::time::Instant::now(),
237 total_loaded_duration: std::time::Duration::ZERO,
238 token_cost: self.config.metadata_token_cost,
239 },
240 memory_size: size_of::<SkillContextEntry>() + name.len() + manifest.description.len(),
241 };
242
243 inner.current_token_usage += self.config.metadata_token_cost;
245 inner.stats.current_token_usage = inner.current_token_usage;
246 inner.stats.peak_token_usage = inner.stats.peak_token_usage.max(inner.current_token_usage);
247
248 inner.active_skills.insert(name, entry);
249 info!("Registered skill metadata: {}", manifest.name);
250
251 Ok(())
252 }
253
254 pub fn load_skill_instructions(&self, name: &str, instructions: String) -> Result<()> {
256 let mut inner = self.inner.write().unwrap_or_else(|poisoned| {
257 warn!(
258 "ContextManager write lock poisoned while loading skill instructions; recovering"
259 );
260 poisoned.into_inner()
261 });
262
263 let instruction_size = instructions.len();
265
266 if inner.current_token_usage + instruction_size > self.config.max_context_tokens {
268 self.evict_skills_to_make_room_internal(&mut inner, instruction_size)?;
270 }
271
272 let mut entry = match inner.loaded_skills.get_mut(name) {
274 Some(entry) => entry.clone(),
275 None => {
276 match inner.active_skills.get(name) {
278 Some(active_entry) => active_entry.clone(),
279 None => return Err(anyhow!("Skill '{}' not found in active skills", name)),
280 }
281 }
282 };
283
284 entry.level = ContextLevel::Instructions;
286 entry.instructions = Some(instructions.clone());
287 entry.usage.token_cost = instruction_size;
288 entry.memory_size += instructions.len();
289
290 inner.current_token_usage += instruction_size;
292 inner.stats.current_token_usage = inner.current_token_usage;
293 inner.stats.peak_token_usage = inner.stats.peak_token_usage.max(inner.current_token_usage);
294 inner.stats.total_tokens_loaded += instruction_size as u64;
295
296 inner.loaded_skills.put(name.to_string(), entry);
298
299 info!(
300 "Loaded instructions for skill: {} ({} chars)",
301 name, instruction_size
302 );
303
304 Ok(())
305 }
306
307 pub fn load_full_skill(&self, skill: Skill) -> Result<()> {
309 let name = skill.name().to_string();
310 let mut inner = self.inner.write().unwrap_or_else(|poisoned| {
311 warn!("ContextManager write lock poisoned while loading full skill; recovering");
312 poisoned.into_inner()
313 });
314
315 let instruction_size = skill.instructions.len();
317 let resource_size = skill.list_resources().len() * self.config.resource_token_cost * 4; let incremental_cost = instruction_size + resource_size;
319
320 if inner.current_token_usage + incremental_cost > self.config.max_context_tokens {
322 self.evict_skills_to_make_room_internal(&mut inner, incremental_cost)?;
323 }
324
325 let entry = SkillContextEntry {
327 name: name.clone(),
328 level: ContextLevel::Full,
329 manifest: skill.manifest.clone(),
330 instructions: Some(skill.instructions.clone()),
331 skill: Some(skill.into()),
332 usage: ContextUsage {
333 access_count: 0,
334 last_access: std::time::Instant::now(),
335 total_loaded_duration: std::time::Duration::ZERO,
336 token_cost: incremental_cost,
337 },
338 memory_size: size_of::<Skill>() + name.len() * 2,
339 };
340
341 inner.current_token_usage += incremental_cost;
343 inner.stats.current_token_usage = inner.current_token_usage;
344 inner.stats.peak_token_usage = inner.stats.peak_token_usage.max(inner.current_token_usage);
345 inner.stats.total_skills_loaded += 1;
346 inner.stats.total_tokens_loaded += incremental_cost as u64;
347
348 let entry_name = entry.name.clone();
350 inner.loaded_skills.put(name, entry);
351
352 info!(
353 "Loaded full skill: {} ({} tokens)",
354 entry_name,
355 incremental_cost + self.config.metadata_token_cost
356 );
357
358 Ok(())
359 }
360
361 pub fn get_skill_context(&self, name: &str) -> Option<SkillContextEntry> {
363 let mut inner = self.inner.write().unwrap_or_else(|poisoned| {
364 warn!("ContextManager write lock poisoned while fetching skill context; recovering");
365 poisoned.into_inner()
366 });
367
368 if let Some(mut entry) = inner.loaded_skills.get_mut(name).cloned() {
370 entry.usage.access_count += 1;
371 entry.usage.last_access = std::time::Instant::now();
372 inner.stats.cache_hits += 1;
373 return Some(entry);
374 }
375
376 if let Some(mut entry) = inner.active_skills.get(name).cloned() {
378 entry.usage.access_count += 1;
379 entry.usage.last_access = std::time::Instant::now();
380 inner.stats.cache_misses += 1;
381 return Some(entry);
382 }
383
384 None
385 }
386
387 fn evict_skills_to_make_room_internal(
389 &self,
390 inner: &mut ContextManagerInner,
391 required_tokens: usize,
392 ) -> Result<()> {
393 let mut freed_tokens = 0;
394 let mut evicted_skills = Vec::new();
395
396 while freed_tokens < required_tokens && !inner.loaded_skills.is_empty() {
398 if let Some((name, entry)) = inner.loaded_skills.pop_lru() {
399 freed_tokens += entry.usage.token_cost;
400 evicted_skills.push(name);
401
402 inner.stats.total_skills_evicted += 1;
403 inner.stats.total_tokens_evicted += entry.usage.token_cost as u64;
404 } else {
405 break;
406 }
407 }
408
409 inner.current_token_usage -= freed_tokens;
410 inner.stats.current_token_usage = inner.current_token_usage;
411
412 info!(
413 "Evicted {} skills to free {} tokens",
414 evicted_skills.len(),
415 freed_tokens
416 );
417 debug!("Evicted skills: {:?}", evicted_skills);
418
419 if freed_tokens < required_tokens {
420 return Err(anyhow!(
421 "Unable to free enough tokens. Required: {}, Freed: {}",
422 required_tokens,
423 freed_tokens
424 ));
425 }
426
427 Ok(())
428 }
429
430 pub fn get_stats(&self) -> ContextStats {
432 self.inner
433 .read()
434 .unwrap_or_else(|poisoned| {
435 warn!("ContextManager read lock poisoned while reading stats; recovering");
436 poisoned.into_inner()
437 })
438 .stats
439 .clone()
440 }
441
442 pub fn get_token_usage(&self) -> usize {
444 self.inner
445 .read()
446 .unwrap_or_else(|poisoned| {
447 warn!("ContextManager read lock poisoned while reading token usage; recovering");
448 poisoned.into_inner()
449 })
450 .current_token_usage
451 }
452
453 pub fn clear_loaded_skills(&self) {
455 let mut inner = self.inner.write().unwrap_or_else(|poisoned| {
456 warn!("ContextManager write lock poisoned while clearing loaded skills; recovering");
457 poisoned.into_inner()
458 });
459
460 let evicted_count = inner.loaded_skills.len();
461 let evicted_tokens = inner.stats.current_token_usage
462 - (inner.active_skills.len() * self.config.metadata_token_cost);
463
464 inner.loaded_skills.clear();
465 inner.current_token_usage = inner.active_skills.len() * self.config.metadata_token_cost;
466 inner.stats.current_token_usage = inner.current_token_usage;
467 inner.stats.total_skills_evicted += evicted_count as u64;
468 inner.stats.total_tokens_evicted += evicted_tokens as u64;
469
470 info!(
471 "Cleared {} loaded skills ({} tokens)",
472 evicted_count, evicted_tokens
473 );
474 }
475
476 pub fn get_active_skills(&self) -> Vec<String> {
478 self.inner
479 .read()
480 .unwrap_or_else(|poisoned| {
481 warn!("ContextManager read lock poisoned while reading active skills; recovering");
482 poisoned.into_inner()
483 })
484 .active_skills
485 .keys()
486 .cloned()
487 .collect()
488 }
489
490 pub fn get_memory_usage(&self) -> usize {
492 let inner = self.inner.read().unwrap_or_else(|poisoned| {
493 warn!("ContextManager read lock poisoned while calculating memory usage; recovering");
494 poisoned.into_inner()
495 });
496 let active_memory: usize = inner
497 .active_skills
498 .values()
499 .map(|entry| entry.memory_size)
500 .sum();
501
502 let loaded_memory: usize = inner
503 .loaded_skills
504 .iter()
505 .map(|(_, entry)| entry.memory_size)
506 .sum();
507
508 active_memory + loaded_memory
509 }
510}
511
512pub struct PersistentContextManager {
514 inner: ContextManager,
515 cache_path: PathBuf,
516}
517
518impl PersistentContextManager {
519 pub fn new(cache_path: PathBuf, config: ContextConfig) -> Result<Self> {
521 let mut manager = Self {
522 inner: ContextManager::with_config(config),
523 cache_path,
524 };
525
526 if let Err(e) = manager.load_cache() {
528 debug!("Failed to load context cache: {}", e);
529 }
530
531 Ok(manager)
532 }
533
534 fn load_cache(&mut self) -> Result<()> {
536 if !self.cache_path.exists() {
537 return Ok(());
538 }
539
540 let cache: ContextCache = read_json_file_sync(&self.cache_path)?;
541
542 let skill_count = cache.active_skills.len();
544 for manifest in cache.active_skills {
545 self.inner.register_skill_metadata(manifest)?;
546 }
547
548 info!("Loaded {} cached skills", skill_count);
549 Ok(())
550 }
551
552 pub fn save_cache(&self) -> Result<()> {
554 let inner = self
555 .inner
556 .inner
557 .read()
558 .map_err(|err| anyhow!("context manager lock poisoned while saving cache: {err}"))
559 .context("Failed to save context manager cache state")?;
560 let cache = ContextCache {
561 version: 1,
562 timestamp: std::time::SystemTime::now()
563 .duration_since(std::time::UNIX_EPOCH)?
564 .as_secs(),
565 active_skills: inner
566 .active_skills
567 .values()
568 .map(|entry| entry.manifest.clone())
569 .collect(),
570 };
571
572 write_json_file_sync(&self.cache_path, &cache)?;
573
574 info!("Saved {} skills to cache", cache.active_skills.len());
575 Ok(())
576 }
577
578 pub fn inner(&self) -> &ContextManager {
580 &self.inner
581 }
582
583 pub fn inner_mut(&mut self) -> &mut ContextManager {
585 &mut self.inner
586 }
587}
588
589#[derive(Debug, Serialize, Deserialize)]
591struct ContextCache {
592 version: u32,
593 timestamp: u64,
594 active_skills: Vec<SkillManifest>,
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[test]
602 fn test_context_config_default() {
603 let config = ContextConfig::default();
604 assert_eq!(config.max_context_tokens, 50_000);
605 assert_eq!(config.max_cached_skills, 100);
606 }
607
608 #[test]
609 fn test_context_manager_creation() {
610 let manager = ContextManager::new();
611 assert_eq!(manager.get_token_usage(), 0);
612 assert_eq!(manager.get_active_skills().len(), 0);
613 }
614
615 #[test]
616 fn boxed_skill_entry_is_smaller_than_inline_option() {
617 use std::mem::size_of;
618
619 assert!(size_of::<Option<Box<Skill>>>() < size_of::<Option<Skill>>());
620 assert!(size_of::<SkillContextEntry>() < size_of::<SkillContextEntryInlineSkill>());
621 }
622
623 #[expect(dead_code)]
624 struct SkillContextEntryInlineSkill {
625 name: String,
626 level: ContextLevel,
627 manifest: SkillManifest,
628 instructions: Option<String>,
629 skill: Option<Skill>,
630 usage: ContextUsage,
631 memory_size: usize,
632 }
633
634 #[test]
635 fn test_skill_metadata_registration() {
636 let manager = ContextManager::new();
637
638 let manifest = SkillManifest {
639 name: "test-skill".to_string(),
640 description: "Test skill".to_string(),
641 version: Some("1.0.0".to_string()),
642 author: Some("Test".to_string()),
643 vtcode_native: Some(true),
644 ..Default::default()
645 };
646
647 manager.register_skill_metadata(manifest).unwrap();
648 assert_eq!(manager.get_active_skills().len(), 1);
649 assert_eq!(manager.get_token_usage(), 50); }
651
652 #[test]
653 fn test_skill_context_retrieval() {
654 let manager = ContextManager::new();
655
656 let manifest = SkillManifest {
657 name: "test-skill".to_string(),
658 description: "Test skill".to_string(),
659 ..Default::default()
660 };
661
662 manager.register_skill_metadata(manifest.clone()).unwrap();
663
664 let context = manager.get_skill_context("test-skill");
665 assert!(context.is_some());
666 assert_eq!(context.unwrap().manifest.name, "test-skill");
667 }
668}