deepstrike_core/context/
skill_catalog.rs1use compact_str::CompactString;
2use std::collections::HashMap;
3
4use crate::types::message::ToolSchema;
5use crate::types::skill::SkillMetadata;
6
7pub const SKILL_TOOL_NAME: &str = "skill";
9
10pub struct SkillCatalog {
20 available: HashMap<CompactString, SkillMetadata>,
21}
22
23impl Default for SkillCatalog {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl SkillCatalog {
30 pub fn new() -> Self {
31 Self {
32 available: HashMap::new(),
33 }
34 }
35
36 pub fn set_available(&mut self, skills: Vec<SkillMetadata>) {
38 self.available = skills.into_iter().map(|s| (s.name.clone(), s)).collect();
39 }
40
41 pub fn upsert_available(&mut self, skill: SkillMetadata) {
43 self.available.insert(skill.name.clone(), skill);
44 }
45
46 pub fn available_count(&self) -> usize {
47 self.available.len()
48 }
49
50 pub fn allowed_tools(&self, name: &str) -> &[CompactString] {
54 self.available
55 .get(name)
56 .map(|s| s.allowed_tools.as_slice())
57 .unwrap_or(&[])
58 }
59
60 pub fn is_empty(&self) -> bool {
61 self.available.is_empty()
62 }
63
64 pub fn build_tool_schema(&self) -> Option<ToolSchema> {
70 if self.available.is_empty() {
71 return None;
72 }
73
74 let mut skills: Vec<&SkillMetadata> = self.available.values().collect();
75 skills.sort_by_key(|s| s.name.as_str());
76
77 let mut xml = String::from("<available_skills>\n");
78 for meta in &skills {
79 xml.push_str(&format!(
80 " <skill>\n <name>{}</name>\n <description>{}</description>\n",
81 meta.name, meta.description,
82 ));
83 if let Some(ref w) = meta.when_to_use {
84 xml.push_str(&format!(" <when_to_use>{w}</when_to_use>\n"));
85 }
86 if let Some(e) = meta.effort {
87 xml.push_str(&format!(" <effort>{e}</effort>\n"));
88 }
89 xml.push_str(" </skill>\n");
90 }
91 xml.push_str("</available_skills>");
92
93 Some(ToolSchema {
94 name: CompactString::new(SKILL_TOOL_NAME),
95 description: format!(
96 "Load a skill into your context to access specialized instructions for a task.\n\n{xml}"
97 ),
98 parameters: serde_json::json!({
99 "type": "object",
100 "properties": {
101 "name": {
102 "type": "string",
103 "description": "The name of the skill to load."
104 }
105 },
106 "required": ["name"]
107 }),
108 })
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::types::skill::SkillMetadata;
116
117 #[test]
118 fn empty_catalog_returns_no_schema() {
119 let catalog = SkillCatalog::new();
120 assert!(catalog.build_tool_schema().is_none());
121 assert!(catalog.is_empty());
122 }
123
124 #[test]
125 fn single_skill_builds_schema() {
126 let mut catalog = SkillCatalog::new();
127 catalog.set_available(vec![SkillMetadata::new("debug", "Debug helper")]);
128 let schema = catalog.build_tool_schema().unwrap();
129 assert_eq!(schema.name.as_str(), SKILL_TOOL_NAME);
130 assert!(schema.description.contains("debug"));
131 assert!(schema.description.contains("Debug helper"));
132 assert!(schema.description.contains("<available_skills>"));
133 }
134
135 #[test]
136 fn set_available_replaces_previous() {
137 let mut catalog = SkillCatalog::new();
138 catalog.set_available(vec![SkillMetadata::new("old", "Old skill")]);
139 catalog.set_available(vec![SkillMetadata::new("new", "New skill")]);
140 assert_eq!(catalog.available_count(), 1);
141 let schema = catalog.build_tool_schema().unwrap();
142 assert!(schema.description.contains("new"));
143 assert!(!schema.description.contains("old"));
144 }
145
146 #[test]
147 fn multiple_skills_all_appear_in_schema() {
148 let mut catalog = SkillCatalog::new();
149 catalog.set_available(vec![
150 SkillMetadata::new("alpha", "Alpha skill"),
151 SkillMetadata::new("beta", "Beta skill"),
152 ]);
153 let schema = catalog.build_tool_schema().unwrap();
154 assert!(schema.description.contains("alpha"));
155 assert!(schema.description.contains("beta"));
156 }
157
158 #[test]
159 fn upsert_adds_single_skill() {
160 let mut catalog = SkillCatalog::new();
161 catalog.upsert_available(SkillMetadata::new("solo", "Solo skill"));
162 assert_eq!(catalog.available_count(), 1);
163 assert!(!catalog.is_empty());
164 }
165
166 #[test]
167 fn allowed_tools_round_trip_through_catalog() {
168 let mut skill = SkillMetadata::new("debug", "Debug helper");
170 skill.allowed_tools = vec![CompactString::new("read"), CompactString::new("grep")];
171 let mut catalog = SkillCatalog::new();
172 catalog.set_available(vec![skill]);
173
174 let tools = catalog.allowed_tools("debug");
175 assert_eq!(tools.len(), 2);
176 assert!(tools.iter().any(|t| t == "read"));
177 assert!(tools.iter().any(|t| t == "grep"));
178 assert!(catalog.allowed_tools("missing").is_empty());
180 }
181}