Skip to main content

tycode_core/skills/
context.rs

1use std::sync::{Arc, RwLock};
2
3use crate::module::{ContextComponent, ContextComponentId};
4
5/// Context component ID for skills.
6pub const SKILLS_CONTEXT_ID: ContextComponentId = ContextComponentId("skills");
7
8/// Tracks which skills have been invoked in the current session.
9pub struct InvokedSkillsState {
10    /// Skills that have been invoked, with their instructions
11    invoked: RwLock<Vec<InvokedSkill>>,
12}
13
14/// Represents a skill that has been invoked.
15#[derive(Clone)]
16pub struct InvokedSkill {
17    pub name: String,
18    pub instructions: String,
19}
20
21impl InvokedSkillsState {
22    pub fn new() -> Self {
23        Self {
24            invoked: RwLock::new(Vec::new()),
25        }
26    }
27
28    /// Records that a skill has been invoked.
29    pub fn add_invoked(&self, name: String, instructions: String) {
30        let mut invoked = self.invoked.write().unwrap();
31        // Check if already invoked (don't duplicate)
32        if !invoked.iter().any(|s| s.name == name) {
33            invoked.push(InvokedSkill { name, instructions });
34        }
35    }
36
37    /// Clears all invoked skills (e.g., when starting a new conversation).
38    pub fn clear(&self) {
39        self.invoked.write().unwrap().clear();
40    }
41
42    /// Returns the list of invoked skills.
43    pub fn get_invoked(&self) -> Vec<InvokedSkill> {
44        self.invoked.read().unwrap().clone()
45    }
46
47    /// Checks if a skill has been invoked.
48    pub fn is_invoked(&self, name: &str) -> bool {
49        self.invoked.read().unwrap().iter().any(|s| s.name == name)
50    }
51}
52
53impl Default for InvokedSkillsState {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59/// Context component that shows currently active skills.
60///
61/// This shows which skills have been invoked in the current session,
62/// including their full instructions.
63pub struct SkillsContextComponent {
64    state: Arc<InvokedSkillsState>,
65}
66
67impl SkillsContextComponent {
68    pub fn new(state: Arc<InvokedSkillsState>) -> Self {
69        Self { state }
70    }
71}
72
73#[async_trait::async_trait(?Send)]
74impl ContextComponent for SkillsContextComponent {
75    fn id(&self) -> ContextComponentId {
76        SKILLS_CONTEXT_ID
77    }
78
79    async fn build_context_section(&self) -> Option<String> {
80        let invoked = self.state.get_invoked();
81
82        if invoked.is_empty() {
83            return None;
84        }
85
86        let mut output = String::new();
87        output.push_str("## Active Skills\n\n");
88        output.push_str("The following skills have been loaded for this task:\n\n");
89
90        for skill in &invoked {
91            output.push_str(&format!("### Skill: {}\n\n", skill.name));
92            output.push_str(&skill.instructions);
93            output.push_str("\n\n---\n\n");
94        }
95
96        Some(output)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[tokio::test]
105    async fn test_context_with_invoked_skills() {
106        let state = Arc::new(InvokedSkillsState::new());
107        state.add_invoked(
108            "commit".to_string(),
109            "# Commit Skill\n\nInstructions for committing.".to_string(),
110        );
111
112        let component = SkillsContextComponent::new(state);
113        let context = component.build_context_section().await.unwrap();
114
115        assert!(context.contains("## Active Skills"));
116        assert!(context.contains("### Skill: commit"));
117        assert!(context.contains("Instructions for committing"));
118    }
119
120    #[tokio::test]
121    async fn test_context_without_invoked_skills() {
122        let state = Arc::new(InvokedSkillsState::new());
123        let component = SkillsContextComponent::new(state);
124        let context = component.build_context_section().await;
125
126        assert!(context.is_none());
127    }
128
129    #[test]
130    fn test_no_duplicate_invocations() {
131        let state = InvokedSkillsState::new();
132        state.add_invoked("commit".to_string(), "Instructions 1".to_string());
133        state.add_invoked("commit".to_string(), "Instructions 2".to_string());
134
135        assert_eq!(state.get_invoked().len(), 1);
136    }
137}