Skip to main content

atomr_agents_skill/
lib.rs

1//! Skills: bundled instruction-fragment + tool overlay + optional
2//! sub-agents + optional memory namespace.
3
4use async_trait::async_trait;
5use atomr_agents_core::{AgentContext, MemoryNamespace, Result, SkillId, TokenBudget, ToolId};
6use atomr_agents_strategy::{SkillRef, SkillStrategy};
7use semver::Version;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Skill {
12    pub id: SkillId,
13    pub name: String,
14    pub instruction_fragment: String,
15    #[serde(default)]
16    pub tool_overlay: Vec<ToolId>,
17    #[serde(default)]
18    pub memory_namespace: Option<MemoryNamespace>,
19    /// Keywords that trigger `KeywordSkillStrategy`.
20    #[serde(default)]
21    pub keywords: Vec<String>,
22    #[serde(default = "default_priority")]
23    pub priority: u8,
24}
25
26fn default_priority() -> u8 {
27    5
28}
29
30#[derive(Clone)]
31pub struct SkillSet {
32    pub id: String,
33    pub version: Version,
34    pub skills: Vec<Skill>,
35}
36
37impl SkillSet {
38    pub fn new(id: impl Into<String>, version: Version, skills: Vec<Skill>) -> Self {
39        Self {
40            id: id.into(),
41            version,
42            skills,
43        }
44    }
45}
46
47/// Always picks the same fixed list of skills.
48pub struct StaticSkillStrategy {
49    skills: Vec<Skill>,
50}
51
52impl StaticSkillStrategy {
53    pub fn new(skills: Vec<Skill>) -> Self {
54        Self { skills }
55    }
56}
57
58#[async_trait]
59impl SkillStrategy for StaticSkillStrategy {
60    async fn applicable(&self, _ctx: &AgentContext, _budget: &mut TokenBudget) -> Result<Vec<SkillRef>> {
61        Ok(self
62            .skills
63            .iter()
64            .map(|s| SkillRef {
65                id: s.id.clone(),
66                name: s.name.clone(),
67                priority: s.priority,
68            })
69            .collect())
70    }
71}
72
73/// Returns the skills whose `keywords` overlap with the user turn.
74pub struct KeywordSkillStrategy {
75    skills: Vec<Skill>,
76}
77
78impl KeywordSkillStrategy {
79    pub fn new(skills: Vec<Skill>) -> Self {
80        Self { skills }
81    }
82}
83
84#[async_trait]
85impl SkillStrategy for KeywordSkillStrategy {
86    async fn applicable(&self, ctx: &AgentContext, _budget: &mut TokenBudget) -> Result<Vec<SkillRef>> {
87        let needle = ctx.turn.user.to_lowercase();
88        let mut out: Vec<SkillRef> = self
89            .skills
90            .iter()
91            .filter(|s| s.keywords.iter().any(|k| needle.contains(&k.to_lowercase())))
92            .map(|s| SkillRef {
93                id: s.id.clone(),
94                name: s.name.clone(),
95                priority: s.priority,
96            })
97            .collect();
98        out.sort_by_key(|s| std::cmp::Reverse(s.priority));
99        Ok(out)
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use atomr_agents_core::{AgentId, TurnInput};
107
108    fn ctx(text: &str) -> AgentContext {
109        AgentContext::for_agent(
110            AgentId::from("a-1"),
111            TurnInput {
112                user: text.into(),
113                history: vec![],
114            },
115        )
116    }
117
118    #[tokio::test]
119    async fn keyword_picks_matching_skills() {
120        let s1 = Skill {
121            id: SkillId::from("rag"),
122            name: "RAG".into(),
123            instruction_fragment: "use the index".into(),
124            tool_overlay: vec![],
125            memory_namespace: None,
126            keywords: vec!["search".into(), "lookup".into()],
127            priority: 7,
128        };
129        let s2 = Skill {
130            id: SkillId::from("math"),
131            name: "Math".into(),
132            instruction_fragment: "use the calculator".into(),
133            tool_overlay: vec![],
134            memory_namespace: None,
135            keywords: vec!["compute".into()],
136            priority: 3,
137        };
138        let strat = KeywordSkillStrategy::new(vec![s1, s2]);
139        let mut b = TokenBudget::new(1000);
140        let out = strat
141            .applicable(&ctx("please search for x"), &mut b)
142            .await
143            .unwrap();
144        assert_eq!(out.len(), 1);
145        assert_eq!(out[0].name, "RAG");
146    }
147}