Skip to main content

aster/skills/
tool.rs

1//! Skill Tool
2//!
3//! Tool implementation for executing skills.
4
5use 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/// Skill tool input parameters
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SkillInput {
16    /// Skill name (e.g., "pdf", "user:my-skill")
17    pub skill: String,
18    /// Optional arguments for the skill
19    pub args: Option<String>,
20}
21
22/// Skill Tool for executing skills
23///
24pub struct SkillTool;
25
26impl Default for SkillTool {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl SkillTool {
33    /// Create a new SkillTool
34    pub fn new() -> Self {
35        Self
36    }
37
38    /// Execute a skill by name
39    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        // First, get all the data we need from the skill
47        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            // Check if model invocation is disabled
68            if skill.disable_model_invocation {
69                return Err(format!(
70                    "Skill '{}' has model invocation disabled",
71                    skill.skill_name
72                ));
73            }
74
75            // Clone the data we need
76            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        // Build skill content
91        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        // Record invocation with write lock
97        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    /// Generate tool description with available skills
113    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    /// 动态生成包含可用 Skills 列表的描述
182    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        // Skills are read-only operations
246        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(&params, &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}