skill-executor 0.2.1

Autonomous skill discovery agent with LLM tool chaining, powered by Rust.
Documentation
use crate::error::ExecutorError;
use skill_core::{ResourceType, Skill, SkillResult};
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Instant;
use tokio::process::Command;
use tracing::{debug, info};

pub mod context;
pub mod error;

pub use context::ExecutionContext;

pub struct SkillExecutor {
    skills_base_dir: PathBuf,
    default_timeout_secs: u64,
}

impl SkillExecutor {
    pub fn new(skills_base_dir: PathBuf) -> Self {
        Self {
            skills_base_dir,
            default_timeout_secs: 300,
        }
    }

    pub fn with_timeout(mut self, secs: u64) -> Self {
        self.default_timeout_secs = secs;
        self
    }

    pub async fn execute_skill(
        &self,
        skill: &Skill,
        input: Option<&str>,
        context: &ExecutionContext,
    ) -> Result<SkillResult, ExecutorError> {
        let start = Instant::now();

        info!("Executing skill: {}", skill.name);
        debug!("Skill resources: {:?}", skill.resources);

        let mut output = String::new();
        let mut errors = Vec::new();

        for resource in &skill.resources {
            if resource.resource_type == ResourceType::Script {
                match self.run_script(&resource.path, input, context).await {
                    Ok(result) => {
                        output.push_str(&format!("\n--- {} ---\n", resource.name));
                        output.push_str(&result);
                    }
                    Err(e) => {
                        errors.push(format!("{}: {}", resource.name, e));
                    }
                }
            }
        }

        if !skill.instructions.is_empty() {
            output.push_str("\n--- Instructions ---\n");
            output.push_str(&skill.instructions);
        }

        let execution_time_ms = start.elapsed().as_millis() as u64;
        let success = errors.is_empty();

        let error_msg = if errors.is_empty() {
            None
        } else {
            Some(errors.join("; "))
        };

        Ok(SkillResult {
            skill_id: skill.id.clone(),
            success,
            output,
            error: error_msg,
            execution_time_ms,
        })
    }

    async fn run_script(
        &self,
        script_path: &PathBuf,
        input: Option<&str>,
        context: &ExecutionContext,
    ) -> Result<String, ExecutorError> {
        debug!("Running script: {:?} with input: {:?}", script_path, input);

        let extension = script_path
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("");

        let (program, args) = match extension {
            "py" => ("python3", vec!["-u"]),
            "sh" => ("bash", vec![]),
            "rb" => ("ruby", vec![]),
            "js" => ("node", vec![]),
            _ => ("bash", vec![]),
        };

        let mut cmd = Command::new(program);
        for arg in &args {
            cmd.arg(arg);
        }
        cmd.arg(script_path)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .current_dir(&context.working_dir);

        for (key, value) in &context.env_vars {
            cmd.env(key, value);
        }

        if let Some(inp) = input {
            cmd.arg(inp);
        }

        let output = cmd
            .output()
            .await
            .map_err(|e| ExecutorError::ExecutionError(e.to_string()))?;

        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();

        if !output.status.success() {
            return Err(ExecutorError::ScriptError(format!(
                "Script failed: {}",
                stderr
            )));
        }

        Ok(stdout)
    }

    pub async fn get_skill_instructions(&self, skill: &Skill) -> Result<String, ExecutorError> {
        Ok(skill.instructions.clone())
    }

    pub async fn get_resource_content(
        &self,
        resource_path: &PathBuf,
    ) -> Result<String, ExecutorError> {
        let content = tokio::fs::read_to_string(resource_path)
            .await
            .map_err(|e| ExecutorError::IoError(e.to_string()))?;
        Ok(content)
    }
}