atomcode_core/tool/
use_skill.rs1use 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
11pub 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}