Skip to main content

robit_agent/tool/
load_skill.rs

1//! `load_skill` tool — loads a skill's full content by name.
2//!
3//! Returns metadata (name, description), the markdown body, and the source
4//! file path so the LLM can optionally use `read` to inspect the raw file.
5
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use serde::Deserialize;
10use serde_json::Value;
11
12use super::{Tool, ToolContext, ToolResult};
13use crate::error::Result;
14use crate::skill::SkillRegistry;
15
16pub struct LoadSkillTool {
17    skills: Arc<SkillRegistry>,
18}
19
20#[derive(Debug, Deserialize)]
21struct LoadSkillArgs {
22    /// Skill name to load. Use /skills to list available skills.
23    skill_name: String,
24}
25
26impl LoadSkillTool {
27    pub fn new(skills: Arc<SkillRegistry>) -> Self {
28        Self { skills }
29    }
30}
31
32#[async_trait]
33impl Tool for LoadSkillTool {
34    fn name(&self) -> &str {
35        "load_skill"
36    }
37
38    fn description(&self) -> &str {
39        "Load detailed content of a specified skill. Returns skill metadata (name, description), full Markdown content, and source file path."
40    }
41
42    fn parameters_schema(&self) -> Value {
43        serde_json::json!({
44            "type": "object",
45            "properties": {
46                "skill_name": {
47                    "type": "string",
48                    "description": "Name of the skill to load (English name, exact match)"
49                }
50            },
51            "required": ["skill_name"]
52        })
53    }
54
55    fn requires_confirmation(&self) -> bool {
56        false
57    }
58
59    async fn execute(&self, args: Value, _ctx: &ToolContext) -> Result<ToolResult> {
60        let parsed: LoadSkillArgs = match serde_json::from_value(args) {
61            Ok(a) => a,
62            Err(e) => return Ok(ToolResult::error(format!("Argument parsing failed: {}", e))),
63        };
64
65        let skill_name = parsed.skill_name.trim();
66
67        match self.skills.get(skill_name) {
68            Some(skill) => {
69                let output = format!(
70                    "## Skill: {name}\n\n\
71                     **Description**: {description}\n\
72                     **Version**: {version}\n\
73                     **Trigger commands**: {triggers}\n\
74                     **Source file**: {source_path}\n\n\
75                     ---\n\n\
76                     {content}",
77                    name = skill.frontmatter.name,
78                    description = skill.frontmatter.description,
79                    version = skill.frontmatter.version,
80                    triggers = if skill.frontmatter.triggers.is_empty() {
81                        "(None)".to_string()
82                    } else {
83                        skill.frontmatter.triggers.join(", ")
84                    },
85                    source_path = skill.source_path.display(),
86                    content = skill.content,
87                );
88                Ok(ToolResult::success(output))
89            }
90            None => {
91                let available: Vec<&str> = self.skills.skill_names();
92                Ok(ToolResult::error(format!(
93                    "Skill '{}' does not exist. Available skills: {:?}",
94                    skill_name, available
95                )))
96            }
97        }
98    }
99}