claude_agent/skills/
skill_tool.rs1use std::sync::Arc;
4
5use async_trait::async_trait;
6use schemars::JsonSchema;
7use serde::Deserialize;
8use tokio::sync::RwLock;
9
10use super::{SkillExecutor, SkillIndex};
11use crate::common::IndexRegistry;
12use crate::tools::{ExecutionContext, SchemaTool};
13use crate::types::ToolResult;
14
15pub struct SkillTool {
20 executor: Arc<RwLock<SkillExecutor>>,
21}
22
23impl SkillTool {
24 pub fn new(executor: SkillExecutor) -> Self {
26 Self {
27 executor: Arc::new(RwLock::new(executor)),
28 }
29 }
30
31 pub fn with_defaults() -> Self {
33 Self::new(SkillExecutor::with_defaults())
34 }
35
36 pub fn with_registry(registry: IndexRegistry<SkillIndex>) -> Self {
38 Self::new(SkillExecutor::new(registry))
39 }
40
41 pub fn executor(&self) -> &Arc<RwLock<SkillExecutor>> {
43 &self.executor
44 }
45
46 pub async fn description_with_skills(&self) -> String {
55 let executor = self.executor.read().await;
56 let registry = executor.registry();
57
58 let skills_section = if registry.is_empty() {
59 "<skill>\n<name>No skills registered</name>\n<description>Register skills using IndexRegistry</description>\n</skill>".to_string()
60 } else {
61 registry
62 .iter()
63 .map(|skill| {
64 let tools_hint = if skill.allowed_tools.is_empty() {
65 String::new()
66 } else {
67 format!("\n<tools>{}</tools>", skill.allowed_tools.join(", "))
68 };
69
70 let args_hint = skill
71 .argument_hint
72 .as_ref()
73 .map(|h| format!("\n<args>{}</args>", h))
74 .unwrap_or_default();
75
76 format!(
77 "<skill>\n<name>{}</name>\n<description>{}</description>{}{}\n</skill>",
78 skill.name, skill.description, tools_hint, args_hint
79 )
80 })
81 .collect::<Vec<_>>()
82 .join("\n")
83 };
84
85 format!(
86 r#"Execute a skill within the main conversation
87
88<skills_instructions>
89When 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.
90
91When 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.
92
93<example>
94User: "run /commit"
95Assistant: [Calls Skill tool with skill: "commit"]
96</example>
97
98How to invoke:
99- Use this tool with the skill name and optional arguments
100- Examples:
101 - `skill: "pdf"` - invoke the pdf skill
102 - `skill: "commit", args: "-m 'Fix bug'"` - invoke with arguments
103 - `skill: "review-pr", args: "123"` - invoke with arguments
104 - `skill: "ms-office-suite:pdf"` - invoke using fully qualified name
105
106Important:
107- When a skill is relevant, you must invoke this tool IMMEDIATELY as your first action
108- NEVER just announce or mention a skill in your text response without actually calling this tool
109- This is a BLOCKING REQUIREMENT: invoke the relevant Skill tool BEFORE generating any other response about the task
110- Only use skills listed in <available_skills> below
111- Do not invoke a skill that is already running
112</skills_instructions>
113
114<available_skills>
115{}
116</available_skills>"#,
117 skills_section
118 )
119 }
120
121 pub async fn register_skill(&self, skill: SkillIndex) {
123 let mut executor = self.executor.write().await;
124 executor.registry_mut().register(skill);
125 }
126
127 pub async fn register_skills(&self, skills: impl IntoIterator<Item = SkillIndex>) {
129 let mut executor = self.executor.write().await;
130 executor.registry_mut().register_all(skills);
131 }
132}
133
134impl Default for SkillTool {
135 fn default() -> Self {
136 Self::with_defaults()
137 }
138}
139
140#[derive(Debug, Clone, Deserialize, JsonSchema)]
141#[schemars(deny_unknown_fields)]
142pub struct SkillInput {
143 pub skill: String,
145 #[serde(default)]
147 pub args: Option<String>,
148}
149
150#[async_trait]
151impl SchemaTool for SkillTool {
152 type Input = SkillInput;
153
154 const NAME: &'static str = "Skill";
155 const DESCRIPTION: &'static str = r#"Execute a skill within the main conversation
156
157<skills_instructions>
158When 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.
159
160When 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.
161
162<example>
163User: "run /commit"
164Assistant: [Calls Skill tool with skill: "commit"]
165</example>
166
167How to invoke:
168- Use this tool with the skill name and optional arguments
169- Examples:
170 - `skill: "pdf"` - invoke the pdf skill
171 - `skill: "commit", args: "-m 'Fix bug'"` - invoke with arguments
172 - `skill: "review-pr", args: "123"` - invoke with arguments
173 - `skill: "ms-office-suite:pdf"` - invoke using fully qualified name
174
175Important:
176- When a skill is relevant, you must invoke this tool IMMEDIATELY as your first action
177- NEVER just announce or mention a skill in your text response without actually calling this tool
178- This is a BLOCKING REQUIREMENT: invoke the relevant Skill tool BEFORE generating any other response about the task
179- Only use skills that are registered and available
180- Do not invoke a skill that is already running
181</skills_instructions>
182
183Note: For the full list of available skills, use description_with_skills() method when building system prompts."#;
184
185 async fn handle(&self, input: SkillInput, _context: &ExecutionContext) -> ToolResult {
186 let executor = self.executor.read().await;
187 let result = executor.execute(&input.skill, input.args.as_deref()).await;
188
189 if result.success {
190 ToolResult::success(result.output)
191 } else {
192 ToolResult::error(
193 result
194 .error
195 .unwrap_or_else(|| "Skill execution failed".to_string()),
196 )
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::common::ContentSource;
205 use crate::tools::{ExecutionContext, Tool};
206 use crate::types::ToolOutput;
207
208 fn test_context() -> ExecutionContext {
209 ExecutionContext::default()
210 }
211
212 fn test_skill(name: &str, description: &str, content: &str) -> SkillIndex {
213 SkillIndex::new(name, description).with_source(ContentSource::in_memory(content))
214 }
215
216 #[tokio::test]
217 async fn test_skill_tool_execute() {
218 let mut registry = IndexRegistry::new();
219 registry.register(test_skill("test", "Test skill", "Execute with: $ARGUMENTS"));
220
221 let tool = SkillTool::with_registry(registry);
222 let context = test_context();
223
224 let result = tool
225 .execute(
226 serde_json::json!({
227 "skill": "test",
228 "args": "my args"
229 }),
230 &context,
231 )
232 .await;
233
234 assert!(!result.is_error());
235 if let ToolOutput::Success(content) = &result.output {
236 assert!(content.contains("my args"));
237 }
238 }
239
240 #[tokio::test]
241 async fn test_skill_tool_not_found() {
242 let tool = SkillTool::with_defaults();
243 let context = test_context();
244
245 let result = tool
246 .execute(
247 serde_json::json!({
248 "skill": "nonexistent"
249 }),
250 &context,
251 )
252 .await;
253
254 assert!(result.is_error());
255 }
256
257 #[tokio::test]
258 async fn test_skill_tool_with_defaults() {
259 let tool = SkillTool::with_defaults();
260 let executor = tool.executor.read().await;
261 assert!(!executor.has_skill("nonexistent"));
262 }
263
264 #[tokio::test]
265 async fn test_description_with_skills() {
266 let mut registry = IndexRegistry::new();
267 registry.register(test_skill("test-skill", "A test skill", "template"));
268 registry.register(test_skill("another-skill", "Another skill", "template"));
269
270 let tool = SkillTool::with_registry(registry);
271 let desc = tool.description_with_skills().await;
272
273 assert!(desc.contains("<skills_instructions>"));
274 assert!(desc.contains("</skills_instructions>"));
275 assert!(desc.contains("<available_skills>"));
276 assert!(desc.contains("</available_skills>"));
277 assert!(desc.contains("test-skill"));
278 assert!(desc.contains("another-skill"));
279 }
280
281 #[tokio::test]
282 async fn test_description_with_tools_and_args() {
283 let mut registry = IndexRegistry::new();
284 registry.register(
285 SkillIndex::new("reader", "Read files")
286 .with_source(ContentSource::in_memory("content"))
287 .with_allowed_tools(["Read", "Grep"])
288 .with_argument_hint("<file_path>"),
289 );
290
291 let tool = SkillTool::with_registry(registry);
292 let desc = tool.description_with_skills().await;
293
294 assert!(desc.contains("<tools>Read, Grep</tools>"));
295 assert!(desc.contains("<args><file_path></args>"));
296 }
297
298 #[tokio::test]
299 async fn test_register_skill() {
300 let tool = SkillTool::with_defaults();
301
302 tool.register_skill(test_skill("dynamic", "Dynamic skill", "content"))
303 .await;
304
305 let executor = tool.executor.read().await;
306 assert!(executor.has_skill("dynamic"));
307 }
308
309 #[test]
310 fn test_description_has_skills_instructions_tag() {
311 use crate::tools::Tool;
312
313 let tool = SkillTool::with_defaults();
314 let desc = tool.description();
315
316 assert!(desc.contains("<skills_instructions>"));
317 assert!(desc.contains("</skills_instructions>"));
318 }
319}