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": "要加载的技能名称(必须匹配系统提示中列出的名称)"
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 "加载指定技能的完整指令。当系统提示中列出的技能与用户请求相关时调用此工具。\
52 返回包含技能内容和技能目录中的文件列表,可用 `read` 工具读取。"
53 .to_string(),
54 parameters: json!({
55 "type": "object",
56 "properties": props,
57 "required": ["name"]
58 }),
59 }
60 }
61
62 async fn execute(&self, params: Value) -> Result<String> {
63 let name = params["name"]
64 .as_str()
65 .ok_or_else(|| anyhow::anyhow!("missing 'name'"))?;
66
67 let skill = self.skills.iter().find(|s| s.name == name).ok_or_else(|| {
71 let available: Vec<&str> = self.skills.iter().map(|s| s.name.as_str()).collect();
72 anyhow::anyhow!(
74 "unknown skill '{}'. Available: {}",
75 name,
76 if available.is_empty() {
77 "(none loaded)".to_string()
78 } else {
79 available.join(", ")
80 }
81 )
82 })?;
83
84 let files = list_skill_files(&skill.dir);
85 let files_section = if files.is_empty() {
86 String::new()
87 } else {
88 let mut s = String::from(
89 "\n\n---\nFiles in this skill (read with the `read` tool, paths are relative to the skill directory):\n",
90 );
91 s.push_str(&format!("skill_dir: {}\n", skill.dir.display()));
92 for f in files {
93 s.push_str(&format!("- {}\n", f));
94 }
95 s
96 };
97
98 let result = format!(
99 "# Skill: {}\n\n{}{}",
100 skill.name,
101 skill.body.trim_end(),
102 files_section
103 );
104
105 Ok(result)
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use std::path::PathBuf;
114
115 fn fake_skill(name: &str, body: &str) -> Skill {
116 Skill {
117 name: name.to_string(),
118 description: "desc".to_string(),
119 dir: PathBuf::from("/nonexistent"),
120 body: body.to_string(),
121 source_file: PathBuf::from("/nonexistent/SKILL.md"),
122 }
123 }
124
125 #[tokio::test]
126 async fn returns_skill_body() {
127 let skills = Arc::new(vec![fake_skill("demo", "do the thing")]);
128 let tool = SkillTool::new(skills);
129 let out = tool.execute(json!({"name": "demo"})).await.unwrap();
130 assert!(out.contains("# Skill: demo"));
131 assert!(out.contains("do the thing"));
132 }
133
134 #[tokio::test]
135 async fn unknown_skill_errors() {
136 let skills = Arc::new(vec![fake_skill("demo", "x")]);
137 let tool = SkillTool::new(skills);
138 let err = tool
139 .execute(json!({"name": "missing"}))
140 .await
141 .unwrap_err()
142 .to_string();
143 assert!(err.contains("unknown skill"));
144 assert!(err.contains("demo"));
145 }
146
147 #[tokio::test]
148 async fn missing_name_errors() {
149 let skills = Arc::new(vec![]);
150 let tool = SkillTool::new(skills);
151 let err = tool.execute(json!({})).await.unwrap_err().to_string();
152 assert!(err.contains("missing 'name'"));
153 }
154
155 #[test]
156 fn definition_enum_only_when_skills_present() {
157 let empty = SkillTool::new(Arc::new(vec![]));
158 let def = empty.definition();
159 assert!(def.parameters["properties"]["name"].get("enum").is_none());
160
161 let full = SkillTool::new(Arc::new(vec![fake_skill("a", ""), fake_skill("b", "")]));
162 let def = full.definition();
163 let names = def.parameters["properties"]["name"]["enum"]
164 .as_array()
165 .unwrap();
166 assert_eq!(names.len(), 2);
167 }
168}