Skip to main content

Modular Behavior Tree execution engine for Python

Project description

BTEng — Behavior Tree Engine

BTEng — Behavior Tree Engine for Python

Python License Version

BTEng is a modular, production-grade Behavior Tree execution engine for Python. It is designed for robotics, automation, simulation, and any tick-driven control system that needs robust, reactive decision-making.

The library provides a compact core runtime, a rich node library, a thread-safe blackboard, XML tree loading, plugin-based custom nodes, execution tracing, structured logging, runtime introspection, and optional ZMQ event streaming — without imposing a large framework on your application.


Table of Contents


Features

Category Capabilities
Execution Tick-based engine, event-loop executor, pause/resume, max-tick budget
Control nodes Sequence, Fallback/Selector, Parallel, ReactiveSequence, ReactiveFallback
Decorators Inverter, Retry, Timeout, RateController, ForceSuccess, ForceFailure
Leaf nodes ActionNode, ConditionNode, StatefulActionNode, AsyncActionNode
Blackboard Scoped, observable, provenance history, schema validation, change subscriptions
Builder API Fluent TreeBuilder for programmatic tree construction
XML loader Full XML parser with port-default resolution and subtree support
Factory @register_node decorator, NodeManifest, dynamic plugin loading
Introspection Inspector — per-node stats, active-path tracking, explain log
Logging Logger — console and JSON-lines sinks, auto-wired to Inspector
Tracing ExecutionTracer — per-tick frame recording, JSON export, replay
Concurrency ThreadPool (auto-injected), CancellationToken for async actions
Testing MockActionNode, MockConditionNode, BehaviorTreeTest
Streaming ZmqPublisher — streams tick events via ZMQ PUB (optional dep)
CLI bteng run for command-line tree execution

Installation

From PyPI

pip install bteng

Optional extras

pip install "bteng[zmq]"   # ZMQ publisher — stream tick events to dashboards
pip install "bteng[dev]"   # Development tools: pytest, build, twine

Requirements: Python 3.9 or newer.


Quick Start

For new code, start with TreeBuilder + TreeExecutor.

from bteng import (
    Blackboard,
    NodeStatus,
    TreeBuilder,
    TreeExecutor,
)

bb = Blackboard.create("demo")
bb.set("battery_level", 87)

tree = (
    TreeBuilder(blackboard=bb)
    .tree_id("Mission")
    .sequence("root")
        .condition("BatteryOK", lambda: bb.get("battery_level", 0) > 20)
        .action("Navigate",     lambda: NodeStatus.SUCCESS)
    .end()
    .build()
)

executor = TreeExecutor()
executor.set_tree(tree)
status = executor.tick_until_result(max_ticks=100)
print(status)  # NodeStatus.SUCCESS

BehaviorTreeEngine is still available for legacy code, but TreeExecutor is the recommended runtime for new projects.


Core Concepts

Status model

Every node returns a NodeStatus each tick:

Status Meaning
SUCCESS Node completed successfully
FAILURE Node failed
RUNNING Node is active and must be ticked again
IDLE Node is inactive or has been halted

Control nodes

Node Behavior
Sequence Ticks children left-to-right; stops at first FAILURE or RUNNING
Fallback / Selector Ticks children left-to-right; stops at first SUCCESS or RUNNING
Parallel Ticks all children simultaneously with configurable success/failure thresholds
ReactiveSequence Restarts from child[0] every tick; earlier conditions interrupt later actions
ReactiveFallback Restarts from child[0] every tick; higher-priority child can preempt

Decorators

Decorator Purpose
Inverter Swaps SUCCESSFAILURE; passes RUNNING unchanged
Retry(max_attempts) Retries child on FAILURE up to N times
Timeout(duration) Returns FAILURE if child exceeds duration (seconds)
RateController(hz) Rate-limits child ticking; returns cached status between ticks
ForceSuccess Always returns SUCCESS unless child is RUNNING
ForceFailure Always returns FAILURE unless child is RUNNING

Blackboard

A thread-safe, scoped key-value store shared across the tree:

from bteng import Blackboard

