mecha10-behavior-runtime

Crates.iomecha10-behavior-runtime
lib.rsmecha10-behavior-runtime
version0.1.25
created_at2025-11-24 18:38:36.604586+00
updated_at2026-01-01 01:57:28.81851+00
descriptionBehavior tree runtime for Mecha10 - unified AI and logic composition system
homepage
repositoryhttps://github.com/mecha10/mecha10
max_upload_size
id1948379
size266,095
Peter C (PeterChauYEG)

documentation

README

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("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, &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>,
}

Environment-Specific Configuration

Behavior trees support a three-tier configuration hierarchy for environment-specific overrides:

  1. Template (required): behaviors/wander.json - Base behavior tree
  2. Common (optional): configs/common/behaviors/wander.json - Shared overrides
  3. 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 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

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 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)
  • 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:

  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

Commit count: 0

cargo fmt