Skip to main content

Compile-time Plugins

Compile-time plugins are Rust implementations that are compiled directly into your agent binary, providing maximum performance with zero-cost abstractions. They’re ideal for performance-critical operations like LLM integration, data processing, and system integration.

Why Compile-time Plugins?

Performance

Zero overhead - compiled to native code

Type Safety

Full Rust type system and compiler checks

Native Integration

Direct access to OS and system APIs

Reusability

Publish to crates.io for community use

Creating a Custom Plugin

Let’s build a calculator tool plugin from scratch:
1

Define the Tool

Create a struct implementing ToolExecutor:
examples/plugin_system/src/main.rs
use mofa_plugins::{
    ToolDefinition, ToolExecutor, PluginResult, PluginError
};

struct CalculatorTool {
    definition: ToolDefinition,
}

impl CalculatorTool {
    fn new() -> Self {
        Self {
            definition: ToolDefinition {
                name: "calculator".to_string(),
                description: "Perform basic arithmetic operations".to_string(),
                parameters: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "operation": {
                            "type": "string",
                            "enum": ["add", "subtract", "multiply", "divide"]
                        },
                        "a": { "type": "number" },
                        "b": { "type": "number" }
                    },
                    "required": ["operation", "a", "b"]
                }),
                requires_confirmation: false,
            },
        }
    }
}
2

Implement ToolExecutor

Add execution logic:
examples/plugin_system/src/main.rs
#[async_trait::async_trait]
impl ToolExecutor for CalculatorTool {
    fn definition(&self) -> &ToolDefinition {
        &self.definition
    }

    async fn execute(&self, arguments: serde_json::Value) -> PluginResult<serde_json::Value> {
        let op = arguments["operation"].as_str().unwrap_or("add");
        let a = arguments["a"].as_f64().unwrap_or(0.0);
        let b = arguments["b"].as_f64().unwrap_or(0.0);

        let result = match op {
            "add" => a + b,
            "subtract" => a - b,
            "multiply" => a * b,
            "divide" => {
                if b == 0.0 {
                    return Err(PluginError::ExecutionFailed(
                        "Division by zero".to_string()
                    ));
                }
                a / b
            }
            _ => return Err(PluginError::ExecutionFailed(
                format!("Unknown operation: {}", op)
            )),
        };

        Ok(serde_json::json!({
            "result": result,
            "operation": op,
            "a": a,
            "b": b
        }))
    }
}
3

Register the Tool

Add to a ToolPlugin:
examples/plugin_system/src/main.rs
let mut tool_plugin = ToolPlugin::new("tools_main");
tool_plugin.register_tool(CalculatorTool::new());

manager.register(tool_plugin).await?;
4

Use the Tool

Execute via the plugin manager:
examples/plugin_system/src/main.rs
let calc_call = serde_json::json!({
    "name": "calculator",
    "arguments": {
        "operation": "multiply",
        "a": 7,
        "b": 8
    },
    "call_id": "calc_001"
});

let result = manager.execute("tools_main", calc_call.to_string()).await?;
// Result: {"call_id":"calc_001","success":true,"result":{"result":56,"operation":"multiply","a":7,"b":8}}

Built-in Plugin Types

MoFA provides several ready-to-use plugin types:

LLM Plugin

Integrate language models:
mofa-plugins/src/lib.rs
use mofa_plugins::{LLMPlugin, LLMPluginConfig, ChatMessage};

// Configure LLM
let llm_config = LLMPluginConfig {
    model: "gpt-4".to_string(),
    max_tokens: 4096,
    temperature: 0.7,
    ..Default::default()
};

// Create and register
let mut llm = LLMPlugin::new("llm_main")
    .with_config(llm_config);

manager.register(llm).await?;

// Use for chat
let messages = vec![
    ChatMessage::system("You are a helpful assistant"),
    ChatMessage::user("What is Rust?"),
];

let response = manager.execute(
    "llm_main",
    serde_json::to_string(&messages)?
).await?;

Storage Plugin

Persistent key-value storage:
mofa-plugins/src/lib.rs
use mofa_plugins::{StoragePlugin, MemoryStorage};

