tycode_core/skills/
context.rs1use std::sync::{Arc, RwLock};
2
3use crate::module::{ContextComponent, ContextComponentId};
4
5pub const SKILLS_CONTEXT_ID: ContextComponentId = ContextComponentId("skills");
7
8pub struct InvokedSkillsState {
10 invoked: RwLock<Vec<InvokedSkill>>,
12}
13
14#[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 pub fn add_invoked(&self, name: String, instructions: String) {
30 let mut invoked = self.invoked.write().unwrap();
31 if !invoked.iter().any(|s| s.name == name) {
33 invoked.push(InvokedSkill { name, instructions });
34 }
35 }
36
37 pub fn clear(&self) {
39 self.invoked.write().unwrap().clear();
40 }
41
42 pub fn get_invoked(&self) -> Vec<InvokedSkill> {
44 self.invoked.read().unwrap().clone()
45 }
46
47 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
59pub 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}