use crate::common::Result;
use serde::Deserialize;
use crate::skills::SkillRegistry;
use crate::tools::tool_index::ToolIndex;
#[derive(Debug, Deserialize)]
pub struct SkillInput {
pub skill: String,
#[serde(default)]
pub args: Option<String>,
}
pub fn definition() -> serde_json::Value {
serde_json::json!({
"type": "function",
"function": {
"name": "skill",
"description": "Invoke an Agent Skill to load specialized instructions and capabilities. Use this when a task matches an available skill's description. The skill's instructions will be loaded into your context.",
"parameters": {
"type": "object",
"properties": {
"skill": {
"type": "string",
"description": "The skill name to invoke (e.g., 'lint-code', 'deploy-app'). Supports fuzzy matching."
},
"args": {
"type": "string",
"description": "Optional arguments for the skill (e.g., file paths, options)."
}
},
"required": ["skill"]
}
}
})
}
pub fn execute(
input: SkillInput,
registry: &SkillRegistry,
tool_index: &ToolIndex,
) -> Result<String> {
let invocation = registry
.invoke_with_index(&input.skill, tool_index)
.map_err(|e| crate::common::AgentError::Config(format!("Skill invocation failed: {e}")))?;
let mut result = invocation.to_context_string();
if let Some(ref args) = input.args {
result.push_str(&format!("\n\n<skill-args>{args}</skill-args>"));
}
tracing::info!(
skill = %invocation.name,
resources = invocation.available_resources.len(),
"Skill invoked",
);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::tool_index::ToolIndex;
use std::fs;
fn create_registry_with_skill(dir: &std::path::Path) -> SkillRegistry {
let skill_dir = dir.join(".claude").join("skills").join("test-skill");
fs::create_dir_all(&skill_dir).unwrap();
fs::write(
skill_dir.join("SKILL.md"),
"---\nname: test-skill\ndescription: A test skill\n---\n\n# Instructions\nDo the test thing.",
).unwrap();
SkillRegistry::discover(dir)
}
fn empty_index() -> ToolIndex {
ToolIndex::new()
}
fn indexed_registry(reg: &SkillRegistry) -> ToolIndex {
let mut idx = ToolIndex::new();
idx.index_skills(reg);
idx
}
#[test]
fn test_skill_definition() {
let def = definition();
assert_eq!(def["function"]["name"], "skill");
assert!(
def["function"]["description"]
.as_str()
.unwrap()
.contains("Skill")
);
}
#[test]
fn test_skill_execute() {
let dir = tempfile::tempdir().unwrap();
let reg = create_registry_with_skill(dir.path());
let input = SkillInput {
skill: "test-skill".to_string(),
args: None,
};
let idx = indexed_registry(®);
let result = execute(input, ®, &idx).unwrap();
assert!(result.contains("<skill name=\"test-skill\">"));
assert!(result.contains("Do the test thing"));
assert!(result.contains("</skill>"));
}
#[test]
fn test_skill_execute_with_args() {
let dir = tempfile::tempdir().unwrap();
let reg = create_registry_with_skill(dir.path());
let idx = indexed_registry(®);
let input = SkillInput {
skill: "test-skill".to_string(),
args: Some("--verbose".to_string()),
};
let result = execute(input, ®, &idx).unwrap();
assert!(result.contains("<skill-args>--verbose</skill-args>"));
}
#[test]
fn test_skill_execute_not_found() {
let reg = SkillRegistry::new();
let idx = empty_index();
let input = SkillInput {
skill: "nonexistent".to_string(),
args: None,
};
assert!(execute(input, ®, &idx).is_err());
}
#[test]
fn test_skill_execute_fuzzy() {
let dir = tempfile::tempdir().unwrap();
let reg = create_registry_with_skill(dir.path());
let idx = indexed_registry(®);
let input = SkillInput {
skill: "test-sk".to_string(),
args: None,
};
let result = execute(input, ®, &idx).unwrap();
assert!(result.contains("test-skill"));
}
}