use crate::skills::Skill;
use crate::tools::ToolRegistry;
use anyhow::Result;
use std::sync::Arc;
#[derive(Clone)]
pub struct PluginContext {
pub llm: Option<Arc<dyn crate::llm::LlmClient>>,
pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
}
impl PluginContext {
pub fn new() -> Self {
Self {
llm: None,
skill_registry: None,
}
}
pub fn with_llm(mut self, llm: Arc<dyn crate::llm::LlmClient>) -> Self {
self.llm = Some(llm);
self
}
pub fn with_skill_registry(mut self, registry: Arc<crate::skills::SkillRegistry>) -> Self {
self.skill_registry = Some(registry);
self
}
}
impl Default for PluginContext {
fn default() -> Self {
Self::new()
}
}
pub trait Plugin: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn tool_names(&self) -> &[&str];
fn load(&self, registry: &Arc<ToolRegistry>, ctx: &PluginContext) -> Result<()>;
fn unload(&self, registry: &Arc<ToolRegistry>) {
for name in self.tool_names() {
registry.unregister(name);
}
}
fn description(&self) -> &str {
""
}
fn skills(&self) -> Vec<Arc<Skill>> {
vec![]
}
}
#[derive(Default)]
pub struct PluginManager {
plugins: Vec<Arc<dyn Plugin>>,
}
impl PluginManager {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, plugin: impl Plugin + 'static) {
self.plugins.push(Arc::new(plugin));
}
pub fn register_arc(&mut self, plugin: Arc<dyn Plugin>) {
self.plugins.push(plugin);
}
pub fn load_all(&self, registry: &Arc<ToolRegistry>, ctx: &PluginContext) {
for plugin in &self.plugins {
tracing::info!("Loading plugin '{}' v{}", plugin.name(), plugin.version());
match plugin.load(registry, ctx) {
Ok(()) => {
if let Some(ref skill_reg) = ctx.skill_registry {
for skill in plugin.skills() {
tracing::debug!(
"Plugin '{}' registered skill '{}'",
plugin.name(),
skill.name
);
skill_reg.register_unchecked(skill);
}
}
}
Err(e) => {
tracing::error!("Plugin '{}' failed to load: {}", plugin.name(), e);
}
}
}
}
pub fn unload(&mut self, name: &str, registry: &Arc<ToolRegistry>) {
if let Some(pos) = self.plugins.iter().position(|p| p.name() == name) {
let plugin = self.plugins.remove(pos);
tracing::info!("Unloading plugin '{}'", plugin.name());
plugin.unload(registry);
}
}
pub fn unload_all(&mut self, registry: &Arc<ToolRegistry>) {
for plugin in self.plugins.drain(..).rev() {
tracing::info!("Unloading plugin '{}'", plugin.name());
plugin.unload(registry);
}
}
pub fn is_loaded(&self, name: &str) -> bool {
self.plugins.iter().any(|p| p.name() == name)
}
pub fn len(&self) -> usize {
self.plugins.len()
}
pub fn is_empty(&self) -> bool {
self.plugins.is_empty()
}
pub fn plugin_names(&self) -> Vec<&str> {
self.plugins.iter().map(|p| p.name()).collect()
}
}
pub struct SkillPlugin {
plugin_name: String,
plugin_version: String,
skill_contents: Vec<String>,
}
impl SkillPlugin {
pub fn new(name: impl Into<String>) -> Self {
Self {
plugin_name: name.into(),
plugin_version: "1.0.0".into(),
skill_contents: vec![],
}
}
pub fn with_skill(mut self, content: impl Into<String>) -> Self {
self.skill_contents.push(content.into());
self
}
pub fn with_skills(mut self, contents: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.skill_contents
.extend(contents.into_iter().map(|s| s.into()));
self
}
}
impl Plugin for SkillPlugin {
fn name(&self) -> &str {
&self.plugin_name
}
fn version(&self) -> &str {
&self.plugin_version
}
fn tool_names(&self) -> &[&str] {
&[]
}
fn load(&self, _registry: &Arc<ToolRegistry>, _ctx: &PluginContext) -> Result<()> {
Ok(())
}
fn skills(&self) -> Vec<Arc<Skill>> {
self.skill_contents
.iter()
.filter_map(|content| Skill::parse(content).map(Arc::new))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ToolRegistry;
use std::path::PathBuf;
fn make_registry() -> Arc<ToolRegistry> {
Arc::new(ToolRegistry::new(PathBuf::from("/tmp")))
}
#[test]
fn plugin_manager_register_and_query() {
let mut mgr = PluginManager::new();
assert!(mgr.is_empty());
mgr.register(SkillPlugin::new("example"));
assert_eq!(mgr.len(), 1);
assert!(mgr.is_loaded("example"));
}
#[test]
fn plugin_manager_load_all() {
let mut mgr = PluginManager::new();
mgr.register(SkillPlugin::new("example"));
let registry = make_registry();
let ctx = PluginContext::new();
mgr.load_all(®istry, &ctx);
assert!(registry.get("example").is_none());
}
#[test]
fn plugin_manager_unload() {
let mut mgr = PluginManager::new();
mgr.register(SkillPlugin::new("example"));
let registry = make_registry();
let ctx = PluginContext::new();
mgr.load_all(®istry, &ctx);
mgr.unload("example", ®istry);
assert!(!mgr.is_loaded("example"));
}
#[test]
fn plugin_manager_unload_all() {
let mut mgr = PluginManager::new();
mgr.register(SkillPlugin::new("example"));
let registry = make_registry();
let ctx = PluginContext::new();
mgr.load_all(®istry, &ctx);
mgr.unload_all(®istry);
assert!(mgr.is_empty());
}
#[test]
fn plugin_skills_registered_on_load_all() {
use crate::skills::SkillRegistry;
let mut mgr = PluginManager::new();
mgr.register(SkillPlugin::new("test-plugin").with_skill(
r#"---
name: test-skill
description: Test skill
allowed-tools: "read(*)"
kind: instruction
---
Read carefully."#,
));
let registry = make_registry();
let skill_reg = Arc::new(SkillRegistry::new());
let ctx = PluginContext::new().with_skill_registry(Arc::clone(&skill_reg));
mgr.load_all(®istry, &ctx);
assert!(skill_reg.get("test-skill").is_some());
}
#[test]
fn plugin_skills_not_registered_when_no_skill_registry_in_ctx() {
let mut mgr = PluginManager::new();
mgr.register(SkillPlugin::new("test-plugin"));
let registry = make_registry();
let ctx = PluginContext::new();
mgr.load_all(®istry, &ctx);
}
#[test]
fn skill_plugin_no_tools_and_injects_skills() {
use crate::skills::SkillRegistry;
let skill_md = r#"---
name: test-skill
description: Test skill
allowed-tools: "bash(*)"
kind: instruction
---
Test instruction."#;
let mut mgr = PluginManager::new();
mgr.register(SkillPlugin::new("test-plugin").with_skill(skill_md));
let registry = make_registry();
let skill_reg = Arc::new(SkillRegistry::new());
let ctx = PluginContext::new().with_skill_registry(Arc::clone(&skill_reg));
mgr.load_all(®istry, &ctx);
assert!(registry.get("test-plugin").is_none());
assert!(skill_reg.get("test-skill").is_some());
}
#[test]
fn skill_plugin_with_skills_builder() {
let skill1 = "---\nname: s1\ndescription: d1\nkind: instruction\n---\nContent 1";
let skill2 = "---\nname: s2\ndescription: d2\nkind: instruction\n---\nContent 2";
let plugin = SkillPlugin::new("multi").with_skills([skill1, skill2]);
assert_eq!(plugin.skills().len(), 2);
}
#[test]
fn plugin_names() {
let mut mgr = PluginManager::new();
mgr.register(SkillPlugin::new("a"));
mgr.register(SkillPlugin::new("b"));
let names = mgr.plugin_names();
assert!(names.contains(&"a"));
assert!(names.contains(&"b"));
}
}