1use crate::skills::Skill;
17use crate::tools::{register_program, register_program_with_catalog, ToolRegistry};
18use anyhow::{bail, 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
289pub struct ProgramPlugin {
295 plugin_name: String,
296 plugin_version: String,
297 templates: Vec<crate::program::ProgramTemplate>,
298 include_builtin_programs: bool,
299}
300
301impl ProgramPlugin {
302 pub fn new(name: impl Into<String>) -> Self {
303 Self {
304 plugin_name: name.into(),
305 plugin_version: "1.0.0".into(),
306 templates: Vec::new(),
307 include_builtin_programs: true,
308 }
309 }
310
311 pub fn with_version(mut self, version: impl Into<String>) -> Self {
312 self.plugin_version = version.into();
313 self
314 }
315
316 pub fn with_template(mut self, template: crate::program::ProgramTemplate) -> Self {
317 self.templates.push(template);
318 self
319 }
320
321 pub fn with_templates(
322 mut self,
323 templates: impl IntoIterator<Item = crate::program::ProgramTemplate>,
324 ) -> Self {
325 self.templates.extend(templates);
326 self
327 }
328
329 pub fn without_builtin_programs(mut self) -> Self {
330 self.include_builtin_programs = false;
331 self
332 }
333
334 pub fn from_json(name: impl Into<String>, content: &str) -> Result<Self> {
335 let asset = serde_json::from_str::<ProgramTemplateAsset>(content)?;
336 Ok(Self::new(name).with_templates(asset.into_templates()))
337 }
338
339 pub fn from_yaml(name: impl Into<String>, content: &str) -> Result<Self> {
340 let asset = serde_yaml::from_str::<ProgramTemplateAsset>(content)?;
341 Ok(Self::new(name).with_templates(asset.into_templates()))
342 }
343}
344
345impl Plugin for ProgramPlugin {
346 fn name(&self) -> &str {
347 &self.plugin_name
348 }
349
350 fn version(&self) -> &str {
351 &self.plugin_version
352 }
353
354 fn tool_names(&self) -> &[&str] {
355 &["program"]
356 }
357
358 fn load(&self, registry: &Arc<ToolRegistry>, _ctx: &PluginContext) -> Result<()> {
359 if self.templates.is_empty() {
360 bail!(
361 "ProgramPlugin '{}' has no program templates",
362 self.plugin_name
363 );
364 }
365
366 let mut catalog = if self.include_builtin_programs {
367 crate::program::ProgramCatalog::with_builtin_programs()
368 } else {
369 crate::program::ProgramCatalog::new()
370 };
371 for template in &self.templates {
372 catalog.try_register(template.clone())?;
373 }
374 register_program_with_catalog(registry, catalog);
375 Ok(())
376 }
377
378 fn unload(&self, registry: &Arc<ToolRegistry>) {
379 register_program(registry);
380 }
381
382 fn description(&self) -> &str {
383 "Registers programmatic tool calling templates"
384 }
385}
386
387#[derive(Debug, serde::Deserialize)]
388#[serde(untagged)]
389enum ProgramTemplateAsset {
390 Template(crate::program::ProgramTemplate),
391 Templates(Vec<crate::program::ProgramTemplate>),
392 Catalog {
393 programs: Vec<crate::program::ProgramTemplate>,
394 },
395}
396
397impl ProgramTemplateAsset {
398 fn into_templates(self) -> Vec<crate::program::ProgramTemplate> {
399 match self {
400 Self::Template(template) => vec![template],
401 Self::Templates(templates) => templates,
402 Self::Catalog { programs } => programs,
403 }
404 }
405}
406
407impl Plugin for SkillPlugin {
408 fn name(&self) -> &str {
409 &self.plugin_name
410 }
411
412 fn version(&self) -> &str {
413 &self.plugin_version
414 }
415
416 fn tool_names(&self) -> &[&str] {
417 &[]
418 }
419
420 fn load(&self, _registry: &Arc<ToolRegistry>, _ctx: &PluginContext) -> Result<()> {
421 Ok(())
422 }
423
424 fn skills(&self) -> Vec<Arc<Skill>> {
425 self.skill_contents
426 .iter()
427 .filter_map(|content| Skill::parse(content).map(Arc::new))
428 .collect()
429 }
430}
431
432#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::tools::{Tool, ToolContext, ToolOutput, ToolRegistry};
440 use async_trait::async_trait;
441 use std::path::PathBuf;
442
443 fn make_registry() -> Arc<ToolRegistry> {
444 Arc::new(ToolRegistry::new(PathBuf::from("/tmp")))
445 }
446
447 struct EchoTool;
448
449 #[async_trait]
450 impl Tool for EchoTool {
451 fn name(&self) -> &str {
452 "echo"
453 }
454
455 fn description(&self) -> &str {
456 "Echoes a message"
457 }
458
459 fn parameters(&self) -> serde_json::Value {
460 serde_json::json!({
461 "type": "object",
462 "additionalProperties": false,
463 "properties": {
464 "message": { "type": "string" }
465 },
466 "required": ["message"]
467 })
468 }
469
470 async fn execute(
471 &self,
472 args: &serde_json::Value,
473 _ctx: &ToolContext,
474 ) -> Result<ToolOutput> {
475 Ok(ToolOutput::success(
476 args["message"].as_str().unwrap_or_default(),
477 ))
478 }
479 }
480
481 #[test]
482 fn plugin_manager_register_and_query() {
483 let mut mgr = PluginManager::new();
484 assert!(mgr.is_empty());
485 mgr.register(SkillPlugin::new("example"));
486 assert_eq!(mgr.len(), 1);
487 assert!(mgr.is_loaded("example"));
488 }
489
490 #[test]
491 fn plugin_manager_load_all() {
492 let mut mgr = PluginManager::new();
493 mgr.register(SkillPlugin::new("example"));
494 let registry = make_registry();
495 let ctx = PluginContext::new();
496 mgr.load_all(®istry, &ctx);
497 assert!(registry.get("example").is_none());
498 }
499
500 #[test]
501 fn plugin_manager_unload() {
502 let mut mgr = PluginManager::new();
503 mgr.register(SkillPlugin::new("example"));
504 let registry = make_registry();
505 let ctx = PluginContext::new();
506 mgr.load_all(®istry, &ctx);
507 mgr.unload("example", ®istry);
508 assert!(!mgr.is_loaded("example"));
509 }
510
511 #[test]
512 fn plugin_manager_unload_all() {
513 let mut mgr = PluginManager::new();
514 mgr.register(SkillPlugin::new("example"));
515 let registry = make_registry();
516 let ctx = PluginContext::new();
517 mgr.load_all(®istry, &ctx);
518 mgr.unload_all(®istry);
519 assert!(mgr.is_empty());
520 }
521
522 #[test]
523 fn plugin_skills_registered_on_load_all() {
524 use crate::skills::SkillRegistry;
525
526 let mut mgr = PluginManager::new();
527 mgr.register(SkillPlugin::new("test-plugin").with_skill(
528 r#"---
529name: test-skill
530description: Test skill
531allowed-tools: "read(*)"
532kind: instruction
533---
534Read carefully."#,
535 ));
536
537 let registry = make_registry();
538 let skill_reg = Arc::new(SkillRegistry::new());
539 let ctx = PluginContext::new().with_skill_registry(Arc::clone(&skill_reg));
540
541 mgr.load_all(®istry, &ctx);
542 assert!(skill_reg.get("test-skill").is_some());
543 }
544
545 #[test]
546 fn plugin_skills_not_registered_when_no_skill_registry_in_ctx() {
547 let mut mgr = PluginManager::new();
548 mgr.register(SkillPlugin::new("test-plugin"));
549
550 let registry = make_registry();
551 let ctx = PluginContext::new();
553 mgr.load_all(®istry, &ctx);
554 }
556
557 #[tokio::test]
558 async fn program_plugin_registers_template_catalog() {
559 let registry = make_registry();
560 registry.register(Arc::new(EchoTool));
561 let plugin = ProgramPlugin::new("program-pack").with_template(
562 crate::program::ProgramTemplate::new("custom_echo", "Run a custom echo")
563 .with_parameter(crate::program::ProgramParameter::required(
564 "message",
565 "Message to echo",
566 ))
567 .with_step(
568 crate::program::ProgramStepTemplate::new(
569 "echo",
570 serde_json::json!({ "message": "{{message}}" }),
571 )
572 .with_label("echo_message"),
573 ),
574 );
575
576 plugin.load(®istry, &PluginContext::new()).unwrap();
577
578 let result = registry
579 .execute_with_context(
580 "program",
581 &serde_json::json!({
582 "name": "custom_echo",
583 "inputs": { "message": "hello" }
584 }),
585 &ToolContext::new(PathBuf::from("/tmp")),
586 )
587 .await
588 .unwrap();
589
590 assert_eq!(result.exit_code, 0);
591 assert!(result.output.contains("hello"));
592 assert_eq!(
593 result.metadata.as_ref().unwrap()["trace"]["program_name"],
594 "custom_echo"
595 );
596 }
597
598 #[tokio::test]
599 async fn program_plugin_can_load_templates_from_yaml_asset() {
600 let registry = make_registry();
601 registry.register(Arc::new(EchoTool));
602 let plugin = ProgramPlugin::from_yaml(
603 "program-pack",
604 r#"
605programs:
606 - name: asset_echo
607 description: Echo from a YAML asset
608 parameters:
609 - name: message
610 description: Message to echo
611 required: true
612 steps:
613 - tool_name: echo
614 label: echo_message
615 args:
616 message: "{{message}}"
617"#,
618 )
619 .unwrap()
620 .without_builtin_programs();
621
622 plugin.load(®istry, &PluginContext::new()).unwrap();
623
624 let result = registry
625 .execute_with_context(
626 "program",
627 &serde_json::json!({
628 "name": "asset_echo",
629 "inputs": { "message": "from asset" }
630 }),
631 &ToolContext::new(PathBuf::from("/tmp")),
632 )
633 .await
634 .unwrap();
635
636 assert_eq!(result.exit_code, 0);
637 assert!(result.output.contains("from asset"));
638 assert_eq!(
639 result.metadata.as_ref().unwrap()["trace"]["program_name"],
640 "asset_echo"
641 );
642 }
643
644 #[test]
645 fn program_plugin_rejects_empty_catalog() {
646 let registry = make_registry();
647 let plugin = ProgramPlugin::new("empty-program-pack");
648
649 let err = plugin.load(®istry, &PluginContext::new()).unwrap_err();
650
651 assert!(err.to_string().contains("has no program templates"));
652 }
653
654 #[test]
655 fn program_plugin_rejects_invalid_template_assets() {
656 let registry = make_registry();
657 let plugin =
658 ProgramPlugin::new("bad-program-pack").with_template(crate::program::ProgramTemplate {
659 name: "bad-template".to_string(),
660 description: "Bad template".to_string(),
661 parameters: vec![],
662 steps: vec![crate::program::ProgramStepTemplate {
663 tool_name: "grep".to_string(),
664 args: serde_json::json!({ "pattern": "{{missing}}" }),
665 label: None,
666 }],
667 });
668
669 let err = plugin.load(®istry, &PluginContext::new()).unwrap_err();
670
671 assert!(err.to_string().contains("unknown program parameter"));
672 }
673
674 #[test]
675 fn skill_plugin_no_tools_and_injects_skills() {
676 use crate::skills::SkillRegistry;
677
678 let skill_md = r#"---
679name: test-skill
680description: Test skill
681allowed-tools: "bash(*)"
682kind: instruction
683---
684Test instruction."#;
685
686 let mut mgr = PluginManager::new();
687 mgr.register(SkillPlugin::new("test-plugin").with_skill(skill_md));
688
689 let registry = make_registry();
690 let skill_reg = Arc::new(SkillRegistry::new());
691 let ctx = PluginContext::new().with_skill_registry(Arc::clone(&skill_reg));
692
693 mgr.load_all(®istry, &ctx);
694
695 assert!(registry.get("test-plugin").is_none());
697 assert!(skill_reg.get("test-skill").is_some());
699 }
700
701 #[test]
702 fn skill_plugin_with_skills_builder() {
703 let skill1 = "---\nname: s1\ndescription: d1\nkind: instruction\n---\nContent 1";
704 let skill2 = "---\nname: s2\ndescription: d2\nkind: instruction\n---\nContent 2";
705
706 let plugin = SkillPlugin::new("multi").with_skills([skill1, skill2]);
707 assert_eq!(plugin.skills().len(), 2);
708 }
709
710 #[test]
711 fn plugin_names() {
712 let mut mgr = PluginManager::new();
713 mgr.register(SkillPlugin::new("a"));
714 mgr.register(SkillPlugin::new("b"));
715 let names = mgr.plugin_names();
716 assert!(names.contains(&"a"));
717 assert!(names.contains(&"b"));
718 }
719}