use crate::messages::ToolDefinition;
use crate::skills::{SkillInfo, SkillRegistry};
use crate::tools::actor::{ExecuteToolDirect, ToolActor, ToolActorResponse};
use crate::tools::{ToolConfig, ToolError, ToolExecutionFuture, ToolExecutorTrait};
use acton_reactive::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::sync::Arc;
#[derive(Debug)]
pub struct ListSkillsTool {
registry: Arc<SkillRegistry>,
}
#[acton_actor]
pub struct ListSkillsToolActor {
registry: Arc<SkillRegistry>,
}
#[derive(Debug, Deserialize)]
struct ListSkillsArgs {
#[serde(default)]
filter: Option<String>,
}
#[derive(Debug, Serialize)]
struct ListSkillsResult {
skills: Vec<SkillSummary>,
count: usize,
}
#[derive(Debug, Serialize)]
struct SkillSummary {
name: String,
description: String,
tags: Vec<String>,
}
impl From<&SkillInfo> for SkillSummary {
fn from(info: &SkillInfo) -> Self {
Self {
name: info.name.clone(),
description: info.description.clone(),
tags: info.tags.clone(),
}
}
}
impl ListSkillsTool {
#[must_use]
pub fn new(registry: Arc<SkillRegistry>) -> Self {
Self { registry }
}
#[must_use]
pub fn config() -> ToolConfig {
ToolConfig::new(ToolDefinition {
name: "list_skills".to_string(),
description: "List available agent skills with their descriptions. Use this to discover what skills are available before activating one.".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"filter": {
"type": "string",
"description": "Optional filter pattern for skill names (case-insensitive substring match)"
}
}
}),
})
}
}
impl ToolExecutorTrait for ListSkillsTool {
fn execute(&self, args: Value) -> ToolExecutionFuture {
let registry = Arc::clone(&self.registry);
Box::pin(async move {
let args: ListSkillsArgs = serde_json::from_value(args).map_err(|e| {
ToolError::validation_failed("list_skills", format!("invalid arguments: {e}"))
})?;
let skills: Vec<SkillSummary> = registry
.list()
.iter()
.filter(|info| {
args.filter.as_ref().is_none_or(|pattern| {
let pattern_lower = pattern.to_lowercase();
info.name.to_lowercase().contains(&pattern_lower)
|| info.description.to_lowercase().contains(&pattern_lower)
})
})
.map(SkillSummary::from)
.collect();
let count = skills.len();
Ok(json!(ListSkillsResult { skills, count }))
})
}
fn validate_args(&self, _args: &Value) -> Result<(), ToolError> {
Ok(())
}
}
impl ToolActor for ListSkillsToolActor {
fn name() -> &'static str {
"list_skills"
}
fn definition() -> ToolDefinition {
ListSkillsTool::config().definition
}
async fn spawn(runtime: &mut ActorRuntime) -> ActorHandle {
let mut builder = runtime.new_actor_with_name::<Self>("list_skills_tool".to_string());
builder.act_on::<ExecuteToolDirect>(|actor, envelope| {
let msg = envelope.message();
let correlation_id = msg.correlation_id.clone();
let tool_call_id = msg.tool_call_id.clone();
let args = msg.args.clone();
let registry = Arc::clone(&actor.model.registry);
let broker = actor.broker().clone();
Reply::pending(async move {
let tool = ListSkillsTool::new(registry);
let result = tool.execute(args).await;
let response = match result {
Ok(value) => {
let result_str = serde_json::to_string(&value)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
ToolActorResponse::success(correlation_id, tool_call_id, result_str)
}
Err(e) => ToolActorResponse::error(correlation_id, tool_call_id, e.to_string()),
};
broker.broadcast(response).await;
})
});
builder.start().await
}
}
impl ListSkillsToolActor {
pub async fn spawn_with_registry(
runtime: &mut ActorRuntime,
registry: Arc<SkillRegistry>,
) -> ActorHandle {
let mut builder = runtime.new_actor_with_name::<Self>("list_skills_tool".to_string());
builder.act_on::<ExecuteToolDirect>(move |actor, envelope| {
let msg = envelope.message();
let correlation_id = msg.correlation_id.clone();
let tool_call_id = msg.tool_call_id.clone();
let args = msg.args.clone();
let registry = Arc::clone(®istry);
let broker = actor.broker().clone();
Reply::pending(async move {
let tool = ListSkillsTool::new(registry);
let result = tool.execute(args).await;
let response = match result {
Ok(value) => {
let result_str = serde_json::to_string(&value)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
ToolActorResponse::success(correlation_id, tool_call_id, result_str)
}
Err(e) => ToolActorResponse::error(correlation_id, tool_call_id, e.to_string()),
};
broker.broadcast(response).await;
})
});
builder.start().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::skills::LoadedSkill;
fn create_test_registry() -> Arc<SkillRegistry> {
let mut registry = SkillRegistry::new();
registry.add(LoadedSkill {
info: SkillInfo {
name: "code-review".to_string(),
description: "Review code for quality".to_string(),
path: std::path::PathBuf::from("/skills/code-review.md"),
tags: vec!["code".to_string(), "review".to_string()],
},
content: "# Code Review\n\nInstructions...".to_string(),
triggers: vec![],
enabled_by_default: false,
});
registry.add(LoadedSkill {
info: SkillInfo {
name: "documentation".to_string(),
description: "Generate documentation".to_string(),
path: std::path::PathBuf::from("/skills/documentation.md"),
tags: vec!["docs".to_string()],
},
content: "# Documentation\n\nInstructions...".to_string(),
triggers: vec![],
enabled_by_default: false,
});
Arc::new(registry)
}
#[tokio::test]
async fn list_all_skills() {
let registry = create_test_registry();
let tool = ListSkillsTool::new(registry);
let result = tool.execute(json!({})).await.unwrap();
assert_eq!(result["count"], 2);
let skills = result["skills"].as_array().unwrap();
assert_eq!(skills.len(), 2);
}
#[tokio::test]
async fn list_skills_with_filter() {
let registry = create_test_registry();
let tool = ListSkillsTool::new(registry);
let result = tool
.execute(json!({
"filter": "code"
}))
.await
.unwrap();
assert_eq!(result["count"], 1);
let skills = result["skills"].as_array().unwrap();
assert_eq!(skills[0]["name"], "code-review");
}
#[tokio::test]
async fn list_skills_filter_no_match() {
let registry = create_test_registry();
let tool = ListSkillsTool::new(registry);
let result = tool
.execute(json!({
"filter": "nonexistent"
}))
.await
.unwrap();
assert_eq!(result["count"], 0);
let skills = result["skills"].as_array().unwrap();
assert!(skills.is_empty());
}
#[tokio::test]
async fn list_skills_filter_case_insensitive() {
let registry = create_test_registry();
let tool = ListSkillsTool::new(registry);
let result = tool
.execute(json!({
"filter": "CODE"
}))
.await
.unwrap();
assert_eq!(result["count"], 1);
}
#[test]
fn config_has_correct_schema() {
let config = ListSkillsTool::config();
assert_eq!(config.definition.name, "list_skills");
assert!(config.definition.description.contains("List available"));
let schema = &config.definition.input_schema;
assert!(schema["properties"]["filter"].is_object());
}
}