pub mod host_methods;
pub mod manifest;
pub mod shell_bridge;
pub mod slots;
pub mod wasm_runtime;
use std::collections::HashMap;
use std::sync::Arc;
use anyhow::Result;
pub use manifest::{
LEGACY_MANIFEST_FILE, MANIFEST_FILE, PluginManifest, PluginToolDef, load_manifest, scan_plugins,
};
pub use shell_bridge::Plugin;
pub use slots::{ContextEngineSlot, MemoryItem, MemorySlot, MemoryStoreSlot, SlotRegistry};
use tracing::{info, warn};
pub use wasm_runtime::{WasmPlugin, WasmToolDef, load_wasm_plugin};
use crate::config::schema::PluginsConfig;
pub struct PluginRegistry {
plugins: HashMap<String, Plugin>,
wasm_plugins: Vec<WasmPlugin>,
pub slots: SlotRegistry,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: HashMap::new(),
wasm_plugins: Vec::new(),
slots: SlotRegistry::new(),
}
}
pub fn get_shell(&self, name: &str) -> Option<&Plugin> {
self.plugins.get(name)
}
pub fn shell_plugins_iter(&self) -> impl Iterator<Item = (&String, &Plugin)> {
self.plugins.iter()
}
pub fn all(&self) -> impl Iterator<Item = &Plugin> {
self.plugins.values()
}
pub fn wasm_all(&self) -> &[WasmPlugin] {
&self.wasm_plugins
}
pub fn len(&self) -> usize {
self.plugins.len() + self.wasm_plugins.len()
}
pub fn is_empty(&self) -> bool {
self.plugins.is_empty() && self.wasm_plugins.is_empty()
}
pub fn js_count(&self) -> usize {
self.plugins.len()
}
pub fn wasm_count(&self) -> usize {
self.wasm_plugins.len()
}
pub fn take_wasm_plugins(&mut self) -> Vec<WasmPlugin> {
std::mem::take(&mut self.wasm_plugins)
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
pub async fn load_all_plugins(
plugins_dir: &std::path::Path,
config: Option<&PluginsConfig>,
wasm_browser: Arc<tokio::sync::Mutex<Option<crate::browser::BrowserSession>>>,
notify_tx: Option<tokio::sync::broadcast::Sender<crate::channel::OutboundMessage>>,
) -> Result<PluginRegistry> {
let manifests = scan_plugins(plugins_dir)?;
let mut registry = PluginRegistry::new();
let host_dispatch = Arc::new(host_methods::HostMethodRegistry::new(
notify_tx,
Arc::clone(&wasm_browser),
));
let wasm_engine = if manifests.iter().any(|m| m.is_wasm()) {
let mut wasm_config = wasmtime::Config::new();
wasm_config.async_support(true);
wasm_config.epoch_interruption(true);
let engine = wasmtime::Engine::new(&wasm_config)?;
let tick_engine = engine.clone();
tokio::spawn(async move {
let mut ticker = tokio::time::interval(std::time::Duration::from_millis(100));
loop {
ticker.tick().await;
tick_engine.increment_epoch();
}
});
Some(engine)
} else {
None
};
for manifest in manifests {
let enabled = config
.and_then(|c| c.entries.as_ref())
.and_then(|e| e.get(&manifest.name))
.and_then(|e| e.enabled)
.unwrap_or(true);
if !enabled {
info!(plugin = %manifest.name, "plugin disabled via config");
continue;
}
if manifest.is_wasm() {
let engine = wasm_engine.as_ref().expect("wasm engine initialized");
match load_wasm_plugin(&manifest, engine, Arc::clone(&wasm_browser)).await {
Ok(plugin) => {
info!(
plugin = %plugin.name,
tools = plugin.tools.len(),
version = ?manifest.version,
"WASM plugin loaded"
);
registry.wasm_plugins.push(plugin);
}
Err(e) => {
warn!(plugin = %manifest.name, "failed to load WASM plugin: {e:#}");
}
}
} else {
match Plugin::spawn(manifest, host_dispatch.clone()).await {
Ok(plugin) => {
info!(plugin = %plugin.manifest.name, "shell plugin started");
registry
.plugins
.insert(plugin.manifest.name.clone(), plugin);
}
Err(e) => {
warn!("failed to start plugin: {e:#}");
}
}
}
}
info!(
total = registry.len(),
js = registry.js_count(),
wasm = registry.wasm_count(),
"plugins loaded"
);
Ok(registry)
}