1use anyhow::{Context, Result};
11use std::collections::HashMap;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14
15use super::metadata::{Skill, SkillExecutionMode, SkillResult};
16use super::parser::render_template;
17use super::registry::SkillRegistry;
18
19pub struct SkillExecutor {
21 registry: Arc<RwLock<SkillRegistry>>,
23}
24
25impl SkillExecutor {
26 pub fn new(registry: Arc<RwLock<SkillRegistry>>) -> Self {
28 Self { registry }
29 }
30
31 pub async fn execute_by_name(
35 &self,
36 skill_name: &str,
37 args: HashMap<String, String>,
38 ) -> Result<SkillResult> {
39 let mut registry = self.registry.write().await;
40 let skill = registry
41 .get_skill(skill_name)
42 .with_context(|| format!("Failed to load skill '{}'", skill_name))?
43 .clone();
44 drop(registry);
45
46 self.execute(&skill, args).await
47 }
48
49 pub async fn execute(
53 &self,
54 skill: &Skill,
55 args: HashMap<String, String>,
56 ) -> Result<SkillResult> {
57 let instructions = render_template(&skill.instructions, &args);
58
59 match skill.execution_mode {
60 SkillExecutionMode::Inline => self.execute_inline(skill, &instructions).await,
61 SkillExecutionMode::Subagent => self.execute_subagent(skill, &instructions).await,
62 SkillExecutionMode::Script => self.execute_script(skill, &instructions).await,
63 }
64 }
65
66 async fn execute_inline(&self, skill: &Skill, instructions: &str) -> Result<SkillResult> {
68 tracing::info!("Executing skill '{}' inline", skill.name());
69
70 let full_instructions = format!(
71 "## Skill: {}\n\n{}\n\n---\n\n{}",
72 skill.name(),
73 skill.description(),
74 instructions
75 );
76
77 Ok(SkillResult::inline(
78 full_instructions,
79 skill.model().cloned(),
80 ))
81 }
82
83 async fn execute_subagent(&self, skill: &Skill, instructions: &str) -> Result<SkillResult> {
85 tracing::info!("Executing skill '{}' as subagent", skill.name());
86
87 let agent_id = format!("skill-{}-{}", skill.name(), uuid::Uuid::new_v4());
88
89 tracing::debug!(
90 "Prepared subagent task '{}' with {} instructions chars",
91 agent_id,
92 instructions.len()
93 );
94
95 Ok(SkillResult::Subagent { agent_id })
96 }
97
98 async fn execute_script(&self, skill: &Skill, script: &str) -> Result<SkillResult> {
100 tracing::info!("Executing skill '{}' as script", skill.name());
101
102 if !script.contains("let ") && !script.contains("fn ") && !script.contains(";") {
103 tracing::warn!(
104 "Script for skill '{}' doesn't look like valid Rhai code",
105 skill.name()
106 );
107 }
108
109 Ok(SkillResult::Script {
110 output: script.to_string(),
111 is_error: false,
112 })
113 }
114
115 fn filter_allowed_tools(&self, skill: &Skill, available: &[String]) -> Vec<String> {
117 if let Some(allowed_tools) = skill.allowed_tools() {
118 available
119 .iter()
120 .filter(|name| {
121 allowed_tools.iter().any(|allowed| {
122 *name == allowed || name.ends_with(&format!("__{}", allowed))
124 })
125 })
126 .cloned()
127 .collect()
128 } else {
129 available.to_vec()
130 }
131 }
132
133 pub async fn prepare_subagent(
138 &self,
139 skill: &Skill,
140 available_tool_names: &[String],
141 args: HashMap<String, String>,
142 ) -> Result<SubagentPrepared> {
143 let instructions = render_template(&skill.instructions, &args);
144 let allowed_tool_names = self.filter_allowed_tools(skill, available_tool_names);
145
146 let system_prompt = format!(
147 "You are executing the '{}' skill.\n\n\
148 **Description**: {}\n\n\
149 **Instructions**:\n{}",
150 skill.name(),
151 skill.description(),
152 instructions
153 );
154
155 Ok(SubagentPrepared {
156 task_description: instructions,
157 allowed_tool_names,
158 system_prompt,
159 model_override: skill.model().cloned(),
160 })
161 }
162
163 pub async fn prepare_script(
168 &self,
169 skill: &Skill,
170 available_tool_names: &[String],
171 args: HashMap<String, String>,
172 ) -> Result<ScriptPrepared> {
173 let script_content = render_template(&skill.instructions, &args);
174 let allowed_tool_names = self.filter_allowed_tools(skill, available_tool_names);
175
176 Ok(ScriptPrepared {
177 script_content,
178 allowed_tool_names,
179 model_override: skill.model().cloned(),
180 skill_name: skill.name().to_string(),
181 })
182 }
183
184 pub async fn get_execution_mode(&self, skill_name: &str) -> Result<SkillExecutionMode> {
186 let registry = self.registry.read().await;
187 let metadata = registry
188 .get_metadata(skill_name)
189 .ok_or_else(|| anyhow::anyhow!("Skill not found: {}", skill_name))?;
190 Ok(metadata.execution_mode())
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct SubagentPrepared {
197 pub task_description: String,
199 pub allowed_tool_names: Vec<String>,
201 pub system_prompt: String,
203 pub model_override: Option<String>,
205}
206
207#[derive(Debug, Clone)]
209pub struct ScriptPrepared {
210 pub script_content: String,
212 pub allowed_tool_names: Vec<String>,
214 pub model_override: Option<String>,
216 pub skill_name: String,
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::metadata::SkillMetadata;
224
225 fn create_available_tools() -> Vec<String> {
226 vec![
227 "Read".to_string(),
228 "Write".to_string(),
229 "Grep".to_string(),
230 "git_diff".to_string(),
231 ]
232 }
233
234 fn create_test_skill() -> Skill {
235 let mut metadata = SkillMetadata::new("test-skill".to_string(), "A test skill".to_string());
236 metadata.allowed_tools = Some(vec!["Read".to_string(), "Grep".to_string()]);
237
238 Skill {
239 metadata,
240 instructions: "Do the test with {{arg1}}".to_string(),
241 execution_mode: SkillExecutionMode::Inline,
242 }
243 }
244
245 #[tokio::test]
246 async fn test_execute_inline() {
247 let registry = Arc::new(RwLock::new(SkillRegistry::new()));
248 let executor = SkillExecutor::new(registry);
249
250 let skill = create_test_skill();
251 let mut args = HashMap::new();
252 args.insert("arg1".to_string(), "value1".to_string());
253
254 let result = executor.execute(&skill, args).await.unwrap();
255
256 match result {
257 SkillResult::Inline { instructions, .. } => {
258 assert!(instructions.contains("test-skill"));
259 assert!(instructions.contains("value1"));
260 }
261 _ => panic!("Expected inline result"),
262 }
263 }
264
265 #[tokio::test]
266 async fn test_execute_subagent() {
267 let registry = Arc::new(RwLock::new(SkillRegistry::new()));
268 let executor = SkillExecutor::new(registry);
269
270 let mut skill = create_test_skill();
271 skill.execution_mode = SkillExecutionMode::Subagent;
272
273 let args = HashMap::new();
274 let result = executor.execute(&skill, args).await.unwrap();
275
276 match result {
277 SkillResult::Subagent { agent_id } => {
278 assert!(agent_id.starts_with("skill-test-skill-"));
279 }
280 _ => panic!("Expected subagent result"),
281 }
282 }
283
284 #[tokio::test]
285 async fn test_execute_script() {
286 let registry = Arc::new(RwLock::new(SkillRegistry::new()));
287 let executor = SkillExecutor::new(registry);
288
289 let mut skill = create_test_skill();
290 skill.execution_mode = SkillExecutionMode::Script;
291 skill.instructions = "let x = 1; x + 1".to_string();
292
293 let args = HashMap::new();
294 let result = executor.execute(&skill, args).await.unwrap();
295
296 match result {
297 SkillResult::Script { output, is_error } => {
298 assert!(!is_error);
299 assert!(output.contains("let x = 1"));
300 }
301 _ => panic!("Expected script result"),
302 }
303 }
304
305 #[tokio::test]
306 async fn test_filter_allowed_tools() {
307 let registry = Arc::new(RwLock::new(SkillRegistry::new()));
308 let executor = SkillExecutor::new(registry);
309
310 let skill = create_test_skill(); let available = create_available_tools();
312
313 let filtered = executor.filter_allowed_tools(&skill, &available);
314
315 assert_eq!(filtered.len(), 2);
316 assert!(filtered.contains(&"Read".to_string()));
317 assert!(filtered.contains(&"Grep".to_string()));
318 assert!(!filtered.contains(&"Write".to_string()));
319 assert!(!filtered.contains(&"git_diff".to_string()));
320 }
321
322 #[tokio::test]
323 async fn test_no_tool_restrictions() {
324 let registry = Arc::new(RwLock::new(SkillRegistry::new()));
325 let executor = SkillExecutor::new(registry);
326
327 let mut skill = create_test_skill();
328 skill.metadata.allowed_tools = None; let available = create_available_tools();
331 let filtered = executor.filter_allowed_tools(&skill, &available);
332
333 assert_eq!(filtered.len(), 4);
335 }
336
337 #[tokio::test]
338 async fn test_prepare_subagent() {
339 let registry = Arc::new(RwLock::new(SkillRegistry::new()));
340 let executor = SkillExecutor::new(registry);
341
342 let mut skill = create_test_skill();
343 skill.execution_mode = SkillExecutionMode::Subagent;
344
345 let available = create_available_tools();
346 let mut args = HashMap::new();
347 args.insert("arg1".to_string(), "test_value".to_string());
348
349 let prepared = executor
350 .prepare_subagent(&skill, &available, args)
351 .await
352 .unwrap();
353
354 assert!(prepared.task_description.contains("test_value"));
355 assert!(prepared.system_prompt.contains("test-skill"));
356 assert_eq!(prepared.allowed_tool_names.len(), 2);
358 assert!(prepared.allowed_tool_names.contains(&"Read".to_string()));
359 assert!(prepared.allowed_tool_names.contains(&"Grep".to_string()));
360 }
361
362 #[tokio::test]
363 async fn test_prepare_script() {
364 let registry = Arc::new(RwLock::new(SkillRegistry::new()));
365 let executor = SkillExecutor::new(registry);
366
367 let mut skill = create_test_skill();
368 skill.execution_mode = SkillExecutionMode::Script;
369 skill.instructions = "let result = {{value}}; result".to_string();
370
371 let available = create_available_tools();
372 let mut args = HashMap::new();
373 args.insert("value".to_string(), "42".to_string());
374
375 let prepared = executor
376 .prepare_script(&skill, &available, args)
377 .await
378 .unwrap();
379
380 assert!(prepared.script_content.contains("let result = 42"));
381 assert_eq!(prepared.skill_name, "test-skill");
382 assert_eq!(prepared.allowed_tool_names.len(), 2);
384 }
385}