Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Forester

Forester is a domain-specific language (DSL) and high-performance Rust runtime designed for building, testing, and executing behavior trees.

It is specifically engineered for two core domains:

  • 🧠 AI Agents: Orchestrating LLM-based tools, state management, and fallback loops without unmaintainable Python code or DAG spaghettification.
  • 🤖 Robotics & Industrial Systems: Managing deterministic, reactive hardware control loops, with built-in export to ROS Nav2 and simulation support for Webots.

Why Forester?

As systems grow in complexity, state machines and Directed Acyclic Graphs (DAGs) break down when handling fallbacks, retries, and dynamic error recovery. Forester separates decision logic (tree orchestration) from execution logic (actions), enabling modular, maintainable control flow.

Key Differentiators

  • Dedicated Orchestration DSL: Write tree logic once in a strongly-typed, functional language featuring higher-order trees, lambdas, and live Blackboard pointers.
  • Sync & Async Execution: Non-blocking async execution for I/O and remote calls, alongside predictable sync execution for tight control loops.
  • Local & Remote Action Clients: Execute actions natively in Rust (forester-rs) or remotely via lightweight clients (such as Python’s forester-http-ra-py for LangChain/LlamaIndex tools).
  • Rich Analysis & Simulation: Inspect execution traces, visualize trees, and test orchestration in simulation before deploying to physical hardware or production agents.
  • ROS Nav2 & Webots Integration: Export Forester trees directly into ROS Nav2 XML or run against Webots simulation environments.
  • Static Validations & Optimizations: Catch tree errors, numeric overflows, and structural flaws at compile time rather than runtime.

Why Behavior Trees?

Behavior trees provide a clean, mathematical abstraction over decision logic:

  • Modularity: Small, independent nodes (sequences, fallbacks, decorators) can be composed into deep hierarchical behaviors.
  • Reactivity: Ticks evaluate preconditions dynamically, allowing real-time preemptions and fallbacks when environment state changes.
  • Separation of Concerns: Tree nodes decide what to do next; external action handlers perform how it gets done.

Learn More About Behavior Trees

  • BehaviorTree.CPP — C++ behavior tree framework widely used in ROS2.
  • Beehave — Behavior tree AI library for the Godot Engine.
  • Bonsai — Pure Rust behavior tree implementation.

Forester Architecture & Components

Forester is structured into four primary subsystems: the Language & Compiler, the Runtime Engine, Analysis & Simulation, and Integrations & Tooling.


1. Language & Compiler

Forester provides a domain-specific language (DSL) tailored for behavior tree orchestration.

  • Tree DSL (.tree): A strongly-typed language for describing execution logic, higher-order trees, lambdas, decorators, and Blackboard memory references.
  • Static Analysis & Compiler: Parses scripts, verifies type constraints, validates argument bounds, and detects structural errors before runtime.

2. Runtime Engine

The Rust runtime engine manages tree execution and state.

  • Engine Core: Ticks nodes reactively, supporting synchronous execution (for fast control loops) and asynchronous execution (for non-blocking I/O).
  • Blackboard: High-performance, shared memory store for passing data between tree nodes dynamically.
  • Action Keeper: Registers and executes actions (local Rust callbacks or remote RPC handlers).

3. Analysis & Simulation

Tools to inspect, debug, and validate behavior trees during development.

  • f-tree CLI: Unified command-line tool for compiling, analyzing, and running trees.
  • Visualization: Generates tree diagrams (Graphviz) to inspect tree structure visually.
  • Execution Tracing: Detailed execution telemetry for stepping through ticks and debugging node state.
  • Simulator: Executes trees against stubbed action responses to test orchestration logic before linking real hardware or external LLM APIs.

4. Integrations & Tooling

Bridge Forester to external runtimes, robotics engines, and developer editors.

  • Remote Action SDKs: Lightweight clients allowing actions to run outside the Rust process:
    • Python (forester-http-ra-py): Execute Python functions and LangChain/LlamaIndex tools seamlessly over HTTP.
    • Rust (forester-rs): Native Rust action integration.
  • ROS Nav2 Exporter: Exports Forester trees directly to ROS2 Nav2 XML format for robotics navigation pipelines.
  • Editor Tooling: Syntax highlighting, linting, and Language Server Protocol (LSP) support for IDEs (VS Code, IntelliJ).

Setup & Quick Start

Forester can be used as a CLI tool for simulation/analysis, embedded as a Rust dependency, or integrated remotely from Python.


1. CLI Tool Setup (f-tree)

Install the f-tree command-line utility via cargo:

cargo install f-tree

Verify installation and view available subcommands:

f-tree --help

Common CLI Subcommands

  • Simulation (f-tree sim): Run behavior tree execution against simulated action stubs.
  • Visualization (f-tree vis): Generate SVG/Graphviz visual diagrams of .tree files.
  • Tree Validation (f-tree check): Statically validate tree syntax, types, and imports.

2. Rust Project Integration

Add forester-rs to your Cargo.toml:

[dependencies]
forester-rs = "0.2"

Example 1: Loading Trees from the Filesystem

use forester_rs::runtime::builder::ForesterBuilder;
use forester_rs::runtime::action::Action;
use forester_rs::tracer::Tracer;

fn main() {
    let mut fb = ForesterBuilder::from_file_system();
    fb.main_file("main.tree".to_string());
    fb.root("main");
    fb.tracer(Tracer::default());
    fb.bb_load("db/initial_state.json".to_string());
    
    let mut forester = fb.build().expect("Failed to build Forester runtime");
    let result = forester.run().expect("Tree execution failed");
    
    println!("Execution completed with result: {:?}", result);
}

Example 2: In-Memory / Inline DSL Script Execution

use forester_rs::runtime::builder::ForesterBuilder;

