agent_core_runtime/skills/
registry.rs1use crate::skills::types::Skill;
4use std::collections::HashMap;
5use std::sync::RwLock;
6
7#[derive(Debug, Default)]
9pub struct SkillRegistry {
10 skills: RwLock<HashMap<String, Skill>>,
11}
12
13impl SkillRegistry {
14 pub fn new() -> Self {
16 Self::default()
17 }
18
19 pub fn register(&self, skill: Skill) -> Option<Skill> {
23 let mut skills = self.skills.write().unwrap();
24 skills.insert(skill.metadata.name.clone(), skill)
25 }
26
27 pub fn unregister(&self, name: &str) -> Option<Skill> {
31 let mut skills = self.skills.write().unwrap();
32 skills.remove(name)
33 }
34
35 pub fn get(&self, name: &str) -> Option<Skill> {
37 let skills = self.skills.read().unwrap();
38 skills.get(name).cloned()
39 }
40
41 pub fn list(&self) -> Vec<Skill> {
43 let skills = self.skills.read().unwrap();
44 skills.values().cloned().collect()
45 }
46
47 pub fn names(&self) -> Vec<String> {
49 let skills = self.skills.read().unwrap();
50 skills.keys().cloned().collect()
51 }
52
53 pub fn contains(&self, name: &str) -> bool {
55 let skills = self.skills.read().unwrap();
56 skills.contains_key(name)
57 }
58
59 pub fn len(&self) -> usize {
61 let skills = self.skills.read().unwrap();
62 skills.len()
63 }
64
65 pub fn is_empty(&self) -> bool {
67 self.len() == 0
68 }
69
70 pub fn clear(&self) {
72 let mut skills = self.skills.write().unwrap();
73 skills.clear();
74 }
75
76 pub fn to_prompt_xml(&self) -> String {
89 let skills = self.skills.read().unwrap();
90
91 if skills.is_empty() {
92 return String::new();
93 }
94
95 let mut xml = String::from("<available_skills>\n");
96
97 let mut sorted_skills: Vec<_> = skills.values().collect();
99 sorted_skills.sort_by(|a, b| a.metadata.name.cmp(&b.metadata.name));
100
101 for skill in sorted_skills {
102 xml.push_str(" <skill>\n");
103 xml.push_str(&format!(" <name>{}</name>\n", escape_xml(&skill.metadata.name)));
104 xml.push_str(&format!(
105 " <description>{}</description>\n",
106 escape_xml(&skill.metadata.description)
107 ));
108 xml.push_str(&format!(
109 " <location>{}</location>\n",
110 escape_xml(&skill.skill_md_path.display().to_string())
111 ));
112 xml.push_str(" </skill>\n");
113 }
114
115 xml.push_str("</available_skills>");
116 xml
117 }
118}
119
120fn escape_xml(s: &str) -> String {
122 s.replace('&', "&")
123 .replace('<', "<")
124 .replace('>', ">")
125 .replace('"', """)
126 .replace('\'', "'")
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::skills::types::SkillMetadata;
133 use std::path::PathBuf;
134
135 fn create_test_skill(name: &str, description: &str) -> Skill {
136 Skill {
137 metadata: SkillMetadata {
138 name: name.to_string(),
139 description: description.to_string(),
140 license: None,
141 compatibility: None,
142 metadata: None,
143 allowed_tools: None,
144 },
145 path: PathBuf::from(format!("/skills/{}", name)),
146 skill_md_path: PathBuf::from(format!("/skills/{}/SKILL.md", name)),
147 }
148 }
149
150 #[test]
151 fn test_register_and_get() {
152 let registry = SkillRegistry::new();
153 let skill = create_test_skill("test-skill", "A test skill");
154
155 assert!(registry.register(skill).is_none());
156
157 let retrieved = registry.get("test-skill").unwrap();
158 assert_eq!(retrieved.metadata.name, "test-skill");
159 assert_eq!(retrieved.metadata.description, "A test skill");
160 }
161
162 #[test]
163 fn test_register_replaces_existing() {
164 let registry = SkillRegistry::new();
165
166 let skill1 = create_test_skill("my-skill", "First version");
167 let skill2 = create_test_skill("my-skill", "Second version");
168
169 registry.register(skill1);
170 let replaced = registry.register(skill2);
171
172 assert!(replaced.is_some());
173 assert_eq!(replaced.unwrap().metadata.description, "First version");
174
175 let current = registry.get("my-skill").unwrap();
176 assert_eq!(current.metadata.description, "Second version");
177 }
178
179 #[test]
180 fn test_unregister() {
181 let registry = SkillRegistry::new();
182 let skill = create_test_skill("to-remove", "Will be removed");
183
184 registry.register(skill);
185 assert!(registry.contains("to-remove"));
186
187 let removed = registry.unregister("to-remove");
188 assert!(removed.is_some());
189 assert!(!registry.contains("to-remove"));
190
191 assert!(registry.unregister("nonexistent").is_none());
193 }
194
195 #[test]
196 fn test_list() {
197 let registry = SkillRegistry::new();
198
199 registry.register(create_test_skill("skill-a", "A"));
200 registry.register(create_test_skill("skill-b", "B"));
201 registry.register(create_test_skill("skill-c", "C"));
202
203 let skills = registry.list();
204 assert_eq!(skills.len(), 3);
205
206 let names: Vec<_> = skills.iter().map(|s| s.metadata.name.as_str()).collect();
207 assert!(names.contains(&"skill-a"));
208 assert!(names.contains(&"skill-b"));
209 assert!(names.contains(&"skill-c"));
210 }
211
212 #[test]
213 fn test_names() {
214 let registry = SkillRegistry::new();
215
216 registry.register(create_test_skill("alpha", "A"));
217 registry.register(create_test_skill("beta", "B"));
218
219 let names = registry.names();
220 assert_eq!(names.len(), 2);
221 assert!(names.contains(&"alpha".to_string()));
222 assert!(names.contains(&"beta".to_string()));
223 }
224
225 #[test]
226 fn test_len_and_is_empty() {
227 let registry = SkillRegistry::new();
228
229 assert!(registry.is_empty());
230 assert_eq!(registry.len(), 0);
231
232 registry.register(create_test_skill("one", "1"));
233 assert!(!registry.is_empty());
234 assert_eq!(registry.len(), 1);
235
236 registry.register(create_test_skill("two", "2"));
237 assert_eq!(registry.len(), 2);
238 }
239
240 #[test]
241 fn test_clear() {
242 let registry = SkillRegistry::new();
243
244 registry.register(create_test_skill("a", "A"));
245 registry.register(create_test_skill("b", "B"));
246 assert_eq!(registry.len(), 2);
247
248 registry.clear();
249 assert!(registry.is_empty());
250 }
251
252 #[test]
253 fn test_to_prompt_xml_empty() {
254 let registry = SkillRegistry::new();
255 let xml = registry.to_prompt_xml();
256 assert!(xml.is_empty());
257 }
258
259 #[test]
260 fn test_to_prompt_xml() {
261 let registry = SkillRegistry::new();
262
263 registry.register(create_test_skill("pdf-tools", "Extract text from PDFs"));
264 registry.register(create_test_skill("git-helper", "Git operations helper"));
265
266 let xml = registry.to_prompt_xml();
267
268 assert!(xml.starts_with("<available_skills>"));
269 assert!(xml.ends_with("</available_skills>"));
270 assert!(xml.contains("<name>git-helper</name>"));
271 assert!(xml.contains("<name>pdf-tools</name>"));
272 assert!(xml.contains("<description>Extract text from PDFs</description>"));
273 assert!(xml.contains("<location>/skills/git-helper/SKILL.md</location>"));
274 }
275
276 #[test]
277 fn test_to_prompt_xml_escapes_special_chars() {
278 let registry = SkillRegistry::new();
279
280 registry.register(create_test_skill("test", "Uses <xml> & \"quotes\""));
281
282 let xml = registry.to_prompt_xml();
283
284 assert!(xml.contains("<xml>"));
285 assert!(xml.contains("&"));
286 assert!(xml.contains(""quotes""));
287 }
288
289 #[test]
290 fn test_thread_safety() {
291 use std::sync::Arc;
292 use std::thread;
293
294 let registry = Arc::new(SkillRegistry::new());
295 let mut handles = vec![];
296
297 for i in 0..10 {
299 let registry = Arc::clone(®istry);
300 handles.push(thread::spawn(move || {
301 let skill = create_test_skill(&format!("skill-{}", i), &format!("Skill {}", i));
302 registry.register(skill);
303 }));
304 }
305
306 for handle in handles {
307 handle.join().unwrap();
308 }
309
310 assert_eq!(registry.len(), 10);
311 }
312
313 #[test]
315 fn test_discovery_to_xml_workflow() {
316 use crate::skills::SkillDiscovery;
317 use std::fs;
318 use tempfile::TempDir;
319
320 let temp_dir = TempDir::new().unwrap();
322 let skill_dir = temp_dir.path().join("my-skill");
323 fs::create_dir_all(&skill_dir).unwrap();
324 fs::write(
325 skill_dir.join("SKILL.md"),
326 r#"---
327name: my-skill
328description: A test skill for integration testing.
329---
330
331# My Skill
332
333Instructions for the LLM.
334"#,
335 )
336 .unwrap();
337
338 let mut discovery = SkillDiscovery::empty();
340 discovery.add_path(temp_dir.path().to_path_buf());
341 let discovered = discovery.discover_valid();
342 assert_eq!(discovered.len(), 1);
343
344 let registry = SkillRegistry::new();
346 for skill in discovered {
347 registry.register(skill);
348 }
349
350 let xml = registry.to_prompt_xml();
352
353 assert!(xml.contains("<available_skills>"));
355 assert!(xml.contains("</available_skills>"));
356 assert!(xml.contains("<name>my-skill</name>"));
357 assert!(xml.contains("<description>A test skill for integration testing.</description>"));
358 assert!(xml.contains("SKILL.md</location>"));
359 }
360}