pulse_system_types/plugin.rs
1//! Plugin trait — the shared contract for pulse-null plugins.
2//!
3//! Plugins are constructed via async factory functions, not through the trait.
4//! By the time a [`Plugin`] exists, it is fully initialized and ready for
5//! [`start()`](Plugin::start).
6//!
7//! # Factory Pattern
8//!
9//! Each plugin crate exports a factory function:
10//!
11//! ```rust,ignore
12//! pub async fn create(
13//! config: &serde_json::Value,
14//! ctx: &PluginContext,
15//! ) -> Result<Box<dyn Plugin>, Box<dyn Error + Send + Sync>>
16//! ```
17//!
18//! This avoids two-phase initialization — the plugin is either fully
19//! constructed or the factory returns an error.
20//!
21//! # Object Safety
22//!
23//! The trait is object-safe and designed for `Box<dyn Plugin>`. Async methods
24//! use `Pin<Box<dyn Future>>` instead of `async_trait`.
25
26use std::any::Any;
27use std::future::Future;
28use std::path::PathBuf;
29use std::pin::Pin;
30use std::sync::Arc;
31
32use crate::llm::LmProvider;
33use crate::tool::Tool;
34use crate::{HealthStatus, PluginMeta, ScheduledTask, SetupPrompt};
35
36/// Async result type for plugin lifecycle operations.
37pub type PluginResult<'a> =
38 Pin<Box<dyn Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send + 'a>>;
39
40/// Context passed to plugin factories during construction.
41///
42/// Contains everything a plugin needs to initialize:
43/// filesystem root, entity identity, and LLM access.
44pub struct PluginContext {
45 /// Root directory of the entity (e.g., `/home/echo`).
46 pub entity_root: PathBuf,
47 /// Entity name from config (e.g., `"Echo"`).
48 pub entity_name: String,
49 /// LLM provider for plugin use (summarization, analysis, etc.).
50 pub provider: Arc<dyn LmProvider>,
51}
52
53/// Declares what role a plugin fills in the system.
54///
55/// Some roles are constrained:
56/// - [`Memory`](PluginRole::Memory): exactly one required.
57/// - All others: zero or more.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub enum PluginRole {
60 /// Persistent memory system. **Required — exactly one.**
61 Memory,
62 /// Pipeline state monitoring.
63 Pipeline,
64 /// Cognitive signal monitoring.
65 Cognitive,
66 /// Outcome tracking.
67 Outcome,
68 /// Communication interface (HTTP, voice, chat, etc.).
69 Interface,
70 /// General-purpose extension. No constraints.
71 Extension,
72}
73
74/// The core plugin contract.
75///
76/// Plugins are constructed via async factory functions — by the time this
77/// trait is available, the plugin is fully initialized and ready for
78/// [`start()`](Plugin::start).
79///
80/// # Dyn-compatible
81///
82/// Uses `Pin<Box<dyn Future>>` for async methods. No `async_trait` dependency.
83///
84/// # Extension via `as_any()`
85///
86/// Host-specific capabilities (e.g., HTTP routes via axum) are not part of
87/// this trait. Plugins that expose such capabilities implement [`as_any()`](Plugin::as_any)
88/// to allow the host to downcast to the concrete type.
89pub trait Plugin: Send + Sync {
90 /// Plugin identity (name, version, description).
91 fn meta(&self) -> PluginMeta;
92
93 /// What role this plugin fills in the system.
94 fn role(&self) -> PluginRole;
95
96 /// Start the plugin. Called once after construction.
97 fn start(&mut self) -> PluginResult<'_>;
98
99 /// Stop the plugin gracefully.
100 fn stop(&mut self) -> PluginResult<'_>;
101
102 /// Report current health.
103 fn health(&self) -> Pin<Box<dyn Future<Output = HealthStatus> + Send + '_>>;
104
105 /// Optional: contribute scheduled tasks.
106 fn scheduled_tasks(&self) -> Vec<ScheduledTask> {
107 Vec::new()
108 }
109
110 /// Optional: setup wizard prompts for first-time configuration.
111 fn setup_prompts(&self) -> Vec<SetupPrompt> {
112 Vec::new()
113 }
114
115 /// Optional: contribute tools to the entity's tool registry.
116 fn tools(&self) -> Vec<Box<dyn Tool>> {
117 Vec::new()
118 }
119
120 /// Downcast support for host-specific extensions.
121 ///
122 /// Plugins that expose capabilities beyond this trait (e.g., axum routes)
123 /// return `self` here so the host can downcast to the concrete type.
124 fn as_any(&self) -> &dyn Any;
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn plugin_role_equality() {
133 assert_eq!(PluginRole::Memory, PluginRole::Memory);
134 assert_ne!(PluginRole::Memory, PluginRole::Pipeline);
135 }
136
137 #[test]
138 fn plugin_role_is_copy() {
139 let role = PluginRole::Memory;
140 let copy = role;
141 assert_eq!(role, copy);
142 }
143
144 #[test]
145 fn plugin_role_debug() {
146 let debug = format!("{:?}", PluginRole::Interface);
147 assert_eq!(debug, "Interface");
148 }
149}