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:

  1. Parent ticks child: Parent node calls tick() on its children
  2. Child executes: Child performs its logic (check condition, execute action, etc.)
  3. Child returns status: Running, Success, Failure, or Skipped
  4. 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 → Return Running, resume next tick
  • If child returns Failure → Return Failure immediately
  • 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 → Return Success immediately
  • If child returns Running → Return Running, 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 for Success
  • RequireOne: Any success returns Success

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:

  • SuccessFailure
  • FailureSuccess
  • RunningRunning (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 Running until count reached
  • Return Success when 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 Success immediately
  • 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:

  • Success if key exists and equals expected value
  • Failure otherwise

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 Success if plan generated, Failure otherwise

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

  1. Use typed keys: Create constants for key names
  2. Document schema: Document expected keys and types
  3. Initialize early: Set required keys before tree execution
  4. Clean up: Remove temporary keys when done
  5. 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

  1. Return Failure: Let parent handle it (Selector, ReplanOnFailure)
  2. Retry Decorator: Automatic retry on failure
  3. ReplanOnFailure: LLM-powered recovery
  4. Selector Fallback: Multiple alternatives
  5. 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

  1. Minimize LLM calls: Use caching, only call when necessary
  2. Shallow trees: Avoid deep nesting (>10 levels)
  3. Stateless nodes: Prefer stateless over stateful
  4. Batch operations: Group multiple actions when possible
  5. 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


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