Skip to main content

telos_agent/knowledge/skills/
registry.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::knowledge::skills::loader::SkillLoader;
5use crate::knowledge::skills::{Skill, SkillSource};
6
7/// Name-indexed collection of skills with override priority.
8///
9/// Later registrations with the same name override earlier ones,
10/// implementing the priority chain: Bundled < Managed < Project < User.
11#[derive(Debug, Default, Clone)]
12pub struct SkillRegistry {
13    skills: HashMap<String, Skill>,
14}
15
16impl SkillRegistry {
17    pub fn new() -> Self {
18        Self::default()
19    }
20
21    /// Register a skill. Same name → later wins.
22    pub fn register(&mut self, skill: Skill) {
23        self.skills.insert(skill.name.clone(), skill);
24    }
25
26    /// Load all .md files from dir and register them with the given source.
27    pub fn inject_skills_from_dir(
28        &mut self,
29        dir: &Path,
30        source: SkillSource,
31    ) -> std::io::Result<()> {
32        let skills = SkillLoader::load_from_dir(dir)?;
33        for mut skill in skills {
34            skill.source = source.clone();
35            self.register(skill);
36        }
37        Ok(())
38    }
39
40    /// Load all skills bundled with the crate and register them.
41    pub fn load_bundled_skills(&mut self) {
42        for skill in SkillLoader::load_bundled_skills() {
43            self.register(skill);
44        }
45    }
46
47    /// Look up a skill by name.
48    pub fn get(&self, name: &str) -> Option<&Skill> {
49        self.skills.get(name)
50    }
51
52    /// All registered skill names.
53    pub fn names(&self) -> Vec<&String> {
54        self.skills.keys().collect()
55    }
56
57    /// All registered skills.
58    pub fn list(&self) -> Vec<&Skill> {
59        self.skills.values().collect()
60    }
61
62    /// Retrieve the most relevant skills for a free-text query.
63    pub fn retrieve(&self, query: &str, limit: usize) -> Vec<&Skill> {
64        if limit == 0 {
65            return Vec::new();
66        }
67
68        let query_terms = tokenize(query);
69        if query_terms.is_empty() {
70            return Vec::new();
71        }
72
73        let mut ranked = self
74            .skills
75            .values()
76            .filter_map(|skill| {
77                let score = score_skill(skill, &query_terms);
78                (score > 0).then_some((score, skill))
79            })
80            .collect::<Vec<_>>();
81
82        ranked.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.name.cmp(&b.1.name)));
83        ranked.into_iter().take(limit).map(|(_, skill)| skill).collect()
84    }
85
86    /// Render a compact skill index for lightweight prompt injection.
87    pub fn render_index_for_prompt(&self) -> String {
88        if self.skills.is_empty() {
89            return String::new();
90        }
91
92        let mut lines = vec![
93            "## Skill Index".to_string(),
94            "Use the Skill tool only for skills listed as available or recommended below."
95                .to_string(),
96        ];
97        let mut skills: Vec<&Skill> = self.skills.values().collect();
98        skills.sort_by(|a, b| a.name.cmp(&b.name));
99        for skill in skills {
100            lines.push(format!("- **{}**: {}", skill.name, skill.description));
101        }
102        lines.join("\n")
103    }
104
105    /// Render the skills list for injection into the system prompt.
106    pub fn render_for_prompt(&self) -> String {
107        if self.skills.is_empty() {
108            return String::new();
109        }
110        let mut lines = vec!["## Available Skills".to_string()];
111        lines.push("You can invoke skills via the Skill tool. Available skills:".to_string());
112        let mut skills: Vec<&Skill> = self.skills.values().collect();
113        skills.sort_by(|a, b| a.name.cmp(&b.name));
114        for skill in skills {
115            let when =
116                skill.when_to_use.as_ref().map(|w| format!(" — Use when: {w}")).unwrap_or_default();
117            lines.push(format!("- **{}**: {}{}", skill.name, skill.description, when));
118        }
119        lines.join("\n")
120    }
121}
122
123fn tokenize(text: &str) -> Vec<String> {
124    text.split(|c: char| !c.is_ascii_alphanumeric())
125        .filter(|part| !part.is_empty())
126        .map(|part| part.to_ascii_lowercase())
127        .collect()
128}
129
130fn score_skill(skill: &Skill, query_terms: &[String]) -> usize {
131    let name = skill.name.to_ascii_lowercase();
132    let description = skill.description.to_ascii_lowercase();
133    let when = skill.when_to_use.as_deref().unwrap_or("").to_ascii_lowercase();
134    let body = skill.body.to_ascii_lowercase();
135    let prompt = skill.prompt.to_ascii_lowercase();
136
137    query_terms.iter().fold(0usize, |score, term| {
138        score
139            + if name.contains(term) { 6 } else { 0 }
140            + if description.contains(term) { 4 } else { 0 }
141            + if when.contains(term) { 3 } else { 0 }
142            + if prompt.contains(term) { 2 } else { 0 }
143            + if body.contains(term) { 1 } else { 0 }
144    })
145}