bteng

BTEng Node System

Base Classes

TreeNode

All 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._status inside tick() always reflects the previous tick’s result, not the current one. execute_tick() sets self._status only after tick() returns. This is intentional — it lets nodes detect first-entry vs. re-entry without extra flags.


Control Nodes

SequenceNode

Ticks 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

ParallelNode

Ticks all children every tick (in the same thread, sequentially).

Parameters:

from 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.

ReactiveSequenceNode

Like Sequence, but restarts from child[0] every tick. Earlier conditions can interrupt a later running action when they change.

ReactiveFallbackNode

Like Fallback, but restarts from child[0] every tick. Higher-priority conditions can interrupt a running lower-priority action.


Decorator Nodes

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)

Leaf Nodes

ActionNode

Override 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

ConditionNode

Same API as ActionNode. Semantic distinction only — returns SUCCESS or FAILURE, never RUNNING.

StatefulActionNode

Three-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()

AsyncActionNode

Runs 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.

Functional API (inline nodes)

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:


Port System

Ports 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" -->

Registration

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).


Inspector integration

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()

API Reference

::: 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: [”!^_”]