fn main() {
    let mut fb = ForesterBuilder::from_text();
    
    fb.text(r#"
        import "std::actions"
        
        root main sequence {
            action_a()
            action_b()
        }
        
        impl action_a();
        impl action_b();
    "#.to_string());

    let mut forester = fb.build().expect("Failed to parse inline script");
    let result = forester.run().expect("Execution failed");
    
    println!("Result: {:?}", result);
}

Example 3: Programmatic Tree Construction

use forester_rs::runtime::builder::ForesterBuilder;
use forester_rs::flow;

fn main() {
    let mut fb = ForesterBuilder::from_code();
    
    // Programmatically construct tree nodes using Rust macros
    fb.add_rt_node(
        flow!(fallback "recovery_root", args!();
            action!("check_sensor"),
            action!("reset_state")
        )
    );
    
    let mut forester = fb.build().unwrap();
    let result = forester.run().unwrap();
    
    println!("Result: {:?}", result);
}

3. Python Integration (Remote Actions for AI Tools)

For Python-based AI workflows (LangChain, LlamaIndex, custom LLM tools), install the Python remote-action client:

pip install forester-http-ra-py

The Python client connects your Python tools over HTTP to the Forester Rust runtime, executing remote actions seamlessly.

Forester Tree Language (.tree)

The Forester Tree Language is a strongly-typed, functional domain-specific language (DSL) designed to describe behavior tree architecture cleanly without verbosity or repetition.

Unlike traditional XML-based or JSON-based behavior tree formats, Forester treats behavior trees as first-class, composable abstractions.


Why a Dedicated DSL?

Standard behavior tree formats (such as XML trees used in ROS) often suffer from extreme duplication and verbosity. Forester’s DSL solves this by introducing functional primitives:

  • Higher-Order Trees (HOT): Pass trees as parameters to other trees to create reusable patterns (e.g., generic retry loops, fallback wrappers, logging decorators).
  • Strong Type System: Built-in support for strings, numbers, booleans, arrays, objects, and subtrees, validated at compile time.
  • Blackboard Pointers: Pass live references to Blackboard memory into actions, allowing nodes to operate on dynamically updated state (essential for AI agent context).
  • Lambdas: Write inline, anonymous subtrees without declaring throwaway named definitions.

Project Structure & File Conventions

Forester projects consist of one or more .tree files organized within a root project directory:

my_project/
├── main.tree           # Entry point containing the root tree
├── agent_tools.tree    # Custom actions and tool definitions
├── navigation/
│   ├── move.tree       # Sub-tree definitions
│   └── recovery.tree   # Recovery patterns

Key Conventions

  1. File Extension: All Forester source files use the .tree extension.
  2. Root Entry Point: A project must define at least one root node (e.g., root main sequence { ... }).
  3. Module Imports: Imports resolve relative to the root directory (e.g., import "navigation::move").

Language Roadmap & Tooling

To ensure a seamless developer experience, Forester provides language tooling across popular editors:

  • Syntax Highlighting & Linting: Editor support for .tree files.
  • Language Server Protocol (LSP): Autocompletion, inline type errors, and navigation support for VS Code, Neovim, and IntelliJ.

Forester Syntax Overview

The syntax of the .tree language is declarative, strongly typed, and modular. A Forester script consists of six primary elements:

  1. Imports: Module references to include external .tree files or built-in stdlib actions.
  2. Root Declarations: The entry point of the behavior tree (root main ...).
  3. Tree Definitions: Named sub-trees (sequence, fallback, parallel) with typed parameters.
  4. Action Implementations (impl): Contract definitions for native or remote actions executed by the runtime.
  5. Invocations & Decorators: Calls to sub-trees and actions, wrapped with built-in decorators (e.g. retry, inverter, timeout).
  6. Higher-Order Tree Delegates (..): Invoking sub-trees passed as parameters.

Comprehensive Syntax Example

// 1. Imports
import "std::actions"
import "sensors/battery.tree" {
    check_battery => is_battery_ok,
}

// 2. Root Entry Point
root main sequence {
    // Check battery or charge
    fallback {
        is_battery_ok()
        charge_robot()
    }
    
    // Execute target action with retry decorator
    retry(3) execute_task(
        target = {"x": 10, "y": 20},
        sub_action = place_item([100])
    )
}

// 3. Higher-Order Tree Definition
sequence execute_task(target: object, sub_action: tree) {
    fallback {
        is_target_reachable(target)
        approach_target(target)
    }
    sequence {
        savepoint()
        sub_action(..) // Delegate invocation of passed sub_action
    }
}

// 4. Sub-tree Definition
sequence place_item(location: array) {
    validate_location(location)
    info_wrapper(drop_payload({"speed": "slow"}))
}

// 5. Decorator Wrapper Pattern
sequence info_wrapper(action: tree) {
    log("Starting action execution")
    action(..)
    log("Action execution completed")
}

// 6. Action Signatures (linking to runtime implementation)
impl charge_robot();
impl approach_target(pos: object);
impl validate_location(coords: array);
impl drop_payload(config: object);
impl log(message: string);

Core Constructs Quick Reference

ElementExample SyntaxDescription
Root Treeroot main sequence { ... }Mandatory entry point for execution.
Sequence Nodesequence name(params) { ... }Executes children in order until one fails.
Fallback Nodefallback name(params) { ... }Executes children in order until one succeeds.
Action Definitionimpl action_name(arg: type);Defines an action implemented in Rust/Python.
Tree Parametersub_tree: treePass a sub-tree as a parameter (Higher-Order Tree).
Tree Delegatesub_tree(..)Invoke a passed sub-tree parameter.
Decoratorretry(3) node(...)Wraps execution with retry/inverter/timeout logic.
Blackboard Pointer&key_namePass live reference to shared Blackboard memory.

Imports & Modularity

Forester allows behavior tree definitions to be split across multiple files and organized into logical sub-modules. Imports make trees modular, reusable, and easy to maintain across large projects.

Imports are typically declared at the top of .tree files.


1. Import Syntax

Complete File Import

Imports all tree definitions and action signatures from the target file:

import "std::actions"
import "navigation/nav.tree"
import "sensors/battery.tree"

Selective Import with Aliasing

Imports specific definitions from a file and renames them using aliases to prevent naming conflicts:

import "robot_a/vision.tree" {
    detect_object => detect_robot_a_object,
}

import "robot_b/vision.tree" {
    detect_object => detect_robot_b_object,
}

2. Import Paths

Forester supports three types of import paths:

Relative paths are evaluated relative to the project root directory:

Given the project directory structure:

my_project/
├── main.tree
└── modules/
    └── navigation/
        └── move.tree

In main.tree, import move.tree using:

import "modules/navigation/move.tree"

Standard Library Imports

Built-in standard library actions and decorators can be imported using standard package specifiers:

import "std::actions"

Absolute Paths

Absolute file paths can be used (primarily for global system modules):

import "/opt/forester/std/common.tree"

3. Resolving Name Conflicts with Aliases

When two separate .tree files define sub-trees with identical names, use alias mappings inside curly braces { } to assign unique local names:

// Import 'check_status' from hardware module
import "hardware/status.tree" {
    check_status => check_hardware_status,
}

// Import 'check_status' from LLM agent module
import "agent/status.tree" {
    check_status => check_agent_status,
}

root main sequence {
    check_hardware_status()
    check_agent_status()
}

4. Circular Import Handling

The Forester compiler automatically detects and resolves circular dependency graphs during compilation, raising a clear static compilation error if unresolvable recursive imports occur.

Tree Definitions Overview

A Tree Definition declares a reusable sub-tree or node contract in Forester. Definitions isolate control logic into modular components that can accept typed parameters, store state on the Blackboard, or wrap child trees.

Forester classifies tree definitions into four main categories:


Categories of Definitions

1. Control Flow Nodes

Control flow nodes govern how child nodes are scheduled and executed:

  • sequence: Ticks children sequentially until one returns Failure or Running. Returns Success if all children succeed.
  • fallback: Ticks children sequentially until one returns Success or Running. Returns Failure if all children fail (often used for fallbacks and recovery).
  • parallel: Ticks children concurrently based on a specified synchronization policy (e.g. success threshold).

Read more in Control Flow Nodes.


2. Root Entry Point (root)

The root definition declares the top-level starting point of the behavior tree. A Forester script must contain at least one root definition:

root main sequence {
    check_preconditions()
    execute_agent_task()
}

3. Decorator Nodes

Decorators are single-child nodes that modify or control the execution behavior of their child:

  • retry(N): Automatically re-ticks its child up to N times upon failure.
  • inverter: Flips child Success to Failure and vice versa.
  • timeout(ms): Limits child execution time.
  • repeat(N): Executes child N times sequentially.

Read more in Decorators.


4. Action Nodes (impl)

Actions represent the leaves of the behavior tree where actual work gets executed (e.g. motor movement, REST calls, LLM prompt generation). Action contracts are defined using impl:

impl call_llm_tool(prompt: string, model: string);
impl execute_motor_step(velocity: number);

Read more in Actions.


5. Lambdas & Higher-Order Trees

  • Higher-Order Trees (HOT): Definitions that accept other sub-trees as parameters (sub_tree: tree).
  • Lambdas: Anonymous inline sub-tree definitions created and executed at the point of invocation.

Read more in Higher-Order Trees & Lambdas.

Control Flow Nodes Overview

Control flow nodes direct the execution path of a behavior tree. They manage how child nodes are scheduled, evaluated, and short-circuited based on child tick results (Success, Failure, or Running).

Forester supports three fundamental control flow nodes:


1. Sequence Nodes (sequence)

  • Behavior: Evaluates children sequentially from left to right.
  • Short-circuiting:
    • Returns Failure immediately if any child returns Failure.
    • Returns Running if a child returns Running (halting further evaluation during that tick).
    • Returns Success only when all children return Success.
  • Use Case: Step-by-step procedures (e.g. [Check Precondition -> Execute Step 1 -> Execute Step 2]).

Detailed documentation: Sequence Nodes.


2. Fallback / Selector Nodes (fallback)

  • Behavior: Evaluates children sequentially from left to right to find a successful path.
  • Short-circuiting:
    • Returns Success immediately if any child returns Success.
    • Returns Running if a child returns Running.
    • Returns Failure only when all children return Failure.
  • Use Case: Error handling, recovery strategies, and fallback routines (e.g. [Primary Action -> Secondary Fallback Action -> Alert Operator]).

Detailed documentation: Fallback Nodes.


3. Parallel Nodes (parallel)

  • Behavior: Ticks children concurrently during each engine tick.
  • Completion Policy: Evaluates overall status based on child results and configured threshold policies (e.g., success threshold or fail-fast rules).
  • Use Case: Concurrent tasks (e.g. [Maintain Balance while Navigating to Goal]).

Detailed documentation: Parallel Nodes.


Summary of Return States

Node TypeSucceeds WhenFails WhenShort-Circuits On
sequenceAll children return SuccessAny child returns FailureFirst Failure or Running
fallbackAny child returns SuccessAll children return FailureFirst Success or Running
parallelReaches success thresholdReaches failure thresholdPolicy dependant

Sequence Nodes

A Sequence node executes its child nodes in order from left to right as long as each child returns Success. If any child returns Failure or Running, the sequence halts further evaluation and immediately propagates that status up the tree.

In the Forester DSL, sequence nodes are declared using the sequence keyword (or its memory/reactive variants m_sequence and r_sequence).


Standard Sequence (sequence)

Execution Flow & Rules

  1. Initial Tick: Starts at the first child node.
  2. Child Success: Moves to the next child. If the last child succeeds, the sequence returns Success.
  3. Child Running: Halts evaluation and returns Running. On the next tick, execution resumes at the running child.
  4. Child Failure: Immediately aborts remaining children and returns Failure.
  5. Reset / Halt: If restarted or aborted, execution starts back from the first child.

Example

import "std::actions"

root main sequence {
    store("key_a", "1")  // Tick 1: proceed if Success
    store("key_b", "2")  // Tick 2: proceed if Success
    store("key_c", "3")  // Tick 3: finish with Success
}

impl store(key: string, value: string);

Flow Diagram

graph TD
    Root["root main"] --> Seq["sequence"]
    Seq --> A["store (key_a)"]
    Seq --> B["store (key_b)"]
    Seq --> C["store (key_c)"]

Sequence Variants

Forester provides two specialized sequence variants for state persistence and real-time reactive control loops:

1. Memory Sequence (m_sequence)

An m_sequence remembers which child nodes have already succeeded. When re-ticked (e.g. after a decorator retry or on subsequent ticks), it skips previously succeeded children and resumes execution at the first non-successful child.

root main sequence {
    retry(5) m_sequence {
        check_preconditions()  // Returns Success once
        execute_task()         // Returns Failure -> retry triggers m_sequence
        finish_and_cleanup()   // Execution resumes here without re-checking preconditions
    }
}
  • Memory Reset: Memory persists until the entire sequence finishes with Success or is explicitly reset.

2. Reactive Sequence (r_sequence)

An r_sequence re-evaluates all preceding children on every tick, even if they succeeded on prior ticks. This ensures that preconditions remain valid while long-running lower nodes execute.

root main r_sequence {
    is_battery_ok()       // Re-checked on EVERY tick
    navigate_to_target()  // Returns Running for multiple ticks
    perform_docking()
}
  • Preemption & Halting: If is_battery_ok() changes from Success to Failure while navigate_to_target() is Running, the r_sequence immediately halts navigate_to_target() and returns Failure.
  • Halting behavior: Halting ensures graceful teardown of active synchronous and flow nodes before propagating state changes.

Fallback (Selector) Nodes

A Fallback (also known as a Selector) node executes its child nodes in order from left to right until one child returns Success. If all children return Failure, the Fallback node returns Failure.

In the Forester DSL, fallback nodes are declared using the fallback keyword (or its reactive variant r_fallback).


Standard Fallback (fallback)

Execution Flow & Rules

  1. Initial Tick: Starts at the first child node.
  2. Child Failure: Moves to the next child. If the last child fails, the fallback returns Failure.
  3. Child Success: Immediately short-circuits remaining children and returns Success.
  4. Child Running: Halts evaluation and returns Running. On the next tick, execution resumes at the running child.
  5. Reset / Halt: If restarted or aborted, evaluation resets back to the first child.

Precondition Check Pattern

Fallbacks are commonly used to enforce preconditions before running an action (i.e. [Precondition OR Action]):

import "std::actions"

root main sequence {
    // If item is already held, skip move_to_item()
    fallback {
        is_item_in_hand(item = "battery_pack")
        move_to_item(item = "battery_pack")
    }
    
    pickup_item(item = "battery_pack")
}

impl is_item_in_hand(item: string);
impl move_to_item(item: string);
impl pickup_item(item: string);

LLM Fallback Pattern (AI Agents)

Fallbacks provide a clean mechanism for multi-model LLM fallbacks without nested try/except blocks:

root main fallback {
    call_primary_llm({"model": "gpt-4o"})
    call_fallback_llm({"model": "claude-3-5-sonnet"})
    alert_human_operator({"reason": "All LLM APIs failed"})
}

impl call_primary_llm(config: object);
impl call_fallback_llm(config: object);
impl alert_human_operator(config: object);

Flow Diagram

graph TD
    Root["root main"] --> Fall["fallback"]
    Fall --> A["call_primary_llm"]
    Fall --> B["call_fallback_llm"]
    Fall --> C["alert_human_operator"]

Reactive Fallback (r_fallback)

An r_fallback re-evaluates all preceding children on every tick, even while a lower child is currently Running.

root main r_fallback {
    emergency_battery_low()    // Checked on EVERY tick
    perform_long_task()        // Returns Running for multiple ticks
    fallback_idle()
}

impl emergency_battery_low();
impl perform_long_task();
impl fallback_idle();

Preemption Behavior

If perform_long_task() is Running and emergency_battery_low() evaluates to Success on a subsequent tick:

  1. r_fallback immediately halts perform_long_task().
  2. r_fallback executes emergency_battery_low() and returns Success.
  3. Lower nodes are safely preempted.

Parallel Nodes

A Parallel node provides concurrent execution of its child nodes within a single engine tick. Unlike sequence or fallback nodes (which short-circuit immediately upon receiving a result), a parallel node ticks all active children during every tick pass.

In the Forester DSL, parallel nodes are declared using the parallel keyword.


Execution Flow & Rules

  1. Concurrent Ticking: During a tick pass, the parallel node iterates through all child nodes and ticks each one sequentially within that single frame.
  2. Child Processing:
    • If a child returns Success or Failure, its status is stored.
    • If a child returns Running, it continues running. On subsequent ticks, already completed children (Success or Failure) are skipped, while Running children are re-ticked.
  3. Overall State Determination:
    • Success: Returned when all child nodes evaluate to Success.
    • Failure: Returned if any child node returns Failure (unless a custom threshold policy is configured).
    • Running: Returned if at least one child node remains Running and no failure threshold has been breached.

Code Example

import "std::actions"

root main sequence {
    // Run concurrent tasks in parallel
    parallel {
        clean_current_room()   // Async action (returns Running)
        inspect_environment()  // Sync or async action
    }
    
    navigate_to_next_room()
}

impl clean_current_room();
impl inspect_environment();
impl navigate_to_next_room();

Key Considerations for Parallel Execution

  1. Async & Sync Actions: Parallel nodes are ideal for launching multiple non-blocking async tasks simultaneously (e.g. streaming sensor data while running an LLM tool call).
  2. Non-Reactive Skipping: Once a child completes with Success or Failure, the parallel node skips re-ticking it on subsequent frames until the parent parallel node finishes and resets.
  3. Shared Blackboard Access: When children running in parallel write to the Blackboard, care should be taken to avoid key collisions or race conditions.

Decorators

A Decorator is a specialized single-child node that wraps a child node and modifies its execution behavior or transforms its return state (Success, Failure, or Running).

Every decorator accepts exactly one child node (which can be a single action, a sub-tree, or a block wrapped in { }).


Built-In Decorator Reference

DecoratorParametersDefaultDescription
inverterNoneInverts child Success to Failure, and Failure to Success. (Running is untouched).
force_successNoneAlways returns Success regardless of whether the child succeeds or fails.
force_failNoneAlways returns Failure regardless of child outcome.
repeat(N)count: number0 (Infinite)Repeats execution of the child $N$ times. If count is 0, repeats infinitely.
retry(N)attempts: number0 (Infinite)Re-ticks the child up to $N$ times if it returns Failure. If attempts is 0, retries indefinitely.
timeout(ms)limit: number1000Limits child execution time in milliseconds. Halts child and returns Failure if exceeded.
delay(ms)wait: number0Delays the initial execution of the child for the specified duration in milliseconds.

Placement of Decorators

Decorators in Forester can be placed in two main ways:

1. In Tree / Root Definitions (Header Decorators)

Decorators can be placed directly in the declaration header of a root node or sub-tree definition:

// Root node wrapped in a repeat decorator
root main_fixed repeat(5) {
    execute_cycle()
}

// Sub-tree definition wrapped with a retry decorator
sequence retryable_task retry(3) {
    perform_step()
}

2. At Invocation Sites

Decorators can be placed directly before an action invocation, sub-tree call, or inline lambda:

root main sequence {
    // Decorator on an action invocation
    retry(3) perform_task()
    
    // Decorator on a sequence block
    timeout(1000) sequence {
        fetch_data()
    }
    
    // Decorator on an inline lambda
    retry(5) lambda sequence {
        fetch_sensor_data()
        validate_reading()
    }
}

Detailed Examples

1. Inverter (inverter)

Inverts the result of condition checks or actions:

import "std::actions"

root main sequence {
    // Succeeds if obstacle_detected() fails
    inverter obstacle_detected()
    move_forward()
}

impl obstacle_detected();
impl move_forward();

2. Result Overrides (force_success & force_fail)

Enforce specific outcome states regardless of child execution:

root main sequence {
    // Attempt cleanup; continue even if cleanup returns Failure
    force_success cleanup_temp_files()
    proceed_main_task()
}

impl cleanup_temp_files();
impl proceed_main_task();

3. Repeat (repeat)

Executes a child multiple times (or infinitely for continuous control loops):

// Infinite execution loop
root main_idle repeat {
    background_health_check()
}

// Fixed 5-cycle loop
root main_fixed repeat(5) {
    execute_cycle()
}

impl background_health_check();
impl execute_cycle();

4. Retry (retry)

Automatically retries failing actions (ideal for network calls or LLM tool invocations):

root main sequence {
    // Retry up to 5 times if call_llm returns Failure
    retry(5) call_llm({"prompt": "analyze_image"})
}

impl call_llm(params: object);

5. Timeout (timeout) & Delay (delay)

Controls timing for async actions:

root main sequence {
    // Wait 500ms before starting initial tick
    delay(500) initialize_sensors()
    
    // Shut down async action if it runs longer than 3000ms
    timeout(3000) fetch_remote_data()
}

impl initialize_sensors();
impl fetch_remote_data();

Actions (impl)

Actions form the leaf nodes of a behavior tree. They represent the actual work performed by external code—such as controlling hardware motors, querying databases, making REST calls, or invoking LLM tools.

In the Forester DSL, actions are declared using the impl keyword.


Action Signatures (impl)

An action contract defines the action name, input parameters, and expected parameter types:

// Action signature declarations
impl navigate_to_pose(target: object);
impl check_battery_level(min_voltage: number);
impl call_llm_tool(prompt: string, model: string);
impl reset_system_state(){}
  • Semicolons: A trailing semicolon ; or empty block {} is required after an impl declaration.

Action Execution Modes

The Forester runtime supports two primary execution mechanisms:

1. Local Actions (Rust Engine)

Implemented directly in Rust using the Action trait and registered with ForesterBuilder:

#![allow(unused)]
fn main() {
use forester_rs::runtime::action::{Action, ActionArgs, TickResult};

struct NavigateToPose;

impl Action for NavigateToPose {
    fn tick(&self, args: ActionArgs) -> TickResult {
        // Perform navigation logic
        TickResult::Success
    }
}
}
  • Synchronous Actions: Block until execution completes during the current tick.
  • Asynchronous Actions: Return TickResult::Running immediately for non-blocking operations, completing on subsequent ticks.

2. Remote Actions (Python / Microservices)

Actions executed in external processes (e.g. Python AI agents using forester-http-ra-py). The Forester engine sends an RPC/HTTP request to the remote action server and handles the response.


Strict Contract Enforcement

The Forester compiler enforces strict type checking and argument count matching between impl signatures and invocation sites:

impl calculate_route(destination: object, max_speed: number);

root main sequence {
    // ❌ COMPILE ERROR: Missing required 'max_speed' argument
    calculate_route(destination = {"x": 10, "y": 20})
}

Passing Blackboard References (&pointer)

Actions can accept live Blackboard references using the & pointer prefix. This allows nodes to read dynamically updated state without hardcoding values at definition time:

impl analyze_sensor_data(data_ref: string);

root main sequence {
    // Pass live Blackboard reference &sensor_reading
    analyze_sensor_data(data_ref = &sensor_reading)
}

Built-In Standard Library Actions (std::actions)

Forester provides a built-in standard library of common utility actions, Blackboard operations, and state helpers.

To use standard library actions, import std::actions at the top of your .tree file:

import "std::actions"

root main sequence {
    store("session_id", "12345")
    equal("session_id", "12345")
}

Selective imports with aliasing can also be used:

import "std::actions" {
    store => set_blackboard_key,
    fail => throw_failure,
}

Standard Action Reference

1. Terminal / Flow Control Actions

ActionArgumentsReturn StateDescription
success()NoneSuccessInstantly returns Success. Useful as stub or default branch.
fail(reason)reason: stringFailureInstantly fails execution with an explicit error reason.
fail_empty()NoneFailureInstantly fails execution without a reason message.
running()NoneRunningReturns Running. Keeps node active on subsequent ticks.
sleep(duration)duration: numberSuccessNon-blocking sleep for $N$ milliseconds before returning Success.

2. Blackboard Memory Actions

ActionArgumentsDescription
store(key, value)key: string, value: stringStores a value into the Blackboard under key.
equal(key, expected)key: string, expected: stringCompares Blackboard value under key to expected. Returns Success if equal, Failure otherwise.
store_tick(key)key: stringStores the current engine tick number into key.
lock(key)key: stringLocks a Blackboard key to prevent modifications by other nodes.
unlock(key)key: stringUnlocks a previously locked Blackboard key.

3. I/O & Network Actions

ActionArgumentsDescription
http_get(url, bb_key)url: string, bb_key: stringPerforms an HTTP GET request to url and stores the response payload into bb_key.

Code Example

import "std::actions"

root main sequence {
    // 1. Initialize Blackboard state
    store("agent_status", "initializing")
    
    // 2. Perform work
    fallback {
        http_get("https://api.example.com/health", "health_response")
        fail("Health check API unreachable")
    }
    
    // 3. Verify status
    equal("agent_status", "initializing")
    
    // 4. Update status to active
    store("agent_status", "active")
}

Invocations & Higher-Order Composition

In Forester, an Invocation calls a declared sub-tree definition, built-in decorator, action, or inline lambda within another tree definition.

Invocations are the primary mechanism for composing complex behavior trees out of smaller, modular building blocks.


Basic Invocation Syntax

Invocations support both positional and named argument syntax:

import "std::actions"

// Sub-tree definition with parameters
sequence check_distance(item: object, threshold: number) {
    store("target_item", item)
    handle_distance(item, threshold)
}

root main sequence {
    // Positional argument invocation
    check_distance({"x": 10, "y": 20}, 50)
    
    // Named argument invocation
    check_distance(
        item = {"x": 30, "y": 40},
        threshold = 100
    )
}

impl handle_distance(item: object, limit: number);

Functional Composition Capabilities

Forester extends standard behavior tree semantics by offering advanced functional composition primitives:

1. Higher-Order Trees (HOT)

Pass sub-trees as parameters (t: tree) to higher-order tree definitions and delegate execution using the t(..) syntax. This allows developers to write generic retry, fallback, or logging wrappers once and reuse them across the codebase.

Read more in Higher-Order Trees.


2. Lambdas (Anonymous Inline Sub-Trees)

Define and instantly invoke inline, anonymous sub-trees (lambda sequence { ... }) without creating throwaway named definitions.

Read more in Lambdas.


Summary of Invocation Types

Invocation TypeExample SyntaxDescription
Standard Sub-Treenavigate_to(target)Invokes a named sub-tree definition.
Action Invocationcall_llm(prompt)Invokes an external impl action.
Delegated Tree (HOT)sub_action(..)Invokes a sub-tree passed as a parameter.
Inline Lambdalambda sequence { ... }Creates and executes an anonymous sub-tree inline.

Higher-Order Trees (HOT)

A Higher-Order Tree is a tree definition that accepts other sub-trees as typed parameters (param: tree) and delegates execution to them using the param(..) syntax.

This is Forester’s most powerful functional abstraction. It eliminates repetitive copy-pasted retry/fallback/logging patterns by letting you write a pattern once and inject the varying behavior as a parameter.


Motivation

Consider a common robotics pattern: check a precondition, and if it fails, run a corrective task. Without HOTs you repeat this fallback structure for every step:

sequence handle(item: object) {
    fallback { close_enough(item)  approach(item) }
    fallback { is_graspable(item)  grasp(item) }
    fallback { enough_space(item)  sequence { move(item) save(item) } }
}

With a Higher-Order Tree you name the pattern once and inject the steps:

// Define the pattern once
fallback precond_or_fix(condition: tree, fix: tree) {
    condition(..)
    fix(..)
}

// Reuse it without duplication
sequence handle(item: object) {
    precond_or_fix(close_enough(item),  approach(item))
    precond_or_fix(is_graspable(item),  grasp(item))
    precond_or_fix(enough_space(item),  sequence { move(item) save(item) })
}

Syntax

Declare a tree-typed parameter in the definition signature. Invoke it inside the body using param_name(..):

// Generic retry-with-fallback wrapper
fallback retryer(action: tree, on_fail: tree) {
    retry(3) action(..)
    on_fail(..)
}

root main sequence {
    // Pass any tree as action or on_fail
    retryer(
        call_llm({"model": "gpt-4o"}),
        alert_operator()
    )
}

impl call_llm(config: object);
impl alert_operator();

Lambdas as HOT Arguments

Inline lambdas can be passed directly as tree arguments, avoiding throwaway named definitions:

root main sequence {
    retryer(
        lambda sequence {
            fetch_data()
            validate_data()
        },
        notify_failure()
    )
}

Parameter Scoping Rules

Forester does not perform closure-style parameter capturing. The semantics are:

Argument typeResolved when?
Static constants (strings, numbers, objects)Captured at definition site and passed as-is
Blackboard pointers (&key)Resolved at the moment of invocation

This means Blackboard pointer arguments always reflect the live state of the Blackboard at invocation time, not at the point where the HOT definition was written.


Common Reusable Patterns

// Retry-or-notify
fallback retry_or_notify(action: tree, notify: tree) {
    retry(3) action(..)
    notify(..)
}

// Timed action with fallback
fallback timed(action: tree, limit_ms: number, on_timeout: tree) {
    timeout(limit_ms) action(..)
    on_timeout(..)
}

// Logged execution
sequence logged(label: string, action: tree) {
    log(label)
    action(..)
}

impl log(message: string);

Lambdas (Anonymous Inline Sub-Trees)

A Lambda is an anonymous, inline sub-tree that is defined and executed at the point of invocation — no named definition required.

Lambdas are ideal for one-off logic that is too simple to justify a named definition, or for passing inline behavior directly as a Higher-Order Tree argument.


Key Properties

  • No name: Lambdas are anonymous — they cannot be referenced from elsewhere.
  • No parameters: Lambdas do not accept arguments. They operate on Blackboard state directly.
  • Flow nodes only: Lambdas can only contain flow control nodes (sequence, fallback, parallel, decorators) and action invocations. Action signatures must still be declared with impl elsewhere.
  • Unique instances: Each lambda definition creates a distinct node in the compiled tree.

Basic Syntax

Any inline sequence, fallback, or parallel block without a name is a lambda:

import "std::actions"

impl job();

root main sequence {
    // Lambda: unnamed inline sequence
    sequence {
        job()
        job()
        job()
    }
    
    // Lambda: unnamed fallback with nested lambdas
    fallback {
        sequence {
            job()
            job()
        }
        // Decorator on a single-child lambda (brackets omitted)
        retry(3) job()
    }
}

Lambdas as Higher-Order Tree Arguments

Lambdas can be passed directly as tree-typed arguments to Higher-Order Tree definitions. This avoids the need to create throwaway named definitions just to pass a block of logic:

impl savepoint();
impl fetch_data();
impl validate();
impl store_result();

sequence bookmarked(action: tree) {
    savepoint()
    action(..)
    savepoint()
}

root main sequence {
    // Pass an inline lambda as the 'action' argument
    bookmarked(
        sequence {
            fetch_data()
            validate()
            store_result()
        }
    )
    
    // Named argument syntax
    bookmarked(
        action = fallback {
            fetch_data()
            store_result()
        }
    )
}

When to Use a Lambda vs. a Named Definition

SituationRecommendation
One-off logic used in a single placeLambda
Logic reused in 2+ placesNamed sub-tree definition
Passed as a HOT argument inlineLambda
Needs its own parametersNamed sub-tree definition

Parameters

Terminology

  • Parameters are elements of tree definitions.
  • Arguments are elements of tree invocations.
// parameters 'a' and 'b'
sequence tree(a:string,b:num){
    // arguments 'c', 'd'
    job(c = 1, d = "d")
}

Arguments

Therefore, the arguments represent the attachments of the real value to the parameters. The Argument can be one of two types:

  • Named argument
  • Unnamed argument
impl action(a:string, b:num)

root main sequnce {
    // Named Arguments
    action(a="a",b:1)
    
    // Unnamed Arguments
    action("a",1)
}

There is impossible to mix named and unnamed arguments The following code will have an error during the compilation process.

impl action(a:string, b:num)
root main sequnce {
    action("a",b=1 )
}

Types

Number

The numbers are defined with a keyword num There are 4 possible types of numbers presented:

  • Integers(64)
  • Floats(64)
  • Hex
  • Binary

In case of exceeding the maximum value, the error will be raised on the compile time.

impl action(param:num)

root main sequence {
    // Integers
    action(1)
    action(10e2)
    action(-1)
    action(0)
    
    // Floats
    action(0.0)
    action(100.0e1)
    action(-100.0)
    
    // Hex
    action(0x123)
    
    // Binary
    action(0b010101)
}

String

The strings are defined with string

impl action(param:string)
root main action(param = "X")

Boolean

The booleans are defined with a keyword bool and has the following parameters:

  • true for the positive statement
  • false for the negative statement
impl action(param:bool);
root main action(true)

Arrays

The arrays are defined with keyword array Arrays can have several aforementioned elements encompassed in one entity.

The arrays have the following syntax:

  • [ defines the start of array
  • ] defines the end of array
  • , defines the separator between elements
  • the rest is defined by the particular elements

It is expected, the arrays are homogeneous and have all elements only one type

The arrays can have a trailing comma as well, [1,]

impl action(elems:array);
root main sequence {
    action([1,2,3,4])
    action([1.1,0.1])
    action(["a","b"])
}

Objects

The objects are defined with keyword object Objects can have several aforementioned elements encompassed in one entity with the unique key attached to the every entity

The objects have the following syntax:

  • { defines the start of object
  • } defines the end of object
  • , defines the separator between elements
  • “key” defines the name of the element key
  • the rest is defined by the particular elements

The objects can have a trailing comma as well, {"a":1,}

impl action(elems:object);
root main sequence {
    action({"key":1, "key2":"key"})
    action(
        {
            "array": [1,2,3,4,],
            "string":"string",
            "num":1,
            "pointer": pointer
        }
    )
}

Tree

The other tree definitions are defined with a keyword tree **The parameters of this type can be added and defined only in the flow definitions.

impl log(id:string,info:string);
cond check();
cond task();

fallback checked_task(check:tree, task:tree){
    check(..)
    task(..)
}

sequence logged_task(id:string, task:tree){
    log(id,"start task")
    task(..)
    log(id,"end task")
}

root main sequence {
    // invoke the task parameter, passing the invokations with parameters 
    logged_task(
        "1",
        // invoke the task, passing the invokations with parameters
        checked_task(check = check(), task())
    )
}

Pointers

Pointers are identifiers of the objects in the BlackBoard Therefore, they can be used to obtain the value of the cell from bb, in argument invoking.

In the example below, the system expects to find a string value in the cell with a name bb_key.

impl action(value:string);

root main sequence {
    // this is a pointer to a cell in bb with an id 'bb_key'
    action(bb_key) 
}

Any

The any type is a special type that can be used to pass any type of value to the parameter.

The type any can take any message type except call

The intention to provide a simple way to generalize the parameters and pass any type of value to the parameter.

// can take anything except call
impl action(value:any);

root main sequence {
    
    action(1) 
    action("a") 
    action([1]) 
}

Antlr grammar

The grammar bears an introducing character (means it is not used straight in the code for now)

Parser

parser grammar TreeParser;

file
    : (definition | importSt)* EOF
    ;

import_name
    : id (EQ_A id)?
    ;

importCalls
    : LBC (import_name (COMMA import_name)* COMMA?)? RBC
    ;

importSt
    : IMPORT string importCalls?
    ;

definition
    : tree_type id params? (calls? | SEMI)
    ;

call
    : invocation
    | lambda
    ;

invocation
    : id (args | LPR DOT_DOT RPR)
    ;


lambda
    : tree_type args? calls
    ;

calls
    : LBC call* RBC
    | call
    ;


arg
    : id (EQ (message | id | call))?
    | message
    | call
    ;

args
    : LPR (arg (COMMA arg)* COMMA?)? RPR
    ;

params
    : LPR (param (COMMA param)*)? COMMA? RPR
    ;

param
    : id COLON mes_type
    ;

message
    : string
    | num
    | bool
    | array
    | object
    ;

mes_type
    : NUM_T
    | ARRAY_T
    | OBJECT_T
    | STRING_T
    | BOOL_T
    | TREE_T
    ;

tree_type
    : ROOT
    | PARALLEL
    | SEQUENCE
    | MSEQUENCE
    | RSEQUENCE
    | FALLBACK
    | RFALLBACK
    | id          // ambigulty
    ;


object
    : LBC (objectPair (COMMA objectPair)* COMMA? )? RBC
    ;

objectPair
    : string COLON message
    ;


array
    : LBR (message (COMMA message)* COMMA? )? RBR
    ;

bool
    : TRUE
    | FALSE
    ;

num
    : NUMBER
    ;

string
    : STRING
    ;
id
    : ID
    ;

Lexer

lexer grammar TreeLexer;

ROOT: 'ROOT';
PARALLEL : 'parallel';

SEQUENCE : 'sequence';
MSEQUENCE : 'm_sequence';
RSEQUENCE : 'r_sequence';

FALLBACK: 'fallback';
RFALLBACK : 'r_fallback';

ARRAY_T: 'array';
NUM_T: 'num';
OBJECT_T: 'object';
STRING_T: 'string';
BOOL_T: 'bool';
TREE_T: 'tree';
IMPORT: 'import';

ID : [-_a-zA-Z]+ (INT | [-_a-zA-Z]+)*  ;

COMMA : ',';
COLON : ':';
SEMI : ';';
DOT_DOT : '..';

EQ  : '=';
EQ_A  : '=>';

LPR  : '(';
RPR  : ')';

LBC  : '{';
RBC  : '}';

LBR  : '[';
RBR  : ']';

TRUE : 'TRUE';

FALSE : 'FALSE';

STRING  : '"' (ESC | SAFECODEPOINT)* '"' ;

NUMBER  : '-'? INT ('.' [0-9] +)? EXP? ;

Whitespace: [ \t]+ -> skip ;

Newline :   (   '\r' '\n'? | '\n') -> skip ;

BlockComment :   '/*' .*? '*/' -> skip ;

LineComment :   '//' ~[\r\n]* -> skip ;

fragment ESC : '\\' (["\\/bfnrt] | UNICODE) ;

fragment UNICODE : 'u' HEX HEX HEX HEX ;
fragment HEX : [0-9a-fA-F] ;

fragment SAFECODEPOINT : ~ ["\\\u0000-\u001F] ;
fragment INT : '0' | [1-9] [0-9]* ;
fragment EXP : [Ee] [+\-]? [0-9]+ ;

Runtime Engine Overview

The Forester runtime is the execution core that compiles .tree files, builds the behavior tree node graph, and drives the tick-by-tick evaluation loop.

It is built in Rust and is designed for both high-throughput robotics control loops and AI agent orchestration workflows.


Core Subsystems

ComponentDescription
Engine (Forester)Orchestrates the tick loop, manages node state transitions, and schedules sync/async action execution.
BlackboardIn-memory shared key-value store for passing state between tree nodes across ticks.
ActionKeeperRegistry that maps action names declared with impl in .tree files to Rust handlers or remote action clients.

Execution Model

  • Synchronous by default: The engine drives a deterministic tick loop. Synchronous actions block until they return Success, Failure, or Running.
  • Async environment: Async actions return Running immediately and are polled on subsequent ticks without blocking the tick loop.
  • Parallelism: parallel nodes tick multiple children in a single pass; async actions allow true concurrent I/O within a tick cycle.

Entry Point: ForesterBuilder

The ForesterBuilder is the primary API for constructing and configuring a Forester runtime instance. It provides a safe, fluent interface for:

  • Pointing to .tree source files or inline scripts
  • Registering action implementations
  • Pre-loading Blackboard state from JSON
  • Enabling execution tracing
use forester_rs::runtime::builder::ForesterBuilder;
use forester_rs::runtime::action::{Action, ActionArgs, TickResult};
use forester_rs::runtime::action::builtin::data::StoreData;
use forester_rs::tracer::Tracer;

fn main() {
    let mut fb = ForesterBuilder::from_file_system();
    
    // Point to the tree entry file
    fb.main_file("main.tree".to_string());
    
    // Register built-in and custom actions
    fb.register_action("store", Action::sync(StoreData));
    fb.register_sync_action("my_action", MyAction);
    
    // Enable execution tracing
    fb.tracer(Tracer::default());
    
    // Pre-load Blackboard initial state from JSON
    fb.bb_load("db/initial_state.json".to_string());
    
    // Build and run
    let mut forester = fb.build().unwrap();
    let result = forester.run().unwrap();
    
    println!("Execution result: {:?}", result);
}

Detailed Subsystem Documentation

  • Engine internals — tick loop, node lifecycle, and state machine details
  • Runtime arguments — configuring engine behavior at startup
  • Blackboard — shared memory API and locking semantics
  • Actions — registering sync, async, and remote actions
  • Trimming — modifying the running tree on the fly
  • Daemons — background tasks running alongside the tree

Engine Internals

The Forester engine is the central execution runtime. It drives the tick loop, manages node state, dispatches actions, and coordinates the Blackboard, ActionKeeper, and optional tracer.


Internal Components

ComponentDescription
Engine CoreManages the tree node graph, drives the tick loop, and handles state transitions (Running, Success, Failure).
BlackboardIn-memory shared key-value store for passing state between nodes.
ActionKeeperRegistry mapping impl action names to Rust handlers or remote action clients.
TracerOptional execution logger that records tick-by-tick node evaluations for debugging and replay.

Tick Loop

On each tick, the engine traverses the tree from the root node down, evaluating each node according to its type (sequence, fallback, parallel, decorator, or action). The tick loop continues until the root node returns Success or Failure.

Bounded Execution

To prevent infinite loops and enable time-boxed execution (useful for simulation or testing), the tick count can be capped:

#![allow(unused)]
fn main() {
// Run for a maximum of 100 ticks
forester.run_until(Some(100)).unwrap();

// Run until the root returns Success or Failure (unbounded)
forester.run().unwrap();
}

Runtime Environment

The engine uses Tokio as its async runtime to schedule and parallelize async action execution.

By default, the engine creates its own Tokio runtime. If the engine is embedded in an application that already has a Tokio runtime, it can be provided via ForesterBuilder:

use forester_rs::runtime::builder::ForesterBuilder;
use forester_rs::tracer::Tracer;
use forester_rs::runtime::action::Action;
use forester_rs::runtime::action::builtin::data::StoreData;

fn main() {
    let mut fb = ForesterBuilder::from_file_system();
    fb.main_file("main.tree".to_string());
    fb.register_action("store", Action::sync(StoreData));
    fb.tracer(Tracer::default());
    fb.bb_load("db/initial_state.json".to_string());

    let mut forester = fb.build().unwrap();

    // Unbounded run
    let result = forester.run().unwrap();
    println!("Result: {:?}", result);
}

Built-In HTTP Server

The engine optionally exposes an HTTP server during tree execution. This enables external processes — including remote action servers and monitoring tools — to interact with the Blackboard and tracer at runtime.

Enable it by specifying a port in ForesterBuilder:

#![allow(unused)]
fn main() {
fb.http_serv(10000);
}

Note: The HTTP server shuts down automatically when the root tree finishes execution.

HTTP API Endpoints

MethodEndpointDescription
GET/Health check. Returns Ok.
GET/tracer/printPrints current tracer output.
POST/tracer/customAppends a custom event to the tracer log (body: CustomEvent JSON).
GET/bb/:keyReads value at key from the Blackboard.
POST/bb/:keyWrites a value to key in the Blackboard (body: RtValue JSON).
GET/bb/:key/takeReads and removes key from the Blackboard.
GET/bb/:key/lockLocks key to prevent concurrent writes.
GET/bb/:key/unlockUnlocks key.
GET/bb/:key/lockedReturns whether key is currently locked.
GET/bb/:key/containsReturns whether key exists in the Blackboard.

Runtime Arguments (RtValue)

When a behavior tree action is ticked, its parameters are passed to the Rust handler as runtime argument values (RtValue). These are the runtime representations of the typed static arguments declared in the .tree DSL.


The RtValue Enum

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum RtValue {
    String(String),
    Bool(bool),
    Number(RtValueNumber),
    Array(Vec<RtValue>),
    Object(HashMap<String, RtValue>),
    Pointer(BBKey),
}
}

Primitive Types

  • String, Bool, Number: Direct value types, equivalent to their counterparts in standard languages.

Complex Types

  • Array: An ordered list of RtValue elements.
  • Object: A JSON-style key-value map (HashMap<String, RtValue>), useful for structured action configuration.

Pointer (Blackboard Reference)

  • Pointer(BBKey): A live reference to a Blackboard key. When resolved, the action fetches the current value stored at that Blackboard cell rather than using a static literal.

Pointer Resolution

A Pointer allows passing dynamic, live Blackboard state into an action without hardcoding the value at call time.

import "std::actions"

root main r_sequence {
    // Stores the current tick count into bb["tick"]
    store_tick("tick")
    
    // equal() receives a Pointer to bb["tick"], not the literal string "tick"
    r_fallback {
        equal(tick, 10)
        running()
    }
}

Note: store_tick("tick") receives "tick" as a String (the cell name to write into). In contrast, equal(tick, 10) receives tick as a Pointer (meaning: resolve the value stored at Blackboard key "tick" and compare it to 10).

Pointer Indirection Example

Pointers can also point to cells that themselves contain key names (double indirection):

import "std::actions"

root main sequence {
    store("x", "tick")      // bb["x"] = "tick"
    store_tick(x)           // x is a Pointer -> writes to bb["x"] which is tick and resolves to bb["tick"]
    equal(tick, 10)         // tick is a Pointer -> reads bb["tick"] and compares to 10
}

Extracting Argument Values in Rust

Forester provides two methods for reading action arguments in Rust action implementations:

1. Direct Cast: as_<type>()

The fastest approach — directly converts the RtValue to a primitive or complex type. Does not resolve Pointers, so use only when you are certain no Blackboard references will be passed:

#![allow(unused)]
fn main() {
fn handle(v: RtValue) {
    let val: Option<String> = v.as_string();
    let num: Option<f64>    = v.as_float();
}
}

2. Context-Aware Cast: cast(ctx)

Resolves Pointers by looking up the referenced Blackboard key at invocation time. Use this when your action may receive either a literal or a Blackboard reference:

#![allow(unused)]
fn main() {
use forester_rs::runtime::action::{Impl, RtArgs, Tick};
use forester_rs::runtime::context::TreeContextRef;
use forester_rs::runtime::RuntimeError;

impl Impl for CheckEqual {
    fn tick(&self, args: RtArgs, ctx: TreeContextRef) -> Tick {
        let key = args
            .find_or_ith("key".to_string(), 0)
            .ok_or(RuntimeError::fail("the 'key' argument is required".to_string()))?;
        
        // cast(ctx) resolves Pointer values against the live Blackboard
        let resolved: String = key.cast(ctx.clone()).str()?;
        
        Ok(TickResult::success())
    }
}
}

Recommendation: Prefer cast(ctx) in most action implementations. It handles both literal values and Blackboard pointers transparently.

Blackboard

The Blackboard is Forester’s shared in-memory state store — a key-value store that nodes read from and write to across ticks. It is the primary mechanism for passing data between actions and tree nodes without tight coupling.

In AI agent workflows, the Blackboard acts as the agent’s working context: LLM responses, tool call results, and session state are written here and read by downstream nodes.


Data Format

The Blackboard stores pairs of String keys and BBValue values:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub enum BBValue {
    Locked(RtValue),    // Key exists and is locked: read-only, no further writes
    Unlocked(RtValue),  // Key exists and is freely readable and writable
    Taken,              // Key slot exists but value has been consumed (taken)
}
}

Key States

StateDescription
Unlocked(RtValue)Normal state. The value can be read, overwritten, or taken by any node.
Locked(RtValue)The value is protected from modification. Other nodes can still read it, but cannot overwrite or take it.
TakenThe key slot exists but its value has been consumed by a take operation.

Accessing the Blackboard from DSL

Use the built-in std::actions to interact with the Blackboard directly from .tree files:

import "std::actions"

root main sequence {
    // Write a value
    store("agent_mode", "active")
    
    // Lock a key to prevent modification during critical section
    lock("agent_mode")
    
    // Read and compare
    equal("agent_mode", "active")
    
    // Unlock when done
    unlock("agent_mode")
}

Accessing the Blackboard from Rust

In Rust action implementations, the Blackboard is accessed through the TreeContextRef:

#![allow(unused)]
fn main() {
use forester_rs::runtime::context::TreeContextRef;
use forester_rs::runtime::action::{Impl, RtArgs, Tick, TickResult};

impl Impl for MyAction {
    fn tick(&self, args: RtArgs, ctx: TreeContextRef) -> Tick {
        let mut ctx = ctx.lock()?;
        
        // Read a value from the Blackboard
        let mode = ctx.bb().get("agent_mode")?.and_then(|v| v.as_string());
        
        // Write a value to the Blackboard
        ctx.bb().put("result", RtValue::String("done".to_string()))?;
        
        Ok(TickResult::success())
    }
}
}

Persistence: Load & Dump

The Blackboard supports JSON serialization for pre-loading initial state and dumping snapshots:

MethodDescription
bb_load(path)Loads initial Blackboard state from a JSON file at engine startup (via ForesterBuilder).
dump(path)Saves a snapshot of the full Blackboard to a JSON file.
print_dump()Prints the current Blackboard snapshot to stdout in JSON format.
text_dump()Returns the Blackboard snapshot as a JSON string.
#![allow(unused)]
fn main() {
// Pre-load state at startup
fb.bb_load("db/initial_state.json".to_string());
}

HTTP Access

When the engine’s built-in HTTP server is enabled, the Blackboard is also accessible over HTTP at runtime. See the Engine HTTP API for endpoint details.


Utilities

A set of helper utilities for common Blackboard operations is available in the blackboard::utils module, for example:

  • blackboard::utils::push_to_arr(key, value, ctx) — Appends a value to an array stored at a given key.

Runtime Actions

Actions are the leaf nodes of the behavior tree — the points where actual work gets done. Every action declared with impl in a .tree file must be bound to a Rust handler (or a remote client) via ForesterBuilder before the tree can run.


Action Types

Forester supports three execution modes for actions:

TypeBlocking?Use Case
SyncYes — blocks the tick until doneFast, CPU-bound, or deterministic operations
AsyncNo — returns Running immediatelyI/O-bound operations, LLM calls, network requests
RemoteYes — sends an HTTP request and waitsExternal processes (Python agents, microservices)

For heavy or slow operations, prefer async actions to avoid blocking the tick loop.


Action Traits

1. Sync Actions (Impl)

Sync actions block the tick loop until they return a result. They are the only action type that currently supports the halt() callback:

#![allow(unused)]
fn main() {
use forester_rs::runtime::action::{RtArgs, Tick, RtOk};
use forester_rs::runtime::context::TreeContextRef;

pub trait Impl {
    fn tick(&self, args: RtArgs, ctx: TreeContextRef) -> Tick;

    // Called when a reactive flow node (r_sequence, r_fallback) preempts this action.
    // Implement this to clean up resources gracefully.
    // Default is a no-op.
    fn halt(&self, args: RtArgs, ctx: TreeContextRef) -> RtOk {
        Ok(())
    }
}
}

Important: halt() must return as quickly as possible — it must not block execution.


2. Async Actions (ImplAsync)

Async actions are spawned in a separate Tokio task and return Running on the first tick. The engine re-polls them on subsequent ticks until they resolve:

#![allow(unused)]
fn main() {
use forester_rs::runtime::action::{RtArgs, Tick};
use forester_rs::runtime::context::TreeContextRef;

pub trait ImplAsync: Sync + Send {
    fn tick(&self, args: RtArgs, ctx: TreeContextRef) -> Tick;
}
}

Note: When the engine runs with a tick limit (run_until(Some(N))), async actions consume ticks while Running. Account for this when setting tick budgets.


3. Remote Actions (ImplRemote)

Remote actions send an HTTP POST request to an external server and wait for the response. This is the mechanism for Python AI tool integration via forester-http-ra-py.

#![allow(unused)]
fn main() {
pub trait ImplRemote: Sync + Send {
    fn tick(&self, args: RtArgs, ctx: TreeRemoteContextRef) -> Tick;
}

pub struct TreeRemoteContextRef<'a> {
    pub curr_ts: Timestamp, // Current tick timestamp
    pub port: u16,          // Port of the engine HTTP server (for Blackboard access)
    pub env: &'a mut RtEnv, // Runtime env for making HTTP requests
}
}

The default remote action implementation (RemoteHttpAction) handles the HTTP protocol automatically:

#![allow(unused)]
fn main() {
use forester_rs::runtime::action::builtin::remote::RemoteHttpAction;

// Register a remote action pointing to an external Python action server
fb.register_remote_action(
    "call_llm_tool",
    RemoteHttpAction::new("http://localhost:9000/call_llm", None)
);
}

The engine sends each tick as a RemoteActionRequest:

#![allow(unused)]
fn main() {
pub struct RemoteActionRequest {
    pub tick: usize,           // Current tick number
    pub args: Vec<RtArgument>, // Action arguments from the tree
    pub serv_url: String,      // Engine HTTP server URL (for Blackboard access)
}
}

The remote server responds with a TickResult (Success, Failure, or Running).

See Remote Action Clients for how to implement the server side in Python or Rust.


Registering Actions

Register sync, async, and remote actions with ForesterBuilder before running the tree:

#![allow(unused)]
fn main() {
use forester_rs::runtime::builder::ForesterBuilder;
use forester_rs::runtime::action::Action;
use forester_rs::runtime::action::builtin::data::StoreData;
use forester_rs::runtime::action::builtin::remote::RemoteHttpAction;

let mut fb = ForesterBuilder::from_file_system();
fb.main_file("main.tree".to_string());

// Sync action (built-in)
fb.register_action("store", Action::sync(StoreData));

// Custom sync action
fb.register_sync_action("my_sensor_check", MySensorCheck);

// Async action
fb.register_async_action("fetch_data", FetchData);

// Remote action (Python server)
fb.register_remote_action(
    "call_llm",
    RemoteHttpAction::new("http://localhost:9000/call_llm", None)
);
}

Action Statefulness

Actions are intentionally stateless — they cannot hold mutable state between ticks. Persist any inter-tick state on the Blackboard instead:

#![allow(unused)]
fn main() {
impl Impl for CounterAction {
    fn tick(&self, args: RtArgs, ctx: TreeContextRef) -> Tick {
        let mut ctx = ctx.lock()?;
        
        // Read current count from Blackboard
        let count: i64 = ctx.bb().get("count")
            .and_then(|v| v.as_int())
            .unwrap_or(0);
        
        // Write incremented count back to Blackboard
        ctx.bb().put("count", RtValue::Number(RtValueNumber::Int(count + 1)))?;
        
        Ok(TickResult::success())
    }
}
}

Built-In Actions

A set of ready-made action implementations for Blackboard operations and HTTP requests is available in:

#![allow(unused)]
fn main() {
use forester_rs::runtime::action::builtin::*;
}

Trimming (Live Tree Modification)

Trimming is Forester’s mechanism for modifying a running behavior tree at runtime — replacing, removing, or substituting nodes in the live execution graph without stopping the engine.


Use Cases

Performance & JIT Optimization

Cache or fold static subtrees that have already completed, reducing redundant evaluation on subsequent ticks. Analogous to JIT compiler transformations in virtual machines.

Adaptive Logic

Dynamically swap decision branches based on runtime signals — Blackboard state, sensor readings, or LLM responses. Enables online reinforcement learning workflows where policy branches update as the tree runs.

Research & Experimentation

Replace specific nodes mid-execution to compare behavioral outcomes under the same environmental conditions without restarting the engine.


Core Components

1. TrimTask

A TrimTask encapsulates a single runtime modification. When registered with the engine, it is invoked on each tick with a snapshot of the current tree state. The task decides whether to apply, skip, or reject itself:

#![allow(unused)]
fn main() {
pub enum TrimTask {
    RtTree(Box<dyn RtTreeTrimTask>),
}
}

Register a trim task before running the tree:

#![allow(unused)]
fn main() {
forester.add_trim_task(TrimTask::rt_tree(MyTrimTask));
forester.run_until(Some(100)).unwrap();
}

2. TrimRequest — Task Decision States

Each invocation of RtTreeTrimTask::process() returns a TrimRequest indicating how the engine should proceed:

#![allow(unused)]
fn main() {
pub enum TrimRequest {
    Reject, // Permanently cancel this task (another task already made the change,
            // or the tree is no longer a valid target)
    Skip,   // Defer to the next tick (conditions not yet met)
    Attempt(RequestBody), // Proceed with the modification
}
}
StateDescription
RejectPermanently cancels this task. Use when the modification is no longer valid or applicable.
SkipDefers the task to the next tick. Use when waiting for a specific Blackboard value, tick number, or tree state.
Attempt(body)Submits the modification for validation and application by the engine.

3. Validation

Before applying a TrimRequest::Attempt, the engine validates that the nodes targeted for replacement are not currently in a Running state. If they are, the attempt is automatically deferred.


Constraints & Best Practices

No ordering guarantees: There is no guarantee of the order in which multiple trim tasks execute, or the exact tick at which any task will be applied.

  • Design trim tasks to be idempotent — safe to apply multiple times without side effects.
  • Always validate the incoming tree snapshot inside process() before constructing the RequestBody.
  • Use TrimRequest::Reject when another task has already made the intended change.

Example: Replace a Node After Tick 90

#![allow(unused)]
fn main() {
use forester_rs::runtime::trimmer::task::{RtTreeTrimTask, TrimTask};
use forester_rs::runtime::trimmer::{RequestBody, TreeSnapshot, TrimRequest};
use forester_rs::runtime::rtree::builder::{RtNodeBuilder, RtTreeBuilder};
use forester_rs::runtime::rtree::rnode::RNodeName;
use forester_rs::runtime::args::RtArgs;
use forester_rs::runtime::RtResult;

struct ReplaceFailWithSuccess;

impl RtTreeTrimTask for ReplaceFailWithSuccess {
    fn process(&self, snapshot: TreeSnapshot<'_>) -> RtResult<TrimRequest> {
        // Wait until tick 90 before applying this modification
        if snapshot.tick < 90 {
            return Ok(TrimRequest::Skip);
        }

        let tree = snapshot.tree;

        // Find the node named "fail_empty" in the running tree
        let target_id = tree
            .nodes
            .iter()
            .find(|(_, node)| {
                node.name()
                    .and_then(|n| n.name().ok())
                    .filter(|n| n.as_str() == "fail_empty")
                    .is_some()
            })
            .map(|(id, _)| id)
            .unwrap();

        // Build a replacement node: swap fail_empty -> success()
        let mut builder = RtTreeBuilder::new_from(tree.max_id() + 1);
        builder.set_as_root(action!(node_name!("success")), target_id.clone());

        Ok(TrimRequest::attempt(RequestBody::new(
            builder,
            Default::default(),
        )))
    }
}

fn run(mut forester: Forester) {
    forester.add_trim_task(TrimTask::rt_tree(ReplaceFailWithSuccess));
    let result = forester.run_until(Some(100)).unwrap();
    println!("Result: {}", result);
}
}

Daemons (Background Processes)

Daemons are long-running background tasks that execute concurrently alongside the behavior tree. They share the same Tokio runtime environment as the engine and have direct access to the Blackboard.

Common uses:

  • Sensor polling: Continuously reading hardware or network sensor streams and writing values to the Blackboard.
  • Message publishing: Sending telemetry or heartbeat events to external systems.
  • AI context maintenance: Streaming LLM token output or monitoring API rate limits in the background.
  • Watchdogs: Monitoring tree health or enforcing resource constraints.

Performance note: Daemons run in the same async runtime as the tree. Heavy daemon workloads can directly impact tick loop performance. Keep daemon logic lightweight, or offload expensive work to separate processes.


Daemon Types

TypeTraitStop Mechanism
SyncDaemonFnStopFlag — an AtomicBool that flips to true when the engine requests shutdown
AsyncAsyncDaemonFnCancellationToken — a Tokio one-shot cancellation channel

Implementing Daemons

Sync Daemon

Poll the StopFlag in a loop. When it becomes true, the daemon should exit promptly:

#![allow(unused)]
fn main() {
use forester_rs::runtime::env::daemon::{DaemonFn, DaemonContext, StopFlag};
use std::sync::atomic::Ordering::Relaxed;

struct SensorPollerDaemon;

impl DaemonFn for SensorPollerDaemon {
    fn perform(&mut self, ctx: DaemonContext, signal: StopFlag) {
        while !signal.load(Relaxed) {
            std::thread::sleep(std::time::Duration::from_millis(50));
            
            let mut bb = ctx.bb.lock().unwrap();
            let reading = read_sensor(); // your sensor read logic
            bb.put("sensor_value".to_string(), RtValue::int(reading)).unwrap();
        }
    }
}
}

Async Daemon

Use tokio::select! to respond to cancellation alongside your periodic work:

#![allow(unused)]
fn main() {
use forester_rs::runtime::env::daemon::{AsyncDaemonFn, DaemonContext};
use tokio_util::sync::CancellationToken;
use std::pin::Pin;
use std::future::Future;

struct AsyncSensorPollerDaemon;

impl AsyncDaemonFn for AsyncSensorPollerDaemon {
    fn prepare(&mut self, ctx: DaemonContext, signal: CancellationToken) -> Pin<Box<dyn Future<Output = ()> + Send>> {
        Box::pin(async move {
            loop {
                tokio::select! {
                    _ = signal.cancelled() => {
                        // Gracefully shut down
                        return;
                    }
                    _ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
                        let mut bb = ctx.bb.lock().unwrap();
                        let reading = fetch_remote_sensor().await;
                        bb.put("sensor_value".to_string(), RtValue::int(reading)).unwrap();
                    }
                }
            }
        })
    }
}
}

