1use crate::skills::Skill;
17use crate::tools::ToolRegistry;
18use anyhow::Result;
19use std::sync::Arc;
20
21#[derive(Clone)]
31pub struct PluginContext {
32 pub llm: Option<Arc<dyn crate::llm::LlmClient>>,
34 pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
36}
37
38impl PluginContext {
39 pub fn new() -> Self {
40 Self {
41 llm: None,
42 skill_registry: None,
43 }
44 }
45
46 pub fn with_llm(mut self, llm: Arc<dyn crate::llm::LlmClient>) -> Self {
47 self.llm = Some(llm);
48 self
49 }
50
51 pub fn with_skill_registry(mut self, registry: Arc<crate::skills::SkillRegistry>) -> Self {
52 self.skill_registry = Some(registry);
53 self
54 }
55}
56
57impl Default for PluginContext {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63pub trait Plugin: Send + Sync {
93 fn name(&self) -> &str;
95
96 fn version(&self) -> &str;
98
99 fn tool_names(&self) -> &[&str];
103
104 fn load(&self, registry: &Arc<ToolRegistry>, ctx: &PluginContext) -> Result<()>;
108
109 fn unload(&self, registry: &Arc<ToolRegistry>) {
114 for name in self.tool_names() {
115 registry.unregister(name);
116 }
117 }
118
119 fn description(&self) -> &str {
121 ""
122 }
123
124 fn skills(&self) -> Vec<Arc<Skill>> {
135 vec![]
136 }
137}
138
139#[derive(Default)]
148pub struct PluginManager {
149 plugins: Vec<Arc<dyn Plugin>>,
150}
151
152impl PluginManager {
153 pub fn new() -> Self {
154 Self::default()
155 }
156
157 pub fn register(&mut self, plugin: impl Plugin + 'static) {
159 self.plugins.push(Arc::new(plugin));
160 }
161
162 pub fn register_arc(&mut self, plugin: Arc<dyn Plugin>) {
164 self.plugins.push(plugin);
165 }
166
167 pub fn load_all(&self, registry: &Arc<ToolRegistry>, ctx: &PluginContext) {
175 for plugin in &self.plugins {
176 tracing::info!("Loading plugin '{}' v{}", plugin.name(), plugin.version());
177 match plugin.load(registry, ctx) {
178 Ok(()) => {
179 if let Some(ref skill_reg) = ctx.skill_registry {
180 for skill in plugin.skills() {
181 tracing::debug!(
182 "Plugin '{}' registered skill '{}'",
183 plugin.name(),
184 skill.name
185 );
186 skill_reg.register_unchecked(skill);
187 }
188 }
189 }
190 Err(e) => {
191 tracing::error!("Plugin '{}' failed to load: {}", plugin.name(), e);
192 }
193 }
194 }
195 }
196
197 pub fn unload(&mut self, name: &str, registry: &Arc<ToolRegistry>) {
201 if let Some(pos) = self.plugins.iter().position(|p| p.name() == name) {
202 let plugin = self.plugins.remove(pos);
203 tracing::info!("Unloading plugin '{}'", plugin.name());
204 plugin.unload(registry);
205 }
206 }
207
208 pub fn unload_all(&mut self, registry: &Arc<ToolRegistry>) {
210 for plugin in self.plugins.drain(..).rev() {
211 tracing::info!("Unloading plugin '{}'", plugin.name());
212 plugin.unload(registry);
213 }
214 }
215
216 pub fn is_loaded(&self, name: &str) -> bool {
218 self.plugins.iter().any(|p| p.name() == name)
219 }
220
221 pub fn len(&self) -> usize {
223 self.plugins.len()
224 }
225
226 pub fn is_empty(&self) -> bool {
228 self.plugins.is_empty()
229 }
230
231 pub fn plugin_names(&self) -> Vec<&str> {
233 self.plugins.iter().map(|p| p.name()).collect()
234 }
235}
236
237pub struct SkillPlugin {
263 plugin_name: String,
264 plugin_version: String,
265 skill_contents: Vec<String>,
266}
267
268impl SkillPlugin {
269 pub fn new(name: impl Into<String>) -> Self {
270 Self {
271 plugin_name: name.into(),
272 plugin_version: "1.0.0".into(),
273 skill_contents: vec![],
274 }
275 }
276
277 pub fn with_skill(mut self, content: impl Into<String>) -> Self {
278 self.skill_contents.push(content.into());
279 self
280 }
281
282 pub fn with_skills(mut self, contents: impl IntoIterator<Item = impl Into<String>>) -> Self {
283 self.skill_contents
284 .extend(contents.into_iter().map(|s| s.into()));
285 self
286 }
287}
288
289impl Plugin for SkillPlugin {
290 fn name(&self) -> &str {
291 &self.plugin_name
292 }
293
294 fn version(&self) -> &str {
295 &self.plugin_version
296 }
297
298 fn tool_names(&self) -> &[&str] {
299 &[]
300 }
301
302 fn load(&self, _registry: &Arc<ToolRegistry>, _ctx: &PluginContext) -> Result<()> {
303 Ok(())
304 }
305
306 fn skills(&self) -> Vec<Arc<Skill>> {
307 self.skill_contents
308 .iter()
309 .filter_map(|content| Skill::parse(content).map(Arc::new))
310 .collect()
311 }
312}
313
314#[cfg(test)]
319mod tests {
320 use super::*;
321 use crate::tools::ToolRegistry;
322 use std::path::PathBuf;
323
324 fn make_registry() -> Arc<ToolRegistry> {
325 Arc::new(ToolRegistry::new(PathBuf::from("/tmp")))
326 }
327
328 #[test]
329 fn plugin_manager_register_and_query() {
330 let mut mgr = PluginManager::new();
331 assert!(mgr.is_empty());
332 mgr.register(SkillPlugin::new("example"));
333 assert_eq!(mgr.len(), 1);
334 assert!(mgr.is_loaded("example"));
335 }
336
337 #[test]
338 fn plugin_manager_load_all() {
339 let mut mgr = PluginManager::new();
340 mgr.register(SkillPlugin::new("example"));
341 let registry = make_registry();
342 let ctx = PluginContext::new();
343 mgr.load_all(®istry, &ctx);
344 assert!(registry.get("example").is_none());
345 }
346
347 #[test]
348 fn plugin_manager_unload() {
349 let mut mgr = PluginManager::new();
350 mgr.register(SkillPlugin::new("example"));
351 let registry = make_registry();
352 let ctx = PluginContext::new();
353 mgr.load_all(®istry, &ctx);
354 mgr.unload("example", ®istry);
355 assert!(!mgr.is_loaded("example"));
356 }
357
358 #[test]
359 fn plugin_manager_unload_all() {
360 let mut mgr = PluginManager::new();
361 mgr.register(SkillPlugin::new("example"));
362 let registry = make_registry();
363 let ctx = PluginContext::new();
364 mgr.load_all(®istry, &ctx);
365 mgr.unload_all(®istry);
366 assert!(mgr.is_empty());
367 }
368
369 #[test]
370 fn plugin_skills_registered_on_load_all() {
371 use crate::skills::SkillRegistry;
372
373 let mut mgr = PluginManager::new();
374 mgr.register(SkillPlugin::new("test-plugin").with_skill(
375 r#"---
376name: test-skill
377description: Test skill
378allowed-tools: "read(*)"
379kind: instruction
380---
381Read carefully."#,
382 ));
383
384 let registry = make_registry();
385 let skill_reg = Arc::new(SkillRegistry::new());
386 let ctx = PluginContext::new().with_skill_registry(Arc::clone(&skill_reg));
387
388 mgr.load_all(®istry, &ctx);
389 assert!(skill_reg.get("test-skill").is_some());
390 }
391
392 #[test]
393 fn plugin_skills_not_registered_when_no_skill_registry_in_ctx() {
394 let mut mgr = PluginManager::new();
395 mgr.register(SkillPlugin::new("test-plugin"));
396
397 let registry = make_registry();
398 let ctx = PluginContext::new();
400 mgr.load_all(®istry, &ctx);
401 }
403
404 #[test]
405 fn skill_plugin_no_tools_and_injects_skills() {
406 use crate::skills::SkillRegistry;
407
408 let skill_md = r#"---
409name: test-skill
410description: Test skill
411allowed-tools: "bash(*)"
412kind: instruction
413---
414Test instruction."#;
415
416 let mut mgr = PluginManager::new();
417 mgr.register(SkillPlugin::new("test-plugin").with_skill(skill_md));
418
419 let registry = make_registry();
420 let skill_reg = Arc::new(SkillRegistry::new());
421 let ctx = PluginContext::new().with_skill_registry(Arc::clone(&skill_reg));
422
423 mgr.load_all(®istry, &ctx);
424
425 assert!(registry.get("test-plugin").is_none());
427 assert!(skill_reg.get("test-skill").is_some());
429 }
430
431 #[test]
432 fn skill_plugin_with_skills_builder() {
433 let skill1 = "---\nname: s1\ndescription: d1\nkind: instruction\n---\nContent 1";
434 let skill2 = "---\nname: s2\ndescription: d2\nkind: instruction\n---\nContent 2";
435
436 let plugin = SkillPlugin::new("multi").with_skills([skill1, skill2]);
437 assert_eq!(plugin.skills().len(), 2);
438 }
439
440 #[test]
441 fn plugin_names() {
442 let mut mgr = PluginManager::new();
443 mgr.register(SkillPlugin::new("a"));
444 mgr.register(SkillPlugin::new("b"));
445 let names = mgr.plugin_names();
446 assert!(names.contains(&"a"));
447 assert!(names.contains(&"b"));
448 }
449}