// Create with in-memory backend
let storage = StoragePlugin::new("storage_main")
    .with_backend(MemoryStorage::new());

manager.register(storage).await?;

// Store data
manager.execute("storage_main", "set user:name Alice".to_string()).await?;
manager.execute("storage_main", "set user:age 30".to_string()).await?;

// Retrieve data
let name = manager.execute("storage_main", "get user:name".to_string()).await?;
let age = manager.execute("storage_main", "get user:age".to_string()).await?;

println!("User: name={}, age={}", name, age);

Memory Plugin

Agent memory management:
mofa-plugins/src/lib.rs
use mofa_plugins::MemoryPlugin;

let memory = MemoryPlugin::new("memory_main")
    .with_max_memories(1000);

manager.register(memory).await?;

// Add memories with importance scores
manager.execute(
    "memory_main",
    "add User prefers dark mode 0.9".to_string()
).await?;

manager.execute(
    "memory_main",
    "add Last query was about weather 0.6".to_string()
).await?;

// Search memories
let results = manager.execute(
    "memory_main",
    "search weather".to_string()
).await?;

println!("Found memories: {}", results);

Custom Plugin Implementation

Create a complete custom plugin:
examples/plugin_system/src/main.rs
use mofa_plugins::{
    AgentPlugin, PluginContext, PluginMetadata, PluginResult,
    PluginState, PluginType, PluginPriority
};
use std::any::Any;
use std::collections::HashMap;

struct MonitorPlugin {
    metadata: PluginMetadata,
    state: PluginState,
    metrics: HashMap<String, f64>,
    alert_threshold: f64,
}

impl MonitorPlugin {
    fn new(plugin_id: &str) -> Self {
        let metadata = PluginMetadata::new(
            plugin_id,
            "Monitor Plugin",
            PluginType::Monitor
        )
        .with_description("System monitoring and alerting plugin")
        .with_capability("metrics")
        .with_capability("alerting")
        .with_priority(PluginPriority::High);

        Self {
            metadata,
            state: PluginState::Unloaded,
            metrics: HashMap::new(),
            alert_threshold: 80.0,
        }
    }

    fn record_metric(&mut self, name: &str, value: f64) {
        self.metrics.insert(name.to_string(), value);
        if value > self.alert_threshold {
            warn!("ALERT: {} exceeded threshold: {} > {}",
                name, value, self.alert_threshold);
        }
    }
}

#[async_trait::async_trait]
impl AgentPlugin for MonitorPlugin {
    fn metadata(&self) -> &PluginMetadata {
        &self.metadata
    }

    fn state(&self) -> PluginState {
        self.state.clone()
    }

    async fn load(&mut self, _ctx: &PluginContext) -> PluginResult<()> {
        self.state = PluginState::Loading;
        info!("Loading Monitor plugin: {}", self.metadata.id);
        self.state = PluginState::Loaded;
        Ok(())
    }

    async fn init_plugin(&mut self) -> PluginResult<()> {
        info!("Initializing Monitor plugin: {}", self.metadata.id);
        // Initialize base metrics
        self.metrics.insert("cpu_usage".to_string(), 0.0);
        self.metrics.insert("memory_usage".to_string(), 0.0);
        Ok(())
    }

    async fn start(&mut self) -> PluginResult<()> {
        self.state = PluginState::Running;
        info!("Monitor plugin {} started", self.metadata.id);
        Ok(())
    }

    async fn stop(&mut self) -> PluginResult<()> {
        self.state = PluginState::Paused;
        info!("Monitor plugin {} stopped", self.metadata.id);
        Ok(())
    }

    async fn unload(&mut self) -> PluginResult<()> {
        self.metrics.clear();
        self.state = PluginState::Unloaded;
        info!("Monitor plugin {} unloaded", self.metadata.id);
        Ok(())
    }

    async fn execute(&mut self, input: String) -> PluginResult<String> {
        let parts: Vec<&str> = input.split_whitespace().collect();
        match parts.as_slice() {
            ["record", name, value] => {
                let v: f64 = value.parse().unwrap_or(0.0);
                self.record_metric(name, v);
                Ok(format!("Recorded {} = {}", name, v))
            }
            ["get", name] => {
                Ok(self.metrics.get(*name)
                    .map(|v| v.to_string())
                    .unwrap_or_else(|| "null".to_string()))
            }
            ["list"] => Ok(serde_json::to_string(&self.metrics)?),
            _ => Err(PluginError::ExecutionFailed(
                "Invalid command".to_string()
            )),
        }
    }