Registering Daemons

At Startup via ForesterBuilder

Register daemons before building the engine. Named daemons can be controlled from within the tree using built-in actions:

#![allow(unused)]
fn main() {
use forester_rs::runtime::env::daemon::Daemon;

// Named daemon (controllable from the tree via stop_daemon / daemon_alive)
fb.register_named_daemon("sensor_poller".to_string(), Daemon::sync(SensorPollerDaemon));

// Anonymous daemon (runs for the lifetime of the engine, no tree control)
fb.register_daemon(Daemon::a_sync(AsyncSensorPollerDaemon));
}

At Runtime from Inside an Action

Daemons can also be started dynamically during tree execution from within a sync action:

#![allow(unused)]
fn main() {
use forester_rs::runtime::action::{Impl, RtArgs, Tick, TickResult};
use forester_rs::runtime::context::TreeContextRef;
use forester_rs::runtime::env::daemon::Daemon;

impl Impl for StartPollerAction {
    fn tick(&self, args: RtArgs, ctx: TreeContextRef) -> Tick {
        let env = ctx.env().lock()?;
        env.start_daemon(Daemon::a_sync(AsyncSensorPollerDaemon), ctx.into());
        Ok(TickResult::success())
    }
}
}

Controlling Daemons from the Tree

Two built-in standard library actions are available to control named daemons from .tree files:

