use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use serde_json::{Value, json};
use super::{Tool, ToolDefinition};
use crate::skills::{Skill, list_skill_files};
pub struct SkillTool {
skills: Arc<Vec<Skill>>,
}
impl SkillTool {
pub fn new(skills: Arc<Vec<Skill>>) -> Self {
Self { skills }
}
}
#[async_trait]
impl Tool for SkillTool {
fn definition(&self) -> ToolDefinition {
let mut props = json!({
"name": {
"type": "string",
"description": "要加载的技能名称(必须匹配系统提示中列出的名称)"
}
});
if !self.skills.is_empty() {
let names: Vec<Value> = self
.skills
.iter()
.map(|s| Value::String(s.name.clone()))
.collect();
props["name"]["enum"] = Value::Array(names);
}
ToolDefinition {
name: "skill".to_string(),
description:
"加载指定技能的完整指令。当系统提示中列出的技能与用户请求相关时调用此工具。\
返回包含技能内容和技能目录中的文件列表,可用 `read` 工具读取。"
.to_string(),
parameters: json!({
"type": "object",
"properties": props,
"required": ["name"]
}),
}
}
async fn execute(&self, params: Value) -> Result<String> {
let name = params["name"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("missing 'name'"))?;
let skill = self.skills.iter().find(|s| s.name == name).ok_or_else(|| {
let available: Vec<&str> = self.skills.iter().map(|s| s.name.as_str()).collect();
anyhow::anyhow!(
"unknown skill '{}'. Available: {}",
name,
if available.is_empty() {
"(none loaded)".to_string()
} else {
available.join(", ")
}
)
})?;
let files = list_skill_files(&skill.dir);
let files_section = if files.is_empty() {
String::new()
} else {
let mut s = String::from(
"\n\n---\nFiles in this skill (read with the `read` tool, paths are relative to the skill directory):\n",
);
s.push_str(&format!("skill_dir: {}\n", skill.dir.display()));
for f in files {
s.push_str(&format!("- {}\n", f));
}
s
};
let result = format!(
"# Skill: {}\n\n{}{}",
skill.name,
skill.body.trim_end(),
files_section
);
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn fake_skill(name: &str, body: &str) -> Skill {
Skill {
name: name.to_string(),
description: "desc".to_string(),
dir: PathBuf::from("/nonexistent"),
body: body.to_string(),
source_file: PathBuf::from("/nonexistent/SKILL.md"),
}
}
#[tokio::test]
async fn returns_skill_body() {
let skills = Arc::new(vec![fake_skill("demo", "do the thing")]);
let tool = SkillTool::new(skills);
let out = tool.execute(json!({"name": "demo"})).await.unwrap();
assert!(out.contains("# Skill: demo"));
assert!(out.contains("do the thing"));
}
#[tokio::test]
async fn unknown_skill_errors() {
let skills = Arc::new(vec![fake_skill("demo", "x")]);
let tool = SkillTool::new(skills);
let err = tool
.execute(json!({"name": "missing"}))
.await
.unwrap_err()
.to_string();
assert!(err.contains("unknown skill"));
assert!(err.contains("demo"));
}
#[tokio::test]
async fn missing_name_errors() {
let skills = Arc::new(vec![]);
let tool = SkillTool::new(skills);
let err = tool.execute(json!({})).await.unwrap_err().to_string();
assert!(err.contains("missing 'name'"));
}
#[test]
fn definition_enum_only_when_skills_present() {
let empty = SkillTool::new(Arc::new(vec![]));
let def = empty.definition();
assert!(def.parameters["properties"]["name"].get("enum").is_none());
let full = SkillTool::new(Arc::new(vec![fake_skill("a", ""), fake_skill("b", "")]));
let def = full.definition();
let names = def.parameters["properties"]["name"]["enum"]
.as_array()
.unwrap();
assert_eq!(names.len(), 2);
}
}