Quick Start

Build your first hybrid behavior tree in 10 minutes.


Prerequisites

  • Rust 1.70+ installed
  • Basic understanding of async/await in Rust
  • Tokio runtime (added automatically)

Installation

Add igris-btree to your Cargo.toml:

[dependencies]
igris-btree = "0.1"
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"

Your First Behavior Tree

Let's create a simple mission tree that sets some blackboard values in sequence.

Step 1: Create a New Project

cargo new my-btree-app
cd my-btree-app

Step 2: Write the Code

Create src/main.rs:

use igris_btree::prelude::*;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create execution context
    let mut context = BTreeContext::new();

    // Build a simple sequence
    let mut tree = Sequence::new("simple_mission")
        .add_child(Box::new(SetBlackboard::new("init", "status", "starting")))
        .add_child(Box::new(SetBlackboard::new("work", "task", "processing")))
        .add_child(Box::new(SetBlackboard::new("done", "status", "completed")));

    // Execute the tree
    let status = tree.tick(&mut context).await?;

    println!("Tree status: {:?}", status);
    println!("Status: {}", context.blackboard.get::<String>("status").await?);
    println!("Task: {}", context.blackboard.get::<String>("task").await?);

    Ok(())
}

Step 3: Run It

cargo run

Expected output:

Tree status: Success
Status: completed
Task: processing

Congratulations! You just executed your first behavior tree.


Understanding the Code

BTreeContext

The execution context holds shared state:

let mut context = BTreeContext::new();
  • Blackboard: Shared key-value storage
  • LLM Provider: Optional LLM integration (not used yet)
  • Tool Registry: Optional tool execution (not used yet)

Sequence Node

Executes children in order:

let mut tree = Sequence::new("simple_mission")
    .add_child(Box::new(/* first child */))
    .add_child(Box::new(/* second child */))
    .add_child(Box::new(/* third child */));
  • If all children succeed → Returns Success
  • If any child fails → Returns Failure and stops
  • If a child returns Running → Returns Running (continues next tick)

SetBlackboard Action

Sets a key-value pair in the blackboard:

SetBlackboard::new("node_name", "key", "value")
  • Always returns Success immediately
  • Stores the value in the blackboard for other nodes to read

Adding Conditions

Let's add a condition check:

use igris_btree::prelude::*;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut context = BTreeContext::new();

    // Pre-populate blackboard
    context.blackboard.set("ready", true).await;

    // Build tree with condition
    let mut tree = Sequence::new("conditional_mission")
        .add_child(Box::new(CheckBlackboard::new("check_ready", "ready", true)))
        .add_child(Box::new(SetBlackboard::new("execute", "status", "running")));

    let status = tree.tick(&mut context).await?;
    println!("Status: {:?}", status); // Success

    Ok(())
}

The CheckBlackboard node:

  • Returns Success if key exists and matches expected value
  • Returns Failure otherwise

Adding Fallback Logic

Use a Selector for fallback behavior:

use igris_btree::prelude::*;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut context = BTreeContext::new();

    // Try primary, fall back to secondary
    let mut tree = Selector::new("mission_with_fallback")
        .add_child(Box::new(
            CheckBlackboard::new("try_primary", "primary_ready", true)
        ))
        .add_child(Box::new(
            SetBlackboard::new("use_secondary", "status", "using_fallback")
        ));

    let status = tree.tick(&mut context).await?;

    // Primary fails (key doesn't exist), so secondary executes
    assert_eq!(status, NodeStatus::Success);
    assert_eq!(
        context.blackboard.get::<String>("status").await?,
        "using_fallback"
    );

    Ok(())
}

Adding Decorators

Decorators modify child behavior:

Retry Decorator

use igris_btree::prelude::*;
use std::sync::{Arc, Mutex};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut context = BTreeContext::new();

    // Counter to simulate failure then success
    let counter = Arc::new(Mutex::new(0));
    let counter_clone = counter.clone();

    // This would fail first time, succeed second time
    // (In real code, use a proper failing node)

    let mut tree = Retry::new(
        "retry_action",
        Box::new(SetBlackboard::new("action", "status", "done")),
        3  // Max 3 attempts
    );

    let status = tree.tick(&mut context).await?;
    println!("Status: {:?}", status);

    Ok(())
}

Timeout Decorator

use igris_btree::prelude::*;
use std::time::Duration;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut context = BTreeContext::new();

    let mut tree = Timeout::new(
        "timed_action",
        Box::new(SetBlackboard::new("action", "status", "done")),
        Duration::from_secs(5)  // 5 second timeout
    );

    let status = tree.tick(&mut context).await?;
    println!("Status: {:?}", status);

    Ok(())
}

Using the Executor

For automatic tick loop management, use BTreeExecutor:

use igris_btree::prelude::*;
use std::time::Duration;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create executor with limits
    let executor = BTreeExecutor::new()
        .with_max_ticks(100)                    // Max 100 ticks
        .with_deadline(Duration::from_secs(30)) // 30 second deadline
        .with_tracing(true);                     // Enable logging

    // Build tree
    let mut tree = Sequence::new("mission")
        .add_child(Box::new(SetBlackboard::new("step1", "phase", "init")))
        .add_child(Box::new(SetBlackboard::new("step2", "phase", "execute")))
        .add_child(Box::new(SetBlackboard::new("step3", "phase", "complete")));

    // Create context
    let mut context = BTreeContext::new();

    // Execute (automatic tick loop)
    let result = executor.execute(&mut tree, &mut context).await?;

    println!("Execution completed!");
    println!("  Status: {:?}", result.status);
    println!("  Ticks: {}", result.tick_count);
    println!("  Duration: {:?}", result.duration);

    Ok(())
}