    fn stats(&self) -> HashMap<String, serde_json::Value> {
        let mut stats = HashMap::new();
        stats.insert("metric_count".to_string(),
            serde_json::json!(self.metrics.len()));
        stats.insert("alert_threshold".to_string(),
            serde_json::json!(self.alert_threshold));
        stats
    }

    fn as_any(&self) -> &dyn Any { self }
    fn as_any_mut(&mut self) -> &mut dyn Any { self }
    fn into_any(self: Box<Self>) -> Box<dyn Any> { self }
}

Plugin Configuration

Use PluginConfig for flexible configuration:
use mofa_plugins::{PluginConfig, PluginManager};

let mut config = PluginConfig::new();
config.set("model", "gpt-4");
config.set("max_tokens", 4096);
config.set("temperature", 0.7);
config.set("timeout_secs", 30);
config.set("enabled", true);
config.set("auto_start", true);

let llm = LLMPlugin::new("llm_001");
manager.register_with_config(llm, config).await?;

Plugin Priority

Control initialization order with priorities:
use mofa_plugins::PluginPriority;

let metadata = PluginMetadata::new("critical", "Critical Plugin", PluginType::Tool)
    .with_priority(PluginPriority::Critical); // Initialized first

let metadata = PluginMetadata::new("high", "High Plugin", PluginType::Tool)
    .with_priority(PluginPriority::High);

let metadata = PluginMetadata::new("normal", "Normal Plugin", PluginType::Tool)
    .with_priority(PluginPriority::Normal);

let metadata = PluginMetadata::new("low", "Low Plugin", PluginType::Tool)
    .with_priority(PluginPriority::Low); // Initialized last
Plugins are initialized in priority order (highest first). Plugins with the same priority are initialized in registration order.

Error Handling

Handle plugin errors gracefully:
use mofa_plugins::PluginError;

match manager.execute("my_plugin", input).await {
    Ok(result) => println!("Success: {}", result),
    Err(PluginError::InitFailed(msg)) => {
        eprintln!("Plugin not initialized: {}", msg);
    }
    Err(PluginError::ExecutionFailed(msg)) => {
        eprintln!("Execution failed: {}", msg);
    }
    Err(PluginError::NotFound(id)) => {
        eprintln!("Plugin not found: {}", id);
    }
    Err(e) => eprintln!("Error: {}", e),
}

Testing Plugins

Write tests for your plugins:
#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_calculator_tool() {
        let tool = CalculatorTool::new();

        let args = serde_json::json!({
            "operation": "add",
            "a": 10,
            "b": 5
        });

        let result = tool.execute(args).await.unwrap();
        assert_eq!(result["result"], 15.0);
    }

    #[tokio::test]
    async fn test_monitor_plugin_lifecycle() {
        let mut plugin = MonitorPlugin::new("test_monitor");
        let ctx = PluginContext::new("test_agent");

        plugin.load(&ctx).await.unwrap();
        assert_eq!(plugin.state(), PluginState::Loaded);

        plugin.init_plugin().await.unwrap();
        plugin.start().await.unwrap();
        assert_eq!(plugin.state(), PluginState::Running);

        plugin.stop().await.unwrap();
        assert_eq!(plugin.state(), PluginState::Paused);

        plugin.unload().await.unwrap();
        assert_eq!(plugin.state(), PluginState::Unloaded);
    }
}

Best Practices

Always return PluginResult with descriptive error messages. Avoid panicking.
Plugin lifecycle methods should be safe to call multiple times.
Release connections, files, and memory in the unload method.
Use metadata to clearly document what your plugin can do.
Include version information for compatibility tracking.
Use the shared context for state instead of internal mutable state.

Next Steps

Runtime Plugins

Create hot-reloadable Rhai plugins

WASM Plugins

Build sandboxed WebAssembly plugins

Build docs developers (and LLMs) love