import "std::actions"

root main sequence {
    // Start the main task
    execute_mission()
    
    // Check if background poller is still running
    daemon_alive("sensor_poller")
    
    // Stop the background poller when done
    stop_daemon("sensor_poller")
}
ActionDescription
daemon_alive(name)Returns Success if the named daemon is still running, Failure otherwise.
stop_daemon(name)Sends the stop signal to the named daemon and returns Success.

Analysis & Observability

Forester provides a suite of tools for inspecting, debugging, and validating behavior trees before and during execution. These tools are critical for building confidence in complex orchestration logic before deploying to production hardware or live AI agent workflows.


1. Visualization

Generate a visual diagram of any .tree file’s structure using Graphviz. Visualizing the tree makes it easy to review control flow, catch structural mistakes, and communicate logic to stakeholders.

f-tree vis --input main.tree --output tree_diagram.svg

Output is in SVG format, readable in any browser or vector graphics tool.

See Visualization for full usage.


2. Execution Tracing

Enable step-by-step logging of tick evaluations — recording which nodes were visited, what states they returned, and how Blackboard values changed across ticks.

Tracing is configured in ForesterBuilder:

#![allow(unused)]
fn main() {
use forester_rs::tracer::Tracer;

fb.tracer(Tracer::default());
}

The trace output can be printed to stdout, written to a file, or queried via the engine’s HTTP API at /tracer/print.