bb = Blackboard.create("robot")
bb.set("pose", (1.0, 2.0, 0.0))
bb.set("stopped", None)       # None is a valid stored value

bb.get("pose")                # (1.0, 2.0, 0.0)
bb.get("stopped")             # None  (the stored None, not a missing-key default)
bb.has("stopped")             # True

# Change subscriptions
sub = bb.subscribe(lambda key, val: print(f"{key} changed to {val}"))
bb.set("pose", (2.0, 3.0, 0.0))  # fires callback
bb.unsubscribe(sub)

# Scoped child blackboard for subtree port isolation
child = bb.create_child_scope("subtree", remapping={"local_goal": "goal"})
child.set("local_goal", "dock")   # transparently writes bb["goal"]
child.get("local_goal")           # transparently reads bb["goal"]

Custom Nodes

Define new node types by subclassing and declaring ports:

from bteng import ActionNode, InputPort, OutputPort, NodeStatus, register_node


@register_node()
class DetectObject(ActionNode):
    @classmethod
    def provided_ports(cls):
        return [
            InputPort("camera", default="rgb"),
            OutputPort("object_pose"),
        ]

    def tick(self) -> NodeStatus:
        camera = self.get_input("camera")   # resolves XML attribute or default
        pose   = run_detector(camera)
        if pose is None:
            self.set_failure_reason("detector returned no result")
            return NodeStatus.FAILURE
        self.set_output("object_pose", pose)
        return NodeStatus.SUCCESS

Access the full node configuration via self.config (NodeConfig with blackboard, port mappings, and static params). self.blackboard is a convenience shortcut.

For long-running work use StatefulActionNode (three-phase lifecycle: on_start / on_running / on_halted) or AsyncActionNode (runs execute_async(token) in a background thread; the executor automatically injects a shared ThreadPool).


XML Trees

Load and execute trees defined in XML — useful for decoupling behavior from code:

<?xml version="1.0" encoding="UTF-8"?>
<BTEng format_version="1.0" main_tree_to_execute="main">
  <Tree ID="main">
    <ReactiveFallback name="root">
      <ReactiveSequence name="navigate_if_safe">
        <Condition ID="PathClear"/>
        <Timeout duration="10.0">
          <Action ID="NavigateTo" goal="{target_goal}"/>
        </Timeout>
      </ReactiveSequence>
      <Action ID="StopRobot"/>
    </ReactiveFallback>
  </Tree>

  <TreeNodesModel>
    <Condition ID="PathClear"/>
    <Action ID="NavigateTo">
      <input_port name="goal"/>
    </Action>
    <Action ID="StopRobot"/>
  </TreeNodesModel>
</BTEng>

Port syntax:

  • goal="{target_goal}" — blackboard reference; reads/writes key target_goal
  • mode="inspection" — static literal parameter
  • InputPort(default=...) is applied when the XML attribute is absent
from bteng import XMLTreeParser, Blackboard

bb   = Blackboard.create("demo")
bb.set("target_goal", "dock_station")

parser = XMLTreeParser()
root   = parser.parse_file("mission.xml", blackboard=bb)

See docs/reference/xml.md for the full specification.


Introspection & Logging

Attach an Inspector and Logger to any TreeExecutor — no changes to node code required:

from bteng import Inspector, Logger, LogLevel, TreeExecutor

inspector = Inspector.create()
logger    = Logger.create()
logger.add_console_sink(colored=True)
logger.add_json_file_sink("/tmp/bt_run.jsonl")

executor = TreeExecutor()
executor.set_tree(tree)
executor.set_inspector(inspector)
executor.set_logger(logger)       # auto-wired; no extra setup needed
executor.tick_until_result()

# Per-node statistics
for uid, stats in inspector.all_stats().items():
    print(f"{uid:40s}  ticks={stats.tick_count:4d}  "
          f"total={stats.total_duration*1000:.1f}ms")

Execution Tracing & Replay

Record every tick for offline analysis, regression testing, or replay:

from bteng import ExecutionTracer, TreeExecutor

