telos_agent/knowledge/skills/
registry.rs1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::knowledge::skills::loader::SkillLoader;
5use crate::knowledge::skills::{Skill, SkillSource};
6
7#[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 pub fn register(&mut self, skill: Skill) {
23 self.skills.insert(skill.name.clone(), skill);
24 }
25
26 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 pub fn load_bundled_skills(&mut self) {
42 for skill in SkillLoader::load_bundled_skills() {
43 self.register(skill);
44 }
45 }
46
47 pub fn get(&self, name: &str) -> Option<&Skill> {
49 self.skills.get(name)
50 }
51
52 pub fn names(&self) -> Vec<&String> {
54 self.skills.keys().collect()
55 }
56
57 pub fn list(&self) -> Vec<&Skill> {
59 self.skills.values().collect()
60 }
61
62 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 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 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}