1use crate::core::metadata::SkillMetadata;
4use crate::core::service::ServiceError;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11use tokio::sync::RwLock;
12use tracing::warn;
13
14#[async_trait]
16pub trait ProgressiveLoadingService: Send + Sync {
17 async fn load_metadata(&self, skill_ids: &[String])
19 -> Result<Vec<SkillMetadata>, ServiceError>;
20
21 async fn load_skill_content(
23 &self,
24 skill_ids: &[String],
25 context: Option<LoadingContext>,
26 ) -> Result<Vec<LoadedSkill>, ServiceError>;
27
28 async fn load_reference_content(
30 &self,
31 skill_id: &str,
32 reference_path: &str,
33 ) -> Result<String, ServiceError>;
34
35 async fn preload_relevant_skills(
37 &self,
38 context: LoadingContext,
39 ) -> Result<Vec<PreloadedSkill>, ServiceError>;
40
41 async fn optimize_loading_strategy(
43 &self,
44 context: LoadingContext,
45 ) -> Result<LoadingStrategy, ServiceError>;
46
47 async fn clear_cache(&self) -> Result<(), ServiceError>;
49
50 async fn get_cache_stats(&self) -> Result<CacheStats, ServiceError>;
52}
53
54#[derive(Debug, Clone)]
56pub struct LoadingContext {
57 pub query: String,
59
60 pub available_tokens: usize,
62
63 pub urgency: LoadingUrgency,
65
66 pub conversation_history: Vec<String>,
68
69 pub user_preferences: Option<HashMap<String, String>>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum LoadingUrgency {
76 Low,
78 Medium,
80 High,
82 Critical,
84}
85
86#[derive(Debug, Clone)]
88pub struct LoadingStrategy {
89 pub priority: Vec<String>,
91
92 pub load_levels: HashMap<String, LoadLevel>,
94
95 pub estimated_tokens: usize,
97
98 pub reasoning: Vec<String>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub enum LoadLevel {
105 Metadata,
107 Content,
109 References,
111}
112
113#[derive(Debug, Clone)]
115pub struct LoadedSkill {
116 pub metadata: SkillMetadata,
118
119 pub content: Option<String>,
121
122 pub references: Option<HashMap<String, String>>,
124
125 pub script_contents: Option<HashMap<String, String>>,
127
128 pub asset_paths: Vec<PathBuf>,
130
131 pub execution_ready: bool,
133
134 pub loaded_at: Instant,
136
137 pub load_level: LoadLevel,
139}
140
141#[derive(Debug, Clone)]
143pub struct PreloadedSkill {
144 pub metadata: SkillMetadata,
145 pub relevance_score: f64,
146 pub recommended_load_level: LoadLevel,
147}
148
149#[derive(Debug, Clone, Default)]
151pub struct CacheStats {
152 pub metadata_cache_size: usize,
154
155 pub content_cache_size: usize,
157
158 pub metadata_hit_rate: f64,
160
161 pub content_hit_rate: f64,
163
164 pub memory_usage_mb: f64,
166
167 pub avg_metadata_load_time_ms: f64,
169
170 pub avg_content_load_time_ms: f64,
172}
173
174pub struct ProgressiveLoadingServiceImpl {
176 metadata_cache: Arc<RwLock<HashMap<String, (SkillMetadata, Instant)>>>,
178
179 content_cache: Arc<RwLock<HashMap<String, (String, Instant)>>>,
181
182 skills_base_path: PathBuf,
184
185 metadata_cache_ttl: Duration,
187
188 content_cache_ttl: Duration,
190
191 max_metadata_cache_size: usize,
193 max_content_cache_size: usize,
194
195 metadata_cache_hits: Arc<RwLock<usize>>,
197 metadata_cache_misses: Arc<RwLock<usize>>,
198 content_cache_hits: Arc<RwLock<usize>>,
199 content_cache_misses: Arc<RwLock<usize>>,
200}
201
202impl ProgressiveLoadingServiceImpl {
203 pub fn new(skills_base_path: PathBuf) -> Self {
205 Self {
206 metadata_cache: Arc::new(RwLock::new(HashMap::new())),
207 content_cache: Arc::new(RwLock::new(HashMap::new())),
208 skills_base_path,
209 metadata_cache_ttl: Duration::from_secs(3600), content_cache_ttl: Duration::from_secs(1800), max_metadata_cache_size: 1000,
212 max_content_cache_size: 100,
213 metadata_cache_hits: Arc::new(RwLock::new(0)),
214 metadata_cache_misses: Arc::new(RwLock::new(0)),
215 content_cache_hits: Arc::new(RwLock::new(0)),
216 content_cache_misses: Arc::new(RwLock::new(0)),
217 }
218 }
219
220 async fn load_skill_metadata(&self, skill_id: &str) -> Result<SkillMetadata, ServiceError> {
222 {
224 let cache = self.metadata_cache.read().await;
225 if let Some((metadata, loaded_at)) = cache.get(skill_id) {
226 if loaded_at.elapsed() < self.metadata_cache_ttl {
227 let mut hits = self.metadata_cache_hits.write().await;
228 *hits += 1;
229 return Ok(metadata.clone());
230 }
231 }
232 }
233
234 let mut misses = self.metadata_cache_misses.write().await;
236 *misses += 1;
237
238 let skill_path = self.skills_base_path.join(skill_id).join("SKILL.md");
240
241 if !skill_path.exists() {
242 return Err(ServiceError::Custom(format!(
243 "Skill file not found: {}",
244 skill_path.display()
245 )));
246 }
247
248 let content = tokio::fs::read_to_string(&skill_path).await?;
249
250 let metadata = self.parse_skill_metadata(skill_id, &content)?;
252
253 {
255 let mut cache = self.metadata_cache.write().await;
256
257 if cache.len() >= self.max_metadata_cache_size {
259 self.cleanup_expired_metadata(&mut cache).await;
260 }
261
262 cache.insert(skill_id.to_string(), (metadata.clone(), Instant::now()));
263 }
264
265 Ok(metadata)
266 }
267
268 fn parse_skill_metadata(
270 &self,
271 skill_id: &str,
272 content: &str,
273 ) -> Result<SkillMetadata, ServiceError> {
274 let lines = content.lines();
276
277 let mut in_frontmatter = false;
279 let mut frontmatter_lines = Vec::new();
280
281 for line in lines {
282 if line.trim() == "---" {
283 if in_frontmatter {
284 break; } else {
286 in_frontmatter = true;
287 continue;
288 }
289 }
290
291 if in_frontmatter {
292 frontmatter_lines.push(line);
293 }
294 }
295
296 let name = skill_id.replace("-", " ").to_string();
299 let description = content
300 .lines()
301 .next()
302 .unwrap_or("No description")
303 .to_string();
304
305 Ok(SkillMetadata {
306 id: crate::core::service::SkillId::new(skill_id.to_string())?,
307 name,
308 description,
309 version: "1.0.0".to_string(),
310 author: None,
311 enabled: true,
312 token_estimate: content.len() / 4, last_updated: std::time::SystemTime::now().into(),
314 })
315 }
316
317 async fn load_skill_content_internal(&self, skill_id: &str) -> Result<String, ServiceError> {
319 {
321 let cache = self.content_cache.read().await;
322 if let Some((content, loaded_at)) = cache.get(skill_id) {
323 if loaded_at.elapsed() < self.content_cache_ttl {
324 let mut hits = self.content_cache_hits.write().await;
325 *hits += 1;
326 return Ok(content.clone());
327 }
328 }
329 }
330
331 let mut misses = self.content_cache_misses.write().await;
333 *misses += 1;
334
335 let skill_path = self.skills_base_path.join(skill_id).join("SKILL.md");
336
337 if !skill_path.exists() {
338 return Err(ServiceError::Custom(format!(
339 "Skill file not found: {}",
340 skill_path.display()
341 )));
342 }
343
344 let content = tokio::fs::read_to_string(&skill_path).await?;
345
346 {
348 let mut cache = self.content_cache.write().await;
349
350 if cache.len() >= self.max_content_cache_size {
352 self.cleanup_expired_content(&mut cache).await;
353 }
354
355 cache.insert(skill_id.to_string(), (content.clone(), Instant::now()));
356 }
357
358 Ok(content)
359 }
360
361 async fn cleanup_expired_metadata(
363 &self,
364 cache: &mut HashMap<String, (SkillMetadata, Instant)>,
365 ) {
366 let now = Instant::now();
367 cache.retain(|_, (_, loaded_at)| now.duration_since(*loaded_at) < self.metadata_cache_ttl);
368
369 if cache.len() >= self.max_metadata_cache_size {
371 let entries: Vec<_> = cache.iter().map(|(k, (_, _))| k.clone()).collect();
372 let to_remove = cache.len() - self.max_metadata_cache_size + 10; for key in entries.iter().take(to_remove) {
375 cache.remove(key);
376 }
377 }
378 }
379
380 async fn cleanup_expired_content(&self, cache: &mut HashMap<String, (String, Instant)>) {
382 let now = Instant::now();
383 cache.retain(|_, (_, loaded_at)| now.duration_since(*loaded_at) < self.content_cache_ttl);
384
385 if cache.len() >= self.max_content_cache_size {
387 let entries: Vec<_> = cache.iter().map(|(k, (_, _))| k.clone()).collect();
388 let to_remove = cache.len() - self.max_content_cache_size + 5; for key in entries.iter().take(to_remove) {
391 cache.remove(key);
392 }
393 }
394 }
395
396 pub async fn get_cache_stats(&self) -> CacheStats {
398 let metadata_cache_size = self.metadata_cache.read().await.len();
399 let content_cache_size = self.content_cache.read().await.len();
400
401 let metadata_hits = *self.metadata_cache_hits.read().await;
402 let metadata_misses = *self.metadata_cache_misses.read().await;
403 let content_hits = *self.content_cache_hits.read().await;
404 let content_misses = *self.content_cache_misses.read().await;
405
406 let metadata_total = metadata_hits + metadata_misses;
407 let content_total = content_hits + content_misses;
408
409 let metadata_hit_rate = if metadata_total > 0 {
410 metadata_hits as f64 / metadata_total as f64
411 } else {
412 0.0
413 };
414
415 let content_hit_rate = if content_total > 0 {
416 content_hits as f64 / content_total as f64
417 } else {
418 0.0
419 };
420
421 let memory_usage_mb =
423 (metadata_cache_size * 1024 + content_cache_size * 2048) as f64 / (1024.0 * 1024.0);
424
425 CacheStats {
426 metadata_cache_size,
427 content_cache_size,
428 metadata_hit_rate,
429 content_hit_rate,
430 memory_usage_mb,
431 avg_metadata_load_time_ms: 50.0, avg_content_load_time_ms: 100.0, }
434 }
435
436 pub async fn clear_cache(&self) {
438 self.metadata_cache.write().await.clear();
439 self.content_cache.write().await.clear();
440
441 *self.metadata_cache_hits.write().await = 0;
443 *self.metadata_cache_misses.write().await = 0;
444 *self.content_cache_hits.write().await = 0;
445 *self.content_cache_misses.write().await = 0;
446 }
447}
448
449#[async_trait]
450impl ProgressiveLoadingService for ProgressiveLoadingServiceImpl {
451 async fn load_metadata(
453 &self,
454 skill_ids: &[String],
455 ) -> Result<Vec<SkillMetadata>, ServiceError> {
456 let mut results = Vec::new();
457
458 for skill_id in skill_ids {
459 match self.load_skill_metadata(skill_id).await {
460 Ok(metadata) => results.push(metadata),
461 Err(e) => {
462 warn!("Failed to load metadata for skill {}: {}", skill_id, e);
463 }
465 }
466 }
467
468 Ok(results)
469 }
470
471 async fn load_skill_content(
473 &self,
474 skill_ids: &[String],
475 context: Option<LoadingContext>,
476 ) -> Result<Vec<LoadedSkill>, ServiceError> {
477 let mut results = Vec::new();
478
479 for skill_id in skill_ids {
480 let metadata = self.load_skill_metadata(skill_id).await?;
482
483 let load_level = self.determine_load_level(&metadata, context.as_ref());
485
486 let (content, references, execution_ready) = match load_level {
488 LoadLevel::Metadata => (None, None, false),
489 LoadLevel::Content => {
490 let skill_content = self.load_skill_content_internal(skill_id).await?;
492 (Some(skill_content), None, true)
493 }
494 LoadLevel::References => {
495 let skill_content = self.load_skill_content_internal(skill_id).await?;
497 let skill_references = self.load_reference_files(skill_id).await?;
498
499 (Some(skill_content), Some(skill_references), true)
500 }
501 };
502
503 results.push(LoadedSkill {
504 metadata,
505 content,
506 references,
507 script_contents: None, asset_paths: self.get_asset_paths(skill_id),
509 execution_ready,
510 loaded_at: Instant::now(),
511 load_level,
512 });
513 }
514
515 Ok(results)
516 }
517
518 async fn load_reference_content(
520 &self,
521 skill_id: &str,
522 reference_path: &str,
523 ) -> Result<String, ServiceError> {
524 let ref_path = self
525 .skills_base_path
526 .join(skill_id)
527 .join("references")
528 .join(reference_path);
529
530 if !ref_path.exists() {
531 return Err(ServiceError::Custom(format!(
532 "Reference file not found: {}",
533 ref_path.display()
534 )));
535 }
536
537 tokio::fs::read_to_string(&ref_path)
538 .await
539 .map_err(|e| ServiceError::Custom(format!("Failed to read reference file: {}", e)))
540 }
541
542 async fn preload_relevant_skills(
544 &self,
545 _context: LoadingContext,
546 ) -> Result<Vec<PreloadedSkill>, ServiceError> {
547 Ok(Vec::new())
550 }
551
552 async fn optimize_loading_strategy(
554 &self,
555 context: LoadingContext,
556 ) -> Result<LoadingStrategy, ServiceError> {
557 let mut strategy = LoadingStrategy {
561 priority: Vec::new(),
562 load_levels: HashMap::new(),
563 estimated_tokens: 0,
564 reasoning: vec!["Basic strategy based on query analysis".to_string()],
565 };
566
567 strategy
569 .load_levels
570 .insert("default".to_string(), LoadLevel::Metadata);
571 strategy.estimated_tokens = context.available_tokens / 2; Ok(strategy)
574 }
575
576 async fn clear_cache(&self) -> Result<(), ServiceError> {
578 self.clear_cache().await;
579 Ok(())
580 }
581
582 async fn get_cache_stats(&self) -> Result<CacheStats, ServiceError> {
584 Ok(self.get_cache_stats().await)
585 }
586}
587
588impl ProgressiveLoadingServiceImpl {
590 fn determine_load_level(
592 &self,
593 _metadata: &SkillMetadata,
594 context: Option<&LoadingContext>,
595 ) -> LoadLevel {
596 match context {
597 Some(ctx) => {
598 match ctx.urgency {
599 LoadingUrgency::Critical => LoadLevel::References,
600 LoadingUrgency::High => LoadLevel::Content,
601 LoadingUrgency::Medium => {
602 if ctx.query.len() > 100 || ctx.available_tokens > 2000 {
604 LoadLevel::Content
605 } else {
606 LoadLevel::Metadata
607 }
608 }
609 LoadingUrgency::Low => LoadLevel::Metadata,
610 }
611 }
612 None => LoadLevel::Metadata, }
614 }
615
616 async fn load_reference_files(
618 &self,
619 skill_id: &str,
620 ) -> Result<HashMap<String, String>, ServiceError> {
621 let references_dir = self.skills_base_path.join(skill_id).join("references");
622
623 if !references_dir.exists() {
624 return Ok(HashMap::new());
625 }
626
627 let mut references = HashMap::new();
628 let mut read_dir = tokio::fs::read_dir(&references_dir).await?;
629
630 while let Some(entry) = read_dir.next_entry().await? {
631 if entry.path().is_file() {
632 let file_name = entry.file_name().to_string_lossy().to_string();
633 let content = tokio::fs::read_to_string(entry.path()).await?;
634
635 references.insert(file_name, content);
636 }
637 }
638
639 Ok(references)
640 }
641
642 fn get_asset_paths(&self, skill_id: &str) -> Vec<PathBuf> {
644 let assets_dir = self.skills_base_path.join(skill_id).join("assets");
645
646 if !assets_dir.exists() {
647 return Vec::new();
648 }
649
650 Vec::new() }
654}
655
656pub struct LoadingService(ProgressiveLoadingServiceImpl);
658
659impl Default for LoadingService {
660 fn default() -> Self {
661 Self::new()
662 }
663}
664
665impl LoadingService {
666 pub fn new() -> Self {
667 Self(ProgressiveLoadingServiceImpl::new(PathBuf::from(
668 "./skills",
669 )))
670 }
671}
672
673#[async_trait]
674impl ProgressiveLoadingService for LoadingService {
675 async fn load_metadata(
676 &self,
677 skill_ids: &[String],
678 ) -> Result<Vec<SkillMetadata>, ServiceError> {
679 self.0.load_metadata(skill_ids).await
680 }
681
682 async fn load_skill_content(
683 &self,
684 skill_ids: &[String],
685 context: Option<LoadingContext>,
686 ) -> Result<Vec<LoadedSkill>, ServiceError> {
687 self.0.load_skill_content(skill_ids, context).await
688 }
689
690 async fn load_reference_content(
691 &self,
692 skill_id: &str,
693 reference_path: &str,
694 ) -> Result<String, ServiceError> {
695 self.0
696 .load_reference_content(skill_id, reference_path)
697 .await
698 }
699
700 async fn preload_relevant_skills(
701 &self,
702 context: LoadingContext,
703 ) -> Result<Vec<PreloadedSkill>, ServiceError> {
704 self.0.preload_relevant_skills(context).await
705 }
706
707 async fn optimize_loading_strategy(
708 &self,
709 context: LoadingContext,
710 ) -> Result<LoadingStrategy, ServiceError> {
711 self.0.optimize_loading_strategy(context).await
712 }
713
714 async fn clear_cache(&self) -> Result<(), ServiceError> {
715 self.0.clear_cache().await;
716 Ok(())
717 }
718
719 async fn get_cache_stats(&self) -> Result<CacheStats, ServiceError> {
720 Ok(self.0.get_cache_stats().await)
721 }
722}