Runtime Plugins
Runtime plugins are written in Rhai, a lightweight scripting language that integrates seamlessly with Rust. They enable dynamic behavior changes without recompilation and support hot-reloading for rapid development.
Why Runtime Plugins?
Hot Reload Modify plugin behavior without restarting
Rapid Development Iterate quickly without compilation
Dynamic Logic Change business rules at runtime
Easy Distribution Ship updates as script files
Creating a Rhai Plugin
A basic Rhai plugin structure:
examples/rhai_hot_reload/sample_plugin.rhai
// Plugin metadata (optional)
plugin_name = "my_rhai_plugin";
plugin_version = "1.0.0";
plugin_description = "A sample Rhai plugin";
// Lifecycle functions
fn init() {
print("Plugin initialized");
}
fn start() {
print("Plugin started");
}
fn execute(input) {
"Rhai plugin executed: " + input
}
fn stop() {
print("Plugin stopped");
}
fn unload() {
print("Plugin unloaded");
}
Loading Rhai Plugins
From File
From String
With Configuration
examples/rhai_hot_reload/src/main.rs
use mofa_plugins :: { RhaiPlugin , AgentPlugin , PluginContext };
use std :: path :: PathBuf ;
#[tokio :: main]
async fn main () -> Result <(), Box < dyn std :: error :: Error >> {
let plugin_path = PathBuf :: from ( "sample_plugin.rhai" );
// Create plugin from file
let mut plugin = RhaiPlugin :: from_file (
"my_plugin" ,
& plugin_path
) . await ? ;
// Initialize
let ctx = PluginContext :: new ( "test_agent" );
plugin . load ( & ctx ) . await ? ;
plugin . init_plugin () . await ? ;
// Execute
let result = plugin . execute ( "Hello" . to_string ()) . await ? ;
println! ( "Result: {}" , result );
Ok (())
}
use mofa_plugins :: { RhaiPlugin , AgentPlugin , PluginContext };
#[tokio :: main]
async fn main () -> Result <(), Box < dyn std :: error :: Error >> {
let script = r#"
plugin_name = "inline_plugin";
fn execute(input) {
"Processed: " + input
}
"# ;
let mut plugin = RhaiPlugin :: from_content (
"inline" ,
script
) . await ? ;
let ctx = PluginContext :: new ( "test_agent" );
plugin . load ( & ctx ) . await ? ;
plugin . init_plugin () . await ? ;
let result = plugin . execute ( "test" . to_string ()) . await ? ;
println! ( "Result: {}" , result );
Ok (())
}
use mofa_plugins :: {
RhaiPlugin , RhaiPluginConfig , RhaiPluginSource ,
ScriptEngineConfig
};
let config = RhaiPluginConfig {
source : RhaiPluginSource :: File ( PathBuf :: from ( "plugin.rhai" )),
engine_config : ScriptEngineConfig :: default (),
initial_context : HashMap :: new (),
dependencies : vec! [],
plugin_id : "custom_plugin" . to_string (),
};
let plugin = RhaiPlugin :: new ( config ) . await ? ;
Plugin Lifecycle
Rhai plugins support the full plugin lifecycle:
Load
Plugin is loaded and metadata is extracted: // Metadata extraction happens automatically
plugin_name = "my_plugin";
plugin_version = "1.0.0";
plugin_description = "My awesome plugin";
Initialize
Optional init() function is called: fn init() {
// Initialize plugin state
log("Plugin initializing...");
// Set up resources
}
Start
Optional start() function is called: fn start() {
log("Plugin started and ready");
}
Execute
Required execute(input) function handles requests: fn execute(input) {
// Process input
let result = process_data(input);
result
}
Stop
Optional stop() function is called: fn stop() {
log("Plugin stopping...");
// Pause operations
}
Unload
Optional unload() function is called: fn unload() {
log("Plugin unloading...");
// Release resources
}
Hot Reloading
Modify plugins at runtime without restarting:
examples/rhai_hot_reload/src/main.rs
use tokio :: time;
// Check for changes and reload
async fn check_and_reload (
plugin : & mut RhaiPlugin ,
path : & PathBuf
) -> Result < bool , Box < dyn std :: error :: Error >> {
// Get current file modification time
let current_mod = std :: fs :: metadata ( path ) ?
. modified () ?
. duration_since ( std :: time :: UNIX_EPOCH ) ?
. as_secs ();
// Check if plugin needs reload
if plugin . last_modified () != current_mod {
plugin . reload () . await ? ;
Ok ( true )
} else {
Ok ( false )
}
}
#[tokio :: main]
async fn main () -> Result <(), Box < dyn std :: error :: Error >> {
let plugin_path = PathBuf :: from ( "sample_plugin.rhai" );
let mut plugin = RhaiPlugin :: from_file ( "hot_plugin" , & plugin_path ) . await ? ;
// Initialize
let ctx = PluginContext :: new ( "agent" );
plugin . load ( & ctx ) . await ? ;
plugin . init_plugin () . await ? ;
// Main loop with hot reload
loop {
// Check for changes
match check_and_reload ( & mut plugin , & plugin_path ) . await {
Ok ( true ) => println! ( "🔄 Plugin reloaded!" ),
Ok ( false ) => println! ( "✅ Plugin unchanged" ),
Err ( e ) => println! ( "⚠️ Error: {}" , e ),
}
// Execute plugin
let result = plugin . execute ( "test" . to_string ()) . await ? ;
println! ( "Result: {}" , result );
time :: sleep ( time :: Duration :: from_secs ( 2 )) . await ;
}
}
Hot reload preserves the plugin state across reloads when configured with preserve_state: true.
Advanced Plugin Features
State Management
Maintain state across invocations:
// Global state (preserved across calls)
let counter = 0;
let cache = #{};
fn execute(input) {
// Increment counter
counter += 1;
// Use cache
if cache.contains(input) {
cache[input]
} else {
let result = expensive_operation(input);
cache[input] = result;
result
}
}
fn expensive_operation(data) {
// Simulate expensive work
data + " processed (call #" + counter + ")"
}
JSON Processing
Work with structured data:
fn execute(input) {
// Parse JSON input
let data = parse_json(input);
// Process data
let result = #{
status: "success",
data: data,
processed_at: now(),
metadata: #{
plugin: plugin_name,
version: plugin_version
}
};
// Return as JSON
to_json(result)
}
Error Handling
Handle errors gracefully:
fn execute(input) {
// Validate input
if input == "" {
throw "Input cannot be empty";
}
// Try-catch pattern
let result = try_process(input);
if result == () {
throw "Processing failed";
}
result
}
fn try_process(data) {
if data.len() < 3 {
return ();
}
"Processed: " + data
}
Logging
Debug your plugins:
fn execute(input) {
log("Starting execution with input: " + input);
debug("Debug info: input length = " + input.len());
let result = process(input);
log("Execution complete");
result
}
fn process(data) {
log("Processing data...");
// Processing logic
"Result: " + data
}
Built-in Functions
Rhai plugins have access to useful built-in functions:
String Functions
JSON Functions
Array Functions
Time Functions
// String manipulation
let text = " Hello World ";
let trimmed = trim(text); // "Hello World"
let upper = upper(trimmed); // "HELLO WORLD"
let lower = lower(upper); // "hello world"
let len = text.len(); // 17
let sub = text.sub_string(2, 7); // "Hello"
Plugin Statistics
Access runtime statistics:
use mofa_plugins :: AgentPlugin ;
// Get plugin stats
let stats = plugin . stats ();
println! ( "Total calls: {}" , stats . calls_total ());
println! ( "Failed calls: {}" , stats . calls_failed ());
println! ( "Avg latency: {}ms" , stats . avg_latency_ms ());
// Get as JSON
let stats_map = stats . to_map ();
for ( key , value ) in stats_map {
println! ( "{}: {}" , key , value );
}
Calling Script Functions
Invoke specific functions from Rust:
use rhai :: Dynamic ;
// Call a custom function
let args = vec! [ Dynamic :: from ( 5 ), Dynamic :: from ( 3 )];
let result = plugin . call_script_function ( "add" , & args ) . await ? ;
if let Some ( value ) = result {
println! ( "Result: {}" , value . as_int () . unwrap ());
}
// Call with no arguments
let pi = plugin . call_script_function ( "get_pi" , & []) . await ? ;
println! ( "Pi: {}" , pi . unwrap () . as_float () . unwrap ());
Corresponding Rhai script:
fn add(a, b) {
a + b
}
fn get_pi() {
3.14159
}
Security Configuration
Configure script security limits:
use mofa_extra :: rhai :: { ScriptEngineConfig , ScriptSecurityConfig };
let security = ScriptSecurityConfig {
max_execution_time_ms : 1000 , // 1 second timeout
max_call_stack_depth : 32 , // Prevent deep recursion
max_operations : 10_000 , // Limit operations
max_array_size : 1000 , // Limit array size
max_string_size : 10_000 , // Limit string size
allow_loops : true , // Allow loops
allow_file_operations : false , // Disable file I/O
allow_network_operations : false , // Disable network
};
let config = ScriptEngineConfig {
security ,
debug_mode : false ,
strict_mode : true ,
.. Default :: default ()
};
let plugin_config = RhaiPluginConfig :: default ()
. with_engine_config ( config );
Script Validation
Validate scripts before deployment:
use mofa_extra :: rhai :: RhaiScriptEngine ;
let engine = RhaiScriptEngine :: new ( config ) ? ;
let script = r#"
fn execute(input) {
input + 1
}
"# ;
// Validate syntax
let errors = engine . validate ( script ) ? ;
if errors . is_empty () {
println! ( "✓ Script is valid" );
} else {
eprintln! ( "✗ Script has errors:" );
for error in errors {
eprintln! ( " - {}" , error );
}
}
Example: Data Processing Plugin
A complete example processing structured data:
plugin_name = "data_processor";
plugin_version = "1.0.0";
plugin_description = "Process and transform data";
// State
let processed_count = 0;
let error_count = 0;
fn init() {
log("Data processor initialized");
}
fn execute(input) {
try {
// Parse input
let data = parse_json(input);
// Validate
if !validate(data) {
error_count += 1;
throw "Invalid data format";
}
// Transform
let transformed = transform(data);
// Update stats
processed_count += 1;
// Return result
to_json(#{
status: "success",
data: transformed,
stats: #{
processed: processed_count,
errors: error_count
}
})
} catch (error) {
error_count += 1;
to_json(#{
status: "error",
message: error,
stats: #{
processed: processed_count,
errors: error_count
}
})
}
}
fn validate(data) {
// Check required fields
data.contains("id") && data.contains("value")
}
fn transform(data) {
#{
id: data.id,
value: data.value * 2,
processed_at: now(),
metadata: #{
plugin: plugin_name,
version: plugin_version
}
}
}
Best Practices
Each plugin should have a single, well-defined responsibility.
Use throw for errors and validate inputs.
Add comments explaining complex logic.
Always set plugin_name, plugin_version, and plugin_description.
Validate scripts using the validation API.
Use logging and check plugin statistics.
Next Steps
Rhai Scripting Guide Learn the Rhai language in detail
WASM Plugins Build sandboxed WebAssembly plugins