TreeNodeAll nodes inherit from TreeNode. Key interface:
node.execute_tick() -> NodeStatus # called by engine / parents
node.halt() # stop & reset to IDLE
node.reset_node() # unconditional recursive reset to IDLE
node.get_input(port, default) # read from blackboard or params
node.set_output(port, value) # write to blackboard
node.status # current NodeStatus
node.config # NodeConfig — ports, params, blackboard ref
node.blackboard # shortcut to config.blackboard
node.tick_count # total execute_tick() calls
node.last_tick_duration # wall-clock duration of last tick (seconds)
node.failure_reason # string set via set_failure_reason()
Override in subclasses:
def tick(self) -> NodeStatus: ... # required
def _on_halt(self): ... # optional cleanup when halted
def _on_reset(self): ... # optional reset hook (called by reset_node)
@classmethod
def provided_ports(cls) -> List[PortDefinition]: ... # optional port declarations
Important:
self._statusinsidetick()always reflects the previous tick’s result, not the current one.execute_tick()setsself._statusonly aftertick()returns. This is intentional — it lets nodes detect first-entry vs. re-entry without extra flags.
SequenceNodeTicks children left-to-right, resuming from where it left off between ticks.
| Child returns | Sequence returns |
|---|---|
| SUCCESS | advance to next |
| RUNNING | RUNNING (resume next tick) |
| FAILURE | FAILURE (halt all children) |
| all SUCCESS | SUCCESS |
FallbackNode (Selector)Inverse of Sequence.
| Child returns | Fallback returns |
|---|---|
| FAILURE | advance to next |
| RUNNING | RUNNING (resume next tick) |
| SUCCESS | SUCCESS (halt remaining) |
| all FAILURE | FAILURE |
ParallelNodeTicks all children every tick (in the same thread, sequentially).
Parameters:
success_threshold: how many must succeed (-1 = all, default)failure_threshold: how many must fail to abort (1 = any, default)policy: ParallelPolicy enum — overrides threshold semantics when providedfrom bteng import ParallelNode, ParallelPolicy
p = ParallelNode("p", children=[...], policy=ParallelPolicy.REQUIRE_ONE_SUCCESS)
Once a child reaches SUCCESS or FAILURE it is not re-ticked; its result is accumulated until Parallel itself terminates.
ReactiveSequenceNodeLike Sequence, but restarts from child[0] every tick. Earlier conditions can interrupt a later running action when they change.
ReactiveFallbackNodeLike Fallback, but restarts from child[0] every tick. Higher-priority conditions can interrupt a running lower-priority action.
Each decorator wraps exactly one child. TreeBuilder raises RuntimeError at build
time if a decorator scope is closed (end()) without a child having been added.
| Decorator | Behaviour |
|---|---|
Inverter |
Flip SUCCESS ↔ FAILURE, pass RUNNING |
Retry(max_attempts) |
Re-tick child on FAILURE up to N times, returns RUNNING between attempts |
Timeout(duration) |
Return FAILURE if child exceeds duration seconds |
RateController(hz) |
Rate-limit child ticking, return cached status between ticks |
ForceSuccess |
Always return SUCCESS (unless child is RUNNING) |
ForceFailure |
Always return FAILURE (unless child is RUNNING) |
ActionNodeOverride tick():
from bteng import ActionNode, NodeStatus, InputPort
class MyAction(ActionNode):
@classmethod
def provided_ports(cls):
return [InputPort("speed", default="1.0")]
def tick(self) -> NodeStatus:
speed = self.get_input("speed") # "1.0" if not mapped in XML/builder
# do work
return NodeStatus.SUCCESS
ConditionNodeSame API as ActionNode. Semantic distinction only — returns SUCCESS or FAILURE,
never RUNNING.
StatefulActionNodeThree-phase lifecycle for long-running actions:
from bteng import StatefulActionNode, NodeStatus
class MyTask(StatefulActionNode):
def on_start(self) -> NodeStatus: # called on first tick of each activation
self._work = start_work()
return NodeStatus.RUNNING
def on_running(self) -> NodeStatus: # called on subsequent ticks while RUNNING
if self._work.done():
return NodeStatus.SUCCESS
return NodeStatus.RUNNING
def on_halted(self) -> None: # called when the node is halted externally
self._work.cancel()
AsyncActionNodeRuns execute_async() in a background thread. The main tick loop returns RUNNING
immediately and polls the result each tick.
import time
from bteng import AsyncActionNode, CancellationToken, NodeStatus
class SlowScan(AsyncActionNode):
def execute_async(self, token: CancellationToken) -> NodeStatus:
for i in range(10):
if token.is_cancelled(): # cooperative cancellation
return NodeStatus.FAILURE
time.sleep(0.1)
return NodeStatus.SUCCESS
token.is_set() is an alias for token.is_cancelled() for backwards compatibility
with code written against the old threading.Event interface.
Thread pool: TreeExecutor automatically injects a shared ThreadPool into every
AsyncActionNode in the tree when set_tree() or set_thread_pool() is called.
Manual injection via node.set_thread_pool(pool) still works but is no longer required.
from bteng import action, condition, NodeStatus
is_ready = condition("is_ready", lambda: blackboard.get("ready"))
move = action("move", lambda: NodeStatus.SUCCESS)
Lambda receives no arguments. Return value is coerced:
NodeStatus → passed throughSUCCESS, falsy → FAILUREPorts declare what data a node reads from and writes to the Blackboard.
from bteng import ActionNode, NodeStatus, InputPort, OutputPort, BidirectionalPort
class MyAction(ActionNode):
@classmethod
def provided_ports(cls):
return [
InputPort("target", description="Goal position", default="origin"),
OutputPort("result", description="Outcome string"),
BidirectionalPort("counter"), # both read and written
]
def tick(self):
target = self.get_input("target") # reads bb[input_ports["target"]]
# or returns default if not mapped
self.set_output("result", "done") # writes bb[output_ports["result"]]
return NodeStatus.SUCCESS
InputPort defaults are applied at parse time — if the XML attribute is absent,
the declared default is used. Override is always possible:
<!-- uses declared default "origin" -->
<Action ID="MyAction"/>
<!-- overrides default -->
<Action ID="MyAction" target="{current_goal}"/>
<Action ID="MyAction" target="fixed_pos"/>
Port remapping in XML:
<Action ID="MyAction" target="{goal}" result="{outcome}"/>
<!-- "target" reads/writes blackboard key "goal" -->
<!-- "result" reads/writes blackboard key "outcome" -->
from bteng import register_node, NodeFactory
@register_node() # registers as "MyAction"
class MyAction(ActionNode):
@classmethod
def provided_ports(cls):
return [InputPort("speed", default="1.0")]
@register_node("alias") # registers as "alias"
class Another(ActionNode):
...
# Or manually:
NodeFactory.get_instance().register(MyAction)
The factory reads provided_ports() to build the node manifest used by the XML parser,
export_node_models_xml(), and IDE tooling. Always use provided_ports() — not
define_ports() (incorrect alias that the factory does not read).
Nodes automatically report tick events to the Inspector when one is attached by the
executor. No node-level code changes are needed.
executor.set_inspector(inspector)
# → inspector.on_node_tick() called after every execute_tick()
# → inspector.subscribe() callbacks fire with NodeExecutionRecord
NodeExecutionRecord fields:
| Field | Type | Description |
|---|---|---|
uid |
str |
Unique node ID |
name |
str |
Node name |
node_type |
NodeType |
Action, Control, etc. |
old_status |
NodeStatus |
Status before this tick |
status |
NodeStatus |
Status after this tick |
tick_time |
float |
time.monotonic() timestamp |
duration |
float |
Wall-clock tick duration (seconds) |
failure_reason |
str |
Set via set_failure_reason() |
::: bteng.core.node.NodeStatus options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.core.node.NodeConfig options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.core.node.NodeContract options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.core.node.TreeNode options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.leaf.action.ActionNode options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.leaf.condition.ConditionNode options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.leaf.stateful_action.StatefulActionNode options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.leaf.async_action.AsyncActionNode options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.core.node.PortDefinition options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.control.sequence.SequenceNode options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.control.fallback.FallbackNode options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.control.parallel.ParallelNode options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.control.parallel.ParallelPolicy options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.control.reactive_sequence.ReactiveSequenceNode options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.control.reactive_fallback.ReactiveFallbackNode options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.decorators.inverter.Inverter options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.decorators.retry.Retry options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.decorators.timeout.Timeout options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.decorators.rate_controller.RateController options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.decorators.force_result.ForceSuccess options: show_root_heading: true heading_level: 3 filters: [”!^_”]
::: bteng.nodes.decorators.force_result.ForceFailure options: show_root_heading: true heading_level: 3 filters: [”!^_”]