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 *;
// Define a custom behavior
;
// Execute it
async
Core Concepts
BehaviorNode Trait
All behaviors implement this single trait:
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 = new;
Selector Node (Fallback)
Tries children until one succeeds or all fail.
let selector = new;
Parallel Node
Executes all children concurrently.
let parallel = new;
Condition Node
A single-child decorator that gates its child on a predicate: it evaluates the predicate first
and only ticks the child (forwarding the child's status) while the predicate holds; when the
predicate is false it reports Failure without ticking the child at all. The predicate is a
small, closed ConditionKind enum rather than a general expression language - today the only
variant is ControllerModeIs, which checks the project's actual
mecha10_core::config::controller_mode() (Locomotion or Joints) - the same capability-based
dispatch wander/move leaf nodes already use.
let gated = new;
Because a Selector already advances past any child that returns Failure, dropping several
ConditionNode-wrapped children into one gives capability- or state-gated branch selection out of
a small, reusable building block - no bespoke branch-on-capability composite node needed. In JSON,
a selector that drives on a locomotion platform and pan-sweeps on a joints-only one:
The condition whose mode is active ticks its child; the other returns Failure without ticking
its subtree, so the selector falls through to it.
Include Node
Inlines a composition subtree loaded from a separate JSON file, so multiple behaviors can share
one fragment (e.g. a common interrupt layer) without duplicating it in every config.json. Only
works when the tree was loaded via BehaviorLoader::load_from_file - path resolves relative to
the including file's own directory, not the current working directory.
The referenced file's content is a single bare CompositionConfig object - the same tagged
"type" shape as any other node - not a full BehaviorConfig envelope.
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 *;
// Define your custom behavior
// Register it with the registry
let mut registry = new;
registry.register;
Loading Behavior Trees
Use the BehaviorLoader to load behavior trees from JSON:
// Create a loader with your registry
let loader = new;
// 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?;
// Option 2: Load from a file
let behavior = loader.load_from_file?;
// Option 3: Load from a parsed config
let config = from_file?;
let behavior = loader.load?;
Complete Workflow
Here's the complete workflow from registration to execution:
// 1. Create registry and register nodes
let mut registry = new;
registry.register;
registry.register;
// 2. Create loader and load behavior tree
let loader = new;
let behavior = loader.load_from_file?;
// 3. Create executor and run
let ctx = new.await?;
let mut executor = new;
executor.init.await?;
let = executor.run_until_complete.await?;
println!;
See examples/load_and_execute.rs for a complete working example.
JSON Configuration
Behaviors can be defined in JSON for dynamic composition:
Load and execute:
// Step 1: Create a registry and register your node types
let mut registry = new;
registry.register;
registry.register;
registry.register;
// Step 2: Create a loader and load the behavior tree
let loader = new;
let behavior = loader.load_from_file?;
// Step 3: Execute with the behavior executor
let mut executor = new;
let = executor.run_until_complete.await?;
Built-in Action Nodes
The runtime includes several built-in action nodes for common robotics tasks. Register them with:
use register_builtin_actions;
let mut registry = new;
register_builtin_actions;
WanderNode
Generates random movement commands for exploration:
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:
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). For aspeed-limited joints-mode move, this is a safety-minimum floor rather than a hard cutoff - see "Joints mode andspeed" below.speed: Maximum rate of change, in rad/s, for joints-mode position stepping (optional, null = unset) - see "Joints mode andspeed" below.
Behavior: Publishes the specified velocity command. Returns Running while executing, Success when duration elapses. If no duration specified, runs forever.
Joints mode and speed
On a joints-only (pan/tilt, no drivetrain) project, linear/angular aren't velocities - they're
absolute joint position targets for the project's first/second declared joint, clamped to that
joint's configured limits (see the Condition Node example above). Without speed set, MoveNode
publishes that clamped target every tick unchanged, which commands the servo to snap instantly to
it and hold - fine for duration_secs-gated "go to this position and stop", but if linear/
angular deliberately overshoots the joint's real range (e.g. linear: 1000.0 to mean "the max
pan angle, whatever that is") the servo snaps to the extreme instead of sweeping smoothly into it.
Setting speed fixes this: each tick, MoveNode reads the joint's real current position from
/sensor/joint_states and steps the published position toward the (still clamped) target by at
most speed * dt (dt = time since this node's last tick), never overshooting. It reaches
Success once the real position is within tolerance of the target, rather than waiting for
duration_secs. Until the first /sensor/joint_states reading arrives, the node holds
(Running, no publish) rather than stepping from an assumed starting position:
This sweeps the first joint to its real max angle at up to 1 rad/s, smoothly, driven by the
project's own behavior tree tick rate and the robot's real feedback - not a fixed-duration
interpolation between two hardcoded endpoints. speed is ignored in locomotion mode, where
linear/angular are already velocity commands the robot integrates into position over time.
duration_secs is a floor, not a ceiling, for speed-limited moves. duration_secs, if also
set, still applies as a hard safety timeout - but for a speed-limited joints-mode move, the
effective timeout used is max(duration_secs, required_secs * 1.5), where required_secs is
the time to cross the driven joint's full real configured range (resolved from the project's own
mecha10.json actuator config - the same limits linear/angular are clamped to above) at the
configured speed. This fixes a real production bug: a single static duration_secs can't
generalize across robots with different real joint ranges - a value tuned for one robot's sweep
can silently cut off a slower, legitimately-longer sweep on another robot before it reaches the
real joint extremes, well before speed-limited stepping's own feedback-driven completion check
would have reported Success. Treat duration_secs as "the minimum time this move should ever
take" (useful for short/fast moves, or as an explicit override) rather than a hard cutoff - the
dynamic calculation always has final say when a speed-limited move legitimately needs more
time than that. If the driven joint's limits can't be resolved (e.g. not declared in
mecha10.json) or speed is 0, the dynamic calculation can't be made and duration_secs
alone decides, exactly as before this behavior existed.
TimerNode
Waits for a specified duration before succeeding:
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:
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.json - Square patrol loop using timed movements
You can use these templates as starting points for your own behavior trees or load them directly:
let loader = new;
let behavior = loader.load_from_file?;
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
# Create with specific template
# List available templates
# Validate an existing behavior tree
Configuration Validation
The runtime provides comprehensive validation for behavior trees before execution:
use ;
use json;
let mut registry = new;
register_builtin_actions;
let config = json!;
let result = validate_behavior_config?;
if !result.valid
for warning in &result.warnings
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:
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:
- Project (highest):
configs/behaviors/wander/config.json- project-committed override - 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 ;
// 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.await?;
Example: Overriding a Behavior
Framework default (configs/behaviors/@mecha10/wander/config.json):
Project override (configs/behaviors/wander/config.json) - a complete, valid behavior definition,
not just the fields being changed:
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:
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:
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:
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
Illustrative only. This tree is a composition-shape example, not a copy-pasteable config - node types like
detect_emergencyandemergency_stopare not registered byregister_builtin_actionsand aren't implemented anywhere in the codebase. The only built-in action nodes today arewander,move,timer, andsensor_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):
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, andfuse_detectionsare not registered byregister_builtin_actionsand aren't implemented anywhere in the codebase. The only built-in action nodes today arewander,move,timer, andsensor_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:
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 = new // 30 Hz tick rate
.with_max_ticks; // Optional timeout
// Run until completion
let = executor.run_until_complete.await?;
// Or run for a fixed duration
let = executor.run_for_duration.await?;
Execution statistics:
println!;
println!;
println!;
println!;
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)
- ✅ 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 Backlog: Framework Tasks on Linear):
- Wire
list_capable_behaviorsinto the/behavior/availablepublisher. That publisher lives in themecha10-nodes-behavior-executornode runtime (the separately-released crate noted above), which is in the siblingmecha10-nodesrepo, not here -- seesrc/config/loader.rsfor the full note on where the real call site should be. - 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 Backlog: Framework Tasks (Linear) under the Behavior Trees future-enhancements section.
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).
License
MIT
See Also
- Backlog: Framework Tasks (Linear) - Priority 7: AI Native