Skip to main content

atomcode_core/tool/
use_skill.rs

1use std::sync::{Arc, RwLock};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use serde::Deserialize;
6use serde_json::json;
7
8use super::{ApprovalRequirement, Tool, ToolContext, ToolDef, ToolResult};
9use crate::skill::SkillRegistry;
10
11/// Tool that loads a skill's instruction template into the conversation context.
12///
13/// This is the LLM-facing equivalent of the user typing `/skill-name args`.
14/// The skill content is returned as the tool result, which the LLM reads and
15/// then follows using its regular tools — no sub-agent needed.
16pub struct UseSkillTool {
17    pub registry: Arc<RwLock<SkillRegistry>>,
18}
19
20#[derive(Deserialize)]
21struct UseSkillArgs {
22    name: String,
23    #[serde(default)]
24    arguments: String,
25}
26
27#[async_trait]
28impl Tool for UseSkillTool {
29    fn definition(&self) -> ToolDef {
30        ToolDef {
31            name: "use_skill",
32            description: "Load a skill's instruction template into context. \
33                Use this when a task matches a skill's purpose — the skill provides \
34                detailed, reusable instructions that guide how to complete the task. \
35                Available skills are listed in the system prompt under 'Available Skills'. \
36                Returns the expanded skill content for you to follow."
37                .to_string(),
38            parameters: json!({
39                "type": "object",
40                "properties": {
41                    "name": {
42                        "type": "string",
43                        "description": "Skill name (without leading slash)"
44                    },
45                    "arguments": {
46                        "type": "string",
47                        "description": "Arguments passed to the skill. Replaces $ARGUMENTS in the template."
48                    }
49                },
50                "required": ["name"]
51            }),
52        }
53    }
54
55    fn approval(&self, _args: &str) -> ApprovalRequirement {
56        ApprovalRequirement::AutoApprove
57    }
58
59    async fn execute(&self, args: &str, _ctx: &ToolContext) -> Result<ToolResult> {
60        let parsed: UseSkillArgs = serde_json::from_str(args)?;
61
62        let expanded = {
63            let registry = self
64                .registry
65                .read()
66                .map_err(|e| anyhow::anyhow!("registry lock: {}", e))?;
67            let skill = registry.get(&parsed.name).or_else(|| {
68                if parsed.name.contains(':') {
69                    None
70                } else {
71                    registry.get(&format!("skills:{}", parsed.name))
72                }
73            });
74
75            match skill {
76                Some(skill) => {
77                    if skill.disable_model_invocation {
78                        return Ok(ToolResult {
79                            call_id: String::new(),
80                            output: format!(
81                                "Skill '{}' cannot be invoked automatically. Ask the user to run `/{}`.",
82                                parsed.name, parsed.name
83                            ),
84                            success: false,
85                        });
86                    }
87                    skill.expand(&parsed.arguments, "")
88                }
89                None => {
90                    let available: Vec<String> = registry
91                        .invocable_by_llm()
92                        .map(|s| s.name.clone())
93                        .collect();
94                    return Ok(ToolResult {
95                        call_id: String::new(),
96                        output: format!(
97                            "Skill '{}' not found. Available skills: {}",
98                            parsed.name,
99                            if available.is_empty() {
100                                "(none)".to_string()
101                            } else {
102                                available.join(", ")
103                            }
104                        ),
105                        success: false,
106                    });
107                }
108            }
109        };
110
111        if expanded.trim().is_empty() {
112            return Ok(ToolResult {
113                call_id: String::new(),
114                output: format!("Skill '{}' has an empty template.", parsed.name),
115                success: false,
116            });
117        }
118
119        Ok(ToolResult {
120            call_id: String::new(),
121            output: expanded,
122            success: true,
123        })
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::skill::Skill;
131    use std::path::PathBuf;
132
133    fn test_skill(name: &str, template: &str) -> Skill {
134        Skill {
135            name: name.into(),
136            description: "test skill".into(),
137            template: template.into(),
138            disable_model_invocation: false,
139            user_invocable: true,
140            argument_hint: None,
141            allowed_tools: vec![],
142            skill_dir: PathBuf::new(),
143            source_path: PathBuf::new(),
144        }
145    }
146
147    fn tool_with_skills(skills: Vec<Skill>) -> UseSkillTool {
148        let mut registry = SkillRegistry::new();
149        for skill in skills {
150            registry.register(skill);
151        }
152        UseSkillTool {
153            registry: Arc::new(RwLock::new(registry)),
154        }
155    }
156
157    #[tokio::test]
158    async fn resolves_bare_name_to_loose_skills_namespace() {
159        let tool = tool_with_skills(vec![test_skill("skills:brainstorming", "Do $ARGUMENTS")]);
160        let ctx = ToolContext::new(PathBuf::from("/tmp"));
161
162        let result = tool
163            .execute(r#"{"name":"brainstorming","arguments":"ideas"}"#, &ctx)
164            .await
165            .unwrap();
166
167        assert!(result.success);
168        assert_eq!(result.output, "Do ideas");
169    }
170
171    #[tokio::test]
172    async fn keeps_explicit_namespace_lookup_working() {
173        let tool = tool_with_skills(vec![test_skill("skills:brainstorming", "Namespaced")]);
174        let ctx = ToolContext::new(PathBuf::from("/tmp"));
175
176        let result = tool
177            .execute(r#"{"name":"skills:brainstorming"}"#, &ctx)
178            .await
179            .unwrap();
180
181        assert!(result.success);
182        assert_eq!(result.output, "Namespaced");
183    }
184
185    #[tokio::test]
186    async fn does_not_fallback_for_other_namespaces() {
187        let tool = tool_with_skills(vec![test_skill("skills:brainstorming", "Namespaced")]);
188        let ctx = ToolContext::new(PathBuf::from("/tmp"));
189
190        let result = tool
191            .execute(r#"{"name":"plugin:brainstorming"}"#, &ctx)
192            .await
193            .unwrap();
194
195        assert!(!result.success);
196        assert!(result
197            .output
198            .contains("Skill 'plugin:brainstorming' not found"));
199    }
200}