mycorrhizal 0.1.2__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mycorrhizal/_version.py +1 -1
- mycorrhizal/common/__init__.py +15 -3
- mycorrhizal/common/cache.py +114 -0
- mycorrhizal/common/compilation.py +263 -0
- mycorrhizal/common/interface_detection.py +159 -0
- mycorrhizal/common/interfaces.py +3 -50
- mycorrhizal/common/mermaid.py +124 -0
- mycorrhizal/common/wrappers.py +1 -1
- mycorrhizal/hypha/core/builder.py +11 -1
- mycorrhizal/hypha/core/runtime.py +242 -107
- mycorrhizal/mycelium/__init__.py +174 -0
- mycorrhizal/mycelium/core.py +619 -0
- mycorrhizal/mycelium/exceptions.py +30 -0
- mycorrhizal/mycelium/hypha_bridge.py +1143 -0
- mycorrhizal/mycelium/instance.py +440 -0
- mycorrhizal/mycelium/pn_context.py +276 -0
- mycorrhizal/mycelium/runner.py +165 -0
- mycorrhizal/mycelium/spores_integration.py +655 -0
- mycorrhizal/mycelium/tree_builder.py +102 -0
- mycorrhizal/mycelium/tree_spec.py +197 -0
- mycorrhizal/rhizomorph/README.md +82 -33
- mycorrhizal/rhizomorph/core.py +287 -119
- mycorrhizal/septum/TRANSITION_REFERENCE.md +385 -0
- mycorrhizal/{enoki → septum}/core.py +326 -100
- mycorrhizal/{enoki → septum}/testing_utils.py +7 -7
- mycorrhizal/{enoki → septum}/util.py +44 -21
- mycorrhizal/spores/__init__.py +3 -3
- mycorrhizal/spores/core.py +149 -28
- mycorrhizal/spores/dsl/__init__.py +8 -8
- mycorrhizal/spores/dsl/hypha.py +3 -15
- mycorrhizal/spores/dsl/rhizomorph.py +3 -11
- mycorrhizal/spores/dsl/{enoki.py → septum.py} +26 -77
- mycorrhizal/spores/encoder/json.py +21 -12
- mycorrhizal/spores/extraction.py +14 -11
- mycorrhizal/spores/models.py +53 -20
- mycorrhizal-0.2.1.dist-info/METADATA +335 -0
- mycorrhizal-0.2.1.dist-info/RECORD +54 -0
- mycorrhizal-0.1.2.dist-info/METADATA +0 -198
- mycorrhizal-0.1.2.dist-info/RECORD +0 -39
- /mycorrhizal/{enoki → septum}/__init__.py +0 -0
- {mycorrhizal-0.1.2.dist-info → mycorrhizal-0.2.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tree builder - collects tree metadata during decoration.
|
|
4
|
+
|
|
5
|
+
This module provides the MyceliumTreeBuilder which is responsible for
|
|
6
|
+
collecting FSM/BT integrations, BT nodes, and other metadata during the
|
|
7
|
+
@tree decoration phase.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Callable, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from .tree_spec import MyceliumTreeSpec, FSMIntegration, BTIntegration, NodeDefinition
|
|
15
|
+
from .exceptions import TreeDefinitionError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MyceliumTreeBuilder:
|
|
19
|
+
"""
|
|
20
|
+
Builder that collects tree metadata during decoration.
|
|
21
|
+
|
|
22
|
+
This is used internally by the @tree decorator to gather all
|
|
23
|
+
FSM/BT integrations, BT nodes, and other metadata before creating
|
|
24
|
+
the final MyceliumTreeSpec.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, name: str, tree_func: Callable):
|
|
28
|
+
self.name = name
|
|
29
|
+
self.tree_func = tree_func
|
|
30
|
+
self.actions: Dict[str, NodeDefinition] = {}
|
|
31
|
+
self.conditions: Dict[str, NodeDefinition] = {}
|
|
32
|
+
self.root_node: Optional[NodeDefinition] = None
|
|
33
|
+
self.fsm_integrations: Dict[str, FSMIntegration] = {}
|
|
34
|
+
self.bt_integrations: Dict[str, BTIntegration] = {}
|
|
35
|
+
|
|
36
|
+
def add_action(self, node_def: NodeDefinition) -> None:
|
|
37
|
+
"""Add an action node definition."""
|
|
38
|
+
# Allow replacing (func() may be called multiple times)
|
|
39
|
+
self.actions[node_def.name] = node_def
|
|
40
|
+
|
|
41
|
+
def add_condition(self, node_def: NodeDefinition) -> None:
|
|
42
|
+
"""Add a condition node definition."""
|
|
43
|
+
# Allow replacing (func() may be called multiple times)
|
|
44
|
+
self.conditions[node_def.name] = node_def
|
|
45
|
+
|
|
46
|
+
def set_root(self, node_def: NodeDefinition) -> None:
|
|
47
|
+
"""Set the root node."""
|
|
48
|
+
# Allow replacing (func() may be called multiple times)
|
|
49
|
+
self.root_node = node_def
|
|
50
|
+
|
|
51
|
+
def add_fsm_integration(self, integration: FSMIntegration) -> None:
|
|
52
|
+
"""Add an FSM integration for an action."""
|
|
53
|
+
self.fsm_integrations[integration.action_name] = integration
|
|
54
|
+
|
|
55
|
+
def add_bt_integration(self, integration: BTIntegration) -> None:
|
|
56
|
+
"""Add a BT integration for a state."""
|
|
57
|
+
self.bt_integrations[integration.state_name] = integration
|
|
58
|
+
|
|
59
|
+
def build(self) -> MyceliumTreeSpec:
|
|
60
|
+
"""
|
|
61
|
+
Build the final MyceliumTreeSpec.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Complete tree specification
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
TreeDefinitionError: If validation fails
|
|
68
|
+
"""
|
|
69
|
+
# Create tree spec
|
|
70
|
+
spec = MyceliumTreeSpec(
|
|
71
|
+
name=self.name,
|
|
72
|
+
tree_func=self.tree_func,
|
|
73
|
+
actions=self.actions,
|
|
74
|
+
conditions=self.conditions,
|
|
75
|
+
root_node=self.root_node,
|
|
76
|
+
fsm_integrations=self.fsm_integrations,
|
|
77
|
+
bt_integrations=self.bt_integrations,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Validate
|
|
81
|
+
errors = spec.validate()
|
|
82
|
+
if errors:
|
|
83
|
+
raise TreeDefinitionError(
|
|
84
|
+
"Tree validation failed:\n" + "\n".join(f" - {e}" for e in errors)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return spec
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Context variable to track current tree builder
|
|
91
|
+
_current_builder: Optional[MyceliumTreeBuilder] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_current_builder() -> Optional[MyceliumTreeBuilder]:
|
|
95
|
+
"""Get the currently active tree builder (if any)."""
|
|
96
|
+
return _current_builder
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def set_current_builder(builder: Optional[MyceliumTreeBuilder]) -> None:
|
|
100
|
+
"""Set the current tree builder."""
|
|
101
|
+
global _current_builder
|
|
102
|
+
_current_builder = builder
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tree specification - metadata about a Mycelium tree.
|
|
4
|
+
|
|
5
|
+
This module defines the data structures that hold tree metadata,
|
|
6
|
+
including FSM/BT integrations, BT nodes, and their relationships.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Dict, List, Optional, Type, Callable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class FSMIntegration:
|
|
17
|
+
"""Metadata about an FSM integrated into a BT action."""
|
|
18
|
+
|
|
19
|
+
initial_state: Type # StateSpec or callable returning StateSpec
|
|
20
|
+
action_name: str # The action that uses this FSM
|
|
21
|
+
|
|
22
|
+
def __post_init__(self):
|
|
23
|
+
"""Validate FSM integration."""
|
|
24
|
+
if not self.action_name:
|
|
25
|
+
raise ValueError("Action name cannot be empty")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class BTIntegration:
|
|
30
|
+
"""Metadata about a BT integrated into an FSM state."""
|
|
31
|
+
|
|
32
|
+
bt_tree: Any # BT namespace from bt.tree()
|
|
33
|
+
state_name: str # The state that uses this BT (fully qualified)
|
|
34
|
+
state_func: Callable # The on_state function
|
|
35
|
+
|
|
36
|
+
def __post_init__(self):
|
|
37
|
+
"""Validate BT integration."""
|
|
38
|
+
if not self.state_name:
|
|
39
|
+
raise ValueError("State name cannot be empty")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class PNIntegration:
|
|
44
|
+
"""Metadata about a BT or FSM integrated into a PN transition."""
|
|
45
|
+
|
|
46
|
+
transition_name: str # The transition that uses this integration
|
|
47
|
+
integration_type: str = "bt" # "bt" or "fsm"
|
|
48
|
+
mode: str = "token" # "token" or "batch" (BT only)
|
|
49
|
+
|
|
50
|
+
# BT integration fields
|
|
51
|
+
bt_tree: Any = None # BT namespace from bt.tree()
|
|
52
|
+
|
|
53
|
+
# FSM integration fields
|
|
54
|
+
initial_state: Any = None # Initial state for FSM (StateSpec or callable)
|
|
55
|
+
|
|
56
|
+
# Output destinations
|
|
57
|
+
output_places: Optional[List[Any]] = None # List of PlaceRef objects (output destinations)
|
|
58
|
+
|
|
59
|
+
def __post_init__(self):
|
|
60
|
+
"""Validate PN integration."""
|
|
61
|
+
if not self.transition_name:
|
|
62
|
+
raise ValueError("Transition name cannot be empty")
|
|
63
|
+
|
|
64
|
+
# Validate integration type
|
|
65
|
+
if self.integration_type not in ("bt", "fsm"):
|
|
66
|
+
raise ValueError(f"Invalid integration_type: {self.integration_type}. Must be 'bt' or 'fsm'")
|
|
67
|
+
|
|
68
|
+
# Validate BT-specific fields
|
|
69
|
+
if self.integration_type == "bt":
|
|
70
|
+
if self.bt_tree is None:
|
|
71
|
+
raise ValueError("BT integration requires bt_tree parameter")
|
|
72
|
+
if self.mode not in ("token", "batch"):
|
|
73
|
+
raise ValueError(f"Invalid mode: {self.mode}. Must be 'token' or 'batch'")
|
|
74
|
+
|
|
75
|
+
# Validate FSM-specific fields
|
|
76
|
+
if self.integration_type == "fsm":
|
|
77
|
+
if self.initial_state is None:
|
|
78
|
+
raise ValueError("FSM integration requires initial_state parameter")
|
|
79
|
+
|
|
80
|
+
# Initialize output_places if None
|
|
81
|
+
if self.output_places is None:
|
|
82
|
+
self.output_places = []
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def fsm_state_name(self) -> str:
|
|
86
|
+
"""Get the name of the FSM initial state for display purposes."""
|
|
87
|
+
if self.initial_state is None:
|
|
88
|
+
return "Unknown"
|
|
89
|
+
|
|
90
|
+
# If it's a StateSpec, extract the name
|
|
91
|
+
if hasattr(self.initial_state, 'name'):
|
|
92
|
+
name = self.initial_state.name
|
|
93
|
+
# Extract just the simple name (last component after dot)
|
|
94
|
+
if '.' in name:
|
|
95
|
+
return name.split('.')[-1]
|
|
96
|
+
return name
|
|
97
|
+
|
|
98
|
+
# Try to get the name from the state
|
|
99
|
+
if hasattr(self.initial_state, '__name__'):
|
|
100
|
+
return self.initial_state.__name__
|
|
101
|
+
|
|
102
|
+
return str(self.initial_state)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class NodeDefinition:
|
|
107
|
+
"""
|
|
108
|
+
Metadata about a BT node defined in a tree.
|
|
109
|
+
|
|
110
|
+
Now includes optional FSM integration for actions.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
name: str
|
|
114
|
+
node_type: str # "action", "condition", "sequence", "selector", "parallel"
|
|
115
|
+
func: Optional[Callable] = None
|
|
116
|
+
children: List[NodeDefinition] = field(default_factory=list)
|
|
117
|
+
is_root: bool = False
|
|
118
|
+
fsm_integration: Optional[FSMIntegration] = None # FSM for actions
|
|
119
|
+
|
|
120
|
+
def __post_init__(self):
|
|
121
|
+
"""Validate node definition."""
|
|
122
|
+
if not self.name:
|
|
123
|
+
raise ValueError("Node name cannot be empty")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class MyceliumTreeSpec:
|
|
128
|
+
"""
|
|
129
|
+
Complete specification of a Mycelium tree.
|
|
130
|
+
|
|
131
|
+
Contains all metadata about FSM/BT integrations, BT nodes, and their relationships.
|
|
132
|
+
This is what gets built by the @tree decorator and used by TreeInstance.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
name: str
|
|
136
|
+
tree_func: Optional[Callable] = None # The original @tree decorated function (for reference)
|
|
137
|
+
bt_namespace: Optional[Any] = None # The BT namespace returned by bt.tree() (set after build)
|
|
138
|
+
actions: Dict[str, NodeDefinition] = field(default_factory=dict)
|
|
139
|
+
conditions: Dict[str, NodeDefinition] = field(default_factory=dict)
|
|
140
|
+
root_node: Optional[NodeDefinition] = None
|
|
141
|
+
# FSM integrations keyed by action name
|
|
142
|
+
fsm_integrations: Dict[str, FSMIntegration] = field(default_factory=dict)
|
|
143
|
+
# BT integrations keyed by state name (fully qualified)
|
|
144
|
+
bt_integrations: Dict[str, BTIntegration] = field(default_factory=dict)
|
|
145
|
+
# PN integrations keyed by transition name (fully qualified)
|
|
146
|
+
pn_integrations: Dict[str, PNIntegration] = field(default_factory=dict)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def fsms(self) -> Dict[str, FSMIntegration]:
|
|
150
|
+
"""
|
|
151
|
+
Access FSM integrations (alias for fsm_integrations).
|
|
152
|
+
|
|
153
|
+
This provides convenient access like:
|
|
154
|
+
spec.fsms['robot']
|
|
155
|
+
"""
|
|
156
|
+
return self.fsm_integrations
|
|
157
|
+
|
|
158
|
+
def get_action(self, name: str) -> Optional[NodeDefinition]:
|
|
159
|
+
"""Get action definition by name."""
|
|
160
|
+
return self.actions.get(name)
|
|
161
|
+
|
|
162
|
+
def get_condition(self, name: str) -> Optional[NodeDefinition]:
|
|
163
|
+
"""Get condition definition by name."""
|
|
164
|
+
return self.conditions.get(name)
|
|
165
|
+
|
|
166
|
+
def get_fsm_integration(self, action_name: str) -> Optional[FSMIntegration]:
|
|
167
|
+
"""Get FSM integration for an action."""
|
|
168
|
+
return self.fsm_integrations.get(action_name)
|
|
169
|
+
|
|
170
|
+
def get_bt_integration(self, state_name: str) -> Optional[BTIntegration]:
|
|
171
|
+
"""Get BT integration for a state."""
|
|
172
|
+
return self.bt_integrations.get(state_name)
|
|
173
|
+
|
|
174
|
+
def get_pn_integration(self, transition_name: str) -> Optional["PNIntegration"]:
|
|
175
|
+
"""Get PN integration for a transition."""
|
|
176
|
+
return self.pn_integrations.get(transition_name)
|
|
177
|
+
|
|
178
|
+
def validate(self) -> List[str]:
|
|
179
|
+
"""
|
|
180
|
+
Validate the tree specification.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
List of error messages (empty if valid)
|
|
184
|
+
"""
|
|
185
|
+
errors = []
|
|
186
|
+
|
|
187
|
+
# Check root node exists
|
|
188
|
+
if self.root_node is None:
|
|
189
|
+
errors.append("Tree has no root node")
|
|
190
|
+
|
|
191
|
+
# Check action names don't collide with condition names
|
|
192
|
+
all_names = list(self.actions.keys()) + list(self.conditions.keys())
|
|
193
|
+
duplicates = [name for name in set(all_names) if all_names.count(name) > 1]
|
|
194
|
+
if duplicates:
|
|
195
|
+
errors.append(f"Duplicate names: {duplicates}")
|
|
196
|
+
|
|
197
|
+
return errors
|
mycorrhizal/rhizomorph/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
Asyncio-friendly **Behavior Trees** for Python, designed for clarity, composability, and ergonomics in asynchronous systems.
|
|
5
5
|
|
|
6
|
-
- **Fluent decorator chains**: `bt.failer().gate(
|
|
6
|
+
- **Fluent decorator chains**: `bt.failer().gate(condition).timeout(0.5)(action)`
|
|
7
7
|
- **Owner-aware composites**: factories expand with the correct class owner, so large trees can be split across modules
|
|
8
|
-
- **Cross-tree composition**: embed full subtrees with `bt.subtree()`
|
|
8
|
+
- **Cross-tree composition**: embed full subtrees with `bt.subtree()`
|
|
9
9
|
- **Mermaid diagrams**: export static structure diagrams for documentation/debugging
|
|
10
10
|
- **Timebases**: simulate or control time with pluggable clocks (wall/UTC/monotonic/cycle/dictated)
|
|
11
11
|
|
|
@@ -34,9 +34,9 @@ def Patrol():
|
|
|
34
34
|
|
|
35
35
|
@bt.root
|
|
36
36
|
@bt.sequence(memory=True)
|
|
37
|
-
def root(
|
|
38
|
-
yield
|
|
39
|
-
yield bt.timeout(1.0)(
|
|
37
|
+
def root():
|
|
38
|
+
yield has_waypoints
|
|
39
|
+
yield bt.timeout(1.0)(go_to_next)
|
|
40
40
|
|
|
41
41
|
async def main():
|
|
42
42
|
bb = BB(waypoints=3)
|
|
@@ -55,15 +55,15 @@ async def main():
|
|
|
55
55
|
Decorator wrappers are available via **fluent chains** that read left→right and apply to a child when called at the end:
|
|
56
56
|
|
|
57
57
|
```python
|
|
58
|
-
yield bt.failer().gate(
|
|
58
|
+
yield bt.failer().gate(battery_ok).timeout(0.25)(engage)
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
**Reads left→right:**
|
|
62
62
|
|
|
63
63
|
1. `failer()` → forces **FAILURE** once the child completes (whether the child succeeds or fails). While the child is RUNNING, RUNNING bubbles.
|
|
64
|
-
2. `gate(
|
|
64
|
+
2. `gate(battery_ok)` → only ticks the child if `battery_ok` returns SUCCESS. If the condition returns RUNNING, the whole gate is RUNNING. If it returns FAILURE, the gate returns FAILURE without ticking the child.
|
|
65
65
|
3. `timeout(0.25)` → if the child does not finish before 0.25 (per the configured timebase), it is cancelled/reset and the decorator returns FAILURE.
|
|
66
|
-
4. Applied to `
|
|
66
|
+
4. Applied to `engage`.
|
|
67
67
|
|
|
68
68
|
### Wrapper Cheat‑Sheet
|
|
69
69
|
|
|
@@ -75,7 +75,7 @@ yield bt.failer().gate(N.battery_ok).timeout(0.25)(N.engage)
|
|
|
75
75
|
| `timeout()` | `bt.timeout(seconds: float)` | RUNNING until deadline; then **FAILURE** | Child status on completion | Child status on completion (unless deadline hit) | Uses the tree’s `Timebase`; cancels child when expired |
|
|
76
76
|
| `retry()` | `bt.retry(max_attempts: int, retry_on=(FAILURE, ERROR))` | Pass-through | **SUCCESS** and resets attempt count | Retries while child in `retry_on` until attempts exhausted → **FAILURE** | Does not retry on RUNNING or CANCELLED by default |
|
|
77
77
|
| `ratelimit()` | `bt.ratelimit(hz: float=None, period: float=None)` | If child is currently RUNNING, pass-through (no throttling) | Propagates child result and schedules next allowed start | If throttled: returns **RUNNING** (child not started) | Accepts exactly one of `hz` or `period` |
|
|
78
|
-
| `gate()` | `bt.gate(condition: NodeSpec | @bt.condition)` | If condition RUNNING → **RUNNING** | Ticks child and propagates its terminal status | If condition not SUCCESS → **FAILURE** | Condition is
|
|
78
|
+
| `gate()` | `bt.gate(condition: NodeSpec | @bt.condition)` | If condition RUNNING → **RUNNING** | Ticks child and propagates its terminal status | If condition not SUCCESS → **FAILURE** | Condition node is resolved from the owning tree |
|
|
79
79
|
|
|
80
80
|
> Tip: You can compose wrappers in any order. The chain is applied **outside‑in** (the leftmost wrapper becomes the outermost decorator).
|
|
81
81
|
|
|
@@ -93,9 +93,9 @@ yield bt.failer().gate(N.battery_ok).timeout(0.25)(N.engage)
|
|
|
93
93
|
- Truthy → `SUCCESS`, falsy → `FAILURE`.
|
|
94
94
|
- Returning a `Status` is allowed but discouraged; use plain booleans.
|
|
95
95
|
|
|
96
|
-
### Composites
|
|
96
|
+
### Composites
|
|
97
97
|
|
|
98
|
-
Define composites as **generator factories** that yield children.
|
|
98
|
+
Define composites as **generator factories** that yield children. Node references are resolved directly by name from the owning tree.
|
|
99
99
|
|
|
100
100
|
- `@bt.sequence(memory: bool = True)`
|
|
101
101
|
**AND**: ticks children in order.
|
|
@@ -104,11 +104,12 @@ Define composites as **generator factories** that yield children. The factory re
|
|
|
104
104
|
- All `SUCCESS` → `SUCCESS`.
|
|
105
105
|
- With `memory=True`, on a subsequent tick it **resumes from the last RUNNING child**. With `memory=False`, it restarts from the first child each tick.
|
|
106
106
|
|
|
107
|
-
- `@bt.selector(memory: bool = True)`
|
|
108
|
-
**OR**: ticks children until one returns `SUCCESS`.
|
|
109
|
-
- `RUNNING` bubbles (and may set/resume index depending on `memory
|
|
110
|
-
- If none succeed → `FAILURE`.
|
|
111
|
-
- With `memory=True`, the selector **remembers the last RUNNING child** and resumes there next tick.
|
|
107
|
+
- `@bt.selector(memory: bool = True)`
|
|
108
|
+
**OR**: ticks children until one returns `SUCCESS`.
|
|
109
|
+
- `RUNNING` bubbles (and may set/resume index depending on `memory`).
|
|
110
|
+
- If none succeed → `FAILURE`.
|
|
111
|
+
- With `memory=True`, the selector **remembers the last RUNNING child** and resumes there next tick.
|
|
112
|
+
- With `memory=False` (reactive mode), the selector **re-evaluates from the first child** each tick.
|
|
112
113
|
|
|
113
114
|
|
|
114
115
|
- `@bt.parallel(success_threshold: int, failure_threshold: Optional[int] = None)`
|
|
@@ -118,16 +119,15 @@ Define composites as **generator factories** that yield children. The factory re
|
|
|
118
119
|
- Otherwise `RUNNING`.
|
|
119
120
|
- Default `failure_threshold = n - success_threshold + 1` (i.e., once success is impossible).
|
|
120
121
|
|
|
121
|
-
#### On
|
|
122
|
+
#### On node resolution
|
|
122
123
|
|
|
123
|
-
Inside diagramming and building, composites are expanded with the correct **owner class** context. This lets you split behavior across modules while keeping references
|
|
124
|
+
Inside diagramming and building, composites are expanded with the correct **owner class** context. This lets you split behavior across modules while keeping references type‑safe and string‑free.
|
|
124
125
|
|
|
125
126
|
---
|
|
126
127
|
|
|
127
128
|
## Cross‑Tree Composition
|
|
128
129
|
|
|
129
|
-
- `bt.subtree(OtherTree)` — mounts another tree
|
|
130
|
-
- `bt.bind(Owner, Owner.Composite)` — borrows a composite from another class and expands it using that **Owner** as the context.
|
|
130
|
+
- `bt.subtree(OtherTree)` — mounts another tree's `ROOT` as a child; for Mermaid export the `ROOT` spec is attached so it expands visually.
|
|
131
131
|
|
|
132
132
|
**Example**
|
|
133
133
|
|
|
@@ -143,9 +143,9 @@ def Engage():
|
|
|
143
143
|
|
|
144
144
|
@bt.root
|
|
145
145
|
@bt.sequence(memory=False)
|
|
146
|
-
def engage_threat(
|
|
147
|
-
yield
|
|
148
|
-
yield bt.failer().timeout(0.25)(
|
|
146
|
+
def engage_threat():
|
|
147
|
+
yield threat_detected
|
|
148
|
+
yield bt.failer().timeout(0.25)(engage)
|
|
149
149
|
|
|
150
150
|
# Telemetry subtree
|
|
151
151
|
@bt.tree
|
|
@@ -155,17 +155,17 @@ def Telemetry():
|
|
|
155
155
|
|
|
156
156
|
@bt.root
|
|
157
157
|
@bt.sequence()
|
|
158
|
-
def telemetry_seq(
|
|
159
|
-
yield bt.ratelimit(hz=2.0)(
|
|
158
|
+
def telemetry_seq():
|
|
159
|
+
yield bt.ratelimit(hz=2.0)(push)
|
|
160
160
|
|
|
161
|
-
# Main tree
|
|
161
|
+
# Main tree - compose both subtrees
|
|
162
162
|
@bt.tree
|
|
163
163
|
def Main():
|
|
164
164
|
@bt.root
|
|
165
|
-
@bt.selector(
|
|
166
|
-
def root(
|
|
165
|
+
@bt.selector(memory=False)
|
|
166
|
+
def root():
|
|
167
167
|
yield bt.subtree(Engage)
|
|
168
|
-
yield bt.
|
|
168
|
+
yield bt.subtree(Telemetry)
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
---
|
|
@@ -183,7 +183,56 @@ diagram = MainTree.to_mermaid()
|
|
|
183
183
|
print(diagram)
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
-
Paste the output into the Mermaid Live Editor.
|
|
186
|
+
Paste the output into the Mermaid Live Editor or any Markdown viewer that supports Mermaid.
|
|
187
|
+
|
|
188
|
+
### Example Diagram
|
|
189
|
+
|
|
190
|
+
Here's an example behavior tree diagram showing a threat response system:
|
|
191
|
+
|
|
192
|
+
```mermaid
|
|
193
|
+
flowchart TD
|
|
194
|
+
N1["Selector<br/>root"]
|
|
195
|
+
N1 --> N2
|
|
196
|
+
N2["Subtree<br/>Engage"]
|
|
197
|
+
N2 --> N3
|
|
198
|
+
N3["Sequence<br/>engage_threat"]
|
|
199
|
+
N3 --> N4
|
|
200
|
+
N4((CONDITION<br/>threat_detected))
|
|
201
|
+
N3 --> N5
|
|
202
|
+
N5["Decor<br/>Failer(Gate(cond=battery_ok)(Timeout(0.12s)(engage)))"]
|
|
203
|
+
N5 --> N6
|
|
204
|
+
N6["Decor<br/>Gate(cond=battery_ok)(Timeout(0.12s)(engage))"]
|
|
205
|
+
N6 --> N7
|
|
206
|
+
N7["Decor<br/>Timeout(0.12s)(engage)"]
|
|
207
|
+
N7 --> N8
|
|
208
|
+
N8((ACTION<br/>engage))
|
|
209
|
+
N1 --> N9
|
|
210
|
+
N9["Sequence<br/>patrol"]
|
|
211
|
+
N9 --> N10
|
|
212
|
+
N10((CONDITION<br/>has_waypoints))
|
|
213
|
+
N9 --> N11
|
|
214
|
+
N11((ACTION<br/>go_to_next))
|
|
215
|
+
N9 --> N12
|
|
216
|
+
N12["Decor<br/>Succeeder(Retry(3)(Timeout(1.0s)(scan_area)))"]
|
|
217
|
+
N12 --> N13
|
|
218
|
+
N13["Decor<br/>Retry(3)(Timeout(1.0s)(scan_area))"]
|
|
219
|
+
N13 --> N14
|
|
220
|
+
N14["Decor<br/>Timeout(1.0s)(scan_area)"]
|
|
221
|
+
N14 --> N15
|
|
222
|
+
N15((ACTION<br/>scan_area))
|
|
223
|
+
N1 --> N16
|
|
224
|
+
N16["Decor<br/>Failer(RateLimit(0.200000s)(telemetry_push))"]
|
|
225
|
+
N16 --> N17
|
|
226
|
+
N17["Decor<br/>RateLimit(0.200000s)(telemetry_push)"]
|
|
227
|
+
N17 --> N18
|
|
228
|
+
N18((ACTION<br/>telemetry_push))
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
This diagram shows:
|
|
232
|
+
- A **Selector** root that chooses between Engage, Patrol, or Telemetry
|
|
233
|
+
- **Decorator chains** like `Failer(Gate(Timeout(engage)))` shown as nested decorators
|
|
234
|
+
- **Composite nodes** (Sequence, Selector) with their children
|
|
235
|
+
- **Leaf nodes** (Conditions, Actions) clearly marked
|
|
187
236
|
|
|
188
237
|
---
|
|
189
238
|
|
|
@@ -208,9 +257,9 @@ Paste the output into the Mermaid Live Editor.
|
|
|
208
257
|
|
|
209
258
|
## Design Principles
|
|
210
259
|
|
|
211
|
-
- **Async‑first** — every node can be `async def`; scheduling is `asyncio`‑based.
|
|
212
|
-
- **String‑free** — reference nodes
|
|
213
|
-
- **Composable** — split and recombine trees across modules with `subtree
|
|
260
|
+
- **Async‑first** — every node can be `async def`; scheduling is `asyncio`‑based.
|
|
261
|
+
- **String‑free** — reference nodes directly by name (no magic strings).
|
|
262
|
+
- **Composable** — split and recombine trees across modules with `subtree`.
|
|
214
263
|
- **Fluent‑only** — decorator chains read naturally left→right.
|
|
215
264
|
|
|
216
265
|
---
|