Core Concepts
Understand the fundamental concepts behind behavior trees and the hybrid execution model.
What is a Behavior Tree?
A behavior tree is a hierarchical control structure that determines how an agent (robot, game character, autonomous system) behaves. Think of it as a flowchart where each node represents a decision or action.
Key Characteristics
- Hierarchical: Trees of nodes, not linear scripts
- Modular: Reusable subtrees and components
- Reactive: Responds to state changes each tick
- Visual: Easy to diagram and understand
- Composable: Build complex behaviors from simple parts
The Tick Mechanism
Behavior trees execute through ticking - a traversal of the tree where each node executes for one cycle.
Tick Flow
async fn tick(&mut self, context: &mut BTreeContext) -> Result<NodeStatus>
Every node implements the tick() method:
- Parent ticks child: Parent node calls
tick()on its children - Child executes: Child performs its logic (check condition, execute action, etc.)
- Child returns status:
Running,Success,Failure, orSkipped - Parent reacts: Parent decides what to do based on child status
Example: Sequence Node
Tick 1: Sequence → Child A (returns Success)
Tick 2: Sequence → Child B (returns Running)
Tick 3: Sequence → Child B (returns Running)
Tick 4: Sequence → Child B (returns Success)
Tick 5: Sequence → Child C (returns Success)
Result: Sequence returns Success
Node Status
Every tick returns a NodeStatus:
pub enum NodeStatus {
Running, // Still executing, call tick() again
Success, // Completed successfully (terminal)
Failure, // Failed (terminal)
Skipped, // Skipped by parent (rare)
}
Status Types
Running
- Node is still executing
- Parent should tick again next cycle
- Example: Waiting for robot to reach destination
Success (Terminal)
- Node completed successfully
- Parent can proceed to next child (Sequence) or stop (Selector)
- Example: Navigation reached target position
Failure (Terminal)
- Node failed
- Parent stops execution (Sequence) or tries next child (Selector)
- Example: Sensor detected obstacle, path blocked
Skipped
- Node was not executed (parent decided to skip)
- Rare, mostly used in advanced control flow
- Example: Parallel node policy skips remaining children
Terminal Status
A status is terminal if it's not Running:
status.is_terminal() // true for Success, Failure, Skipped
When a node returns terminal status:
- It won't be ticked again until reset
- Parent can move to the next phase
- Internal state should be cleared
BTree Node Types
1. Composite Nodes (Control Flow)
Control flow nodes manage multiple children.
Sequence (AND Logic)
Executes children in order. All must succeed.
Sequence::new("mission")
.add_child(Box::new(step1))
.add_child(Box::new(step2))
.add_child(Box::new(step3))
Behavior:
- Tick children left to right
- If child returns
Success→ Move to next child - If child returns
Running→ ReturnRunning, resume next tick - If child returns
Failure→ ReturnFailureimmediately - If all succeed → Return
Success
Use Cases: Multi-step procedures, precondition chains
Selector (OR Logic / Fallback)
Tries children until one succeeds.
Selector::new("try_methods")
.add_child(Box::new(method1))
.add_child(Box::new(method2))
.add_child(Box::new(method3))
Behavior:
- Tick children left to right
- If child returns
Success→ ReturnSuccessimmediately - If child returns
Running→ ReturnRunning, resume next tick - If child returns
Failure→ Move to next child - If all fail → Return
Failure
Use Cases: Fallback logic, priority lists, error handling
Parallel (Concurrent Execution)
Executes all children concurrently.
Parallel::new("concurrent_tasks", ParallelPolicy::RequireAll)
.add_child(Box::new(task1))
.add_child(Box::new(task2))
.add_child(Box::new(task3))
Policies:
RequireAll: All must succeed forSuccessRequireOne: Any success returnsSuccess
Use Cases: Concurrent monitoring, multi-sensor fusion, parallel actions
2. Decorator Nodes (Modifiers)
Decorators wrap a single child and modify its behavior.
Retry
Retry child on failure up to N times.
Retry::new("retry_connection", child, 3)
Behavior:
- If child succeeds → Return
Success - If child fails → Increment counter, retry
- If max attempts reached → Return
Failure
Timeout
Fail child if it exceeds time limit.
Timeout::new("timed_action", child, Duration::from_secs(5))
Behavior:
- Start timer on first tick
- If child returns terminal before timeout → Return child status
- If timeout expires → Halt child, return
Failure
Inverter
Inverts Success/Failure results.
Inverter::new("not_ready", child)
Behavior:
Success→FailureFailure→SuccessRunning→Running(unchanged)
Repeat
Repeats child N times or infinitely.
Repeat::new("patrol", child, Some(5)) // 5 times
Repeat::new("monitor", child, None) // Infinite
Behavior:
- Tick child, reset when terminal
- Increment counter
- Return
Runninguntil count reached - Return
Successwhen complete (finite) or never (infinite)
ReplanOnFailure (LLM-Powered)
Automatically replan when child fails.
ReplanOnFailure::new(
"adaptive_action",
child,
"task_description",
"recovery_plan",
3 // Max replans
)
Behavior:
- Tick child normally
- On
Failure→ Ask LLM to generate recovery plan - Load and execute recovery plan
- If recovery succeeds → Return
Success - If max replans reached → Return
Failure
3. Action Nodes (Leaf Behaviors)
Actions perform actual work.
SetBlackboard
Sets a key-value pair in the blackboard.
SetBlackboard::new("set_status", "status", "ready")
Behavior:
- Always returns
Successimmediately - Stores value in blackboard for other nodes
ToolAction
Executes a tool from the registry.
ToolAction::new("navigate", "nav_tool", params)
Behavior:
- Looks up tool in context.tool_registry
- Executes tool with parameters
- Returns tool's result status
4. Condition Nodes (Tests)
Conditions check state without side effects.
CheckBlackboard
Checks if a blackboard key exists and matches value.
CheckBlackboard::new("check_ready", "ready", true)
Behavior:
Successif key exists and equals expected valueFailureotherwise
5. LLM Nodes (Adaptive)
LLM-powered nodes for dynamic behavior.
LLMPlannerNode
Generates a behavior tree plan using LLM.
LLMPlannerNode::new(
"planner",
"task_description_key", // Read task from blackboard
"plan_output_key" // Write plan JSON to blackboard
)
Behavior:
- Reads task description from blackboard
- Sends prompt to LLM provider
- Parses LLM response as JSON tree
- Stores plan in blackboard
- Returns
Successif plan generated,Failureotherwise
SubtreeLoader
Loads and executes a dynamically generated subtree.
SubtreeLoader::new("executor", "plan_key")
Behavior:
- Reads JSON plan from blackboard
- Parses JSON into behavior tree
- Ticks the loaded subtree
- Returns subtree's status
Blackboard (Shared State)
The blackboard is a shared key-value store accessible to all nodes.
API
// Set values
context.blackboard.set("key", value).await;
// Get values
let value: String = context.blackboard.get("key").await?;
// Check existence
if context.blackboard.contains("key").await {
// Key exists
}
// Remove values
context.blackboard.remove("key").await;
Thread Safety
- Thread-safe (Arc<RwLock<HashMap>>)
- Async read/write operations
- Can be shared across concurrent nodes
Use Cases
- State sharing: Pass data between nodes
- Configuration: Store mission parameters
- LLM communication: Pass tasks and plans
- Sensor data: Store latest readings
- Results: Store action outcomes
Best Practices
- Use typed keys: Create constants for key names
- Document schema: Document expected keys and types
- Initialize early: Set required keys before tree execution
- Clean up: Remove temporary keys when done
- Avoid large data: Blackboard is not a database
BTreeContext (Execution Context)
The context holds everything needed for execution:
pub struct BTreeContext {
pub blackboard: Blackboard,
llm_provider: Option<Arc<dyn LlmProvider>>,
rt_executor: Option<Arc<dyn Any>>,
tool_registry: Option<Arc<dyn Any>>,
}
Components
Blackboard
Shared state storage (see above).
LLM Provider
Optional LLM integration for adaptive nodes:
let llm = Arc::new(MockLlmProvider::new(responses));
let context = BTreeContext::new().with_llm(llm);
RT Executor
Optional igris-rt executor for real-time bounded execution.
Tool Registry
Optional tool registry for ToolAction nodes.
Execution Flow
Single Tick Execution
let mut tree = Sequence::new("mission");
let mut context = BTreeContext::new();
// Manual tick
let status = tree.tick(&mut context).await?;
if status.is_terminal() {
println!("Tree completed: {:?}", status);
} else {
// Tick again next cycle
}
Automatic Tick Loop (Executor)
let executor = BTreeExecutor::new()
.with_max_ticks(100)
.with_deadline(Duration::from_secs(30));
let result = executor.execute(&mut tree, &mut context).await?;
The executor:
- Ticks automatically until terminal status
- Enforces max ticks limit
- Enforces time deadline
- Supports cancellation
- Collects execution metadata
Lifecycle
Node Lifecycle
[Created] → [Ticking] → [Terminal] → [Reset] → [Ticking] ...
Creation
let node = Sequence::new("mission");
Ticking
let status = node.tick(&mut context).await?;
Terminal
When status.is_terminal() is true.
Reset
node.reset().await;
Clears internal state, ready to execute again.
Halt (Emergency Stop)
node.halt().await;
Immediately stops execution, cancels async operations.
Tree Lifecycle
[Build] → [Execute] → [Complete] → [Reset] → [Execute] ...
Build Phase
let tree = Sequence::new("mission")
.add_child(Box::new(child1))
.add_child(Box::new(child2));
Execute Phase
let executor = BTreeExecutor::new();
let result = executor.execute(&mut tree, &mut context).await?;
Complete Phase
Tree returns terminal status.
Reset Phase
tree.reset().await;
Hybrid Execution Model
Deterministic Layer
Traditional BTree nodes provide predictable control flow:
Sequence::new("deterministic_mission")
.add_child(Box::new(CheckSensors))
.add_child(Box::new(Navigate))
.add_child(Box::new(PerformTask))
Advantages:
- Predictable behavior
- Fast execution
- Easy to test and debug
- No LLM costs
Adaptive Layer
LLM nodes add dynamic intelligence:
Sequence::new("adaptive_mission")
.add_child(Box::new(SetBlackboard::new("task", "Navigate to warehouse")))
.add_child(Box::new(LLMPlannerNode::new("planner", "task", "plan")))
.add_child(Box::new(SubtreeLoader::new("executor", "plan")))
Advantages:
- Handles novel situations
- No exhaustive pre-planning
- Adaptive recovery
- Natural language task specification
Hybrid Combination
Best of both worlds:
Sequence::new("hybrid_mission")
.add_child(Box::new(/* Deterministic setup */))
.add_child(Box::new(
ReplanOnFailure::new(
"adaptive_recovery",
Box::new(/* Deterministic action */),
"task",
"recovery",
3
)
))
.add_child(Box::new(/* Deterministic cleanup */))
Strategy:
- Use deterministic nodes for known, well-defined behaviors
- Use LLM nodes for uncertain, dynamic situations
- Use ReplanOnFailure for recovery when deterministic fails
- Keep LLM usage minimal for cost and performance
Error Handling
Node-Level Errors
Nodes can return errors for critical failures:
async fn tick(&mut self, context: &mut BTreeContext) -> Result<NodeStatus>
When to return errors:
- Missing required dependencies
- Invalid configuration
- Unrecoverable system failures
When to return Failure status:
- Normal operational failures
- Condition checks that fail
- Actions that don't succeed
Tree-Level Errors
The executor propagates node errors:
let result = executor.execute(&mut tree, &mut context).await?;
// ? operator propagates errors
Recovery Strategies
- Return Failure: Let parent handle it (Selector, ReplanOnFailure)
- Retry Decorator: Automatic retry on failure
- ReplanOnFailure: LLM-powered recovery
- Selector Fallback: Multiple alternatives
- Error Status: Return error to caller
Performance Considerations
Tick Frequency
- Fast ticks: 100-1000 Hz for reactive behaviors
- Slow ticks: 1-10 Hz for high-level planning
- Variable ticks: Use tick_delay to control rate
Node Complexity
- Simple nodes: <1ms per tick (actions, conditions)
- LLM nodes: 100ms-5s per tick (bounded by inference)
- Parallel nodes: Sum of concurrent children
Memory Usage
- Tree structure: ~100 bytes per node
- Blackboard: Depends on stored data
- Context overhead: ~1KB
Optimization Tips
- Minimize LLM calls: Use caching, only call when necessary
- Shallow trees: Avoid deep nesting (>10 levels)
- Stateless nodes: Prefer stateless over stateful
- Batch operations: Group multiple actions when possible
- Profile execution: Use visualizer to identify bottlenecks
Design Patterns
Pattern 1: Check-Act-Check
Sequence::new("safe_action")
.add_child(Box::new(CheckPreconditions))
.add_child(Box::new(PerformAction))
.add_child(Box::new(CheckPostconditions))
Pattern 2: Try-Fallback-Fallback
Selector::new("robust_action")
.add_child(Box::new(PrimaryMethod))
.add_child(Box::new(BackupMethod))
.add_child(Box::new(EmergencyMethod))
Pattern 3: Retry-Until-Success
Retry::new(
"persistent_action",
Box::new(UnreliableAction),
5
)
Pattern 4: Timeout-Or-Fail
Timeout::new(
"bounded_action",
Box::new(LongRunningAction),
Duration::from_secs(10)
)
Pattern 5: Adaptive-With-Deterministic-Fallback
Selector::new("adaptive_with_fallback")
.add_child(Box::new(
ReplanOnFailure::new("adaptive", child, "task", "plan", 3)
))
.add_child(Box::new(SafetyFallback))
Next Steps
- Node Types - Complete reference for all node types
- LLM Integration - Deep dive into adaptive behaviors
- Runtime Execution - Advanced executor usage
- API Reference - Full API documentation
Summary
Key takeaways:
- Behavior trees execute through ticking (one cycle per tick)
- Nodes return status (Running, Success, Failure, Skipped)
- Composite nodes control flow (Sequence, Selector, Parallel)
- Decorator nodes modify behavior (Retry, Timeout, etc.)
- Action/Condition nodes do work and check state
- LLM nodes provide adaptive intelligence
- Blackboard shares state between nodes
- Context holds execution environment
- Hybrid model combines deterministic + adaptive behaviors