See Tracing for full usage.


3. Simulation

Run the behavior tree with stub action implementations instead of real hardware or external API calls. The simulator lets you define per-action responses (Success, Failure, or Running) and observe how the tree navigates them — without any external dependencies.

f-tree sim --profile sim_profile.json

This is especially valuable for:

  • Robotics: Validate control flow before connecting to hardware.
  • AI agents: Test fallback chains and retry logic before incurring LLM API costs.

See Simulation for full usage.


4. ROS Nav2 Export

Export Forester trees to ROS Nav2 XML format for direct use in ROS2 navigation pipelines. This allows Forester-authored trees to be dropped into existing Nav2 workflows as a validated behavior plugin.

See ROS Nav2 Export for full usage.

Visualization

Forester can generate a visual diagram of any behavior tree project, rendering the full node graph as an SVG file. This makes it easy to review control flow, spot structural issues, and communicate tree logic to teammates.


Prerequisites

Visualization uses Graphviz under the hood. Install it before running the vis command:

# macOS
brew install graphviz

# Ubuntu / Debian
sudo apt-get install graphviz

# Windows
winget install graphviz

Example Output


Usage

CLI (f-tree vis)

f-tree vis --root project/ --main main.tree --tree main --output viz.svg
FlagDefaultDescription
--rootCurrent working directoryPath to the project root directory containing .tree files.
--mainmain.treeThe entry .tree file to visualize.
--treeFirst root definition foundName of the specific root tree to visualize (required if the file has multiple roots).
--output<main-filename>.svgOutput SVG file path.

