agent_air_runtime/controller/tools/
list_skills.rs1use 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
13pub const LIST_SKILLS_TOOL_NAME: &str = "list_skills";
15
16pub 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
19pub const LIST_SKILLS_TOOL_SCHEMA: &str = r#"{
21 "type": "object",
22 "properties": {},
23 "additionalProperties": false
24}"#;
25
26pub struct ListSkillsTool {
28 skill_registry: Arc<SkillRegistry>,
29}
30
31impl ListSkillsTool {
32 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 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 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}