1use super::registry::global_registry;
6use super::types::SkillExecutionResult;
7use crate::tools::base::{PermissionCheckResult, Tool};
8use crate::tools::context::{ToolContext, ToolResult};
9use crate::tools::error::ToolError;
10use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SkillInput {
16 pub skill: String,
18 pub args: Option<String>,
20}
21
22pub struct SkillTool;
25
26impl Default for SkillTool {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl SkillTool {
33 pub fn new() -> Self {
35 Self
36 }
37
38 pub fn execute_skill(
40 &self,
41 skill_name: &str,
42 args: Option<&str>,
43 ) -> Result<SkillExecutionResult, String> {
44 let registry = global_registry();
45
46 let (skill_data, file_path) = {
48 let registry_guard = registry.read().map_err(|e| e.to_string())?;
49
50 let skill = registry_guard.find(skill_name).ok_or_else(|| {
51 let available: Vec<_> = registry_guard
52 .get_all()
53 .iter()
54 .map(|s| s.skill_name.as_str())
55 .collect();
56 format!(
57 "Skill '{}' not found. Available skills: {}",
58 skill_name,
59 if available.is_empty() {
60 "none".to_string()
61 } else {
62 available.join(", ")
63 }
64 )
65 })?;
66
67 if skill.disable_model_invocation {
69 return Err(format!(
70 "Skill '{}' has model invocation disabled",
71 skill.skill_name
72 ));
73 }
74
75 let data = (
77 skill.skill_name.clone(),
78 skill.display_name.clone(),
79 skill.markdown_content.clone(),
80 skill.allowed_tools.clone(),
81 skill.model.clone(),
82 );
83 let path = skill.file_path.clone();
84
85 (data, path)
86 };
87
88 let (skill_name_owned, display_name, markdown_content, allowed_tools, model) = skill_data;
89
90 let mut skill_content = markdown_content;
92 if let Some(args_str) = args {
93 skill_content.push_str(&format!("\n\n**ARGUMENTS:** {}", args_str));
94 }
95
96 if let Ok(mut registry_write) = registry.write() {
98 registry_write.record_invoked(&skill_name_owned, &file_path, &skill_content);
99 }
100
101 Ok(SkillExecutionResult {
102 success: true,
103 output: Some(format!("Launching skill: {}", display_name)),
104 error: None,
105 steps_completed: Vec::new(),
106 command_name: Some(display_name),
107 allowed_tools,
108 model,
109 })
110 }
111
112 fn generate_description(&self) -> String {
114 let registry = global_registry();
115 let skills_xml = if let Ok(registry_guard) = registry.read() {
116 registry_guard
117 .get_all()
118 .iter()
119 .map(|skill| {
120 format!(
121 r#"<skill>
122<name>{}</name>
123<description>{}</description>
124<location>{}</location>
125</skill>"#,
126 skill.skill_name, skill.description, skill.source
127 )
128 })
129 .collect::<Vec<_>>()
130 .join("\n")
131 } else {
132 String::new()
133 };
134
135 format!(
136 r#"Execute a skill within the main conversation
137
138<skills_instructions>
139When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
140
141When users ask you to run a "slash command" or reference "/<something>" (e.g., "/commit", "/review-pr"), they are referring to a skill. Use this tool to invoke the corresponding skill.
142
143<example>
144User: "run /commit"
145Assistant: [Calls Skill tool with skill: "commit"]
146</example>
147
148How to invoke:
149- Use this tool with the skill name and optional arguments
150- Examples:
151 - `skill: "pdf"` - invoke the pdf skill
152 - `skill: "commit", args: "-m 'Fix bug'"` - invoke with arguments
153 - `skill: "user:pdf"` - invoke using fully qualified name
154
155Important:
156- When a skill is relevant, invoke this tool IMMEDIATELY as your first action
157- Only use skills listed in <available_skills> below
158- Do not invoke a skill that is already running
159</skills_instructions>
160
161<available_skills>
162{}
163</available_skills>
164"#,
165 skills_xml
166 )
167 }
168}
169
170#[async_trait]
171impl Tool for SkillTool {
172 fn name(&self) -> &str {
173 "Skill"
174 }
175
176 fn description(&self) -> &str {
177 "Execute a skill within the main conversation. \
178 Skills provide specialized capabilities and domain knowledge."
179 }
180
181 fn dynamic_description(&self) -> Option<String> {
183 Some(self.generate_description())
184 }
185
186 fn input_schema(&self) -> serde_json::Value {
187 serde_json::json!({
188 "type": "object",
189 "properties": {
190 "skill": {
191 "type": "string",
192 "description": "The skill name. E.g., 'pdf', 'user:my-skill'"
193 },
194 "args": {
195 "type": "string",
196 "description": "Optional arguments for the skill"
197 }
198 },
199 "required": ["skill"]
200 })
201 }
202
203 async fn execute(
204 &self,
205 params: serde_json::Value,
206 _context: &ToolContext,
207 ) -> Result<ToolResult, ToolError> {
208 let skill_name = params
209 .get("skill")
210 .and_then(|v| v.as_str())
211 .ok_or_else(|| ToolError::invalid_params("Missing required parameter: skill"))?;
212
213 let args = params.get("args").and_then(|v| v.as_str());
214
215 match self.execute_skill(skill_name, args) {
216 Ok(result) => {
217 let output = result
218 .output
219 .unwrap_or_else(|| "Skill executed".to_string());
220 let mut tool_result = ToolResult::success(output);
221
222 if let Some(cmd_name) = result.command_name {
223 tool_result =
224 tool_result.with_metadata("command_name", serde_json::json!(cmd_name));
225 }
226 if let Some(tools) = result.allowed_tools {
227 tool_result =
228 tool_result.with_metadata("allowed_tools", serde_json::json!(tools));
229 }
230 if let Some(model) = result.model {
231 tool_result = tool_result.with_metadata("model", serde_json::json!(model));
232 }
233
234 Ok(tool_result)
235 }
236 Err(error) => Err(ToolError::execution_failed(error)),
237 }
238 }
239
240 async fn check_permissions(
241 &self,
242 _params: &serde_json::Value,
243 _context: &ToolContext,
244 ) -> PermissionCheckResult {
245 PermissionCheckResult::allow()
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::skills::types::{SkillDefinition, SkillExecutionMode, SkillSource};
254 use std::path::PathBuf;
255
256 fn create_test_skill() -> SkillDefinition {
257 SkillDefinition {
258 skill_name: "test:example".to_string(),
259 display_name: "Example Skill".to_string(),
260 description: "A test skill".to_string(),
261 has_user_specified_description: true,
262 markdown_content: "# Example\n\nDo something.".to_string(),
263 allowed_tools: Some(vec!["read_file".to_string()]),
264 argument_hint: Some("--flag".to_string()),
265 when_to_use: Some("When testing".to_string()),
266 version: Some("1.0.0".to_string()),
267 model: Some("claude-3-opus".to_string()),
268 disable_model_invocation: false,
269 user_invocable: true,
270 source: SkillSource::User,
271 base_dir: PathBuf::from("/test"),
272 file_path: PathBuf::from("/test/SKILL.md"),
273 supporting_files: vec![],
274 execution_mode: SkillExecutionMode::default(),
275 provider: None,
276 workflow: None,
277 }
278 }
279
280 #[test]
281 fn test_skill_tool_new() {
282 let tool = SkillTool::new();
283 assert_eq!(tool.name(), "Skill");
284 }
285
286 #[test]
287 fn test_skill_tool_input_schema() {
288 let tool = SkillTool::new();
289 let schema = tool.input_schema();
290
291 assert_eq!(schema["type"], "object");
292 assert!(schema["properties"]["skill"].is_object());
293 assert!(schema["properties"]["args"].is_object());
294 assert_eq!(schema["required"], serde_json::json!(["skill"]));
295 }
296
297 #[test]
298 fn test_generate_description() {
299 let tool = SkillTool::new();
300 let desc = tool.generate_description();
301
302 assert!(desc.contains("skills_instructions"));
303 assert!(desc.contains("available_skills"));
304 }
305
306 #[tokio::test]
307 async fn test_skill_tool_check_permissions() {
308 let tool = SkillTool::new();
309 let context = ToolContext::new(PathBuf::from("/tmp"));
310 let params = serde_json::json!({"skill": "test"});
311
312 let result = tool.check_permissions(¶ms, &context).await;
313 assert!(result.is_allowed());
314 }
315
316 #[tokio::test]
317 async fn test_skill_tool_execute_not_found() {
318 let tool = SkillTool::new();
319 let context = ToolContext::new(PathBuf::from("/tmp"));
320 let params = serde_json::json!({"skill": "nonexistent-skill-xyz"});
321
322 let result = tool.execute(params, &context).await;
323 assert!(result.is_err());
324 }
325
326 #[tokio::test]
327 async fn test_skill_tool_execute_missing_param() {
328 let tool = SkillTool::new();
329 let context = ToolContext::new(PathBuf::from("/tmp"));
330 let params = serde_json::json!({});
331
332 let result = tool.execute(params, &context).await;
333 assert!(result.is_err());
334 }
335}