4 releases
| 0.1.25 | Jan 1, 2026 |
|---|---|
| 0.1.23 | Dec 30, 2025 |
| 0.1.22 | Dec 30, 2025 |
| 0.1.0 | Nov 24, 2025 |
#366 in Machine learning
Used in 5 crates
(3 directly)
1MB
15K
SLoC
Mecha10 Behavior Runtime
A unified behavior composition system for robotics and AI that enables building complex robot behaviors from simple, composable pieces.
Overview
The behavior runtime provides a tick-based execution model where all behaviors implement a single BehaviorNode trait. This creates a uniform interface for:
- Custom Rust behaviors (complex logic, high performance)
- AI inference nodes (YOLO, LLMs, RL policies)
- Planning and navigation (A*, RRT, SLAM)
- Composition primitives (Sequence, Selector, Parallel)
Quick Start
use mecha10_behavior_runtime::prelude::*;
// Define a custom behavior
#[derive(Debug)]
struct MyBehavior;
#[async_trait]
impl BehaviorNode for MyBehavior {
async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
// Your behavior logic here
println!("Hello from my behavior!");
Ok(NodeStatus::Success)
}
}
// Execute it
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let ctx = Context::new("my_node").await?;
let behavior = Box::new(MyBehavior);
let mut executor = BehaviorExecutor::new(behavior, 30.0); // 30 Hz
executor.init(&ctx).await?;
let (status, stats) = executor.run_until_complete(&ctx).await?;
println!("Completed with status: {} in {} ticks", status, stats.tick_count);
Ok(())
}
Core Concepts
BehaviorNode Trait
All behaviors implement this single trait:
#[async_trait]
pub trait BehaviorNode: Send + Sync + Debug {
async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus>;
fn name(&self) -> &str { /* ... */ }
async fn reset(&mut self) -> anyhow::Result<()> { /* ... */ }
async fn on_init(&mut self, ctx: &Context) -> anyhow::Result<()> { /* ... */ }
async fn on_terminate(&mut self, ctx: &Context) -> anyhow::Result<()> { /* ... */ }
}
Node Status
Behaviors return one of three statuses:
NodeStatus::Success- Behavior completed successfullyNodeStatus::Failure- Behavior failed (irrecoverable error)NodeStatus::Running- Behavior is still executing
Composition Primitives
Sequence Node
Executes children in order until one fails or all succeed.
let sequence = SequenceNode::new(vec![
Box::new(ApproachObject),
Box::new(GraspObject),
Box::new(LiftObject),
]);
Selector Node (Fallback)
Tries children until one succeeds or all fail.
let selector = SelectOrNode::new(vec![
Box::new(UseHighPrecisionSensor),
Box::new(UseLowPrecisionSensor),
Box::new(UseEstimatedPosition),
]);
Parallel Node
Executes all children concurrently.
let parallel = ParallelNode::new(
vec![
Box::new(MonitorObstacles),
Box::new(MoveToGoal),
Box::new(UpdateMap),
],
ParallelPolicy::RequireAll,
);
Node Registry & Loading
The behavior runtime provides a powerful system for loading behavior trees from JSON configurations. This enables configuration-driven behavior and separation between behavior logic (Rust) and composition (JSON). Hot-reloading support is planned for future releases (see Hot Reload Status below).
Registering Custom Nodes
Create a NodeRegistry and register your custom behavior types:
use mecha10_behavior_runtime::prelude::*;
// Define your custom behavior
#[derive(Debug)]
struct MoveToGoal {
speed: f64,
}
impl MoveToGoal {
fn from_config(config: serde_json::Value) -> anyhow::Result<Self> {
let speed = config.get("speed")
.and_then(|v| v.as_f64())
.unwrap_or(1.0);
Ok(Self { speed })
}
}
#[async_trait]
impl BehaviorNode for MoveToGoal {
async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
// Your behavior logic here
Ok(NodeStatus::Success)
}
}
// Register it with the registry
let mut registry = NodeRegistry::new();
registry.register("move_to_goal", |config| {
Ok(Box::new(MoveToGoal::from_config(config)?))
});
Loading Behavior Trees
Use the BehaviorLoader to load behavior trees from JSON:
// Create a loader with your registry
let loader = BehaviorLoader::new(registry);
// Option 1: Load from a JSON string
let json = r#"{
"name": "my_behavior",
"root": {
"type": "node",
"node": "move_to_goal",
"config": { "speed": 2.0 }
}
}"#;
let behavior = loader.load_from_json(json)?;
// Option 2: Load from a file
let behavior = loader.load_from_file("behaviors/patrol.json")?;
// Option 3: Load from a parsed config
let config = BehaviorConfig::from_file("behaviors/patrol.json")?;
let behavior = loader.load(&config)?;
Complete Workflow
Here's the complete workflow from registration to execution:
// 1. Create registry and register nodes
let mut registry = NodeRegistry::new();
registry.register("move_to_goal", |config| {
Ok(Box::new(MoveToGoal::from_config(config)?))
});
registry.register("detect_obstacles", |config| {
Ok(Box::new(DetectObstacles::from_config(config)?))
});
// 2. Create loader and load behavior tree
let loader = BehaviorLoader::new(registry);
let behavior = loader.load_from_file("patrol_mission.json")?;
// 3. Create executor and run
let ctx = Context::new("robot").await?;
let mut executor = BehaviorExecutor::new(behavior, 30.0);
executor.init(&ctx).await?;
let (status, stats) = executor.run_until_complete(&ctx).await?;
println!("Completed with status: {} in {} ticks", status, stats.tick_count);
See examples/load_and_execute.rs for a complete working example.
JSON Configuration
Behaviors can be defined in JSON for dynamic composition:
{
"$schema": "https://mecha10.dev/schemas/behavior-composition-v1.json",
"name": "patrol_mission",
"root": {
"type": "sequence",
"children": [
{
"type": "node",
"node": "safety_check",
"config_ref": "safety"
},
{
"type": "selector",
"children": [
{
"type": "node",
"node": "detect_obstacles"
},
{
"type": "node",
"node": "wander"
}
]
}
]
},
"configs": {
"safety": { "max_speed": 1.0 }
}
}
Load and execute:
// Step 1: Create a registry and register your node types
let mut registry = NodeRegistry::new();
registry.register("safety_check", |config| {
Ok(Box::new(SafetyCheck::from_config(config)?))
});
registry.register("detect_obstacles", |config| {
Ok(Box::new(DetectObstacles::from_config(config)?))
});
registry.register("wander", |config| {
Ok(Box::new(Wander::from_config(config)?))
});
// Step 2: Create a loader and load the behavior tree
let loader = BehaviorLoader::new(registry);
let behavior = loader.load_from_file("patrol_mission.json")?;
// Step 3: Execute with the behavior executor
let mut executor = BehaviorExecutor::new(behavior, 30.0);
let (status, stats) = executor.run_until_complete(&ctx).await?;
Built-in Action Nodes
The runtime includes several built-in action nodes for common robotics tasks. Register them with:
use mecha10_behavior_runtime::register_builtin_actions;
let mut registry = NodeRegistry::new();
register_builtin_actions(&mut registry);
WanderNode
Generates random movement commands for exploration:
{
"type": "wander",
"config": {
"topic": "/motor/cmd_vel",
"linear_speed": 0.3,
"angular_speed": 0.5,
"change_interval_secs": 3.0
}
}
Parameters:
topic: Topic to publish velocity commands (required)linear_speed: Forward speed in m/s (default: 0.3)angular_speed: Max turning speed in rad/s (default: 0.5)change_interval_secs: Seconds between direction changes (default: 3.0)
Behavior: Publishes constant linear velocity with periodically changing random angular velocity. Returns Running continuously.
MoveNode
Executes a fixed velocity command for a specified duration:
{
"type": "move",
"config": {
"topic": "/motor/cmd_vel",
"linear": 0.5,
"angular": 0.0,
"duration_secs": 2.0
}
}
Parameters:
topic: Topic to publish velocity commands (required)linear: Forward/backward velocity in m/s (required)angular: Turning velocity in rad/s (required)duration_secs: Duration to execute (optional, null = forever)
Behavior: Publishes the specified velocity command. Returns Running while executing, Success when duration elapses. If no duration specified, runs forever.
TimerNode
Waits for a specified duration before succeeding:
{
"type": "timer",
"config": {
"duration_secs": 5.0
}
}
Parameters:
duration_secs: Duration to wait in seconds (required)
Behavior: Returns Running until the duration elapses, then returns Success. Useful for delays in sequences.
SensorCheckNode
Checks sensor conditions (placeholder for future implementation):
{
"type": "sensor_check",
"config": {
"topic": "/sensors/distance",
"field": "value",
"operator": "less_than",
"threshold": 0.5
}
}
Parameters:
topic: Sensor topic to subscribe to (required)field: Field name in sensor message (required)operator: Comparison operator -less_than,greater_than,equal(required)threshold: Comparison value (required)
Current Status: Returns Success immediately (stub). Full sensor subscription implementation pending.
Seed Templates
The package includes production-ready behavior tree templates in the seeds/ directory:
- basic_navigation.json - Simple waypoint navigation with battery check and path planning
- obstacle_avoidance.json - Reactive collision avoidance using sensor fusion
- patrol_simple.json - Basic patrol loop cycling through waypoints
- idle_wander.json - Random wandering for exploration and idle movement
You can use these templates as starting points for your own behavior trees or load them directly:
let loader = BehaviorLoader::new(registry);
let behavior = loader.load_from_file("seeds/basic_navigation.json")?;
Creating Behavior Trees with the CLI
The Mecha10 CLI provides an interactive wizard for creating behavior trees:
# Interactive mode - wizard will guide you through template selection
mecha10 behaviors create
# Create with specific template
mecha10 behaviors create --name my_behavior --template navigation
# List available templates
mecha10 behaviors list
# Validate an existing behavior tree
mecha10 behaviors validate behaviors/my_behavior.json
Configuration Validation
The runtime provides comprehensive validation for behavior trees before execution:
use mecha10_behavior_runtime::{validate_behavior_config, NodeRegistry, register_builtin_actions};
use serde_json::json;
let mut registry = NodeRegistry::new();
register_builtin_actions(&mut registry);
let config = json!({
"name": "wander_behavior",
"root": {
"type": "sequence",
"children": [
{
"type": "wander",
"config": {
"topic": "/motor/cmd_vel",
"linear_speed": 0.5
}
}
]
}
});
let result = validate_behavior_config(&config, ®istry)?;
if !result.valid {
for error in &result.errors {
eprintln!("Error: {}", error);
}
}
for warning in &result.warnings {
eprintln!("Warning: {}", warning);
}
Validation Checks:
- Required fields (
name,root) - Node types are registered or are composition nodes
- Tree structure is well-formed
- Children arrays exist for composition nodes
- Schema reference is present (warning if missing)
- Empty children arrays (warning)
ValidationResult:
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
Environment-Specific Configuration
Behavior trees support a three-tier configuration hierarchy for environment-specific overrides:
- Template (required):
behaviors/wander.json- Base behavior tree - Common (optional):
configs/common/behaviors/wander.json- Shared overrides - Environment (optional):
configs/dev/behaviors/wander.json- Environment-specific overrides
Configurations are deep-merged, with later tiers overriding earlier ones.
Loading Environment-Specific Configs
use mecha10_behavior_runtime::{load_behavior_config, detect_project_root, get_current_environment};
// Auto-detect project root (walks up to find mecha10.json)
let project_root = detect_project_root()?;
// Get environment from MECHA10_ENVIRONMENT or default to "dev"
let environment = get_current_environment();
// Load with environment-specific overrides
let config = load_behavior_config("wander", &project_root, &environment).await?;
Example: Environment Overrides
Base Template (behaviors/wander.json):
{
"name": "wander_behavior",
"root": {
"type": "wander",
"config": {
"topic": "/motor/cmd_vel",
"linear_speed": 0.3,
"angular_speed": 0.5,
"change_interval_secs": 3.0
}
}
}
Development Override (configs/dev/behaviors/wander.json):
{
"root": {
"config": {
"linear_speed": 0.1,
"angular_speed": 0.2
}
}
}
Production Override (configs/production/behaviors/wander.json):
{
"root": {
"config": {
"linear_speed": 0.5,
"angular_speed": 0.8,
"change_interval_secs": 5.0
}
}
}
Result: In development, the robot moves slowly (0.1 m/s linear). In production, it moves faster (0.5 m/s linear) with wider turns.
Deep Merge Behavior
The merge is recursive for nested objects:
// Base
{ "a": 1, "b": { "x": 1, "y": 2 } }
// Override
{ "b": { "y": 3, "z": 4 }, "c": 5 }
// Result
{ "a": 1, "b": { "x": 1, "y": 3, "z": 4 }, "c": 5 }
Non-object values are replaced entirely.
Composition Patterns
Pattern 1: Sequential Execution (Sequence Node)
Use sequences when tasks must be performed in order, and all must succeed:
{
"type": "sequence",
"children": [
{ "type": "node", "node": "check_battery" },
{ "type": "node", "node": "plan_path" },
{ "type": "node", "node": "follow_path" },
{ "type": "node", "node": "check_arrival" }
]
}
Behavior: Executes children left-to-right. If any child fails, the sequence fails immediately. All children must succeed for the sequence to succeed.
Use cases: Multi-step tasks, initialization sequences, workflows
Pattern 2: Fallback/Priority (Selector Node)
Use selectors when you have multiple strategies and want to try them in priority order:
{
"type": "selector",
"children": [
{ "type": "node", "node": "use_high_precision_sensor" },
{ "type": "node", "node": "use_low_precision_sensor" },
{ "type": "node", "node": "use_estimated_position" }
]
}
Behavior: Tries children left-to-right. If a child succeeds, the selector succeeds immediately. If all children fail, the selector fails.
Use cases: Fallback strategies, priority-based behavior selection, fault tolerance
Pattern 3: Concurrent Execution (Parallel Node)
Use parallel nodes when tasks can run simultaneously:
{
"type": "parallel",
"policy": "require_all",
"children": [
{ "type": "node", "node": "monitor_obstacles" },
{ "type": "node", "node": "move_to_goal" },
{ "type": "node", "node": "update_map" }
]
}
Behavior: Executes all children concurrently. Success/failure depends on the policy:
require_all: All children must succeedrequire_one: At least one child must succeedrequire_n(N): At least N children must succeed
Use cases: Monitoring + action, sensor fusion, multi-tasking
Pattern 4: Subsumption Architecture
Layered behavior with priority suppression (high-priority behaviors override low-priority ones):
{
"type": "selector",
"name": "subsumption_layers",
"children": [
{
"type": "sequence",
"name": "emergency_layer",
"children": [
{ "type": "node", "node": "detect_emergency" },
{ "type": "node", "node": "emergency_stop" }
]
},
{
"type": "sequence",
"name": "avoidance_layer",
"children": [
{ "type": "node", "node": "detect_obstacles" },
{ "type": "node", "node": "avoid_obstacles" }
]
},
{
"type": "sequence",
"name": "navigation_layer",
"children": [
{ "type": "node", "node": "check_goal" },
{ "type": "node", "node": "navigate_to_goal" }
]
},
{ "type": "node", "node": "idle" }
]
}
Use cases: Safety-critical systems, reactive robotics, behavior hierarchies
See packages/behavior-patterns/seeds/safety_subsumption.json for a complete example.
Pattern 5: Ensemble/Fusion
Multiple models or sensors providing input for a single decision:
{
"type": "sequence",
"children": [
{ "type": "node", "node": "capture_sensor_data" },
{
"type": "parallel",
"policy": "require_all",
"children": [
{ "type": "node", "node": "yolo_detector" },
{ "type": "node", "node": "mobilenet_detector" },
{ "type": "node", "node": "depth_detector" }
]
},
{ "type": "node", "node": "fuse_detections" },
{ "type": "node", "node": "make_decision" }
]
}
Use cases: Sensor fusion, multi-model AI, robust perception
See packages/behavior-patterns/seeds/multi_model_ensemble.json for a complete example.
Execution Engine
The BehaviorExecutor manages tick-based execution:
let executor = BehaviorExecutor::new(behavior, 30.0) // 30 Hz tick rate
.with_max_ticks(1000); // Optional timeout
// Run until completion
let (status, stats) = executor.run_until_complete(&ctx).await?;
// Or run for a fixed duration
let (status, stats) = executor.run_for_duration(&ctx, Duration::from_secs(10)).await?;
Execution statistics:
println!("Ticks: {}", stats.tick_count);
println!("Duration: {:?}", stats.total_duration);
println!("Avg tick: {:?}", stats.avg_tick_duration);
println!("Min/Max: {:?}/{:?}", stats.min_tick_duration, stats.max_tick_duration);
Architecture
Philosophy
- Behavior Logic = Rust code (complex logic, high performance)
- Behavior Composition = JSON config (simple orchestration, will support hot-reloading)
- Configuration = JSON with validation (parameters only)
Benefits
- Unified Interface: Everything is a BehaviorNode
- Composable: Mix and match behaviors freely
- Type-Safe: Rust's type system ensures correctness
- Performance: Zero-cost abstractions, compiled Rust
- Debuggable: Clear execution model with stats
Integration with Mecha10
This package is part of the Mecha10 framework and integrates with:
- mecha10-core: Provides
Contextfor messaging and state - AI nodes (planned): YOLO, LLMs, RL policies
- Planning nodes (planned): A*, RRT, SLAM
- Dashboard: Real-time behavior visualization
Status
Current Status: Phase 5 Complete - Config Loading & Validation ✅
Implemented:
- ✅ Core
BehaviorNodetrait - ✅
NodeStatusenum - ✅ Composition primitives (Sequence, Selector, Parallel)
- ✅ JSON configuration types with JSON Schema
- ✅ Tick-based execution engine
- ✅ Execution statistics
- ✅ Node registry for dynamic node instantiation
- ✅ Behavior loader for loading trees from JSON
- ✅ Integration examples and documentation
- ✅ Production-ready seed templates (6 templates)
- ✅ CLI wizard for creating behavior trees
- ✅ Comprehensive composition pattern documentation
- ✅ JSON Schema for validation and IDE support
- ✅ Built-in action nodes (WanderNode, MoveNode, TimerNode, SensorCheckNode)
- ✅ Configuration validation (validate_behavior_config with detailed errors/warnings)
- ✅ Environment-specific configuration (three-tier: template → common → environment)
- ✅ BehaviorExecutor node (mecha10-nodes-behavior-executor for integration)
- ✅ Integration tests (14 tests for action nodes and config validation)
Next Steps (Priority 7 - see TODOS.md):
- Behavior tree catalog service (REST API + PostgreSQL)
- AI node library (Vision, Language, Speech)
- Planning nodes (Path planning, SLAM, OpenCV)
- Dashboard monitoring interface
- Hot-reload support for behavior trees
Hot Reload Status
Current: The behavior runtime supports loading behavior trees from JSON files at startup. You can modify JSON files and restart your nodes to load updated behavior trees.
Planned: Automatic hot-reloading of behavior trees while nodes are running. This will enable:
- File system watching for JSON behavior tree changes
- Validation of new behavior trees before applying
- Seamless tree swapping without node restart
- State preservation for stateful nodes (optional)
- Rollback on reload errors
Timeline: Hot-reload functionality is tracked in TODOS.md under P2 (Medium Priority) tasks.
Current Workflow:
- Edit your behavior tree JSON file
- Stop your running node (Ctrl+C)
- Restart the node with
mecha10 run <node-name> - The node will load the updated behavior tree
Testing
The package currently builds successfully. Integration tests require a running Redis instance (provided by mecha10-core Context).
cargo build -p mecha10-behavior-runtime
cargo test -p mecha10-behavior-runtime
License
MIT
See Also
- AI Features Documentation
- TODOS.md - Priority 7: AI Native
Dependencies
~28–46MB
~639K SLoC