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
53【阻塞要求】当技能匹配用户请求时,必须在生成任何其他响应前调用此工具。
54
55重要规则:
56- 可用技能列在系统提示的 [AVAILABLE SKILLS] 部分
57- 用户引用 \"slash command\" 或 \"/<something>\" 时,指的是技能
58- 绝不要提及技能而不实际调用此工具
59- 不要调用已在运行的技能
60- 如果看到 <command-name> 标签,技能已加载 — 直接遵循指令
61
62返回:包含技能内容和技能目录中的文件列表,可用 `read` 工具读取。"
63 .to_string(),
64 parameters: json!({
65 "type": "object",
66 "properties": props,
67 "required": ["name"]
68 }),
69 ..Default::default()
70 }
71 }
72
73 async fn execute(&self, params: Value) -> Result<String> {
74 let name = params["name"]
75 .as_str()
76 .ok_or_else(|| anyhow::anyhow!("missing 'name'"))?;
77
78 let skill = self.skills.iter().find(|s| s.name == name).ok_or_else(|| {
82 let available: Vec<&str> = self.skills.iter().map(|s| s.name.as_str()).collect();
83 anyhow::anyhow!(
85 "unknown skill '{}'. Available: {}",
86 name,
87 if available.is_empty() {
88 "(none loaded)".to_string()
89 } else {
90 available.join(", ")
91 }
92 )
93 })?;
94
95 let files = list_skill_files(&skill.dir);
96 let files_section = if files.is_empty() {
97 String::new()
98 } else {
99 let mut s = String::from(
100 "\n\n---\nFiles in this skill (read with the `read` tool, paths are relative to the skill directory):\n",
101 );
102 s.push_str(&format!("skill_dir: {}\n", skill.dir.display()));
103 for f in files {
104 s.push_str(&format!("- {}\n", f));
105 }
106 s
107 };
108
109 let result = format!(
110 "# Skill: {}\n\n{}{}",
111 skill.name,
112 skill.body.trim_end(),
113 files_section
114 );
115
116 Ok(result)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use std::path::PathBuf;
125
126 fn fake_skill(name: &str, body: &str) -> Skill {
127 Skill {
128 name: name.to_string(),
129 description: "desc".to_string(),
130 dir: PathBuf::from("/nonexistent"),
131 body: body.to_string(),
132 source_file: PathBuf::from("/nonexistent/SKILL.md"),
133 }
134 }
135
136 #[tokio::test]
137 async fn returns_skill_body() {
138 let skills = Arc::new(vec![fake_skill("demo", "do the thing")]);
139 let tool = SkillTool::new(skills);
140 let out = tool.execute(json!({"name": "demo"})).await.unwrap();
141 assert!(out.contains("# Skill: demo"));
142 assert!(out.contains("do the thing"));
143 }
144
145 #[tokio::test]
146 async fn unknown_skill_errors() {
147 let skills = Arc::new(vec![fake_skill("demo", "x")]);
148 let tool = SkillTool::new(skills);
149 let err = tool
150 .execute(json!({"name": "missing"}))
151 .await
152 .unwrap_err()
153 .to_string();
154 assert!(err.contains("unknown skill"));
155 assert!(err.contains("demo"));
156 }
157
158 #[tokio::test]
159 async fn missing_name_errors() {
160 let skills = Arc::new(vec![]);
161 let tool = SkillTool::new(skills);
162 let err = tool.execute(json!({})).await.unwrap_err().to_string();
163 assert!(err.contains("missing 'name'"));
164 }
165
166 #[test]
167 fn definition_enum_only_when_skills_present() {
168 let empty = SkillTool::new(Arc::new(vec![]));
169 let def = empty.definition();
170 assert!(def.parameters["properties"]["name"].get("enum").is_none());
171
172 let full = SkillTool::new(Arc::new(vec![fake_skill("a", ""), fake_skill("b", "")]));
173 let def = full.definition();
174 let names = def.parameters["properties"]["name"]["enum"]
175 .as_array()
176 .unwrap();
177 assert_eq!(names.len(), 2);
178 }
179}