Skip to main content

skill_executor/
lib.rs

1use crate::error::ExecutorError;
2use skill_core::{ResourceType, Skill, SkillResult};
3use std::path::PathBuf;
4use std::process::Stdio;
5use std::time::Instant;
6use tokio::process::Command;
7use tracing::{debug, info};
8
9pub mod context;
10pub mod error;
11
12pub use context::ExecutionContext;
13
14pub struct SkillExecutor {
15    skills_base_dir: PathBuf,
16    default_timeout_secs: u64,
17}
18
19impl SkillExecutor {
20    pub fn new(skills_base_dir: PathBuf) -> Self {
21        Self {
22            skills_base_dir,
23            default_timeout_secs: 300,
24        }
25    }
26
27    pub fn with_timeout(mut self, secs: u64) -> Self {
28        self.default_timeout_secs = secs;
29        self
30    }
31
32    pub async fn execute_skill(
33        &self,
34        skill: &Skill,
35        input: Option<&str>,
36        context: &ExecutionContext,
37    ) -> Result<SkillResult, ExecutorError> {
38        let start = Instant::now();
39
40        info!("Executing skill: {}", skill.name);
41        debug!("Skill resources: {:?}", skill.resources);
42
43        let mut output = String::new();
44        let mut errors = Vec::new();
45
46        for resource in &skill.resources {
47            if resource.resource_type == ResourceType::Script {
48                match self.run_script(&resource.path, input, context).await {
49                    Ok(result) => {
50                        output.push_str(&format!("\n--- {} ---\n", resource.name));
51                        output.push_str(&result);
52                    }
53                    Err(e) => {
54                        errors.push(format!("{}: {}", resource.name, e));
55                    }
56                }
57            }
58        }
59
60        if !skill.instructions.is_empty() {
61            output.push_str("\n--- Instructions ---\n");
62            output.push_str(&skill.instructions);
63        }
64
65        let execution_time_ms = start.elapsed().as_millis() as u64;
66        let success = errors.is_empty();
67
68        let error_msg = if errors.is_empty() {
69            None
70        } else {
71            Some(errors.join("; "))
72        };
73
74        Ok(SkillResult {
75            skill_id: skill.id.clone(),
76            success,
77            output,
78            error: error_msg,
79            execution_time_ms,
80        })
81    }
82
83    async fn run_script(
84        &self,
85        script_path: &PathBuf,
86        input: Option<&str>,
87        context: &ExecutionContext,
88    ) -> Result<String, ExecutorError> {
89        debug!("Running script: {:?} with input: {:?}", script_path, input);
90
91        let extension = script_path
92            .extension()
93            .and_then(|e| e.to_str())
94            .unwrap_or("");
95
96        let (program, args) = match extension {
97            "py" => ("python3", vec!["-u"]),
98            "sh" => ("bash", vec![]),
99            "rb" => ("ruby", vec![]),
100            "js" => ("node", vec![]),
101            _ => ("bash", vec![]),
102        };
103
104        let mut cmd = Command::new(program);
105        for arg in &args {
106            cmd.arg(arg);
107        }
108        cmd.arg(script_path)
109            .stdout(Stdio::piped())
110            .stderr(Stdio::piped())
111            .current_dir(&context.working_dir);
112
113        for (key, value) in &context.env_vars {
114            cmd.env(key, value);
115        }
116
117        if let Some(inp) = input {
118            cmd.arg(inp);
119        }
120
121        let output = cmd
122            .output()
123            .await
124            .map_err(|e| ExecutorError::ExecutionError(e.to_string()))?;
125
126        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
127        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
128
129        if !output.status.success() {
130            return Err(ExecutorError::ScriptError(format!(
131                "Script failed: {}",
132                stderr
133            )));
134        }
135
136        Ok(stdout)
137    }
138
139    pub async fn get_skill_instructions(&self, skill: &Skill) -> Result<String, ExecutorError> {
140        Ok(skill.instructions.clone())
141    }
142
143    pub async fn get_resource_content(
144        &self,
145        resource_path: &PathBuf,
146    ) -> Result<String, ExecutorError> {
147        let content = tokio::fs::read_to_string(resource_path)
148            .await
149            .map_err(|e| ExecutorError::IoError(e.to_string()))?;
150        Ok(content)
151    }
152}