Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Plugin System

Synaptic's plugin system lets you extend agents with tools, event subscribers, memory providers, services, and interceptors -- all through a unified registration API. Plugins declare their capabilities via a manifest and register components through a scoped PluginApi. The registry supports hot-disable for runtime plugin management.

Setup

Enable the plugin feature on the synaptic-config crate (or through the facade):

[dependencies]
synaptic = { version = "0.4", features = ["plugin"] }

Plugin Trait

Every plugin implements the Plugin trait. The lifecycle has three stages: manifest (declare metadata), register (add components), and start/stop (runtime hooks).

use synaptic::config::plugin::{Plugin, PluginContext, PluginApi, PluginManifest};
use synaptic::core::SynapticError;
use async_trait::async_trait;

pub struct MyPlugin;

#[async_trait]
impl Plugin for MyPlugin {
    fn manifest(&self) -> PluginManifest {
        PluginManifest {
            name: "my-plugin".into(),
            version: "0.1.0".into(),
            description: "A custom plugin".into(),
            author: Some("Your Name".into()),
            license: Some("MIT".into()),
            capabilities: vec![],
            slot: None,
        }
    }

    async fn register(&self, api: &mut PluginApi<'_>) -> Result<(), SynapticError> {
        // Register tools, subscribers, services, etc.
        Ok(())
    }

    async fn start(&self, ctx: PluginContext) -> Result<(), SynapticError> {
        // ctx.data_dir is the plugin-specific data directory
        println!("Plugin data dir: {:?}", ctx.data_dir);
        Ok(())
    }

    async fn stop(&self) -> Result<(), SynapticError> {
        // Cleanup on shutdown
        Ok(())
    }
}

The start and stop methods have default no-op implementations, so you only need to override them if your plugin requires initialization or cleanup.

PluginManifest

The manifest declares metadata and capabilities:

use synaptic::config::plugin::{PluginManifest, PluginCapability, PluginSlot};

let manifest = PluginManifest {
    name: "search-plugin".into(),
    version: "1.0.0".into(),
    description: "Adds web search tools".into(),
    author: Some("Team".into()),
    license: Some("Apache-2.0".into()),
    capabilities: vec![
        PluginCapability::Tools,
        PluginCapability::Hooks,
    ],
    slot: None, // or Some(PluginSlot::Memory) for slot plugins
};

Capabilities describe what the plugin provides:

VariantDescription
ToolsRegisters agent tools
HooksSubscribes to lifecycle events
ChannelsCommunication channels
ProvidersModel or embedding providers
HttpRoutesHTTP endpoint handlers
CommandsCLI commands
ServicesBackground services
CanvasRenderersUI rendering extensions
MemoryMemory providers

Slots (PluginSlot::Memory, PluginSlot::ContextEngine) are exclusive -- only one plugin can occupy a given slot at a time.

PluginApi (Scoped Registration)

During register(), your plugin receives a PluginApi scoped to its plugin ID. All registrations are automatically tracked by the registry.

use synaptic::config::plugin::PluginApi;
use synaptic::core::SynapticError;
use std::sync::Arc;
use async_trait::async_trait;

#[async_trait]
impl Plugin for MyPlugin {
    // ... manifest() ...

    async fn register(&self, api: &mut PluginApi<'_>) -> Result<(), SynapticError> {
        // Register a tool
        api.register_tool(Arc::new(MySearchTool));

        // Register an event subscriber with priority (lower = earlier)
        api.register_event_subscriber(Arc::new(MySubscriber), 10);

        // Register a background service
        api.register_service(Box::new(MyBackgroundService));

        // Register a middleware interceptor
        api.register_interceptor(Arc::new(MyInterceptor));

        // Register a memory provider (claims the Memory slot)
        api.register_memory(Arc::new(MyMemoryProvider));

        // Access the plugin's own ID
        println!("Registering as: {}", api.plugin_id());

        Ok(())
    }
}

Service Trait

Long-running background services implement the Service trait:

use synaptic::config::plugin::Service;
use synaptic::core::SynapticError;
use async_trait::async_trait;

pub struct MetricsService;

#[async_trait]
impl Service for MetricsService {
    fn id(&self) -> &str {
        "metrics-service"
    }

    async fn start(&self) -> Result<(), SynapticError> {
        // Start background work (e.g., metrics collection)
        Ok(())
    }

    async fn health_check(&self) -> bool {
        true
    }

