matrixcode_core/tools/
skill.rs1use std::sync::Arc;
8
9use anyhow::Result;
10use async_trait::async_trait;
11use serde_json::{Value, json};
12
13use super::{Tool, ToolDefinition};
14use crate::skills::{Skill, list_skill_files};
15
16pub struct SkillTool {
17 skills: Arc<Vec<Skill>>,
18}
19
20impl SkillTool {
21 pub fn new(skills: Arc<Vec<Skill>>) -> Self {
22 Self { skills }
23 }
24}
25
26#[async_trait]
27impl Tool for SkillTool {
28 fn definition(&self) -> ToolDefinition {
29 let mut props = json!({
34 "name": {
35 "type": "string",
36 "description": "Name of the skill to load (must match one listed in the system prompt)."
37 }
38 });
39 if !self.skills.is_empty() {
40 let names: Vec<Value> = self
41 .skills
42 .iter()
43 .map(|s| Value::String(s.name.clone()))
44 .collect();
45 props["name"]["enum"] = Value::Array(names);
46 }
47
48 ToolDefinition {
49 name: "skill".to_string(),
50 description:
51 "Load the full instructions for a named skill. Call this when a skill \
52 listed in the system prompt looks relevant to the user's request. \
53 The response includes the skill's body and a list of files in its \
54 directory that you can read with the `read` tool."
55 .to_string(),
56 parameters: json!({
57 "type": "object",
58 "properties": props,
59 "required": ["name"]
60 }),
61 }
62 }
63
64 async fn execute(&self, params: Value) -> Result<String> {
65 let name = params["name"]
66 .as_str()
67 .ok_or_else(|| anyhow::anyhow!("missing 'name'"))?;
68
69 let skill = self
73 .skills
74 .iter()
75 .find(|s| s.name == name)
76 .ok_or_else(|| {
77 let available: Vec<&str> = self.skills.iter().map(|s| s.name.as_str()).collect();
78 anyhow::anyhow!(
80 "unknown skill '{}'. Available: {}",
81 name,
82 if available.is_empty() {
83 "(none loaded)".to_string()
84 } else {
85 available.join(", ")
86 }
87 )
88 })?;
89
90 let files = list_skill_files(&skill.dir);
91 let files_section = if files.is_empty() {
92 String::new()
93 } else {
94 let mut s = String::from("\n\n---\nFiles in this skill (read with the `read` tool, paths are relative to the skill directory):\n");
95 s.push_str(&format!("skill_dir: {}\n", skill.dir.display()));
96 for f in files {
97 s.push_str(&format!("- {}\n", f));
98 }
99 s
100 };
101
102 let result = format!(
103 "# Skill: {}\n\n{}{}",
104 skill.name,
105 skill.body.trim_end(),
106 files_section
107 );
108
109 Ok(result)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use std::path::PathBuf;
118
119 fn fake_skill(name: &str, body: &str) -> Skill {
120 Skill {
121 name: name.to_string(),
122 description: "desc".to_string(),
123 dir: PathBuf::from("/nonexistent"),
124 body: body.to_string(),
125 source_file: PathBuf::from("/nonexistent/SKILL.md"),
126 }
127 }
128
129 #[tokio::test]
130 async fn returns_skill_body() {
131 let skills = Arc::new(vec![fake_skill("demo", "do the thing")]);
132 let tool = SkillTool::new(skills);
133 let out = tool.execute(json!({"name": "demo"})).await.unwrap();
134 assert!(out.contains("# Skill: demo"));
135 assert!(out.contains("do the thing"));
136 }
137
138 #[tokio::test]
139 async fn unknown_skill_errors() {
140 let skills = Arc::new(vec![fake_skill("demo", "x")]);
141 let tool = SkillTool::new(skills);
142 let err = tool
143 .execute(json!({"name": "missing"}))
144 .await
145 .unwrap_err()
146 .to_string();
147 assert!(err.contains("unknown skill"));
148 assert!(err.contains("demo"));
149 }
150
151 #[tokio::test]
152 async fn missing_name_errors() {
153 let skills = Arc::new(vec![]);
154 let tool = SkillTool::new(skills);
155 let err = tool.execute(json!({})).await.unwrap_err().to_string();
156 assert!(err.contains("missing 'name'"));
157 }
158
159 #[test]
160 fn definition_enum_only_when_skills_present() {
161 let empty = SkillTool::new(Arc::new(vec![]));
162 let def = empty.definition();
163 assert!(def.parameters["properties"]["name"].get("enum").is_none());
164
165 let full = SkillTool::new(Arc::new(vec![fake_skill("a", ""), fake_skill("b", "")]));
166 let def = full.definition();
167 let names = def.parameters["properties"]["name"]["enum"]
168 .as_array()
169 .unwrap();
170 assert_eq!(names.len(), 2);
171 }
172}