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