agent_engine/skills/
tool.rs1use crate::skills::{
4 registry::{CommandRegistry, Resolution},
5 LoadedSkill,
6};
7use serde_json::json;
8use std::sync::Arc;
9
10pub struct LoadSkillTool {
11 registry: Arc<CommandRegistry>,
12}
13
14impl LoadSkillTool {
15 pub fn new(registry: Arc<CommandRegistry>) -> Self {
16 Self { registry }
17 }
18
19 pub fn format_body(skill: &LoadedSkill) -> String {
22 format!(
23 "# Skill: {} — {}\n\nFollow these guidelines for the rest of this conversation.\n\n{}",
24 skill.name, skill.description, skill.body
25 )
26 }
27}
28
29#[async_trait::async_trait]
30impl crate::Tool for LoadSkillTool {
31 fn name(&self) -> &str {
32 "load_skill"
33 }
34
35 fn description(&self) -> &str {
36 "Load a skill to guide your behavior for the current conversation. \
37 Skills provide structured guidelines, checklists, and best practices. \
38 Call this when a task would benefit from a specific methodology."
39 }
40
41 fn parameters(&self) -> serde_json::Value {
42 let list: Vec<String> = self
43 .registry
44 .all_skills()
45 .iter()
46 .map(|s| {
47 let qualified = match &s.plugin {
48 Some(p) => format!("{}:{} — {}", p, s.name, s.description),
49 None => format!("{} — {}", s.name, s.description),
50 };
51 qualified
52 })
53 .collect();
54 json!({
55 "type": "object",
56 "properties": {
57 "skill": {
58 "type": "string",
59 "description": format!("Name of the skill to load (bare or plugin:skill). Available:\n{}", list.join("\n"))
60 }
61 },
62 "required": ["skill"]
63 })
64 }
65
66 async fn execute(
67 &self,
68 params: serde_json::Value,
69 _ctx: crate::ToolContext,
70 ) -> crate::Result<String> {
71 let name = params["skill"]
72 .as_str()
73 .ok_or_else(|| crate::RuntimeError::Tool("Missing 'skill' parameter".to_string()))?;
74
75 match self.registry.resolve(name) {
76 Resolution::Skill(s) => Ok(Self::format_body(&s)),
77 Resolution::Ambiguous(opts) => Err(crate::RuntimeError::Tool(format!(
78 "ambiguous skill '{}'; specify one of: {}",
79 name,
80 opts.join(", ")
81 ))),
82 Resolution::PluginCommand(_) | Resolution::Builtin | Resolution::Unknown => Err(
83 crate::RuntimeError::Tool(format!("unknown skill '{}'", name)),
84 ),
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use std::path::PathBuf;
93
94 fn test_ctx() -> crate::ToolContext {
95 crate::ToolContext {
96 channels: crate::tools::ToolChannels {
97 tx_delta: None,
98 tx_events: None,
99 },
100 capabilities: crate::tools::ToolCapabilities {
101 watcher_exit_path: None,
102 tool_register_tx: None,
103 session_manager: None,
104 subagent_registry: None,
105 event_queue: None,
106 secret_prompt: None,
107 },
108 limits: crate::tools::ToolLimits {
109 max_tool_output: 30000,
110 max_tool_buffer: 256 * 1024,
111 bash_timeout: 30,
112 bash_max_timeout: 300,
113 subagent_timeout: 300,
114 },
115 }
116 }
117
118 fn mk(name: &str, plugin: Option<&str>) -> LoadedSkill {
119 LoadedSkill {
120 name: name.to_string(),
121 description: format!("desc-{name}"),
122 body: format!("body-{name}"),
123 plugin: plugin.map(str::to_string),
124 base_dir: PathBuf::from("/"),
125 source_path: PathBuf::from("/SKILL.md"),
126 }
127 }
128
129 #[test]
130 fn format_body_includes_name_and_description() {
131 let s = LoadedSkill {
132 name: "x".into(),
133 description: "y".into(),
134 body: "z".into(),
135 plugin: None,
136 base_dir: PathBuf::from("/"),
137 source_path: PathBuf::from("/SKILL.md"),
138 };
139 let out = LoadSkillTool::format_body(&s);
140 assert!(out.contains("x"));
141 assert!(out.contains("y"));
142 assert!(out.contains("z"));
143 assert!(out.contains("Follow these guidelines"));
144 }
145
146 #[tokio::test]
147 async fn execute_returns_skill_body_on_unique_match() {
148 use crate::Tool;
149 let reg = Arc::new(crate::skills::registry::CommandRegistry::new(
150 &[],
151 vec![mk("search", Some("p1"))],
152 ));
153 let tool = LoadSkillTool::new(reg);
154 let result = tool
155 .execute(serde_json::json!({"skill": "search"}), test_ctx())
156 .await
157 .unwrap();
158 assert!(result.contains("# Skill: search"));
159 assert!(result.contains("desc-search"));
160 assert!(result.contains("body-search"));
161 }
162
163 #[tokio::test]
164 async fn execute_errors_on_ambiguous() {
165 use crate::Tool;
166 let reg = Arc::new(crate::skills::registry::CommandRegistry::new(
167 &[],
168 vec![mk("search", Some("p1")), mk("search", Some("p2"))],
169 ));
170 let tool = LoadSkillTool::new(reg);
171 let err = tool
172 .execute(serde_json::json!({"skill": "search"}), test_ctx())
173 .await
174 .unwrap_err();
175 let msg = format!("{err}");
176 assert!(msg.contains("ambiguous"));
177 assert!(msg.contains("p1:search"));
178 assert!(msg.contains("p2:search"));
179 }
180
181 #[tokio::test]
182 async fn execute_errors_on_unknown() {
183 use crate::Tool;
184 let reg = Arc::new(crate::skills::registry::CommandRegistry::new(&[], vec![]));
185 let tool = LoadSkillTool::new(reg);
186 let err = tool
187 .execute(serde_json::json!({"skill": "nosuch"}), test_ctx())
188 .await
189 .unwrap_err();
190 assert!(format!("{err}").contains("unknown skill 'nosuch'"));
191 }
192
193 #[tokio::test]
194 async fn execute_errors_on_builtin() {
195 use crate::Tool;
196 let reg = Arc::new(crate::skills::registry::CommandRegistry::new(
198 &["clear"],
199 vec![],
200 ));
201 let tool = LoadSkillTool::new(reg);
202 let err = tool
203 .execute(serde_json::json!({"skill": "clear"}), test_ctx())
204 .await
205 .unwrap_err();
206 assert!(format!("{err}").contains("unknown skill 'clear'"));
207 }
208
209 #[tokio::test]
210 async fn execute_errors_on_missing_skill_param() {
211 use crate::Tool;
212 let reg = Arc::new(crate::skills::registry::CommandRegistry::new(&[], vec![]));
213 let tool = LoadSkillTool::new(reg);
214 let err = tool
215 .execute(serde_json::json!({}), test_ctx())
216 .await
217 .unwrap_err();
218 assert!(format!("{err}").contains("Missing 'skill' parameter"));
219 }
220
221 #[test]
222 fn parameters_schema_is_well_formed() {
223 use crate::Tool;
224 let reg = Arc::new(crate::skills::registry::CommandRegistry::new(&[], vec![]));
225 let tool = LoadSkillTool::new(reg);
226 let schema = tool.parameters();
227 assert_eq!(schema["type"], "object");
228 assert_eq!(schema["properties"]["skill"]["type"], "string");
229 assert_eq!(schema["required"], serde_json::json!(["skill"]));
230 }
231}