mycorrhizal 0.1.0__py3-none-any.whl → 0.2.0__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 -0
- 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 +56 -8
- mycorrhizal/hypha/core/runtime.py +242 -107
- mycorrhizal/hypha/core/specs.py +19 -3
- 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 +308 -82
- 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 +72 -19
- mycorrhizal/spores/core.py +907 -75
- 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 +75 -20
- mycorrhizal/spores/transport/__init__.py +9 -2
- mycorrhizal/spores/transport/base.py +36 -17
- mycorrhizal/spores/transport/file.py +126 -0
- mycorrhizal-0.2.0.dist-info/METADATA +335 -0
- mycorrhizal-0.2.0.dist-info/RECORD +54 -0
- mycorrhizal-0.1.0.dist-info/METADATA +0 -198
- mycorrhizal-0.1.0.dist-info/RECORD +0 -37
- /mycorrhizal/{enoki → septum}/__init__.py +0 -0
- {mycorrhizal-0.1.0.dist-info → mycorrhizal-0.2.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Core Mycelium decorators - @tree, @Action, @Condition, @on_state
|
|
4
|
+
|
|
5
|
+
This module provides the main user-facing decorators for creating
|
|
6
|
+
unified Mycelium trees with FSMs and BTs.
|
|
7
|
+
|
|
8
|
+
It also mirrors the Septum API (state, events, on_state, transitions) for
|
|
9
|
+
convenience and to support BT integration in FSM states.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
# BT decorators
|
|
16
|
+
"tree",
|
|
17
|
+
"Action",
|
|
18
|
+
"Condition",
|
|
19
|
+
"Sequence",
|
|
20
|
+
"Selector",
|
|
21
|
+
"Parallel",
|
|
22
|
+
"root",
|
|
23
|
+
# Mirrored Septum API
|
|
24
|
+
"state",
|
|
25
|
+
"events",
|
|
26
|
+
"on_state",
|
|
27
|
+
"transitions",
|
|
28
|
+
# Septum types (re-exported)
|
|
29
|
+
"LabeledTransition",
|
|
30
|
+
"StateConfiguration",
|
|
31
|
+
"Push",
|
|
32
|
+
"Pop",
|
|
33
|
+
"Again",
|
|
34
|
+
"Unhandled",
|
|
35
|
+
"Retry",
|
|
36
|
+
"Restart",
|
|
37
|
+
"Repeat",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
import inspect
|
|
41
|
+
import functools
|
|
42
|
+
from typing import Callable, Type, TYPE_CHECKING, Optional
|
|
43
|
+
from enum import Enum
|
|
44
|
+
|
|
45
|
+
from .tree_spec import FSMIntegration, BTIntegration, NodeDefinition
|
|
46
|
+
from .tree_builder import MyceliumTreeBuilder, get_current_builder, set_current_builder
|
|
47
|
+
from .exceptions import TreeDefinitionError
|
|
48
|
+
|
|
49
|
+
# Re-export BT decorators for convenience
|
|
50
|
+
from ..rhizomorph.core import bt, Status
|
|
51
|
+
|
|
52
|
+
# Import Septum decorators for mirroring
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
from ..septum.core import _SeptumDecoratorAPI as SeptumDecoratorAPI
|
|
55
|
+
else:
|
|
56
|
+
from ..septum.core import _SeptumDecoratorAPI as SeptumDecoratorAPI
|
|
57
|
+
|
|
58
|
+
# Import Septum types for re-export
|
|
59
|
+
from ..septum.core import (
|
|
60
|
+
LabeledTransition,
|
|
61
|
+
StateConfiguration,
|
|
62
|
+
Push,
|
|
63
|
+
Pop,
|
|
64
|
+
Again,
|
|
65
|
+
Unhandled,
|
|
66
|
+
Retry,
|
|
67
|
+
Restart,
|
|
68
|
+
Repeat,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Create a singleton SeptumDecoratorAPI instance for accessing decorators
|
|
72
|
+
_septum = SeptumDecoratorAPI()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ======================================================================================
|
|
76
|
+
# @tree decorator
|
|
77
|
+
# ======================================================================================
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def tree(func: Callable) -> Callable:
|
|
81
|
+
"""
|
|
82
|
+
Decorator to define a Mycelium tree.
|
|
83
|
+
|
|
84
|
+
A tree can contain BT actions (@Action), BT conditions (@Condition),
|
|
85
|
+
BT composites (@Sequence, @Selector), and FSM/BT integrations.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> @tree
|
|
89
|
+
>>> def RobotController():
|
|
90
|
+
>>> @Action(fsm=TaskFSM)
|
|
91
|
+
>>> async def process(bb, tb, fsm_state):
|
|
92
|
+
>>> if "Idle" in fsm_state.name:
|
|
93
|
+
>>> return Status.SUCCESS
|
|
94
|
+
>>> return Status.RUNNING
|
|
95
|
+
>>>
|
|
96
|
+
>>> @root
|
|
97
|
+
>>> @Sequence
|
|
98
|
+
>>> def main():
|
|
99
|
+
>>> yield process
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
# Build the tree spec builder
|
|
103
|
+
builder = MyceliumTreeBuilder(
|
|
104
|
+
name=func.__name__,
|
|
105
|
+
tree_func=func,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Set as current builder so @Action, @Condition, @root can register
|
|
109
|
+
set_current_builder(builder)
|
|
110
|
+
|
|
111
|
+
# Use bt.tree to create the BT namespace
|
|
112
|
+
# This calls func() which registers all BT nodes with the builder
|
|
113
|
+
try:
|
|
114
|
+
bt_namespace = bt.tree(func)
|
|
115
|
+
finally:
|
|
116
|
+
# Clear current builder
|
|
117
|
+
set_current_builder(None)
|
|
118
|
+
|
|
119
|
+
# Now build the Mycelium spec with all the registered nodes and integrations
|
|
120
|
+
spec = builder.build()
|
|
121
|
+
|
|
122
|
+
# Update spec with BT namespace
|
|
123
|
+
spec.bt_namespace = bt_namespace
|
|
124
|
+
spec.tree_func = func
|
|
125
|
+
|
|
126
|
+
# Attach spec to the namespace
|
|
127
|
+
bt_namespace._mycelium_spec = spec # type: ignore[attr-defined]
|
|
128
|
+
|
|
129
|
+
# Also attach to original function for direct access
|
|
130
|
+
func._mycelium_spec = spec # type: ignore[attr-defined]
|
|
131
|
+
|
|
132
|
+
return bt_namespace # type: ignore[return-value]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ======================================================================================
|
|
136
|
+
# @Action decorator with FSM integration
|
|
137
|
+
# ======================================================================================
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def Action(func=None, *, fsm=None):
|
|
141
|
+
"""
|
|
142
|
+
Decorator to define a BT action within a tree.
|
|
143
|
+
|
|
144
|
+
Actions can optionally include an FSM integration via the `fsm` parameter.
|
|
145
|
+
When `fsm` is provided, the FSM will auto-tick each time the action runs,
|
|
146
|
+
and the resulting state will be passed to the action.
|
|
147
|
+
|
|
148
|
+
Examples:
|
|
149
|
+
>>> # Vanilla action (portable)
|
|
150
|
+
>>> @Action
|
|
151
|
+
>>> async def my_action(bb, tb):
|
|
152
|
+
>>> return Status.SUCCESS
|
|
153
|
+
>>>
|
|
154
|
+
>>> # Action with FSM integration
|
|
155
|
+
>>> @Action(fsm=TaskFSM)
|
|
156
|
+
>>> async def process(bb, tb, fsm_state):
|
|
157
|
+
>>> # fsm_state is where the FSM came to rest after ticking
|
|
158
|
+
>>> if "Idle" in fsm_state.name:
|
|
159
|
+
>>> return Status.SUCCESS
|
|
160
|
+
>>> return Status.RUNNING
|
|
161
|
+
"""
|
|
162
|
+
def decorator(f: Callable) -> Callable:
|
|
163
|
+
builder = get_current_builder()
|
|
164
|
+
if builder is None:
|
|
165
|
+
raise TreeDefinitionError(
|
|
166
|
+
f"@Action decorator used outside of @tree. "
|
|
167
|
+
f"Action '{f.__name__}' must be defined inside a @tree."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Check if function has FSM integration
|
|
171
|
+
has_fsm = fsm is not None
|
|
172
|
+
|
|
173
|
+
# Determine signature
|
|
174
|
+
sig = inspect.signature(f)
|
|
175
|
+
params = list(sig.parameters.keys())
|
|
176
|
+
|
|
177
|
+
# Validate signature based on whether FSM is integrated
|
|
178
|
+
if has_fsm:
|
|
179
|
+
# FSM actions should have (bb, tb, fsm_runner) signature
|
|
180
|
+
if len(params) < 3:
|
|
181
|
+
raise TreeDefinitionError(
|
|
182
|
+
f"Action '{f.__name__}' has FSM integration but incorrect signature. "
|
|
183
|
+
f"Expected (bb, tb, fsm_runner), got {params}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Create FSM integration if needed
|
|
187
|
+
fsm_integration = None
|
|
188
|
+
if has_fsm:
|
|
189
|
+
if fsm is None:
|
|
190
|
+
raise ValueError(f"FSM integration requested but fsm parameter is None for action '{f.__name__}'")
|
|
191
|
+
fsm_integration = FSMIntegration(
|
|
192
|
+
initial_state=fsm, # type: ignore[arg-type]
|
|
193
|
+
action_name=f.__name__,
|
|
194
|
+
)
|
|
195
|
+
builder.add_fsm_integration(fsm_integration)
|
|
196
|
+
|
|
197
|
+
# Store action name for runtime FSM lookup
|
|
198
|
+
action_name_for_fsm = f.__name__ if has_fsm else None
|
|
199
|
+
|
|
200
|
+
# Create BT action that wraps the original function
|
|
201
|
+
async def bt_action(bb, tb):
|
|
202
|
+
# FSM integration: tick FSM and pass result
|
|
203
|
+
if has_fsm:
|
|
204
|
+
# Get tree instance from blackboard (injected by TreeInstance)
|
|
205
|
+
tree_instance = getattr(bb, '_mycelium_tree_instance', None)
|
|
206
|
+
if tree_instance is None:
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
f"Action '{f.__name__}' has FSM integration but no tree instance available"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Get FSM runner for this action
|
|
212
|
+
fsm_runner = tree_instance.get_fsm_runner(action_name_for_fsm)
|
|
213
|
+
if fsm_runner is None:
|
|
214
|
+
raise RuntimeError(
|
|
215
|
+
f"Action '{f.__name__}' has FSM integration but no FSM runner found"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Tick the FSM with timeout=0 (non-blocking)
|
|
219
|
+
_ = await fsm_runner.tick(timeout=0)
|
|
220
|
+
|
|
221
|
+
# Call the original function with FSM runner (has current_state + send_message)
|
|
222
|
+
return await f(bb, tb, fsm_runner)
|
|
223
|
+
else:
|
|
224
|
+
# Vanilla action - just call the function
|
|
225
|
+
return await f(bb, tb)
|
|
226
|
+
|
|
227
|
+
# Set name BEFORE applying @bt.action decorator
|
|
228
|
+
bt_action.__name__ = f.__name__
|
|
229
|
+
bt_action.__qualname__ = f.__qualname__
|
|
230
|
+
|
|
231
|
+
# Now apply the BT decorator
|
|
232
|
+
bt_action = bt.action(bt_action)
|
|
233
|
+
|
|
234
|
+
# Create node definition with FSM integration
|
|
235
|
+
node_def = NodeDefinition(
|
|
236
|
+
name=f.__name__,
|
|
237
|
+
node_type="action",
|
|
238
|
+
func=f, # Store original function
|
|
239
|
+
fsm_integration=fsm_integration,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Store metadata for later
|
|
243
|
+
bt_action._mycelium_node_def = node_def # type: ignore[attr-defined]
|
|
244
|
+
bt_action._original_func = f # type: ignore[attr-defined]
|
|
245
|
+
bt_action._has_fsm_integration = has_fsm # type: ignore[attr-defined]
|
|
246
|
+
f._bt_action = bt_action # type: ignore[attr-defined]
|
|
247
|
+
|
|
248
|
+
# Register with builder
|
|
249
|
+
builder.add_action(node_def)
|
|
250
|
+
|
|
251
|
+
return bt_action
|
|
252
|
+
|
|
253
|
+
# Support both @Action and @Action(fsm=...) syntax
|
|
254
|
+
if func is None:
|
|
255
|
+
# Called with arguments: @Action(fsm=...)
|
|
256
|
+
return decorator
|
|
257
|
+
else:
|
|
258
|
+
# Called without arguments: @Action
|
|
259
|
+
return decorator(func)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ======================================================================================
|
|
263
|
+
# @Condition decorator
|
|
264
|
+
# ======================================================================================
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def Condition(func: Callable) -> Callable:
|
|
268
|
+
"""
|
|
269
|
+
Decorator to define a BT condition within a tree.
|
|
270
|
+
|
|
271
|
+
Conditions are like actions but return bool instead of Status.
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
>>> @Condition
|
|
275
|
+
>>> def has_tasks(bb):
|
|
276
|
+
>>> return len(bb.task_queue) > 0
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
builder = get_current_builder()
|
|
280
|
+
if builder is None:
|
|
281
|
+
raise TreeDefinitionError(
|
|
282
|
+
f"@Condition decorator used outside of @tree. "
|
|
283
|
+
f"Condition '{func.__name__}' must be defined inside a @tree."
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Create node definition
|
|
287
|
+
node_def = NodeDefinition(
|
|
288
|
+
name=func.__name__,
|
|
289
|
+
node_type="condition",
|
|
290
|
+
func=func,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Also register with BT system
|
|
294
|
+
def bt_condition(bb):
|
|
295
|
+
return func(bb)
|
|
296
|
+
|
|
297
|
+
# Set name BEFORE applying @bt.condition decorator
|
|
298
|
+
bt_condition.__name__ = func.__name__
|
|
299
|
+
bt_condition.__qualname__ = func.__qualname__
|
|
300
|
+
|
|
301
|
+
# Apply the BT decorator
|
|
302
|
+
bt_condition = bt.condition(bt_condition)
|
|
303
|
+
|
|
304
|
+
# Store metadata
|
|
305
|
+
bt_condition._mycelium_node_def = node_def # type: ignore[attr-defined]
|
|
306
|
+
bt_condition._original_func = func # type: ignore[attr-defined]
|
|
307
|
+
func._bt_condition = bt_condition # type: ignore[attr-defined]
|
|
308
|
+
|
|
309
|
+
# Register with builder
|
|
310
|
+
builder.add_condition(node_def)
|
|
311
|
+
|
|
312
|
+
return bt_condition
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ======================================================================================
|
|
316
|
+
# BT Composite decorators
|
|
317
|
+
# ======================================================================================
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def Sequence(func: Callable) -> Callable:
|
|
321
|
+
"""
|
|
322
|
+
Decorator to define a sequence composite.
|
|
323
|
+
|
|
324
|
+
Children are executed in order until one fails.
|
|
325
|
+
"""
|
|
326
|
+
return bt.sequence(func)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def Selector(func: Callable) -> Callable:
|
|
330
|
+
"""
|
|
331
|
+
Decorator to define a selector composite.
|
|
332
|
+
|
|
333
|
+
Children are executed in order until one succeeds.
|
|
334
|
+
"""
|
|
335
|
+
return bt.selector(func)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def Parallel(func: Optional[Callable] = None, *, success_threshold: int = 1):
|
|
339
|
+
"""
|
|
340
|
+
Decorator to define a parallel composite.
|
|
341
|
+
|
|
342
|
+
All children are executed simultaneously.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
func: The generator function (optional for decorator with args)
|
|
346
|
+
success_threshold: Minimum number of children that must succeed
|
|
347
|
+
|
|
348
|
+
Examples:
|
|
349
|
+
>>> @Parallel
|
|
350
|
+
>>> @Parallel(success_threshold=2)
|
|
351
|
+
"""
|
|
352
|
+
def decorator(f: Callable) -> Callable:
|
|
353
|
+
return bt.parallel(success_threshold=success_threshold)(f)
|
|
354
|
+
|
|
355
|
+
if func is None:
|
|
356
|
+
# Called with arguments: @Parallel(success_threshold=2)
|
|
357
|
+
return decorator
|
|
358
|
+
else:
|
|
359
|
+
# Called without arguments: @Parallel
|
|
360
|
+
return decorator(func)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# ======================================================================================
|
|
364
|
+
# @root decorator
|
|
365
|
+
# ======================================================================================
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def root(func: Callable) -> Callable:
|
|
369
|
+
"""
|
|
370
|
+
Decorator to mark the root node of a tree.
|
|
371
|
+
|
|
372
|
+
Must be used once per tree.
|
|
373
|
+
|
|
374
|
+
Example:
|
|
375
|
+
>>> @root
|
|
376
|
+
>>> @Sequence
|
|
377
|
+
>>> def main():
|
|
378
|
+
>>> yield action1
|
|
379
|
+
>>> yield action2
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
builder = get_current_builder()
|
|
383
|
+
if builder is None:
|
|
384
|
+
raise TreeDefinitionError(
|
|
385
|
+
f"@root decorator used outside of @tree. "
|
|
386
|
+
f"Root '{func.__name__}' must be defined inside a @tree."
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Mark as root in the BT system
|
|
390
|
+
bt_root = bt.root(func) # type: ignore[call-arg]
|
|
391
|
+
|
|
392
|
+
# Create node definition
|
|
393
|
+
node_def = NodeDefinition(
|
|
394
|
+
name=func.__name__,
|
|
395
|
+
node_type="root",
|
|
396
|
+
func=func,
|
|
397
|
+
is_root=True,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Store metadata
|
|
401
|
+
bt_root._mycelium_node_def = node_def # type: ignore[attr-defined]
|
|
402
|
+
|
|
403
|
+
# Register with builder
|
|
404
|
+
builder.set_root(node_def)
|
|
405
|
+
|
|
406
|
+
return bt_root
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ======================================================================================
|
|
410
|
+
# Mirrored Septum API - state, events, on_state, transitions
|
|
411
|
+
# ======================================================================================
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# Global registry to track BT integrations for states
|
|
415
|
+
_state_bt_integrations: dict[str, BTIntegration] = {}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def state(bt=None, **kwargs):
|
|
419
|
+
"""
|
|
420
|
+
Decorator to define a state (mirrors @septum.state).
|
|
421
|
+
|
|
422
|
+
Supports optional BT integration via the `bt` parameter.
|
|
423
|
+
When `bt` is provided, the state's on_state handler will automatically
|
|
424
|
+
receive the BT result as a parameter.
|
|
425
|
+
|
|
426
|
+
Examples:
|
|
427
|
+
>>> # Standard state (no BT integration)
|
|
428
|
+
>>> @state()
|
|
429
|
+
>>> def MyState():
|
|
430
|
+
>>> @events
|
|
431
|
+
>>> class Events(Enum):
|
|
432
|
+
>>> DONE = auto()
|
|
433
|
+
>>>
|
|
434
|
+
>>> @on_state
|
|
435
|
+
>>> async def handler(ctx):
|
|
436
|
+
>>> return Events.DONE
|
|
437
|
+
>>>
|
|
438
|
+
>>> @transitions
|
|
439
|
+
>>> def transitions():
|
|
440
|
+
>>> return [LabeledTransition(Events.DONE, OtherState)]
|
|
441
|
+
|
|
442
|
+
>>> # State with BT integration
|
|
443
|
+
>>> @state(bt=DecideBT)
|
|
444
|
+
>>> def MyState():
|
|
445
|
+
>>> @events
|
|
446
|
+
>>> class Events(Enum):
|
|
447
|
+
>>> DONE = auto()
|
|
448
|
+
>>>
|
|
449
|
+
>>> @on_state
|
|
450
|
+
>>> async def handler(ctx, bt_result):
|
|
451
|
+
>>> if bt_result == Status.SUCCESS:
|
|
452
|
+
>>> return Events.DONE
|
|
453
|
+
>>> return None
|
|
454
|
+
|
|
455
|
+
>>> # State with config
|
|
456
|
+
>>> @state(config=StateConfiguration(can_dwell=True))
|
|
457
|
+
>>> def MyState():
|
|
458
|
+
>>> @events
|
|
459
|
+
>>> class Events(Enum):
|
|
460
|
+
>>> DONE = auto()
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
bt: Optional BT tree to integrate into this state
|
|
464
|
+
**kwargs: Additional arguments passed to septum.state() (e.g., config)
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
State function decorated with septum.state()
|
|
468
|
+
"""
|
|
469
|
+
def decorator(func: Callable) -> Callable:
|
|
470
|
+
# Store BT integration metadata if provided
|
|
471
|
+
if bt is not None:
|
|
472
|
+
integration = BTIntegration(
|
|
473
|
+
bt_tree=bt,
|
|
474
|
+
state_name=func.__qualname__,
|
|
475
|
+
state_func=func,
|
|
476
|
+
)
|
|
477
|
+
_state_bt_integrations[func.__qualname__] = integration
|
|
478
|
+
|
|
479
|
+
# Apply septum.state decorator with additional kwargs
|
|
480
|
+
return _septum.state(**kwargs)(func) # type: ignore[return-value]
|
|
481
|
+
|
|
482
|
+
return decorator
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def events(cls: Type[Enum]) -> Type[Enum]:
|
|
486
|
+
"""
|
|
487
|
+
Decorator to define state events (mirrors @septum.events).
|
|
488
|
+
|
|
489
|
+
Example:
|
|
490
|
+
>>> @state()
|
|
491
|
+
>>> def MyState():
|
|
492
|
+
>>> @events
|
|
493
|
+
>>> class Events(Enum):
|
|
494
|
+
>>> START = auto()
|
|
495
|
+
>>> STOP = auto()
|
|
496
|
+
"""
|
|
497
|
+
return _septum.events(cls)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# Make on_state work without bt parameter for standard usage
|
|
501
|
+
# It will check for BT integration from the @state decorator
|
|
502
|
+
def on_state(func: Optional[Callable] = None, *, bt_tree=None) -> Callable:
|
|
503
|
+
"""
|
|
504
|
+
Decorator for state handlers (mirrors @septum.on_state).
|
|
505
|
+
|
|
506
|
+
When used with @state(bt=SomeBT), this decorator automatically
|
|
507
|
+
wraps the handler to receive the BT result.
|
|
508
|
+
|
|
509
|
+
Examples:
|
|
510
|
+
>>> # Standard usage (no BT integration)
|
|
511
|
+
>>> @state()
|
|
512
|
+
>>> def MyState():
|
|
513
|
+
>>> @on_state
|
|
514
|
+
>>> async def handler(ctx):
|
|
515
|
+
>>> return Events.DONE
|
|
516
|
+
>>>
|
|
517
|
+
>>> @transitions
|
|
518
|
+
>>> def transitions():
|
|
519
|
+
>>> return [LabeledTransition(Events.DONE, OtherState)]
|
|
520
|
+
|
|
521
|
+
>>> # With BT integration (BT from @state decorator)
|
|
522
|
+
>>> @state(bt=DecideBT)
|
|
523
|
+
>>> def MyState():
|
|
524
|
+
>>> @on_state # Automatically wraps to receive bt_result
|
|
525
|
+
>>> async def handler(ctx, bt_result):
|
|
526
|
+
>>> if bt_result == Status.SUCCESS:
|
|
527
|
+
>>> return Events.DONE
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
func: The state handler function
|
|
531
|
+
bt_tree: (Deprecated) Use @state(bt=...) instead
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Decorated state handler function
|
|
535
|
+
"""
|
|
536
|
+
def decorator(f: Callable) -> Callable:
|
|
537
|
+
# Check if this state has BT integration registered
|
|
538
|
+
# Extract state name from qualname (handles nested functions like MyState.<locals>.handler)
|
|
539
|
+
qualname = f.__qualname__
|
|
540
|
+
if '.<locals>.' in qualname:
|
|
541
|
+
# Nested function - extract parent state name
|
|
542
|
+
state_name = qualname.split('.<locals>.')[0]
|
|
543
|
+
else:
|
|
544
|
+
state_name = qualname
|
|
545
|
+
|
|
546
|
+
bt_integration = _state_bt_integrations.get(state_name)
|
|
547
|
+
|
|
548
|
+
if bt_integration is not None:
|
|
549
|
+
# Wrap the function to inject BT result
|
|
550
|
+
sig = inspect.signature(f)
|
|
551
|
+
params = list(sig.parameters.keys())
|
|
552
|
+
expects_bt_result = len(params) >= 2 and params[1] == 'bt_result'
|
|
553
|
+
|
|
554
|
+
@functools.wraps(f)
|
|
555
|
+
async def wrapped(ctx, *args, **kwargs):
|
|
556
|
+
# Get BT runner from context
|
|
557
|
+
bt_runner = getattr(ctx, '_mycelium_bt_runner', None)
|
|
558
|
+
|
|
559
|
+
if bt_runner is None:
|
|
560
|
+
# No BT runner - call original function
|
|
561
|
+
if expects_bt_result:
|
|
562
|
+
return await f(ctx, None, *args, **kwargs)
|
|
563
|
+
else:
|
|
564
|
+
return await f(ctx, *args, **kwargs)
|
|
565
|
+
|
|
566
|
+
# Run the BT to completion
|
|
567
|
+
bt_result = Status.RUNNING
|
|
568
|
+
max_ticks = 10
|
|
569
|
+
ticks = 0
|
|
570
|
+
while bt_result == Status.RUNNING and ticks < max_ticks:
|
|
571
|
+
bt_result = await bt_runner.tick()
|
|
572
|
+
ticks += 1
|
|
573
|
+
|
|
574
|
+
# Call the original function with BT result
|
|
575
|
+
return await f(ctx, bt_result, *args, **kwargs)
|
|
576
|
+
|
|
577
|
+
# Mark as septum on_state for validation (use func itself, not True)
|
|
578
|
+
wrapped._septum_on_state = wrapped # type: ignore[attr-defined]
|
|
579
|
+
|
|
580
|
+
# Add to tracking stack if available
|
|
581
|
+
if _septum._tracking_stack:
|
|
582
|
+
_septum._tracking_stack[-1].append((f.__name__, wrapped))
|
|
583
|
+
|
|
584
|
+
return wrapped
|
|
585
|
+
else:
|
|
586
|
+
# No BT integration - just use septum.on_state
|
|
587
|
+
return _septum.on_state(f)
|
|
588
|
+
|
|
589
|
+
# Support both @on_state and @on_state(bt_tree=...) syntax
|
|
590
|
+
if func is None:
|
|
591
|
+
# Called with arguments: @on_state(...)
|
|
592
|
+
if bt_tree is not None:
|
|
593
|
+
# Legacy syntax - user provided bt_tree here
|
|
594
|
+
# We could handle this, but recommend @state(bt=...) instead
|
|
595
|
+
import warnings
|
|
596
|
+
warnings.warn(
|
|
597
|
+
"Using @on_state(bt_tree=...) is deprecated. "
|
|
598
|
+
"Use @state(bt=...) instead.",
|
|
599
|
+
DeprecationWarning,
|
|
600
|
+
stacklevel=2
|
|
601
|
+
)
|
|
602
|
+
return decorator
|
|
603
|
+
else:
|
|
604
|
+
# Called without arguments: @on_state
|
|
605
|
+
return decorator(func)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def transitions(func: Callable) -> Callable:
|
|
609
|
+
"""
|
|
610
|
+
Decorator to define state transitions (mirrors @septum.transitions).
|
|
611
|
+
|
|
612
|
+
Example:
|
|
613
|
+
>>> @state()
|
|
614
|
+
>>> def MyState():
|
|
615
|
+
>>> @transitions
|
|
616
|
+
>>> def transitions():
|
|
617
|
+
>>> return [LabeledTransition(Events.DONE, OtherState)]
|
|
618
|
+
"""
|
|
619
|
+
return _septum.transitions(func)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Mycelium-specific exceptions.
|
|
4
|
+
|
|
5
|
+
All Mycelium exceptions inherit from MyceliumError for easy catching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MyceliumError(Exception):
|
|
10
|
+
"""Base exception for all Mycelium errors."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TreeDefinitionError(MyceliumError):
|
|
14
|
+
"""Error in tree definition or structure."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FSMDiscoveryError(MyceliumError):
|
|
18
|
+
"""Error discovering or validating FSM state graph."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FSMInstantiationError(MyceliumError):
|
|
22
|
+
"""Error creating or initializing FSM instances."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StateNotFoundError(FSMDiscoveryError):
|
|
26
|
+
"""A required state could not be found in the FSM graph."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CircularStateError(FSMDiscoveryError):
|
|
30
|
+
"""FSM state graph has a problematic cycle (note: normal cycles are OK)."""
|