mycorrhizal 0.1.2__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.
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.0.dist-info/METADATA +335 -0
  37. mycorrhizal-0.2.0.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.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)."""