mecha10-behavior-runtime 0.8.0

Behavior tree runtime for Mecha10 - unified AI and logic composition system
Documentation

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 successfully
  • NodeStatus::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("configs/behaviors/@mecha10/patrol/config.json")?;

// Option 3: Load from a parsed config
let config = BehaviorConfig::from_file("configs/behaviors/@mecha10/patrol/config.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

Subscribes to a sensor topic and evaluates a condition against the latest reading:

{
  "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)

Behavior: Lazily subscribes to topic on the first tick. Each tick, drains any pending messages and keeps the most recent one - if no new message has arrived since the last tick, the previously received reading is reused, since a publisher is not required to publish on every tick. Returns Running until a first sensor reading has been received. Once a reading is available, returns Failure if field is missing or not numeric on that reading, otherwise compares field's value against threshold using operator and returns Success if the comparison holds, Failure otherwise.

Seed Templates

The package includes production-ready behavior tree templates in the seeds/ directory:

  • patrol_simple.json - Square patrol loop using timed movements
  • 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/idle_wander.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 configs/behaviors/my_behavior/config.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, &registry)?;
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>,
}

Configuration Resolution

Behavior configs resolve through a 2-tier framework/project config.json lookup rooted at configs/behaviors/ (see mecha10_cli_core::paths::config::behavior_config_candidates). Highest precedence first:

  1. Project (highest): configs/behaviors/wander/config.json - project-committed override
  2. Framework (lowest): configs/behaviors/@mecha10/wander/config.json - bundled framework default

There is no @local tier for behaviors (LAB-1990) - unlike node configs, nothing gitignores it and it syncs like any other config via mecha10 config push. Behaviors moved from a top-level behaviors/ directory to configs/behaviors/ in LAB-2072: mecha10 config push/pull/list only ever walk configs/, so the top-level behaviors/ directory was never actually reachable by them despite this doc (and LAB-1990's own justification for dropping @local) claiming it was - this move makes that claim true. Push and pull both work correctly for behaviors: every config key is prefixed with its category root (behaviors/... vs nodes/...), so a pulled behavior config lands back under configs/behaviors/... rather than being mistaken for a node config - see mecha10_robot_config::sync::derive_config_key/write_configs_to_dir's doc comments.

Resolution picks the highest-precedence tier that exists on disk - whole files are swapped, not deep-merged (LAB-1990).

Loading a Behavior Config

use mecha10_behavior_runtime::{load_behavior_config, detect_project_root};

// Auto-detect project root (walks up to find mecha10.json)
let project_root = detect_project_root()?;

// Resolves and loads the highest-precedence tier present on disk
let config = load_behavior_config("wander", &project_root).await?;

Example: Overriding a Behavior

Framework default (configs/behaviors/@mecha10/wander/config.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
    }
  }
}

Project override (configs/behaviors/wander/config.json) - a complete, valid behavior definition, not just the fields being changed:

{
  "name": "wander_behavior",
  "root": {
    "type": "wander",
    "config": {
      "topic": "/motor/cmd_vel",
      "linear_speed": 0.1,
      "angular_speed": 0.2,
      "change_interval_secs": 3.0
    }
  }
}

Result: the project override at configs/behaviors/wander/config.json exists, so it wins outright - the robot moves slowly (0.1 m/s linear) regardless of what configs/behaviors/@mecha10/wander/config.json contains.

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": "move", "config": { "linear": 0.5, "angular": 0.0, "duration_secs": 2.0 } },
    { "type": "node", "node": "move", "config": { "linear": 0.0, "angular": 0.8, "duration_secs": 1.0 } },
    { "type": "node", "node": "move", "config": { "linear": 0.5, "angular": 0.0, "duration_secs": 2.0 } }
  ]
}

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, patrol patterns, initialization sequences

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 succeed
  • require_one: At least one child must succeed
  • require_n(N): At least N children must succeed

Use cases: Monitoring + action, sensor fusion, multi-tasking

Pattern 4: Subsumption Architecture

Illustrative only. This tree is a composition-shape example, not a copy-pasteable config - node types like detect_emergency and emergency_stop are not registered by register_builtin_actions and aren't implemented anywhere in the codebase. The only built-in action nodes today are wander, move, timer, and sensor_check (see Built-in Action Nodes); a real subsumption tree would need custom nodes registered for the missing types before it would load.

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

Illustrative only. This tree is a composition-shape example, not a copy-pasteable config - node types like yolo_detector, mobilenet_detector, depth_detector, and fuse_detections are not registered by register_builtin_actions and aren't implemented anywhere in the codebase. The only built-in action nodes today are wander, move, timer, and sensor_check (see Built-in Action Nodes); a real ensemble/fusion tree would need custom nodes registered for the missing types before it would load.

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 Context for 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 BehaviorNode trait
  • ✅ NodeStatus enum
  • ✅ 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)
  • ✅ 2-tier configuration resolution (framework default → project override)
  • ✅ 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:

  1. Edit your behavior tree JSON file
  2. Stop your running node (Ctrl+C)
  3. Restart the node with mecha10 run <node-name>
  4. 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