use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use synaptic_core::{SynapticError, Tool};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillManifest {
pub name: String,
pub description: String,
pub version: String,
pub author: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
}
#[async_trait]
pub trait Skill: Send + Sync {
fn manifest(&self) -> &SkillManifest;
fn tools(&self) -> Vec<Arc<dyn Tool>>;
fn system_prompt_fragment(&self) -> Option<String> {
None
}
async fn init(&self) -> Result<(), SynapticError> {
Ok(())
}
}
pub struct SkillRegistry {
skills: Vec<Arc<dyn Skill>>,
}
impl SkillRegistry {
pub fn new() -> Self {
Self { skills: Vec::new() }
}
pub fn register(&mut self, skill: Arc<dyn Skill>) {
self.skills.push(skill);
}
pub fn skills(&self) -> &[Arc<dyn Skill>] {
&self.skills
}
pub fn get(&self, name: &str) -> Option<&Arc<dyn Skill>> {
self.skills.iter().find(|s| s.manifest().name == name)
}
pub fn all_tools(&self) -> Vec<Arc<dyn Tool>> {
self.skills.iter().flat_map(|s| s.tools()).collect()
}
pub fn system_prompt_additions(&self) -> String {
self.skills
.iter()
.filter_map(|s| s.system_prompt_fragment())
.collect::<Vec<_>>()
.join("\n\n")
}
pub async fn init_all(&self) -> Result<(), SynapticError> {
for skill in &self.skills {
skill.init().await?;
}
Ok(())
}
}
impl Default for SkillRegistry {
fn default() -> Self {
Self::new()
}
}
pub fn load_manifest(path: &Path) -> Result<SkillManifest, SynapticError> {
let content = std::fs::read_to_string(path)
.map_err(|e| SynapticError::Config(format!("cannot read manifest: {}", e)))?;
toml::from_str(&content).map_err(|e| SynapticError::Config(format!("invalid manifest: {}", e)))
}
pub fn discover_skills(dir: &Path) -> Vec<(PathBuf, SkillManifest)> {
let mut results = Vec::new();
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let manifest_path = entry.path().join("manifest.toml");
if manifest_path.exists() {
if let Ok(manifest) = load_manifest(&manifest_path) {
results.push((entry.path(), manifest));
}
}
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
struct TestSkill {
manifest: SkillManifest,
}
#[async_trait]
impl Skill for TestSkill {
fn manifest(&self) -> &SkillManifest {
&self.manifest
}
fn tools(&self) -> Vec<Arc<dyn Tool>> {
vec![]
}
fn system_prompt_fragment(&self) -> Option<String> {
Some("I am a test skill.".to_string())
}
}
#[test]
fn test_registry_register_and_get() {
let mut registry = SkillRegistry::new();
let skill = Arc::new(TestSkill {
manifest: SkillManifest {
name: "test-skill".to_string(),
description: "A test skill".to_string(),
version: "1.0.0".to_string(),
author: None,
tags: vec!["test".to_string()],
},
});
registry.register(skill);
assert_eq!(registry.skills().len(), 1);
assert!(registry.get("test-skill").is_some());
assert!(registry.get("nonexistent").is_none());
}
#[test]
fn test_all_tools_empty() {
let mut registry = SkillRegistry::new();
let skill = Arc::new(TestSkill {
manifest: SkillManifest {
name: "empty".to_string(),
description: "No tools".to_string(),
version: "0.1.0".to_string(),
author: None,
tags: vec![],
},
});
registry.register(skill);
assert!(registry.all_tools().is_empty());
}
#[test]
fn test_system_prompt_additions() {
let mut registry = SkillRegistry::new();
registry.register(Arc::new(TestSkill {
manifest: SkillManifest {
name: "s1".to_string(),
description: "".to_string(),
version: "0.1.0".to_string(),
author: None,
tags: vec![],
},
}));
let additions = registry.system_prompt_additions();
assert!(additions.contains("test skill"));
}
#[test]
fn test_discover_skills_empty_dir() {
let dir = std::env::temp_dir().join("synaptic_test_discover_empty");
let _ = std::fs::create_dir_all(&dir);
let results = discover_skills(&dir);
let _ = results;
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn test_init_all() {
let mut registry = SkillRegistry::new();
registry.register(Arc::new(TestSkill {
manifest: SkillManifest {
name: "init-test".to_string(),
description: "".to_string(),
version: "0.1.0".to_string(),
author: None,
tags: vec![],
},
}));
assert!(registry.init_all().await.is_ok());
}
}