| Crates.io | mecha10-behavior-runtime |
| lib.rs | mecha10-behavior-runtime |
| version | 0.1.25 |
| created_at | 2025-11-24 18:38:36.604586+00 |
| updated_at | 2026-01-01 01:57:28.81851+00 |
| description | Behavior tree runtime for Mecha10 - unified AI and logic composition system |
| homepage | |
| repository | https://github.com/mecha10/mecha10 |
| max_upload_size | |
| id | 1948379 |
| size | 266,095 |
A unified behavior composition system for robotics and AI that enables building complex robot behaviors from simple, composable pieces.
The behavior runtime provides a tick-based execution model where all behaviors implement a single BehaviorNode trait. This creates a uniform interface for:
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(())
}
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<()> { /* ... */ }
}
Behaviors return one of three statuses:
NodeStatus::Success - Behavior completed successfullyNodeStatus::Failure - Behavior failed (irrecoverable error)NodeStatus::Running - Behavior is still executingExecutes children in order until one fails or all succeed.
let sequence = SequenceNode::new(vec![
Box::new(ApproachObject),
Box::new(GraspObject),
Box::new(LiftObject),
]);
Tries children until one succeeds or all fail.
let selector = SelectOrNode::new(vec![
Box::new(UseHighPrecisionSensor),
Box::new(UseLowPrecisionSensor),
Box::new(UseEstimatedPosition),
]);
Executes all children concurrently.
let parallel = ParallelNode::new(
vec![
Box::new(MonitorObstacles),
Box::new(MoveToGoal),
Box::new(UpdateMap),
],
ParallelPolicy::RequireAll,
);
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).
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)?))
});
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)?;
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.
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?;
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);
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.
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.
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.
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.
The package includes production-ready behavior tree templates in the seeds/ directory:
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")?;
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
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:
name, root)ValidationResult:
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
Behavior trees support a three-tier configuration hierarchy for environment-specific overrides:
behaviors/wander.json - Base behavior treeconfigs/common/behaviors/wander.json - Shared overridesconfigs/dev/behaviors/wander.json - Environment-specific overridesConfigurations are deep-merged, with later tiers overriding earlier ones.
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?;
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.
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.
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
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
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 succeedUse cases: Monitoring + action, sensor fusion, multi-tasking
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.
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.
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);
This package is part of the Mecha10 framework and integrates with:
Context for messaging and stateCurrent Status: Phase 5 Complete - Config Loading & Validation ✅
Implemented:
BehaviorNode traitNodeStatus enumNext Steps (Priority 7 - see TODOS.md):
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:
Timeline: Hot-reload functionality is tracked in TODOS.md under P2 (Medium Priority) tasks.
Current Workflow:
mecha10 run <node-name>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
MIT