Skip to main content

tycode_core/skills/
tool.rs

1use std::sync::Arc;
2
3use anyhow::{bail, Result};
4use serde_json::{json, Value};
5
6use crate::chat::events::{ToolExecutionResult, ToolRequest as ToolRequestEvent, ToolRequestType};
7use crate::tools::r#trait::{
8    ContinuationPreference, ToolCallHandle, ToolCategory, ToolExecutor, ToolOutput, ToolRequest,
9};
10use crate::tools::ToolName;
11
12use super::context::InvokedSkillsState;
13use super::discovery::SkillsManager;
14
15/// Tool for invoking skills and loading their instructions.
16pub struct InvokeSkillTool {
17    manager: SkillsManager,
18    state: Arc<InvokedSkillsState>,
19}
20
21impl InvokeSkillTool {
22    pub fn new(manager: SkillsManager, state: Arc<InvokedSkillsState>) -> Self {
23        Self { manager, state }
24    }
25
26    pub fn tool_name() -> ToolName {
27        ToolName::new("invoke_skill")
28    }
29}
30
31struct InvokeSkillHandle {
32    skill_name: String,
33    tool_use_id: String,
34    manager: SkillsManager,
35    state: Arc<InvokedSkillsState>,
36}
37
38#[async_trait::async_trait(?Send)]
39impl ToolCallHandle for InvokeSkillHandle {
40    fn tool_request(&self) -> ToolRequestEvent {
41        ToolRequestEvent {
42            tool_call_id: self.tool_use_id.clone(),
43            tool_name: "invoke_skill".to_string(),
44            tool_type: ToolRequestType::Other {
45                args: json!({ "skill_name": self.skill_name }),
46            },
47        }
48    }
49
50    async fn execute(self: Box<Self>) -> ToolOutput {
51        // Load the skill instructions
52        match self.manager.load_instructions(&self.skill_name) {
53            Ok(skill) => {
54                // Record that this skill has been invoked
55                self.state
56                    .add_invoked(skill.metadata.name.clone(), skill.instructions.clone());
57
58                // Build the response
59                let mut response = format!(
60                    "Skill '{}' loaded successfully.\n\n## Instructions\n\n{}",
61                    skill.metadata.name, skill.instructions
62                );
63
64                // Include reference files if any
65                if !skill.reference_files.is_empty() {
66                    response.push_str("\n\n## Reference Files\n\n");
67                    response.push_str(
68                        "The following reference files are available. Use the read_file tool to access them:\n",
69                    );
70                    for file in &skill.reference_files {
71                        response.push_str(&format!("- {}\n", file.display()));
72                    }
73                }
74
75                // Include scripts if any
76                if !skill.scripts.is_empty() {
77                    response.push_str("\n\n## Scripts\n\n");
78                    response
79                        .push_str("The following scripts are available for use with this skill:\n");
80                    for script in &skill.scripts {
81                        response.push_str(&format!("- {}\n", script.display()));
82                    }
83                }
84
85                ToolOutput::Result {
86                    content: response,
87                    is_error: false,
88                    continuation: ContinuationPreference::Continue,
89                    ui_result: ToolExecutionResult::Other {
90                        result: json!({
91                            "skill_name": skill.metadata.name,
92                            "source": format!("{}", skill.metadata.source),
93                        }),
94                    },
95                }
96            }
97            Err(e) => ToolOutput::Result {
98                content: format!("Failed to load skill '{}': {}", self.skill_name, e),
99                is_error: true,
100                continuation: ContinuationPreference::Continue,
101                ui_result: ToolExecutionResult::Error {
102                    short_message: format!("Skill '{}' not found", self.skill_name),
103                    detailed_message: e.to_string(),
104                },
105            },
106        }
107    }
108}
109
110#[async_trait::async_trait(?Send)]
111impl ToolExecutor for InvokeSkillTool {
112    fn name(&self) -> String {
113        "invoke_skill".to_string()
114    }
115
116    fn description(&self) -> String {
117        "Load and activate a skill's instructions. Use this when a user's request matches \
118         a skill's description from the Available Skills list. The skill will provide \
119         detailed instructions for how to proceed with the task."
120            .to_string()
121    }
122
123    fn input_schema(&self) -> Value {
124        json!({
125            "type": "object",
126            "properties": {
127                "skill_name": {
128                    "type": "string",
129                    "description": "The name of the skill to invoke (from the Available Skills list)"
130                }
131            },
132            "required": ["skill_name"]
133        })
134    }
135
136    fn category(&self) -> ToolCategory {
137        ToolCategory::Meta
138    }
139
140    async fn process(&self, request: &ToolRequest) -> Result<Box<dyn ToolCallHandle>> {
141        let Some(skill_name) = request.arguments["skill_name"].as_str() else {
142            bail!("Missing required argument \"skill_name\"");
143        };
144
145        // Check if skill exists and is enabled
146        if !self.manager.is_enabled(skill_name) {
147            if self.manager.get_skill(skill_name).is_some() {
148                bail!("Skill '{}' is disabled", skill_name);
149            } else {
150                bail!(
151                    "Skill '{}' not found. Use /skills to list available skills.",
152                    skill_name
153                );
154            }
155        }
156
157        Ok(Box::new(InvokeSkillHandle {
158            skill_name: skill_name.to_string(),
159            tool_use_id: request.tool_use_id.clone(),
160            manager: self.manager.clone(),
161            state: self.state.clone(),
162        }))
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::settings::config::SkillsConfig;
170    use std::fs;
171    use tempfile::TempDir;
172
173    fn create_test_skill(dir: &std::path::Path, name: &str, description: &str, instructions: &str) {
174        let skill_dir = dir.join(name);
175        fs::create_dir_all(&skill_dir).unwrap();
176
177        let content = format!(
178            r#"---
179name: {}
180description: {}
181---
182
183{}
184"#,
185            name, description, instructions
186        );
187
188        fs::write(skill_dir.join("SKILL.md"), content).unwrap();
189    }
190
191    #[tokio::test]
192    async fn test_invoke_skill_success() {
193        let temp = TempDir::new().unwrap();
194        let skills_dir = temp.path().join(".tycode").join("skills");
195        fs::create_dir_all(&skills_dir).unwrap();
196
197        create_test_skill(
198            &skills_dir,
199            "test-skill",
200            "A test skill",
201            "# Test Instructions\n\nFollow these steps.",
202        );
203
204        let config = SkillsConfig::default();
205        let manager = SkillsManager::discover(&[], temp.path(), &config);
206        let state = Arc::new(InvokedSkillsState::new());
207        let tool = InvokeSkillTool::new(manager, state.clone());
208
209        let request = ToolRequest::new(json!({"skill_name": "test-skill"}), "test-id".to_string());
210
211        let handle = tool.process(&request).await.unwrap();
212        let output = handle.execute().await;
213
214        if let ToolOutput::Result {
215            content, is_error, ..
216        } = output
217        {
218            assert!(!is_error);
219            assert!(content.contains("Test Instructions"));
220            assert!(state.is_invoked("test-skill"));
221        } else {
222            panic!("Expected ToolOutput::Result");
223        }
224    }
225
226    #[tokio::test]
227    async fn test_invoke_skill_not_found() {
228        let temp = TempDir::new().unwrap();
229
230        let config = SkillsConfig::default();
231        let manager = SkillsManager::discover(&[], temp.path(), &config);
232        let state = Arc::new(InvokedSkillsState::new());
233        let tool = InvokeSkillTool::new(manager, state);
234
235        let request = ToolRequest::new(json!({"skill_name": "nonexistent"}), "test-id".to_string());
236
237        let result = tool.process(&request).await;
238        assert!(result.is_err());
239    }
240}