spec-ai 0.8.4

A framework for building AI agents with structured outputs, policy enforcement, and execution tracking
Documentation
use crate::spec_ai_core::agent::skills::SkillScanner;
use crate::spec_ai_core::tools::{Tool, ToolResult};
use anyhow::Result;
use async_trait::async_trait;
use serde_json::{Value, json};
use std::path::PathBuf;

/// Tool for activating an Agent Skill (https://agentskills.io)
pub struct ActivateSkillTool {
    skills_dirs: Vec<PathBuf>,
}

impl ActivateSkillTool {
    pub fn new(skills_dirs: Vec<PathBuf>) -> Self {
        Self { skills_dirs }
    }
}

#[async_trait]
impl Tool for ActivateSkillTool {
    fn name(&self) -> &str {
        "activate_skill"
    }

    fn description(&self) -> &str {
        "Activates a specialized agent skill by name. Returns the skill's instructions wrapped in <activated_skill> tags. These provide specialized guidance for the current task. Use this when you identify a task that matches a skill's description. ONLY use names exactly as they appear in the <available_skills> section."
    }

    fn parameters(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "The exact name of the skill to activate"
                }
            },
            "required": ["name"]
        })
    }

    async fn execute(&self, args: Value) -> Result<ToolResult> {
        let name = args["name"]
            .as_str()
            .ok_or_else(|| anyhow::anyhow!("Missing 'name' argument"))?;

        let scanner = SkillScanner::new(self.skills_dirs.clone());
        let skills = scanner.scan();

        let skill = skills.into_iter().find(|s| s.name == name);

        match skill {
            Some(skill) => {
                let skill_content = std::fs::read_to_string(&skill.path)?;
                let dir = skill.path.parent().unwrap_or(&skill.path);

                // List files in the skill directory to aid discovery
                let files: Vec<String> = walkdir::WalkDir::new(dir)
                    .max_depth(2)
                    .into_iter()
                    .filter_map(|e| e.ok())
                    .map(|e| {
                        e.path()
                            .strip_prefix(dir)
                            .unwrap_or(e.path())
                            .to_string_lossy()
                            .to_string()
                    })
                    .filter(|s| !s.is_empty())
                    .collect();

                let mut output = format!(
                    "<activated_skill>\n<instructions>\n{}\n</instructions>\n",
                    skill_content
                );
                if !files.is_empty() {
                    output.push_str("<available_resources>\n");
                    for file in files {
                        output.push_str(&format!("- {}\n", file));
                    }
                    output.push_str("</available_resources>\n");
                }
                output.push_str("</activated_skill>");

                Ok(ToolResult::success(output))
            }
            None => Ok(ToolResult::failure(format!("Skill '{}' not found", name))),
        }
    }
}