use crate::context;
use crate::tools::context::ToolContext;
use crate::tools::{Tool, ToolExecError};
use schemars::JsonSchema;
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Deserialize, JsonSchema)]
pub(crate) struct LoadSkillArgs {
name: String,
}
fn execute_load_skill(
args: &LoadSkillArgs,
working_dir: Option<&Path>,
skills: Option<&[context::SkillMeta]>,
) -> Result<String, ToolExecError> {
let body = match skills {
Some(skills) => context::load_skill_body_from(skills, &args.name),
None => context::load_skill_body(&args.name, working_dir),
}
.ok_or_else(|| ToolExecError(format!("skill not found: {}", args.name)))?;
let skill_message = format!(
"The following skill instructions are now active:\n\n<skill name=\"{name}\">\n{body}\n</skill>",
name = args.name,
);
Ok(format!(
"Loaded skill: {}\n\n---\n{}",
args.name, skill_message
))
}
pub fn describe_load_skill_invocation(args: &LoadSkillArgs) -> String {
format!("Loading skill `{}`.", args.name)
}
pub(crate) struct LoadSkill;
impl Tool for LoadSkill {
type Args = LoadSkillArgs;
type Return = String;
type Error = ToolExecError;
fn name(&self) -> &'static str {
"load_skill"
}
fn group(&self) -> &'static str {
"core"
}
fn description(&self) -> &'static str {
"Load the full instructions for a skill by name. Use this when a task matches one of the available skill descriptions."
}
fn describe_invocation(&self, args: &Self::Args) -> String {
describe_load_skill_invocation(args)
}
fn return_string(ret: &Self::Return) -> String {
ret.clone()
}
fn execute(
&self,
args: Self::Args,
_x_credentials: Option<&crate::tools::ServiceCredential>,
working_dir: Option<&std::path::Path>,
ctx: Option<&ToolContext>,
) -> Result<Self::Return, Self::Error> {
let skills = ctx
.and_then(|c| c.discovered_skills.as_deref())
.map(|v| v.as_slice());
execute_load_skill(&args, working_dir, skills)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn execute_load_skill_not_found() {
let dir = tempfile::tempdir().unwrap();
let result = execute_load_skill(
&LoadSkillArgs {
name: "nonexistent".into(),
},
Some(dir.path()),
Some(&[]),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("skill not found"));
}
#[test]
fn execute_load_skill_none_working_dir_not_found() {
let result = execute_load_skill(
&LoadSkillArgs {
name: "definitely-no-such-skill-xyz".into(),
},
None,
Some(&[]),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("skill not found"));
}
#[test]
fn execute_load_skill_found() {
let dir = tempfile::tempdir().unwrap();
let skill_dir = dir.path().join(".agents/skills/test-skill");
std::fs::create_dir_all(&skill_dir).unwrap();
let skill_content = "\
---
name: test-skill
description: A test skill
---
Hello, this is the skill body.
---
";
let skill_md = skill_dir.join("SKILL.md");
std::fs::write(&skill_md, skill_content).unwrap();
let skills = [context::SkillMeta {
name: "test-skill".into(),
description: "A test skill".into(),
path: skill_md,
}];
let result = execute_load_skill(
&LoadSkillArgs {
name: "test-skill".into(),
},
Some(dir.path()),
Some(&skills),
);
assert!(result.is_ok());
let msg = result.unwrap();
assert!(msg.contains("Loaded skill: test-skill"));
assert!(msg.contains("Hello, this is the skill body."));
}
}