Skip to main content

agent_air_runtime/controller/tools/
list_skills.rs

1//! Tool for listing available skills.
2
3use crate::controller::tools::{
4    DisplayConfig, DisplayResult, Executable, ResultContentType, ToolContext, ToolType,
5};
6use crate::skills::SkillRegistry;
7use serde_json::{Value, json};
8use std::collections::HashMap;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13/// Tool name constant.
14pub const LIST_SKILLS_TOOL_NAME: &str = "list_skills";
15
16/// Tool description constant.
17pub const LIST_SKILLS_TOOL_DESCRIPTION: &str = "List all available skills. Returns name, description, and SKILL.md path for each skill. Use this to discover what capabilities are available.";
18
19/// Tool input schema (JSON Schema).
20pub const LIST_SKILLS_TOOL_SCHEMA: &str = r#"{
21    "type": "object",
22    "properties": {},
23    "additionalProperties": false
24}"#;
25
26/// Tool for listing available skills from the skill registry.
27pub struct ListSkillsTool {
28    skill_registry: Arc<SkillRegistry>,
29}
30
31impl ListSkillsTool {
32    /// Create a new ListSkillsTool with the given skill registry.
33    pub fn new(skill_registry: Arc<SkillRegistry>) -> Self {
34        Self { skill_registry }
35    }
36}
37
38impl Executable for ListSkillsTool {
39    fn name(&self) -> &str {
40        LIST_SKILLS_TOOL_NAME
41    }
42
43    fn description(&self) -> &str {
44        LIST_SKILLS_TOOL_DESCRIPTION
45    }
46
47    fn input_schema(&self) -> &str {
48        LIST_SKILLS_TOOL_SCHEMA
49    }
50
51    fn tool_type(&self) -> ToolType {
52        ToolType::Custom
53    }
54
55    fn execute(
56        &self,
57        _context: ToolContext,
58        _input: HashMap<String, Value>,
59    ) -> Pin<Box<dyn Future<Output = Result<String, String>> + Send>> {
60        let skill_registry = self.skill_registry.clone();
61
62        Box::pin(async move {
63            let skills = skill_registry.list();
64
65            if skills.is_empty() {
66                return Ok("No skills available.".to_string());
67            }
68
69            let output: Vec<Value> = skills
70                .iter()
71                .map(|s| {
72                    json!({
73                        "name": s.metadata.name,
74                        "description": s.metadata.description,
75                        "skill_md_path": s.skill_md_path.display().to_string(),
76                    })
77                })
78                .collect();
79
80            serde_json::to_string_pretty(&output)
81                .map_err(|e| format!("Failed to serialize skills: {}", e))
82        })
83    }
84
85    fn display_config(&self) -> DisplayConfig {
86        DisplayConfig {
87            display_name: "List Skills".to_string(),
88            display_title: Box::new(|_input| "Available Skills".to_string()),
89            display_content: Box::new(|_input, result| {
90                let line_count = result.lines().count();
91                DisplayResult {
92                    content: result.to_string(),
93                    content_type: ResultContentType::Json,
94                    is_truncated: false,
95                    full_length: line_count,
96                }
97            }),
98        }
99    }
100
101    fn compact_summary(&self, _input: &HashMap<String, Value>, result: &str) -> String {
102        // Count skills from JSON array
103        let count = serde_json::from_str::<Vec<Value>>(result)
104            .map(|v| v.len())
105            .unwrap_or(0);
106
107        format!("[List Skills: {} available]", count)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::skills::{Skill, SkillMetadata};
115    use std::path::PathBuf;
116
117    fn create_test_skill(name: &str, description: &str) -> Skill {
118        Skill {
119            metadata: SkillMetadata {
120                name: name.to_string(),
121                description: description.to_string(),
122                license: None,
123                compatibility: None,
124                metadata: None,
125                allowed_tools: None,
126            },
127            path: PathBuf::from(format!("/skills/{}", name)),
128            skill_md_path: PathBuf::from(format!("/skills/{}/SKILL.md", name)),
129        }
130    }
131
132    #[tokio::test]
133    async fn test_list_skills_empty() {
134        let registry = Arc::new(SkillRegistry::new());
135        let tool = ListSkillsTool::new(registry);
136
137        let context = ToolContext {
138            session_id: 1,
139            tool_use_id: "test".to_string(),
140            turn_id: None,
141            permissions_pre_approved: false,
142        };
143
144        let result = tool.execute(context, HashMap::new()).await;
145        assert!(result.is_ok());
146        assert_eq!(result.unwrap(), "No skills available.");
147    }
148
149    #[tokio::test]
150    async fn test_list_skills_with_skills() {
151        let registry = Arc::new(SkillRegistry::new());
152        registry.register(create_test_skill("pdf-tools", "Extract text from PDFs"));
153        registry.register(create_test_skill("git-helper", "Git operations helper"));
154
155        let tool = ListSkillsTool::new(registry);
156
157        let context = ToolContext {
158            session_id: 1,
159            tool_use_id: "test".to_string(),
160            turn_id: None,
161            permissions_pre_approved: false,
162        };
163
164        let result = tool.execute(context, HashMap::new()).await.unwrap();
165
166        // Parse the JSON output
167        let skills: Vec<Value> = serde_json::from_str(&result).unwrap();
168        assert_eq!(skills.len(), 2);
169
170        let names: Vec<&str> = skills
171            .iter()
172            .filter_map(|s| s.get("name").and_then(|n| n.as_str()))
173            .collect();
174
175        assert!(names.contains(&"pdf-tools"));
176        assert!(names.contains(&"git-helper"));
177    }
178
179    #[test]
180    fn test_tool_metadata() {
181        let registry = Arc::new(SkillRegistry::new());
182        let tool = ListSkillsTool::new(registry);
183
184        assert_eq!(tool.name(), LIST_SKILLS_TOOL_NAME);
185        assert_eq!(tool.description(), LIST_SKILLS_TOOL_DESCRIPTION);
186        assert!(!tool.input_schema().is_empty());
187    }
188
189    #[test]
190    fn test_compact_summary() {
191        let registry = Arc::new(SkillRegistry::new());
192        let tool = ListSkillsTool::new(registry);
193
194        let result = r#"[{"name":"a"},{"name":"b"},{"name":"c"}]"#;
195        let summary = tool.compact_summary(&HashMap::new(), result);
196
197        assert_eq!(summary, "[List Skills: 3 available]");
198    }
199
200    #[test]
201    fn test_compact_summary_empty() {
202        let registry = Arc::new(SkillRegistry::new());
203        let tool = ListSkillsTool::new(registry);
204
205        let summary = tool.compact_summary(&HashMap::new(), "No skills available.");
206        assert_eq!(summary, "[List Skills: 0 available]");
207    }
208}