use anyhow::{Context, Result};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use super::metadata::{Skill, SkillExecutionMode, SkillResult};
use super::parser::render_template;
use super::registry::SkillRegistry;
pub struct SkillExecutor {
registry: Arc<RwLock<SkillRegistry>>,
}
impl SkillExecutor {
pub fn new(registry: Arc<RwLock<SkillRegistry>>) -> Self {
Self { registry }
}
pub async fn execute_by_name(
&self,
skill_name: &str,
args: HashMap<String, String>,
) -> Result<SkillResult> {
let mut registry = self.registry.write().await;
let skill = registry
.get_skill(skill_name)
.with_context(|| format!("Failed to load skill '{}'", skill_name))?
.clone();
drop(registry);
self.execute(&skill, args).await
}
pub async fn execute(
&self,
skill: &Skill,
args: HashMap<String, String>,
) -> Result<SkillResult> {
let instructions = render_template(&skill.instructions, &args);
match skill.execution_mode {
SkillExecutionMode::Inline => self.execute_inline(skill, &instructions).await,
SkillExecutionMode::Subagent => self.execute_subagent(skill, &instructions).await,
SkillExecutionMode::Script => self.execute_script(skill, &instructions).await,
}
}
async fn execute_inline(&self, skill: &Skill, instructions: &str) -> Result<SkillResult> {
tracing::info!("Executing skill '{}' inline", skill.name());
let full_instructions = format!(
"## Skill: {}\n\n{}\n\n---\n\n{}",
skill.name(),
skill.description(),
instructions
);
Ok(SkillResult::inline(
full_instructions,
skill.model().cloned(),
))
}
async fn execute_subagent(&self, skill: &Skill, instructions: &str) -> Result<SkillResult> {
tracing::info!("Executing skill '{}' as subagent", skill.name());
let agent_id = format!("skill-{}-{}", skill.name(), uuid::Uuid::new_v4());
tracing::debug!(
"Prepared subagent task '{}' with {} instructions chars",
agent_id,
instructions.len()
);
Ok(SkillResult::Subagent { agent_id })
}
async fn execute_script(&self, skill: &Skill, script: &str) -> Result<SkillResult> {
tracing::info!("Executing skill '{}' as script", skill.name());
if !script.contains("let ") && !script.contains("fn ") && !script.contains(";") {
tracing::warn!(
"Script for skill '{}' doesn't look like valid Rhai code",
skill.name()
);
}
Ok(SkillResult::Script {
output: script.to_string(),
is_error: false,
})
}
fn filter_allowed_tools(&self, skill: &Skill, available: &[String]) -> Vec<String> {
if let Some(allowed_tools) = skill.allowed_tools() {
available
.iter()
.filter(|name| {
allowed_tools.iter().any(|allowed| {
*name == allowed || name.ends_with(&format!("__{}", allowed))
})
})
.cloned()
.collect()
} else {
available.to_vec()
}
}
pub async fn prepare_subagent(
&self,
skill: &Skill,
available_tool_names: &[String],
args: HashMap<String, String>,
) -> Result<SubagentPrepared> {
let instructions = render_template(&skill.instructions, &args);
let allowed_tool_names = self.filter_allowed_tools(skill, available_tool_names);
let system_prompt = format!(
"You are executing the '{}' skill.\n\n\
**Description**: {}\n\n\
**Instructions**:\n{}",
skill.name(),
skill.description(),
instructions
);
Ok(SubagentPrepared {
task_description: instructions,
allowed_tool_names,
system_prompt,
model_override: skill.model().cloned(),
})
}
pub async fn prepare_script(
&self,
skill: &Skill,
available_tool_names: &[String],
args: HashMap<String, String>,
) -> Result<ScriptPrepared> {
let script_content = render_template(&skill.instructions, &args);
let allowed_tool_names = self.filter_allowed_tools(skill, available_tool_names);
Ok(ScriptPrepared {
script_content,
allowed_tool_names,
model_override: skill.model().cloned(),
skill_name: skill.name().to_string(),
})
}
pub async fn get_execution_mode(&self, skill_name: &str) -> Result<SkillExecutionMode> {
let registry = self.registry.read().await;
let metadata = registry
.get_metadata(skill_name)
.ok_or_else(|| anyhow::anyhow!("Skill not found: {}", skill_name))?;
Ok(metadata.execution_mode())
}
}
#[derive(Debug, Clone)]
pub struct SubagentPrepared {
pub task_description: String,
pub allowed_tool_names: Vec<String>,
pub system_prompt: String,
pub model_override: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ScriptPrepared {
pub script_content: String,
pub allowed_tool_names: Vec<String>,
pub model_override: Option<String>,
pub skill_name: String,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::skills::metadata::SkillMetadata;
fn create_available_tools() -> Vec<String> {
vec![
"Read".to_string(),
"Write".to_string(),
"Grep".to_string(),
"git_diff".to_string(),
]
}
fn create_test_skill() -> Skill {
let mut metadata = SkillMetadata::new("test-skill".to_string(), "A test skill".to_string());
metadata.allowed_tools = Some(vec!["Read".to_string(), "Grep".to_string()]);
Skill {
metadata,
instructions: "Do the test with {{arg1}}".to_string(),
execution_mode: SkillExecutionMode::Inline,
}
}
#[tokio::test]
async fn test_execute_inline() {
let registry = Arc::new(RwLock::new(SkillRegistry::new()));
let executor = SkillExecutor::new(registry);
let skill = create_test_skill();
let mut args = HashMap::new();
args.insert("arg1".to_string(), "value1".to_string());
let result = executor.execute(&skill, args).await.unwrap();
match result {
SkillResult::Inline { instructions, .. } => {
assert!(instructions.contains("test-skill"));
assert!(instructions.contains("value1"));
}
_ => panic!("Expected inline result"),
}
}
#[tokio::test]
async fn test_execute_subagent() {
let registry = Arc::new(RwLock::new(SkillRegistry::new()));
let executor = SkillExecutor::new(registry);
let mut skill = create_test_skill();
skill.execution_mode = SkillExecutionMode::Subagent;
let args = HashMap::new();
let result = executor.execute(&skill, args).await.unwrap();
match result {
SkillResult::Subagent { agent_id } => {
assert!(agent_id.starts_with("skill-test-skill-"));
}
_ => panic!("Expected subagent result"),
}
}
#[tokio::test]
async fn test_execute_script() {
let registry = Arc::new(RwLock::new(SkillRegistry::new()));
let executor = SkillExecutor::new(registry);
let mut skill = create_test_skill();
skill.execution_mode = SkillExecutionMode::Script;
skill.instructions = "let x = 1; x + 1".to_string();
let args = HashMap::new();
let result = executor.execute(&skill, args).await.unwrap();
match result {
SkillResult::Script { output, is_error } => {
assert!(!is_error);
assert!(output.contains("let x = 1"));
}
_ => panic!("Expected script result"),
}
}
#[tokio::test]
async fn test_filter_allowed_tools() {
let registry = Arc::new(RwLock::new(SkillRegistry::new()));
let executor = SkillExecutor::new(registry);
let skill = create_test_skill(); let available = create_available_tools();
let filtered = executor.filter_allowed_tools(&skill, &available);
assert_eq!(filtered.len(), 2);
assert!(filtered.contains(&"Read".to_string()));
assert!(filtered.contains(&"Grep".to_string()));
assert!(!filtered.contains(&"Write".to_string()));
assert!(!filtered.contains(&"git_diff".to_string()));
}
#[tokio::test]
async fn test_no_tool_restrictions() {
let registry = Arc::new(RwLock::new(SkillRegistry::new()));
let executor = SkillExecutor::new(registry);
let mut skill = create_test_skill();
skill.metadata.allowed_tools = None;
let available = create_available_tools();
let filtered = executor.filter_allowed_tools(&skill, &available);
assert_eq!(filtered.len(), 4);
}
#[tokio::test]
async fn test_prepare_subagent() {
let registry = Arc::new(RwLock::new(SkillRegistry::new()));
let executor = SkillExecutor::new(registry);
let mut skill = create_test_skill();
skill.execution_mode = SkillExecutionMode::Subagent;
let available = create_available_tools();
let mut args = HashMap::new();
args.insert("arg1".to_string(), "test_value".to_string());
let prepared = executor
.prepare_subagent(&skill, &available, args)
.await
.unwrap();
assert!(prepared.task_description.contains("test_value"));
assert!(prepared.system_prompt.contains("test-skill"));
assert_eq!(prepared.allowed_tool_names.len(), 2);
assert!(prepared.allowed_tool_names.contains(&"Read".to_string()));
assert!(prepared.allowed_tool_names.contains(&"Grep".to_string()));
}
#[tokio::test]
async fn test_prepare_script() {
let registry = Arc::new(RwLock::new(SkillRegistry::new()));
let executor = SkillExecutor::new(registry);
let mut skill = create_test_skill();
skill.execution_mode = SkillExecutionMode::Script;
skill.instructions = "let result = {{value}}; result".to_string();
let available = create_available_tools();
let mut args = HashMap::new();
args.insert("value".to_string(), "42".to_string());
let prepared = executor
.prepare_script(&skill, &available, args)
.await
.unwrap();
assert!(prepared.script_content.contains("let result = 42"));
assert_eq!(prepared.skill_name, "test-skill");
assert_eq!(prepared.allowed_tool_names.len(), 2);
}
}