Skip to main content

ai_agent/tools/
skill.rs

1//! Skill tool - invoke external skills
2//!
3//! Provides a tool for the agent to invoke external skills.
4
5pub const SKILL_TOOL_NAME: &str = "Skill";
6
7use crate::skills::loader::{load_skills_from_dir, LoadedSkill};
8use crate::types::*;
9use std::collections::HashMap;
10use std::path::Path;
11use std::sync::{Mutex, OnceLock};
12
13/// Global skill registry with Mutex for interior mutability
14static LOADED_SKILLS: OnceLock<Mutex<HashMap<String, LoadedSkill>>> = OnceLock::new();
15
16/// Initialize the skills map (lazy initialization)
17fn init_skills_map() -> Mutex<HashMap<String, LoadedSkill>> {
18    let mut skills = HashMap::new();
19    if let Ok(loaded) = load_skills_from_dir(Path::new("examples/skills")) {
20        for skill in loaded {
21            skills.insert(skill.metadata.name.clone(), skill);
22        }
23    }
24    Mutex::new(skills)
25}
26
27/// Get skills map
28fn get_skills_map() -> &'static Mutex<HashMap<String, LoadedSkill>> {
29    LOADED_SKILLS.get_or_init(init_skills_map)
30}
31
32/// Get a skill by name
33pub fn get_skill(name: &str) -> Option<LoadedSkill> {
34    let guard = get_skills_map().lock().ok()?;
35    guard.get(name).cloned()
36}
37
38/// Get all skill names
39pub fn get_all_skill_names() -> Vec<String> {
40    let guard = get_skills_map().lock().ok();
41    guard
42        .map(|g| g.keys().cloned().collect())
43        .unwrap_or_default()
44}
45
46/// Register skills from a directory
47pub fn register_skills_from_dir(dir: &Path) {
48    // Skip empty path - used as signal for plugin skills registration
49    if dir.as_os_str().is_empty() {
50        return;
51    }
52    if let Ok(loaded) = load_skills_from_dir(dir) {
53        if let Ok(mut skills) = get_skills_map().lock() {
54            for skill in loaded {
55                skills.insert(skill.metadata.name.clone(), skill);
56            }
57        }
58    }
59}
60
61/// Register a single skill directly (used for plugin skills)
62pub fn register_skill(skill: LoadedSkill) {
63    if let Ok(mut skills) = get_skills_map().lock() {
64        skills.insert(skill.metadata.name.clone(), skill);
65    }
66}
67
68/// Register multiple skills directly (used for plugin skills)
69pub fn register_skills(skills: Vec<LoadedSkill>) {
70    if let Ok(mut map) = get_skills_map().lock() {
71        for skill in skills {
72            map.insert(skill.metadata.name.clone(), skill);
73        }
74    }
75}
76
77/// Skill tool - invoke a skill by name
78pub struct SkillTool;
79
80impl SkillTool {
81    pub fn new() -> Self {
82        Self
83    }
84
85    pub fn input_schema(&self) -> ToolInputSchema {
86        ToolInputSchema {
87            schema_type: "object".to_string(),
88            properties: serde_json::json!({
89                "skill": {
90                    "type": "string",
91                    "description": "The name of the skill to invoke"
92                }
93            }),
94            required: Some(vec!["skill".to_string()]),
95        }
96    }
97
98    pub async fn execute(
99        &self,
100        input: serde_json::Value,
101        _context: &ToolContext,
102    ) -> Result<ToolResult, crate::error::AgentError> {
103        let skill_name = input["skill"].as_str().unwrap_or("");
104
105        if let Some(skill) = get_skill(skill_name) {
106            // Return skill content as a user message so the model can read it and use tools
107            // This matches the TypeScript implementation behavior
108            let content = format!(
109                "Skill '{}' loaded. Here is the skill content:\n\n{}\n\nYou can now use tools to complete the task.",
110                skill_name, skill.content
111            );
112
113            Ok(ToolResult {
114                result_type: "text".to_string(),
115                tool_use_id: "skill".to_string(),
116                content,
117                is_error: Some(false),
118            })
119        } else {
120            // List available skills
121            let available = get_all_skill_names();
122            Ok(ToolResult {
123                result_type: "text".to_string(),
124                tool_use_id: "skill".to_string(),
125                content: format!(
126                    "Skill '{}' not found. Available skills: {:?}",
127                    skill_name, available
128                ),
129                is_error: Some(true),
130            })
131        }
132    }
133}
134
135impl Default for SkillTool {
136    fn default() -> Self {
137        Self::new()
138    }
139}