Modular Behavior Tree execution engine for Python
Project description
BTEng — Behavior Tree Engine for Python
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
- Installation
- Quick Start
- Core Concepts
- Custom Nodes
- XML Trees
- Introspection & Logging
- Execution Tracing & Replay
- ZMQ Event Streaming
- Unit Testing
- CLI
- Documentation
- License
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 SUCCESS ↔ FAILURE; 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 keytarget_goalmode="inspection"— static literal parameterInputPort(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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
04635bd4a4dffda2a1f3ca745a6c98850885e57ef0347674c518369c014ad424
|
|
| MD5 |
c3eed745d71f278748d02b6fbcb9b0f9
|
|
| BLAKE2b-256 |
daecaa11cb5bb4188be957b0e0a530f11ab4f8d9efebbca1b15efaa7113dc179
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26c720e1ff3158f2943f65976c6c3989109c4af7d2e7c5412b360f29ccd38106
|
|
| MD5 |
61f9e7389d612e41670731976deb272f
|
|
| BLAKE2b-256 |
a6b72f7c4d68c7845188797b1cbf06c1d4e44da8a2aa2b0234abc8fbf1059c6f
|