1use crate::skills::Skill;
29use crate::tools::ToolRegistry;
30use anyhow::Result;
31use std::sync::Arc;
32
33#[derive(Clone)]
43pub struct PluginContext {
44 pub llm: Option<Arc<dyn crate::llm::LlmClient>>,
46 pub document_parsers: Option<Arc<crate::document_parser::DocumentParserRegistry>>,
48 pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
50}
51
52impl PluginContext {
53 pub fn new() -> Self {
54 Self {
55 llm: None,
56 document_parsers: None,
57 skill_registry: None,
58 }
59 }
60
61 pub fn with_llm(mut self, llm: Arc<dyn crate::llm::LlmClient>) -> Self {
62 self.llm = Some(llm);
63 self
64 }
65
66 pub fn with_document_parsers(
67 mut self,
68 registry: Arc<crate::document_parser::DocumentParserRegistry>,
69 ) -> Self {
70 self.document_parsers = Some(registry);
71 self
72 }
73
74 pub fn with_skill_registry(mut self, registry: Arc<crate::skills::SkillRegistry>) -> Self {
75 self.skill_registry = Some(registry);
76 self
77 }
78}
79
80impl Default for PluginContext {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86pub trait Plugin: Send + Sync {
116 fn name(&self) -> &str;
118
119 fn version(&self) -> &str;
121
122 fn tool_names(&self) -> &[&str];
126
127 fn load(&self, registry: &Arc<ToolRegistry>, ctx: &PluginContext) -> Result<()>;
131
132 fn unload(&self, registry: &Arc<ToolRegistry>) {
137 for name in self.tool_names() {
138 registry.unregister(name);
139 }
140 }
141
142 fn description(&self) -> &str {
144 ""
145 }
146
147 fn skills(&self) -> Vec<Arc<Skill>> {
158 vec![]
159 }
160}
161
162#[derive(Default)]
171pub struct PluginManager {
172 plugins: Vec<Arc<dyn Plugin>>,
173}
174
175impl PluginManager {
176 pub fn new() -> Self {
177 Self::default()
178 }
179
180 pub fn register(&mut self, plugin: impl Plugin + 'static) {
182 self.plugins.push(Arc::new(plugin));
183 }
184
185 pub fn register_arc(&mut self, plugin: Arc<dyn Plugin>) {
187 self.plugins.push(plugin);
188 }
189
190 pub fn load_all(&self, registry: &Arc<ToolRegistry>, ctx: &PluginContext) {
198 for plugin in &self.plugins {
199 tracing::info!("Loading plugin '{}' v{}", plugin.name(), plugin.version());
200 match plugin.load(registry, ctx) {
201 Ok(()) => {
202 if let Some(ref skill_reg) = ctx.skill_registry {
203 for skill in plugin.skills() {
204 tracing::debug!(
205 "Plugin '{}' registered skill '{}'",
206 plugin.name(),
207 skill.name
208 );
209 skill_reg.register_unchecked(skill);
210 }
211 }
212 }
213 Err(e) => {
214 tracing::error!("Plugin '{}' failed to load: {}", plugin.name(), e);
215 }
216 }
217 }
218 }
219
220 pub fn unload(&mut self, name: &str, registry: &Arc<ToolRegistry>) {
224 if let Some(pos) = self.plugins.iter().position(|p| p.name() == name) {
225 let plugin = self.plugins.remove(pos);
226 tracing::info!("Unloading plugin '{}'", plugin.name());
227 plugin.unload(registry);
228 }
229 }
230
231 pub fn unload_all(&mut self, registry: &Arc<ToolRegistry>) {
233 for plugin in self.plugins.drain(..).rev() {
234 tracing::info!("Unloading plugin '{}'", plugin.name());
235 plugin.unload(registry);
236 }
237 }
238
239 pub fn is_loaded(&self, name: &str) -> bool {
241 self.plugins.iter().any(|p| p.name() == name)
242 }
243
244 pub fn len(&self) -> usize {
246 self.plugins.len()
247 }
248
249 pub fn is_empty(&self) -> bool {
251 self.plugins.is_empty()
252 }
253
254 pub fn plugin_names(&self) -> Vec<&str> {
256 self.plugins.iter().map(|p| p.name()).collect()
257 }
258}
259
260const AGENTIC_SEARCH_SKILL_MD: &str = include_str!("../skills/agentic-search.md");
267const AGENTIC_PARSE_SKILL_MD: &str = include_str!("../skills/agentic-parse.md");
268
269fn parse_embedded_skill(content: &str, name: &str) -> Option<Arc<Skill>> {
270 match Skill::parse(content) {
271 Some(skill) => Some(Arc::new(skill)),
272 None => {
273 tracing::warn!(
274 "Failed to parse embedded skill '{}' — skill will not be registered",
275 name
276 );
277 None
278 }
279 }
280}
281
282pub struct SkillPlugin {
308 plugin_name: String,
309 plugin_version: String,
310 skill_contents: Vec<String>,
311}
312
313impl SkillPlugin {
314 pub fn new(name: impl Into<String>) -> Self {
315 Self {
316 plugin_name: name.into(),
317 plugin_version: "1.0.0".into(),
318 skill_contents: vec![],
319 }
320 }
321
322 pub fn with_skill(mut self, content: impl Into<String>) -> Self {
323 self.skill_contents.push(content.into());
324 self
325 }
326
327 pub fn with_skills(mut self, contents: impl IntoIterator<Item = impl Into<String>>) -> Self {
328 self.skill_contents
329 .extend(contents.into_iter().map(|s| s.into()));
330 self
331 }
332}
333
334impl Plugin for SkillPlugin {
335 fn name(&self) -> &str {
336 &self.plugin_name
337 }
338
339 fn version(&self) -> &str {
340 &self.plugin_version
341 }
342
343 fn tool_names(&self) -> &[&str] {
344 &[]
345 }
346
347 fn load(&self, _registry: &Arc<ToolRegistry>, _ctx: &PluginContext) -> Result<()> {
348 Ok(())
349 }
350
351 fn skills(&self) -> Vec<Arc<Skill>> {
352 self.skill_contents
353 .iter()
354 .filter_map(|content| Skill::parse(content).map(Arc::new))
355 .collect()
356 }
357}
358
359pub struct AgenticSearchPlugin;
365
366impl AgenticSearchPlugin {
367 pub fn new() -> Self {
368 Self
369 }
370}
371
372impl Default for AgenticSearchPlugin {
373 fn default() -> Self {
374 Self::new()
375 }
376}
377
378impl Plugin for AgenticSearchPlugin {
379 fn name(&self) -> &str {
380 "agentic-search"
381 }
382
383 fn version(&self) -> &str {
384 env!("CARGO_PKG_VERSION")
385 }
386
387 fn tool_names(&self) -> &[&str] {
388 &["agentic_search"]
389 }
390
391 fn description(&self) -> &str {
392 "Multi-phase semantic code search with IDF-weighted relevance scoring \
393 and Monte Carlo deep-search mode."
394 }
395
396 fn load(&self, registry: &Arc<ToolRegistry>, _ctx: &PluginContext) -> Result<()> {
397 use crate::tools::AgenticSearchTool;
398 registry.register(Arc::new(AgenticSearchTool::new()));
399 tracing::debug!("agentic_search tool registered");
400 Ok(())
401 }
402
403 fn skills(&self) -> Vec<Arc<Skill>> {
404 parse_embedded_skill(AGENTIC_SEARCH_SKILL_MD, "agentic-search")
405 .into_iter()
406 .collect()
407 }
408}
409
410pub struct AgenticParsePlugin;
418
419impl AgenticParsePlugin {
420 pub fn new() -> Self {
421 Self
422 }
423}
424
425impl Default for AgenticParsePlugin {
426 fn default() -> Self {
427 Self::new()
428 }
429}
430
431impl Plugin for AgenticParsePlugin {
432 fn name(&self) -> &str {
433 "agentic-parse"
434 }
435
436 fn version(&self) -> &str {
437 env!("CARGO_PKG_VERSION")
438 }
439
440 fn tool_names(&self) -> &[&str] {
441 &["agentic_parse"]
442 }
443
444 fn description(&self) -> &str {
445 "LLM-enhanced document parsing with structural extraction, \
446 5 parse strategies, and semantic QA."
447 }
448
449 fn load(&self, registry: &Arc<ToolRegistry>, ctx: &PluginContext) -> Result<()> {
450 use crate::tools::AgenticParseTool;
451 let llm = ctx.llm.clone().ok_or_else(|| {
452 anyhow::anyhow!("agentic-parse plugin requires an LLM client in PluginContext")
453 })?;
454 registry.register(Arc::new(AgenticParseTool::new(llm)));
455 tracing::debug!("agentic_parse tool registered");
456 Ok(())
457 }
458
459 fn skills(&self) -> Vec<Arc<Skill>> {
460 parse_embedded_skill(AGENTIC_PARSE_SKILL_MD, "agentic-parse")
461 .into_iter()
462 .collect()
463 }
464}
465
466#[cfg(test)]
471mod tests {
472 use super::*;
473 use crate::tools::ToolRegistry;
474 use std::path::PathBuf;
475
476 fn make_registry() -> Arc<ToolRegistry> {
477 Arc::new(ToolRegistry::new(PathBuf::from("/tmp")))
478 }
479
480 #[test]
481 fn plugin_manager_register_and_query() {
482 let mut mgr = PluginManager::new();
483 assert!(mgr.is_empty());
484 mgr.register(AgenticSearchPlugin::new());
485 assert_eq!(mgr.len(), 1);
486 assert!(mgr.is_loaded("agentic-search"));
487 assert!(!mgr.is_loaded("agentic-parse"));
488 }
489
490 #[test]
491 fn plugin_manager_load_all() {
492 let mut mgr = PluginManager::new();
493 mgr.register(AgenticSearchPlugin::new());
494 let registry = make_registry();
495 let ctx = PluginContext::new();
496 mgr.load_all(®istry, &ctx);
497 assert!(registry.get("agentic_search").is_some());
498 }
499
500 #[test]
501 fn plugin_manager_unload() {
502 let mut mgr = PluginManager::new();
503 mgr.register(AgenticSearchPlugin::new());
504 let registry = make_registry();
505 let ctx = PluginContext::new();
506 mgr.load_all(®istry, &ctx);
507 assert!(registry.get("agentic_search").is_some());
508 mgr.unload("agentic-search", ®istry);
509 assert!(registry.get("agentic_search").is_none());
510 assert!(!mgr.is_loaded("agentic-search"));
511 }
512
513 #[test]
514 fn plugin_manager_unload_all() {
515 let mut mgr = PluginManager::new();
516 mgr.register(AgenticSearchPlugin::new());
517 let registry = make_registry();
518 let ctx = PluginContext::new();
519 mgr.load_all(®istry, &ctx);
520 mgr.unload_all(®istry);
521 assert!(mgr.is_empty());
522 assert!(registry.get("agentic_search").is_none());
523 }
524
525 #[test]
526 fn agentic_search_plugin_metadata() {
527 let p = AgenticSearchPlugin::new();
528 assert_eq!(p.name(), "agentic-search");
529 assert_eq!(p.tool_names(), &["agentic_search"]);
530 assert!(!p.description().is_empty());
531 }
532
533 #[test]
534 fn agentic_search_plugin_provides_skill() {
535 let p = AgenticSearchPlugin::new();
536 let skills = p.skills();
537 assert_eq!(skills.len(), 1);
538 assert_eq!(skills[0].name, "agentic-search");
539 assert!(skills[0].is_tool_allowed("agentic_search"));
540 }
541
542 #[test]
543 fn agentic_parse_plugin_provides_skill() {
544 let p = AgenticParsePlugin::new();
545 let skills = p.skills();
546 assert_eq!(skills.len(), 1);
547 assert_eq!(skills[0].name, "agentic-parse");
548 assert!(skills[0].is_tool_allowed("agentic_parse"));
549 }
550
551 #[test]
552 fn plugin_skills_registered_on_load_all() {
553 use crate::skills::SkillRegistry;
554
555 let mut mgr = PluginManager::new();
556 mgr.register(AgenticSearchPlugin::new());
557
558 let registry = make_registry();
559 let skill_reg = Arc::new(SkillRegistry::new());
560 let ctx = PluginContext::new().with_skill_registry(Arc::clone(&skill_reg));
561
562 mgr.load_all(®istry, &ctx);
563
564 assert!(registry.get("agentic_search").is_some());
566 assert!(skill_reg.get("agentic-search").is_some());
568 }
569
570 #[test]
571 fn plugin_skills_not_registered_when_no_skill_registry_in_ctx() {
572 let mut mgr = PluginManager::new();
573 mgr.register(AgenticSearchPlugin::new());
574
575 let registry = make_registry();
576 let ctx = PluginContext::new();
578 mgr.load_all(®istry, &ctx);
579
580 assert!(registry.get("agentic_search").is_some());
582 }
584
585 #[test]
586 fn failed_plugin_skills_not_registered() {
587 use crate::skills::SkillRegistry;
588
589 let mut mgr = PluginManager::new();
591 mgr.register(AgenticParsePlugin::new());
592
593 let registry = make_registry();
594 let skill_reg = Arc::new(SkillRegistry::new());
595 let ctx = PluginContext::new().with_skill_registry(Arc::clone(&skill_reg));
596 mgr.load_all(®istry, &ctx);
599
600 assert!(registry.get("agentic_parse").is_none());
601 assert!(skill_reg.get("agentic-parse").is_none());
602 }
603
604 #[test]
605 fn agentic_parse_plugin_fails_without_llm() {
606 let p = AgenticParsePlugin::new();
607 let registry = make_registry();
608 let ctx = PluginContext::new(); let result = p.load(®istry, &ctx);
610 assert!(result.is_err());
611 assert!(result.unwrap_err().to_string().contains("LLM client"));
612 }
613
614 #[test]
615 fn skill_plugin_no_tools_and_injects_skills() {
616 use crate::skills::SkillRegistry;
617
618 let skill_md = r#"---
619name: test-skill
620description: Test skill
621allowed-tools: "bash(*)"
622kind: instruction
623---
624Test instruction."#;
625
626 let mut mgr = PluginManager::new();
627 mgr.register(SkillPlugin::new("test-plugin").with_skill(skill_md));
628
629 let registry = make_registry();
630 let skill_reg = Arc::new(SkillRegistry::new());
631 let ctx = PluginContext::new().with_skill_registry(Arc::clone(&skill_reg));
632
633 mgr.load_all(®istry, &ctx);
634
635 assert!(registry.get("test-plugin").is_none());
637 assert!(skill_reg.get("test-skill").is_some());
639 }
640
641 #[test]
642 fn skill_plugin_with_skills_builder() {
643 let skill1 = "---\nname: s1\ndescription: d1\nkind: instruction\n---\nContent 1";
644 let skill2 = "---\nname: s2\ndescription: d2\nkind: instruction\n---\nContent 2";
645
646 let plugin = SkillPlugin::new("multi").with_skills([skill1, skill2]);
647 assert_eq!(plugin.skills().len(), 2);
648 }
649
650 #[test]
651 fn plugin_names() {
652 let mut mgr = PluginManager::new();
653 mgr.register(AgenticSearchPlugin::new());
654 mgr.register(AgenticParsePlugin::new());
655 let names = mgr.plugin_names();
656 assert!(names.contains(&"agentic-search"));
657 assert!(names.contains(&"agentic-parse"));
658 }
659}