Skip to main content

telos_agent/knowledge/skills/
loader.rs

1use crate::knowledge::skills::{Skill, SkillArg, SkillSource};
2use std::path::Path;
3
4impl SkillLoader {
5    /// Load all skills compiled into the binary.
6    pub fn load_bundled_skills() -> Vec<Skill> {
7        let mut skills = Vec::new();
8        let files: &[(&str, &str)] = &[
9            ("verify.md", include_str!("bundled/verify.md")),
10            ("debug.md", include_str!("bundled/debug.md")),
11            ("remember.md", include_str!("bundled/remember.md")),
12            ("brainstorm.md", include_str!("bundled/brainstorm.md")),
13            ("update-config.md", include_str!("bundled/update-config.md")),
14            ("explore.md", include_str!("bundled/explore.md")),
15            ("web-access.md", include_str!("bundled/web-access.md")),
16            ("powershell-use.md", include_str!("bundled/powershell-use.md")),
17        ];
18        for (_name, content) in files {
19            if let Some(skill) = Self::parse_skill(content, SkillSource::Bundled) {
20                skills.push(skill);
21            }
22        }
23        skills
24    }
25}
26
27/// Loads skills from a directory of markdown files.
28pub struct SkillLoader;
29
30impl SkillLoader {
31    /// Load a single skill from a markdown file with YAML frontmatter.
32    pub fn load_skill_file(path: &Path, source: SkillSource) -> Option<Skill> {
33        let content = std::fs::read_to_string(path).ok()?;
34        Self::parse_skill(&content, source)
35    }
36
37    /// Scan `dir` for `.md` files, parse YAML frontmatter, return skills.
38    /// Non-fatal errors (unparseable files) are logged and skipped.
39    pub fn load_from_dir(dir: &Path) -> Result<Vec<Skill>, std::io::Error> {
40        let mut skills = Vec::new();
41        if !dir.exists() {
42            return Ok(skills);
43        }
44        let entries = std::fs::read_dir(dir)?;
45        for entry in entries {
46            let entry = entry?;
47            let path = entry.path();
48            if path.extension().is_none_or(|ext| ext != "md") {
49                continue;
50            }
51            if let Ok(content) = std::fs::read_to_string(&path) {
52                if let Some(skill) = Self::parse_skill(&content, SkillSource::Project) {
53                    skills.push(skill);
54                } else {
55                    tracing::warn!("failed to parse skill file: {}", path.display());
56                }
57            }
58        }
59        Ok(skills)
60    }
61
62    /// Parse a single skill from markdown content with YAML frontmatter.
63    /// Frontmatter is delimited by `---` at the start and end.
64    fn parse_skill(content: &str, source: SkillSource) -> Option<Skill> {
65        let content = content.trim();
66        // Must start with "---"
67        let rest = content.strip_prefix("---")?;
68        // Find closing "---"
69        let (frontmatter, body) = rest.split_once("\n---")?;
70        let body = body.trim().to_string();
71
72        let fm: serde_yaml::Value = serde_yaml::from_str(frontmatter).ok()?;
73
74        let name = fm.get("name")?.as_str()?.to_string();
75        let description = fm.get("description")?.as_str()?.to_string();
76        let when_to_use = fm.get("whenToUse").and_then(|v| v.as_str()).map(String::from);
77        let prompt = fm.get("prompt")?.as_str()?.to_string();
78
79        let arguments = fm
80            .get("arguments")
81            .and_then(|v| v.as_sequence())
82            .map(|args| {
83                args.iter()
84                    .map(|a| SkillArg {
85                        name: a.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
86                        description: a
87                            .get("description")
88                            .and_then(|v| v.as_str())
89                            .unwrap_or("")
90                            .to_string(),
91                        required: a.get("required").and_then(|v| v.as_bool()).unwrap_or(false),
92                    })
93                    .collect()
94            })
95            .unwrap_or_default();
96
97        Some(Skill { name, description, when_to_use, prompt, arguments, body, source })
98    }
99}