From Rust

Visualization can also be triggered programmatically from Rust using the ForesterBuilder and Visualizer API:

use forester_rs::tracer::Tracer;
use forester_rs::visualizer::Visualizer;
use forester_rs::runtime::builder::ForesterBuilder;
use std::path::PathBuf;

fn main() {
    let mut fb = ForesterBuilder::from_file_system();
    fb.main_file("main.tree".to_string());

    let svg = Visualizer::build_svg(fb).expect("Visualization failed");
    std::fs::write("output.svg", svg).unwrap();
}

Execution Tracing

The Forester tracer records a tick-by-tick log of node evaluations as the behavior tree runs. It captures node IDs, tick results, node state parameters, and any custom messages emitted by actions — making it the primary tool for debugging and post-execution analysis.


Reading Trace Output

Each trace line follows the format:

[tick]  node_id : Status(parameters...)
  • [tick]: The current tick number.
  • Indent: Reflects nesting depth in the tree.
  • node_id: Unique integer ID of the node being evaluated.
  • Status(...): Result state (Running, Success, Failure) with relevant parameters (cursor position, child count, key-value pairs, etc.).

Example Trace

[1]  1 : Running(cursor=0,len=1)
[1]    2 : Running(cursor=0,len=3)
[1]      3 : Success(key=x,value=tick)
[1]    2 : Running(cursor=1,len=3)
[1]      4 : Success(name=tick)
[1]    2 : Running(cursor=2,len=3)
[1]      5 : Running(cursor=0,len=2)
[1]        6 : Success(k=a,i=1)
[1]      5 : Running(cursor=1,len=2)
[1]        7 : Running(cursor=0,len=2)
[1]          8 : Failure(key=x,expected=10,reason=1 != 10)
[1]        7 : Running(cursor=1,len=2)
[1]          9 : Running()
[2]  next tick
[2]    2 : Running(cursor=0,len=3)
[2]      3 : Success(key=x,value=tick)
...

