use async_trait::async_trait;
use serde::Deserialize;
use serde_json::{json, Value};
use crate::error::{Error, Result};
use crate::skills::LoopSkill;
use crate::tools::{Tool, ToolContext};
pub const SKILL_TOOL: &str = "skill";
const MAX_SUGGESTIONS: usize = 40;
pub struct SkillTool {
skills: Vec<LoopSkill>,
shell: crate::skills::ShellInjection,
}
#[derive(Deserialize)]
struct SkillArgs {
name: String,
#[serde(default)]
arguments: Option<String>,
}
impl SkillTool {
pub fn new(skills: Vec<LoopSkill>) -> Self {
Self {
skills,
shell: crate::skills::ShellInjection::disabled(),
}
}
pub fn with_shell(mut self, shell: crate::skills::ShellInjection) -> Self {
self.shell = shell;
self
}
pub fn skills(&self) -> &[LoopSkill] {
&self.skills
}
pub fn find(&self, name: &str) -> Option<&LoopSkill> {
crate::skills::find_skill(&self.skills, name)
}
fn unknown(&self, name: &str) -> Error {
let mut names: Vec<&str> = self
.skills
.iter()
.map(|skill| skill.name.as_str())
.take(MAX_SUGGESTIONS)
.collect();
names.sort_unstable();
let known = if names.is_empty() {
"no skills are installed in the roots this config reads".to_string()
} else {
format!("known skills: {}", names.join(", "))
};
Error::tool(SKILL_TOOL, format!("no skill named `{name}` — {known}"))
}
}
#[async_trait]
impl Tool for SkillTool {
fn name(&self) -> &str {
SKILL_TOOL
}
fn description(&self) -> &str {
"Load a skill's full instructions on demand. The system prompt lists only each \
skill's name and description; call this with a name from that list to read the \
skill's body into the conversation, then follow it. `arguments` is substituted \
for `$ARGUMENTS` in the body."
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Skill name exactly as the skills list gives it (qualified `dir:skill` / `plugin:skill` names included)."
},
"arguments": {
"type": "string",
"description": "Text substituted for `$ARGUMENTS` (and `$1`..`$9`) inside the skill body."
}
},
"required": ["name"],
"additionalProperties": false
})
}
async fn execute(&self, args: Value, _ctx: &ToolContext) -> Result<String> {
let a: SkillArgs = serde_json::from_value(args).map_err(|e| Error::InvalidArguments {
tool: SKILL_TOOL.to_string(),
message: e.to_string(),
})?;
let skill = self.find(&a.name).ok_or_else(|| self.unknown(&a.name))?;
let arguments = a.arguments.unwrap_or_default();
let body = skill
.body_with_shell(&arguments, &self.shell)
.map_err(|e| Error::tool(SKILL_TOOL, format!("{}: {e}", skill.manifest.display())))?;
Ok(crate::skills::render_skill(skill, &body))
}
}