    async fn stop(&self) {
        // Graceful shutdown
    }
}

PluginRegistry (Hot-Disable)

The PluginRegistry manages all registered plugins and their components. It supports hot-disabling plugins at runtime without restarting the agent.

use synaptic::config::plugin::PluginRegistry;
use synaptic::config::plugin::EventBus;
use std::sync::Arc;

// Create a registry
let event_bus = Arc::new(EventBus::new());
let mut registry = PluginRegistry::new(event_bus);

// Register a plugin
let plugin = MyPlugin;
registry.register_plugin(&plugin).await?;

// Inspect registered components
let tools = registry.tools();
let services = registry.services();
let plugins = registry.plugins();

// Check what a plugin registered
if let Some(regs) = registry.plugin_registrations("my-plugin") {
    println!("Tools: {:?}", regs.tools);
    println!("Services: {:?}", regs.services);
    println!("Interceptors: {:?}", regs.interceptors);
    println!("Subscribers: {:?}", regs.subscribers);
}

// Hot-disable: removes all components registered by a plugin
let removed = registry.unregister_plugin("my-plugin");
println!("Removed {} registrations", removed.len());

// Memory slot management
if let Some(provider) = registry.memory_slot() {
    println!("Memory slot owned by: {:?}", registry.memory_slot_owner());
}

The unregister_plugin method returns a list of all component names that were removed, making it easy to log or audit plugin lifecycle changes.

PluginHookInterceptor (EventBus Bridge)

PluginHookInterceptor (in synaptic-middleware) bridges the middleware pipeline to the plugin EventBus. It converts middleware lifecycle events into bus events that plugin subscribers can react to:

Middleware hookEventBus event
before_modelBeforeModelCall
after_modelLlmOutput
wrap_tool_callBeforeToolCall / AfterToolCall

This lets plugins observe and react to model calls and tool executions without modifying the core middleware chain.

AgentPlugins

AgentPlugins (in synaptic-graph) collects interceptors and wires them into the agent's processing pipeline:

use synaptic::graph::plugins::AgentPlugins;
use std::sync::Arc;

let plugins = AgentPlugins::new()
    .with_interceptor(Arc::new(MyInterceptor));

// Or build incrementally
let mut plugins = AgentPlugins::new();
plugins.add_interceptor(Arc::new(AnotherInterceptor));

// Get the composed chain for use in agent execution
let chain = plugins.interceptor_chain();

Example Plugin

Here is a complete plugin that registers a custom tool and a background service:

use synaptic::config::plugin::{
    Plugin, PluginApi, PluginContext, PluginManifest,
    PluginCapability, Service,
};
use synaptic::core::{Tool, ToolDefinition, SynapticError};
use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;

// -- Tool ----------------------------------------------------------

struct PingTool;

#[async_trait]
impl Tool for PingTool {
    fn definition(&self) -> ToolDefinition {
        ToolDefinition {
            name: "ping".into(),
            description: "Returns pong".into(),
            parameters: serde_json::json!({}),
        }
    }

    async fn call(&self, _input: Value) -> Result<String, SynapticError> {
        Ok("pong".into())
    }
}

// -- Service -------------------------------------------------------

struct HealthService;

#[async_trait]
impl Service for HealthService {
    fn id(&self) -> &str { "health" }
    async fn start(&self) -> Result<(), SynapticError> { Ok(()) }
    async fn health_check(&self) -> bool { true }
    async fn stop(&self) {}
}

// -- Plugin --------------------------------------------------------

pub struct PingPlugin;

#[async_trait]
impl Plugin for PingPlugin {
    fn manifest(&self) -> PluginManifest {
        PluginManifest {
            name: "ping-plugin".into(),
            version: "0.1.0".into(),
            description: "Adds a ping tool and health service".into(),
            author: None,
            license: None,
            capabilities: vec![
                PluginCapability::Tools,
                PluginCapability::Services,
            ],
            slot: None,
        }
    }

    async fn register(&self, api: &mut PluginApi<'_>) -> Result<(), SynapticError> {
        api.register_tool(Arc::new(PingTool));
        api.register_service(Box::new(HealthService));
        Ok(())
    }

    async fn start(&self, ctx: PluginContext) -> Result<(), SynapticError> {
        println!("PingPlugin started, data dir: {:?}", ctx.data_dir);
        Ok(())
    }

    async fn stop(&self) -> Result<(), SynapticError> {
        println!("PingPlugin stopped");
        Ok(())
    }
}