Enabling the Tracer

Enable tracing in ForesterBuilder before running the engine:

#![allow(unused)]
fn main() {
use forester_rs::tracer::{Tracer, TracerConfiguration};
use forester_rs::runtime::builder::ForesterBuilder;

let mut fb = ForesterBuilder::from_file_system();
fb.main_file("main.tree".to_string());

// Default tracer (stdout output)
fb.tracer(Tracer::default());

// Configured tracer (output to file, custom indent)
fb.tracer(Tracer::create(TracerConfiguration {
    indent: 2,
    to_file: Some("output/main.trace".to_string()),
    time_format: None,
}));
}

Configuration Options

OptionTypeDescription
indentusizeNumber of spaces per nesting level in the trace output.
to_fileOption<String>If set, writes the trace to the specified file path instead of stdout.
time_formatOption<String>If set, prepends a formatted timestamp to each trace line.

Custom Trace Messages

Actions can emit custom messages inline within the trace output using ctx.trace(). This is useful for logging intermediate Blackboard values or action-specific diagnostic information:

import "std::actions"

impl tracked_action();

root main repeat(3) {
    tracked_action()
}
#![allow(unused)]
fn main() {
use forester_rs::runtime::action::{Impl, RtArgs, Tick, TickResult};
use forester_rs::runtime::context::TreeContextRef;

struct TrackedAction;

impl Impl for TrackedAction {
    fn tick(&self, args: RtArgs, ctx: TreeContextRef) -> Tick {
        let mut ctx = ctx.lock()?;
        
        // Read current counter from Blackboard
        let i = ctx.bb()
            .get("counter".to_string())?
            .and_then(|v| v.clone().as_int())
            .map(|v| v + 1)
            .unwrap_or(0);
        
        ctx.bb().put("counter".to_string(), RtValue::int(i))?;
        
        // Emit a custom trace message
        ctx.trace(format!("counter = {:?}", i));
        
        Ok(TickResult::success())
    }
}
}

This produces inline messages in the trace output:

[1]    2 : Running(len=1)
[1]      counter = 0
[1]      3 : Success()
[2]  next tick
[2]      counter = 1
[2]      3 : Success()
[3]  next tick
[3]      counter = 2
[3]      3 : Success()
[3]  1 : Success(cursor=0,len=1)

Accessing the Tracer via HTTP

When the engine HTTP server is running, the tracer output can be fetched or appended to at runtime:

# Print current trace
curl http://localhost:10000/tracer/print

# Append a custom event to the trace
curl -X POST http://localhost:10000/tracer/custom \
  -H "Content-Type: application/json" \
  -d '{"message": "external checkpoint reached"}'

Simulation

Forester provides a simulation environment to execute a behavior tree by replacing actual action implementations with stubs. This allows you to validate tree logic and test execution branches under specific conditions without writing the application code.

Using a simulation profile, you can inject a specific Blackboard state, trace execution changes, and generate visual graphs of the tree.

Preparations

Configuration Profile

Note: All paths in the configuration file can be either absolute or relative to the root folder.

The YAML configuration file contains simulation settings and stub definitions for actions.

Example configuration:

config:
  tracer: 
    file: gen/main.trace
    dt_fmt: "%d %H:%M:%S%.3f"
  graph: gen/main.svg
  bb:
    dump: gen/bb.json
  max_ticks: 10

actions:
  -
    name: task
    stub: failure
    params:
      delay: 100

config Section

SettingDescriptionDefaultExample
tracer.fileFile path to write the execution trace.None (disabled)gen/main.trace
tracer.dt_fmtDatetime format for the trace logs.None"%d %H:%M:%S%.3f"
graphFile path to output the SVG tree visualization.None (disabled)gen/main.svg
bb.dumpFile path to dump the final Blackboard state as JSON.None (disabled)gen/bb.json
bb.loadFile path of a JSON file used to initialize the Blackboard before execution.None (disabled)gen/init_bb.json
max_ticksMaximum number of ticks before the simulation forcefully terminates.0 (unlimited)10
http.portPort for the HTTP server to listen for remote action callbacks.None (disabled)8080

actions Section

The actions section is an array mapping action names to their stubbed behavior.

SettingDescriptionDefaultExample
nameName of the target action.Requiredtask
stubStub behavior (success, failure, random, remote).Requiredsuccess
params.delayExecution delay in milliseconds.0100
params.url(Remote stub) URL of the remote server.Required for remotehttp://localhost:10000/action
params.server(Remote stub) Callback URL the remote action uses to access the Blackboard.http://localhosthttp://localhost:8080

Default Profile

A simulation can run without a specific profile. In this case, Forester replaces all unimplemented actions with a success stub. No traces, graphs, or dumps are generated.

Stubs

  • success: Always returns Success.
  • failure: Always returns Failure.
  • random: Returns Success or Failure randomly.
  • remote: Connects to a remote server and returns the result. Details are in the Remote actions documentation.

Parameters:

  • The success, failure, and random stubs accept a delay parameter (in milliseconds).
  • The remote stub requires a url parameter and accepts an optional server parameter for Blackboard access.

Process

You can perform a simulation via the CLI or directly in Rust code.

In the Console

Use the f-tree CLI to run a simulation:

f-tree sim --root tree/tests/simulator/smoke/ --profile sim.yaml

CLI Defaults:

  • --root: If omitted, defaults to the current working directory (<pwd>).
  • --main: If omitted, defaults to main.tree.
  • --tree: Can be omitted if there is only one root definition in the file.
  • --profile: If omitted, the default success-stub profile is used.

In the Code

Use SimulatorBuilder from the simulator module to configure and run the simulation programmatically.

From the file system:

#![allow(unused)]
fn main() {
fn smoke() {
    let mut sb = SimulatorBuilder::new();
    let root = PathBuf::from("simulator/smoke");

    sb.root(root.clone());
    sb.profile(PathBuf::from("sim.yaml"));
     
    let mut fb = ForesterBuilder::from_file_system();
    fb.main_file("main.tree".to_string());
    fb.root(root);

    sb.forester_builder(fb);
     
    let mut sim = sb.build().unwrap();
    sim.run().unwrap();
}
}

From raw text:

