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.
Files changed (41) hide show
  1. mycorrhizal/_version.py +1 -1
  2. mycorrhizal/common/__init__.py +15 -3
  3. mycorrhizal/common/cache.py +114 -0
  4. mycorrhizal/common/compilation.py +263 -0
  5. mycorrhizal/common/interface_detection.py +159 -0
  6. mycorrhizal/common/interfaces.py +3 -50
  7. mycorrhizal/common/mermaid.py +124 -0
  8. mycorrhizal/common/wrappers.py +1 -1
  9. mycorrhizal/hypha/core/builder.py +11 -1
  10. mycorrhizal/hypha/core/runtime.py +242 -107
  11. mycorrhizal/mycelium/__init__.py +174 -0
  12. mycorrhizal/mycelium/core.py +619 -0
  13. mycorrhizal/mycelium/exceptions.py +30 -0
  14. mycorrhizal/mycelium/hypha_bridge.py +1143 -0
  15. mycorrhizal/mycelium/instance.py +440 -0
  16. mycorrhizal/mycelium/pn_context.py +276 -0
  17. mycorrhizal/mycelium/runner.py +165 -0
  18. mycorrhizal/mycelium/spores_integration.py +655 -0
  19. mycorrhizal/mycelium/tree_builder.py +102 -0
  20. mycorrhizal/mycelium/tree_spec.py +197 -0
  21. mycorrhizal/rhizomorph/README.md +82 -33
  22. mycorrhizal/rhizomorph/core.py +287 -119
  23. mycorrhizal/septum/TRANSITION_REFERENCE.md +385 -0
  24. mycorrhizal/{enoki → septum}/core.py +326 -100
  25. mycorrhizal/{enoki → septum}/testing_utils.py +7 -7
  26. mycorrhizal/{enoki → septum}/util.py +44 -21
  27. mycorrhizal/spores/__init__.py +3 -3
  28. mycorrhizal/spores/core.py +149 -28
  29. mycorrhizal/spores/dsl/__init__.py +8 -8
  30. mycorrhizal/spores/dsl/hypha.py +3 -15
  31. mycorrhizal/spores/dsl/rhizomorph.py +3 -11
  32. mycorrhizal/spores/dsl/{enoki.py → septum.py} +26 -77
  33. mycorrhizal/spores/encoder/json.py +21 -12
  34. mycorrhizal/spores/extraction.py +14 -11
  35. mycorrhizal/spores/models.py +53 -20
  36. mycorrhizal-0.2.1.dist-info/METADATA +335 -0
  37. mycorrhizal-0.2.1.dist-info/RECORD +54 -0
  38. mycorrhizal-0.1.2.dist-info/METADATA +0 -198
  39. mycorrhizal-0.1.2.dist-info/RECORD +0 -39
  40. /mycorrhizal/{enoki → septum}/__init__.py +0 -0
  41. {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
@@ -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(N.condition).timeout(0.5)(N.action)`
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()` or borrow composites with `bt.bind()`
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(N):
38
- yield N.has_waypoints
39
- yield bt.timeout(1.0)(N.go_to_next)
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(N.battery_ok).timeout(0.25)(N.engage)
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(N.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.
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 `N.engage`.
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 built owner‑aware under the hood |
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 (owner‑aware factories)
96
+ ### Composites
97
97
 
98
- Define composites as **generator factories** that yield children. The factory receives the **owner class** `N`, so you reference members with `N.<member>` (no strings).
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`/`reactive`).
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 owner‑aware expansion
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 (`N.something`) type‑safe and string‑free.
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 trees `ROOT` as a child; for Mermaid export the `ROOT` spec is attached so it expands visually.
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(N):
147
- yield N.threat_detected
148
- yield bt.failer().timeout(0.25)(N.engage)
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(N):
159
- yield bt.ratelimit(hz=2.0)(N.push)
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(reactive=True, memory=True)
166
- def root(N):
165
+ @bt.selector(memory=False)
166
+ def root():
167
167
  yield bt.subtree(Engage)
168
- yield bt.bind(Telemetry, Telemetry.telemetry_seq)
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 via `N.<member>` (no magic strings).
213
- - **Composable** — split and recombine trees across modules with `subtree`/`bind`.
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
  ---