The executor handles:

  • Automatic tick loop until terminal status
  • Max ticks enforcement
  • Deadline enforcement
  • Execution metadata collection

Adding LLM Intelligence

Now let's add adaptive LLM-powered planning:

use igris_btree::prelude::*;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create mock LLM provider for testing
    let llm = Arc::new(MockLlmProvider::with_navigation_plan());

    // Create context with LLM
    let mut context = BTreeContext::new()
        .with_llm(llm);

    // Build hybrid tree
    let mut tree = Sequence::new("hybrid_mission")
        .add_child(Box::new(SetBlackboard::new(
            "set_task",
            "mission_task",
            "Navigate to warehouse"
        )))
        .add_child(Box::new(LLMPlannerNode::new(
            "planner",
            "mission_task",  // Read task from this blackboard key
            "plan"           // Write plan to this key
        )))
        .add_child(Box::new(SubtreeLoader::new(
            "executor",
            "plan"  // Load and execute plan from this key
        )));

    // Execute
    let executor = BTreeExecutor::new();
    let result = executor.execute(&mut tree, &mut context).await?;

    println!("Hybrid execution completed!");
    println!("  Status: {:?}", result.status);

    Ok(())
}

This tree:

  1. Sets a mission task in the blackboard
  2. Asks the LLM to generate a plan (JSON subtree)
  3. Loads and executes the LLM-generated plan
  4. Returns the final status

Complete Example

Here's a complete example combining everything:

use igris_btree::prelude::*;
use std::time::Duration;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Setup
    let llm = Arc::new(MockLlmProvider::with_navigation_plan());
    let mut context = BTreeContext::new().with_llm(llm);

    let executor = BTreeExecutor::new()
        .with_max_ticks(50)
        .with_deadline(Duration::from_secs(30))
        .with_tracing(true);

    // Build complex tree
    let mut tree = Sequence::new("complete_mission")
        .add_child(Box::new(SetBlackboard::new("init", "status", "starting")))
        .add_child(Box::new(
            Selector::new("main_logic")
                .add_child(Box::new(
                    CheckBlackboard::new("check_plan", "manual_plan", true)
                ))
                .add_child(Box::new(
                    Sequence::new("llm_planning")
                        .add_child(Box::new(SetBlackboard::new(
                            "set_task",
                            "task",
                            "Navigate warehouse"
                        )))
                        .add_child(Box::new(LLMPlannerNode::new(
                            "planner",
                            "task",
                            "plan"
                        )))
                ))
        ))
        .add_child(Box::new(SubtreeLoader::new("executor", "plan")))
        .add_child(Box::new(SetBlackboard::new("complete", "status", "done")));

    // Execute
    let result = executor.execute(&mut tree, &mut context).await?;

    println!("\n=== Execution Complete ===");
    println!("Status: {:?}", result.status);
    println!("Ticks: {}", result.tick_count);
    println!("Duration: {:?}", result.duration);

    // Check blackboard
    let status = context.blackboard.get::<String>("status").await?;
    println!("Final status: {}", status);

    Ok(())
}

Next Steps

Now that you've built your first behavior trees, explore these topics:

  1. Core Concepts - Deep dive into BTree fundamentals
  2. Node Types - Learn about all available nodes
  3. LLM Integration - Advanced LLM usage patterns
  4. Visualization - Monitor and debug your trees
  5. Runtime Execution - Advanced executor configuration

Common Patterns

Pattern 1: Check-Then-Act

Sequence::new("check_then_act")
    .add_child(Box::new(CheckBlackboard::new("check", "ready", true)))
    .add_child(Box::new(SetBlackboard::new("act", "status", "executing")))

Pattern 2: Try-With-Fallback

Selector::new("try_with_fallback")
    .add_child(Box::new(/* primary action */))
    .add_child(Box::new(/* fallback action */))

Pattern 3: Retry-With-Timeout

Retry::new(
    "retry_wrapper",
    Box::new(Timeout::new(
        "timeout_wrapper",
        Box::new(/* your action */),
        Duration::from_secs(5)
    )),
    3
)

Pattern 4: Adaptive Recovery

ReplanOnFailure::new(
    "adaptive_action",
    Box::new(/* primary action */),
    "task_description",
    "recovery_plan",
    3  // Max replans
)

Troubleshooting

Tree Never Completes

  • Check for nodes that always return Running
  • Use with_max_ticks() to limit execution
  • Add timeout decorators to long-running actions

Blackboard Key Not Found

  • Use contains() to check before get()
  • Use CheckBlackboard before actions that depend on keys
  • Initialize required keys before tree execution

LLM Planning Fails

  • Check LLM provider is correctly configured
  • Verify blackboard keys are set correctly
  • Use MockLlmProvider for testing
  • Check LLM response format (must be valid JSON)

Tips

  1. Start simple: Build and test small trees before composing them
  2. Use the executor: Let BTreeExecutor handle the tick loop
  3. Test with mocks: Use MockLlmProvider before integrating real LLMs
  4. Add logging: Enable tracing with with_tracing(true)
  5. Visualize: Use TreeVisualizer to understand execution flow
  6. Set limits: Always use max_ticks and deadline in production

Ready to dive deeper? Continue to Core Concepts.