tracer   = ExecutionTracer()
executor = TreeExecutor()
executor.set_tree(tree)
executor.set_tracer(tracer)
executor.tick_until_result()

# Export full trace
with open("trace.json", "w") as f:
    f.write(tracer.export_json())

# Compact replay format (smaller file)
with open("replay.json", "w") as f:
    f.write(tracer.export_replay())

# Load and inspect a previously recorded run
tracer2 = ExecutionTracer()
tracer2.load_replay(open("replay.json").read())
frame = tracer2.replay_frame(0)
print(frame.tick_index, frame.blackboard_snapshot)

ZMQ Event Streaming

Stream tick events to an external dashboard or visualiser with zero coupling between the engine and the consumer:

pip install "bteng[zmq]"
from bteng.introspection import ZmqPublisher

pub = ZmqPublisher(port=1667)   # default port — compatible with BehaviorTree.CPP convention
pub.attach(inspector)
pub.start()

# ... run tree as normal ...

pub.stop()

Each published message is a JSON object on ZMQ topic bteng:

{
  "ts":     1234.567,
  "uid":    "a1b2c3d4",
  "name":   "NavigateTo",
  "type":   "action",
  "status": "SUCCESS",
  "dur_ms": 12.3,
  "reason": ""
}

Unit Testing

BehaviorTreeTest and MockActionNode / MockConditionNode provide a purpose-built test harness:

from bteng import (
    MockActionNode, MockConditionNode, BehaviorTreeTest,
    NodeStatus, SequenceNode, Tree, TreeMetadata,
)

condition = MockConditionNode("IsReady")
condition.set_result(True)

action = MockActionNode("Navigate")
action.set_ticks_to_complete(3)    # returns RUNNING for 2 ticks, then SUCCESS

root = SequenceNode("root", children=[condition, action])
tree = Tree(TreeMetadata(id="test"), root)

result = (
    BehaviorTreeTest(tree)
    .expect_final_status(NodeStatus.SUCCESS)
    .set_max_ticks(10)
    .run()
)
assert result, result.error_message

CLI

Execute and visualize trees from the command line:

bteng run mission.xml --plugin my_robot_nodes.py --tree main --hz 10 --log run.json -v

Documentation

Document Contents
5-minute Quick Start Recommended first tree with TreeBuilder + TreeExecutor
Which API should I use? Decision table for Python, XML, legacy, and advanced APIs
Beginner Guide Behavior-tree basics, blackboard, TreeBuilder, and first tests
Advanced Guide Ports, reactive internals, plugins, introspection, ZMQ, runtime modification
Architecture Component relationships and data-flow diagram
XML specification Full XML format reference with port syntax
Node system Node lifecycle, port model, custom node authoring

License

BTEng is distributed under a proprietary license. See LICENSE for terms.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bteng-0.2.8.tar.gz (117.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bteng-0.2.8-py3-none-any.whl (78.6 kB view details)

Uploaded Python 3

File details

Details for the file bteng-0.2.8.tar.gz.

File metadata

  • Download URL: bteng-0.2.8.tar.gz
  • Upload date:
  • Size: 117.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for bteng-0.2.8.tar.gz
Algorithm Hash digest
SHA256 04635bd4a4dffda2a1f3ca745a6c98850885e57ef0347674c518369c014ad424
MD5 c3eed745d71f278748d02b6fbcb9b0f9
BLAKE2b-256 daecaa11cb5bb4188be957b0e0a530f11ab4f8d9efebbca1b15efaa7113dc179

See more details on using hashes here.

File details

Details for the file bteng-0.2.8-py3-none-any.whl.

File metadata

  • Download URL: bteng-0.2.8-py3-none-any.whl
  • Upload date:
  • Size: 78.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for bteng-0.2.8-py3-none-any.whl
Algorithm Hash digest
SHA256 26c720e1ff3158f2943f65976c6c3989109c4af7d2e7c5412b360f29ccd38106
MD5 61f9e7389d612e41670731976deb272f
BLAKE2b-256 a6b72f7c4d68c7845188797b1cbf06c1d4e44da8a2aa2b0234abc8fbf1059c6f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page