use crate::error::{AgentError, Result as AgentResult};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;
use turboclaude_skills::{Skill, SkillRegistry};
#[derive(Debug, Clone)]
pub struct ActiveSkill {
pub skill: Skill,
pub activated_at: Instant,
pub usage_count: u32,
}
#[derive(Debug, Clone)]
pub struct SkillDiscoveryResult {
pub loaded: usize,
pub failed: usize,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolValidationResult {
pub allowed: bool,
pub tool_name: String,
pub blocked_by: Option<String>,
}
pub struct SkillManager {
registry: Arc<RwLock<SkillRegistry>>,
active_skills: Arc<RwLock<HashMap<String, ActiveSkill>>>,
}
impl SkillManager {
pub async fn new(registry: SkillRegistry) -> AgentResult<Self> {
Ok(Self {
registry: Arc::new(RwLock::new(registry)),
active_skills: Arc::new(RwLock::new(HashMap::new())),
})
}
pub async fn discover(&self) -> AgentResult<SkillDiscoveryResult> {
let mut registry = self.registry.write().await;
let report = registry
.discover()
.await
.map_err(|e| AgentError::Config(format!("Skill discovery failed: {}", e)))?;
let errors = report
.errors
.into_iter()
.map(|(path, err)| format!("{}: {}", path.display(), err))
.collect();
Ok(SkillDiscoveryResult {
loaded: report.loaded,
failed: report.failed,
errors,
})
}
pub async fn load(&self, name: &str) -> AgentResult<()> {
let registry = self.registry.read().await;
let skill = registry
.get(name)
.await
.map_err(|e| AgentError::Config(format!("Skill '{}' not found: {}", name, e)))?;
let mut active = self.active_skills.write().await;
active.insert(
name.to_string(),
ActiveSkill {
skill,
activated_at: Instant::now(),
usage_count: 0,
},
);
Ok(())
}
pub async fn unload(&self, name: &str) -> AgentResult<()> {
let mut active = self.active_skills.write().await;
if active.remove(name).is_none() {
return Err(AgentError::Config(format!(
"Skill '{}' is not active",
name
)));
}
Ok(())
}
pub async fn list_active(&self) -> Vec<String> {
let active = self.active_skills.read().await;
active.keys().cloned().collect()
}
pub async fn list_available(&self) -> Vec<String> {
let registry = self.registry.read().await;
registry.list().await.into_iter().map(|m| m.name).collect()
}
pub async fn find(&self, query: &str) -> AgentResult<Vec<Skill>> {
let registry = self.registry.read().await;
registry
.find(query)
.await
.map_err(|e| AgentError::Config(format!("Skill search failed: {}", e)))
}
pub async fn get(&self, name: &str) -> AgentResult<Skill> {
let registry = self.registry.read().await;
registry
.get(name)
.await
.map_err(|e| AgentError::Config(format!("Skill '{}' not found: {}", name, e)))
}
pub async fn build_context(&self) -> String {
let active = self.active_skills.read().await;
if active.is_empty() {
return String::new();
}
let mut context = String::from("\n\n# Available Skills\n\n");
context.push_str("You have access to the following skills:\n\n");
for (name, active_skill) in active.iter() {
context.push_str(&format!("## Skill: {}\n\n", name));
context.push_str(&active_skill.skill.content);
context.push_str("\n\n---\n\n");
}
context
}
pub async fn validate_tool(&self, tool_name: &str) -> ToolValidationResult {
let active = self.active_skills.read().await;
if active.is_empty() {
return ToolValidationResult {
allowed: true,
tool_name: tool_name.to_string(),
blocked_by: None,
};
}
for (name, active_skill) in active.iter() {
if !active_skill.skill.metadata.allows_tool(tool_name) {
return ToolValidationResult {
allowed: false,
tool_name: tool_name.to_string(),
blocked_by: Some(name.clone()),
};
}
}
ToolValidationResult {
allowed: true,
tool_name: tool_name.to_string(),
blocked_by: None,
}
}
pub async fn increment_usage(&self) {
let mut active = self.active_skills.write().await;
for active_skill in active.values_mut() {
active_skill.usage_count += 1;
}
}
pub async fn get_usage(&self, name: &str) -> Option<(Instant, u32)> {
let active = self.active_skills.read().await;
active.get(name).map(|s| (s.activated_at, s.usage_count))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_skill_discovery_result() {
let result = SkillDiscoveryResult {
loaded: 4,
failed: 1,
errors: vec!["Error 1".to_string()],
};
assert_eq!(result.loaded, 4);
assert_eq!(result.failed, 1);
assert_eq!(result.errors.len(), 1);
}
#[test]
fn test_tool_validation_result() {
let allowed = ToolValidationResult {
allowed: true,
tool_name: "bash".to_string(),
blocked_by: None,
};
assert!(allowed.allowed);
assert_eq!(allowed.tool_name, "bash");
assert!(allowed.blocked_by.is_none());
let blocked = ToolValidationResult {
allowed: false,
tool_name: "dangerous".to_string(),
blocked_by: Some("pdf-skill".to_string()),
};
assert!(!blocked.allowed);
assert_eq!(blocked.blocked_by, Some("pdf-skill".to_string()));
}
#[tokio::test]
async fn test_active_skill_creation() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let skill_path = temp_dir.path().join("test");
fs::create_dir(&skill_path).unwrap();
let skill_file = skill_path.join("SKILL.md");
let skill_content = r#"---
name: test
description: Test skill
---
Test content"#;
fs::write(&skill_file, skill_content).unwrap();
let skill = turboclaude_skills::Skill::from_file(&skill_file)
.await
.unwrap();
let active = ActiveSkill {
skill: skill.clone(),
activated_at: Instant::now(),
usage_count: 0,
};
assert_eq!(active.skill.metadata.name, "test");
assert_eq!(active.usage_count, 0);
}
}