#![allow(unused)]
fn main() {
fn smoke_from_text() {
    let mut sb = SimulatorBuilder::new();
    let sim = PathBuf::from("simulator/smoke/sim.yaml");
    
    sb.profile(sim);
     
    let mut fb = ForesterBuilder::from_text();
    fb.text(r#"
        import "std::actions"

        root main sequence {
            store("info1", "initial")
            retryer(task(config = obj), success())
            store("info2", "finish")
        }

        fallback retryer(t: tree, default: tree) {
            retry(5) t(..)
            fail("just should fail")
            default(..)
        }

        impl task(config: object);
    "#.to_string());    
    
    sb.forester_builder(fb);
    
    let mut sim = sb.build().unwrap();
    sim.run().unwrap();
}
}

Export to ROS Nav2

ROS in general and ROS Nav2 in particular are highly popular in robotics. They manage critical aspects of robot control, including navigation, localization, mapping, and more.

Forester provides native support for exporting a tree directly to ROS Nav2. The intermediate format is the Nav2 XML format.

The transformation process is straightforward.

Control Nodes

Forester control nodes map directly to Nav2 control nodes:

  • sequence becomes PipelineSequence
  • fallback becomes RoundRobin
  • r_fallback becomes ReactiveFallback

If a Forester control node is explicitly named, that name is applied to the resulting Nav2 XML element.

For example:

sequence FollowPathWithFallback {
    // ... 
}

Becomes:

<PipelineSequence name="FollowPathWithFallback">
</PipelineSequence>

Actions

Forester actions map directly to Nav2 actions.

Every action accepts an implicit name parameter, which dictates the name of the action in the Nav2 tree. This parameter is optional and can be omitted.

For actions that take a subtree as a parameter (like decorators), the parameter must be named sub.

Retry

Retry logic can be represented in two ways, depending on whether you need to name the node in Nav2:

  • The retry decorator: The number of retries is specified compactly (e.g., retry(3)), but you cannot assign a name to the node.
  • The RecoveryNode action: The number of retries is passed as an argument. This is the only way to explicitly assign a name parameter to the retry node.
    // Allows you to convey the name to Nav2
    RecoveryNode(
        number_of_retries = 1,
        name = "ComputePathToPose", 
        sub = ComputePathWithFallback()
    )
    
    // Cannot convey a name to Nav2
    retry(1) ComputePathWithFallback() 
    
    // Everything else functions exactly the same

Example

Forester Tree:

import "ros::nav2"

root MainTree RecoveryNode(number_of_retries = 6, name = "NavigateRecovery", sub = NavigateWithReplanning())

sequence NavigateWithReplanning {
    RateController(
        hz = 1.0,
        sub = RecoveryNode(
            number_of_retries = 1,
            name = "ComputePathToPose",
            sub = retry(1) ComputePathWithFallback()
        )
    )
    retry(1) FollowPathWithFallback()
}

sequence ComputePathWithFallback {
    ComputePathToPose(goal = goal, path = path, planner_id = "GridBased")
    ComputePathToPoseRecoveryFallback()
}

sequence FollowPathWithFallback {
    FollowPath(path = path, controller_id = "FollowPath")
    FollowPathRecoveryFallback()
}

r_fallback ComputePathToPoseRecoveryFallback {
    GoalUpdated()
    ClearEntireCostmap(name = "ClearGlobalCostmap-Context", service_name = "global_costmap/clear_entirely_global_costmap")
}

r_fallback FollowPathRecoveryFallback {
    GoalUpdated()
    ClearEntireCostmap(name = "ClearLocalCostmap-Context", service_name = "local_costmap/clear_entirely_local_costmap")
}

Transformed Nav2 XML:

<root main_tree_to_execute="MainTree">
  <BehaviorTree ID="MainTree">
    <RecoveryNode name="NavigateRecovery" number_of_retries="6">
      <PipelineSequence name="NavigateWithReplanning">
        <RateController hz="1">
          <RecoveryNode name="ComputePathToPose" number_of_retries="1">
            <RecoveryNode number_of_retries="1">
              <PipelineSequence name="ComputePathWithFallback">
                <ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
                <ReactiveFallback name="ComputePathToPoseRecoveryFallback">
                  <GoalUpdated/>
                  <ClearEntireCostmap name="ClearGlobalCostmap-Context" service_name="global_costmap/clear_entirely_global_costmap"/>
                </ReactiveFallback>
              </PipelineSequence>
            </RecoveryNode>
          </RecoveryNode>
        </RateController>
        <RecoveryNode number_of_retries="1">
          <PipelineSequence name="FollowPathWithFallback">
            <FollowPath controller_id="FollowPath" path="{path}"/>
            <ReactiveFallback name="FollowPathRecoveryFallback">
              <GoalUpdated/>
              <ClearEntireCostmap name="ClearLocalCostmap-Context" service_name="local_costmap/clear_entirely_local_costmap"/>
            </ReactiveFallback>
          </PipelineSequence>
        </RecoveryNode>
      </PipelineSequence>
    </RecoveryNode>
  </BehaviorTree>
</root>

Tools

Ensure you are running the latest version of the CLI to use these features:

cargo install f-tree 

Headers

To use Nav2 actions with the correct signatures, you must import the ros::nav2 module in your Forester project.

To view the contents and available actions in this file, run:

f-tree -d print-ros-nav2

Exporting via the Console

To export the tree from the command line, run:

f-tree nav2 

Exporting via the IntelliJ Plugin

Run the task Export to ROS Nav2 directly within your IDE.

Exporting via Code

#![allow(unused)]
fn main() {
#[test]
fn smoke() {
    let mut root_path = test_folder("ros/nav/smoke");

    let project = Project::build("main.tree".to_string(), root_path.clone()).unwrap();
    let tree = RuntimeTree::build(project).unwrap().tree;

    // Define the output file
    root_path.push("test.xml");

    // Export the tree
    tree.to_ros_nav(root_path).unwrap();
}
}

Examples

Examples

To help you get started quickly, we maintain a dedicated repository containing fully functional examples that demonstrate Forester in action across different domains.

You can find the repository here: forester-bt/examples

Implemented Examples

  • Basic Simulation: Demonstrates how to set up a tree, define a profile, and run it using the built-in simulator with stubbed actions.
  • ROS Nav2 Integration: A robotics orchestration example showing how to structure a tree for navigation and export it to ROS Nav2 XML.
  • Webots Robot Control: Demonstrates how to use the Forester-Webots integration layer to control a simulated robot using reactive ticks.
  • Remote Actions (Python & Rust): Shows how to connect external logic—such as LLM agent tools or external APIs—using the HTTP remote action clients, keeping the orchestration in Rust while executing logic in Python.
  • Higher-Order Trees: Examples of passing subtrees as arguments to create reusable patterns like retryer and fallback wrappers.

Tools

The Forester ecosystem includes several auxiliary tools and libraries to help you write, analyze, and execute behavior trees.

IntelliJ Plugin

The Forester IntelliJ Plugin provides native IDE support for the .tree language. Features include:

  • Syntax highlighting
  • Code folding
  • Code navigation
  • Code formatting
  • Code inspections and error highlighting
  • Structure view
  • Built-in tasks to visualize and simulate the tree directly from the IDE

Remote Action Libraries

Remote action clients allow you to decouple the orchestration engine from the execution of individual actions. This is particularly useful for AI agents where the orchestrator is in Rust, but the tool execution is in Python. Features include:

  • The ability to execute specific actions on a remote machine or in a separate process via HTTP, returning the TickResult to the Forester runtime.
  • Bidirectional access, allowing the remote action to read from and write to the shared Blackboard.

Available Clients:

CLI (f-tree)

The f-tree command-line interface is the primary utility for testing and manipulating trees without writing boilerplate application code. It supports:

  • Simulating trees with stubbed actions using YAML profiles.
  • Generating SVG visualizations of the tree structure.
  • Exporting trees to the ROS Nav2 XML format.
  • Printing built-in standard library headers (e.g., ros::nav2, std::actions).

IntelliJ Plugin

Introduction

The Forester IntelliJ Plugin provides native IDE support for writing and testing behavior trees in the .tree language. It does not orchestrate tasks itself; rather, it bridges the IDE environment with the Forester runtime and CLI tools.

This allows you to write type-checked behavior trees, navigate complex hierarchical logic, and run simulations with visual SVG outputs directly from your editor, accelerating the development cycle for robotics and AI agent orchestration.

Installation

  1. Open your IntelliJ IDE (IDEA, CLion, PyCharm, etc.).
  2. Go to Settings (or Preferences on macOS) from the main menu.
  3. Select Plugins from the left-hand menu.
  4. Click on the Marketplace tab.
  5. Search for “Forester”.
  6. Click Install and restart the IDE to activate the plugin.

Features

Syntax Highlighting

Provides specialized syntax highlighting for the .tree language, making keywords, higher-order trees, decorators, and actions easily distinguishable.

Code Folding

Allows you to collapse sections of your behavior trees (such as large sequences or fallbacks), making complex task structures readable and easier to navigate.

Structure View

Displays the hierarchical organization of your behavior trees in the IDE’s Structure tool window. You can quickly see parent-child relationships and jump to specific node definitions.

Task Visualization

Integrates with Forester’s graph generation to visually map out your behavior trees. This outputs interactive or static graphical representations (SVGs) of your execution flows.

Task Simulation

Run and test your behavior trees directly within the IDE using simulation profiles. You can execute stubbed runs to verify the logic and fallback routing without needing the actual application runtime to be active.

Usage

Creating a Run Configuration

  1. Open the Run/Debug Configurations dialog in your IDE.
  2. Click the + (Add New Configuration) button and select the Forester configuration type.
  3. Specify your root folder, main tree file, and simulation profile (if applicable).

Running a Simulation via the Editor

  1. Open the .tree file containing your root node.
  2. Click the green Run icon located in the editor gutter next to the root keyword.
  3. The IDE will execute the tree simulation and output the trace logs and visual graphs to your configured output directories.

Library to create Remote Actions using Rust

The Forester provides a http library that alleviates writing the remote http actions. For now, the libraries are available for Rust and Python.

Library to create Remote Actions using Rust

The Forester provides an http library that alleviates writing the remote http actions.

Usage

forester-http = { version = "0.1.0" }

The contract is defined in the following way:

#![allow(unused)]
fn main() {
pub trait ForesterRemoteAction {
    fn tick(&self, request: RemoteActionRequest) -> TickResult;
}
}

where RemoteActionRequest is defined as:

#![allow(unused)]
fn main() {
pub struct RemoteActionRequest {
    /// current tick
    pub tick: usize,
    /// the list of arguments from the tree invocation
    pub args: Vec<RtArgument>,
    /// the address of the server to access to blackboard and other services
    pub serv_url: String,
}
}

On the other hand, the library provides a helper API ForesterHttpApi and Client ForesterHttpClient (async reqwest) to access the server.

Example

The code is available in the forester-examples repository.

The gist is the following:


#[tokio::main]
async fn main() {
    let routing = Router::new()
        .route("/", get(|| async { "OK" }))
        .route("/action", post(handler))
        .into_make_service_with_connect_info::<SocketAddr>();

    axum::Server::bind(&SocketAddr::from(([127, 0, 0, 1], 10000)))
        .serve(routing)
        .await
        .unwrap();
}


/// RemoteActionRequest defines the request from the tree
async fn handler(Json(req): Json<RemoteActionRequest>) -> impl IntoResponse {
    let url = req.clone().serv_url;
    /// the client to access the server
    let client = ForesterHttpClient::new(url);
    let trace = client .print_trace(); /// print the trace of the tree

    let result = client.put("test".to_string(), json!({"f1":1, "f2":2, "f3":3})).await;
    println!("result of putting {:?}", result);
    
    client.lock("test".to_string()).await.unwrap();

    (StatusCode::OK, Json::from(RemoteAction.tick(req)))
}

struct RemoteAction;

impl ForesterRemoteAction for RemoteAction {
    fn tick(&self, request: RemoteActionRequest) -> TickResult {
        println!("tick: {:?}", request);
        TickResult::Success
    }
}

Library to create Remote Actions using Python

The Forester provides an http library that alleviates writing the remote http actions.

Usage

The latest version can be obtained from the test.pypi.org

pip install -i https://test.pypi.org/simple/ forester-http==0.0.5

The contract is defined in the following way:

from typing import List

class RtArgument:
    """The argument that is sent from the Forester instance

    * The name of the argument
    * The value of the argument is a json
    """

    def __init__(self, name: str, value: str) -> None:
        self.name = name
        self.value = value


class RemoteActionRequest:
    """The request that is sent from the Forester instance

    * It has the current tick and the arguments in the action from tree
    """

    def __init__(self, tick: int, args: List[RtArgument], serv_url: str) -> None:
        self.tick = tick
        self.args = args
        self.serv_url = serv_url


On the other hand, the library provides a helper API ForesterHttpApi and Client ForesterHttpClient to access the server.

Example

The code is available in the forester-examples repository.

The gist is the following:


import json
from http.server import BaseHTTPRequestHandler, HTTPServer

from forester_http.client import *

class MyServer(BaseHTTPRequestHandler):
    def do_POST(self):
    
        if self.path == "/action":
            content_length = int(self.headers["Content-Length"])
            # get body as json and deserialize it to RemoteActionRequest
            body = json.loads(self.rfile.read(content_length))
            req = RemoteActionRequest.from_bytes(body.encode("utf-8"))
    
            client = ForesterHttpClient(req.serv_url)
            client.put("test", "test")
    
            self.send_response(200)
            self.send_header("Content-Type", "application/json;charset=UTF-8")
            self.end_headers()
    
            self.wfile.write(json.dumps("Success").encode("utf-8"))
    
        else:
            self.send_error(404)


if __name__ == "__main__":
    webServer = HTTPServer((hostName, serverPort), MyServer)
    print("Server started http://%s:%s" % (hostName, serverPort))

    try:
        webServer.serve_forever()
    except KeyboardInterrupt:
        pass

    webServer.server_close()
    print("Server stopped.")

Console f-tree

The console utility f-tree can be installed using cargo and can be used to simulate and visualize the tree.

The Intellij plugin basically wraps this utility and provides the same functionality.

cargo install f-tree

and then be used with

~ f-tree --help
Usage: f-tree [OPTIONS] <COMMAND>

Commands:
  print-std-actions  Print the list of std actions from 'import std::actions'
  print-ros-nav2     Print the list of ros actions from 'import ros::nav2'
  sim                Runs simulation. Expects a simulation profile
  vis                Runs visualization. Output is in svg format.
  nav2               Convert to the xml compatable format of nav ros2.
  help               Print this message or the help of the given subcommand(s)

Options:
  -d, --debug    Print debug logs
  -h, --help     Print help
  -V, --version  Print version