Skip to main content

agent_code_lib/tools/
skill_tool.rs

1//! Skill tool: invoke skills dynamically from the agent.
2//!
3//! Unlike slash commands which are user-initiated, the SkillTool
4//! lets the LLM trigger skills programmatically when it determines
5//! one is appropriate for the current task.
6
7use async_trait::async_trait;
8use serde_json::json;
9
10use super::{Tool, ToolContext, ToolResult};
11use crate::error::ToolError;
12
13pub struct SkillTool;
14
15#[async_trait]
16impl Tool for SkillTool {
17    fn name(&self) -> &'static str {
18        "Skill"
19    }
20
21    fn description(&self) -> &'static str {
22        "Invoke a user-defined skill by name. Skills are reusable \
23         workflows loaded from .agent/skills/ or ~/.config/agent-code/skills/."
24    }
25
26    fn input_schema(&self) -> serde_json::Value {
27        json!({
28            "type": "object",
29            "required": ["skill"],
30            "properties": {
31                "skill": {
32                    "type": "string",
33                    "description": "Name of the skill to invoke"
34                },
35                "args": {
36                    "type": "string",
37                    "description": "Optional arguments passed to the skill template"
38                }
39            }
40        })
41    }
42
43    fn is_read_only(&self) -> bool {
44        true // The skill itself may invoke mutation tools, but loading is read-only.
45    }
46
47    fn is_concurrency_safe(&self) -> bool {
48        false
49    }
50
51    async fn call(
52        &self,
53        input: serde_json::Value,
54        ctx: &ToolContext,
55    ) -> Result<ToolResult, ToolError> {
56        let skill_name = input
57            .get("skill")
58            .and_then(|v| v.as_str())
59            .ok_or_else(|| ToolError::InvalidInput("'skill' is required".into()))?;
60
61        let args = input.get("args").and_then(|v| v.as_str());
62
63        let registry = crate::skills::SkillRegistry::load_all(Some(ctx.cwd.as_path()));
64
65        match registry.find(skill_name) {
66            Some(skill) => {
67                let expanded = skill.expand(args);
68                Ok(ToolResult::success(format!(
69                    "Skill '{}' loaded. Execute the following instructions:\n\n{}",
70                    skill_name, expanded
71                )))
72            }
73            None => {
74                let available: Vec<&str> = registry.all().iter().map(|s| s.name.as_str()).collect();
75                Err(ToolError::InvalidInput(format!(
76                    "Skill '{}' not found. Available: {}",
77                    skill_name,
78                    if available.is_empty() {
79                        "none".to_string()
80                    } else {
81                        available.join(", ")
82                    }
83                )))
84            }
85        }
86    }
87}