monoco-toolkit 0.3.10__py3-none-any.whl → 0.3.12__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 (130) hide show
  1. monoco/__main__.py +8 -0
  2. monoco/core/artifacts/__init__.py +16 -0
  3. monoco/core/artifacts/manager.py +575 -0
  4. monoco/core/artifacts/models.py +161 -0
  5. monoco/core/automation/__init__.py +51 -0
  6. monoco/core/automation/config.py +338 -0
  7. monoco/core/automation/field_watcher.py +296 -0
  8. monoco/core/automation/handlers.py +723 -0
  9. monoco/core/config.py +31 -4
  10. monoco/core/executor/__init__.py +38 -0
  11. monoco/core/executor/agent_action.py +254 -0
  12. monoco/core/executor/git_action.py +303 -0
  13. monoco/core/executor/im_action.py +309 -0
  14. monoco/core/executor/pytest_action.py +218 -0
  15. monoco/core/git.py +38 -0
  16. monoco/core/hooks/context.py +74 -13
  17. monoco/core/ingestion/__init__.py +20 -0
  18. monoco/core/ingestion/discovery.py +248 -0
  19. monoco/core/ingestion/watcher.py +343 -0
  20. monoco/core/ingestion/worker.py +436 -0
  21. monoco/core/loader.py +633 -0
  22. monoco/core/registry.py +34 -25
  23. monoco/core/router/__init__.py +55 -0
  24. monoco/core/router/action.py +341 -0
  25. monoco/core/router/router.py +392 -0
  26. monoco/core/scheduler/__init__.py +63 -0
  27. monoco/core/scheduler/base.py +152 -0
  28. monoco/core/scheduler/engines.py +175 -0
  29. monoco/core/scheduler/events.py +171 -0
  30. monoco/core/scheduler/local.py +377 -0
  31. monoco/core/skills.py +119 -80
  32. monoco/core/watcher/__init__.py +57 -0
  33. monoco/core/watcher/base.py +365 -0
  34. monoco/core/watcher/dropzone.py +152 -0
  35. monoco/core/watcher/issue.py +303 -0
  36. monoco/core/watcher/memo.py +200 -0
  37. monoco/core/watcher/task.py +238 -0
  38. monoco/daemon/app.py +77 -1
  39. monoco/daemon/commands.py +10 -0
  40. monoco/daemon/events.py +34 -0
  41. monoco/daemon/mailroom_service.py +196 -0
  42. monoco/daemon/models.py +1 -0
  43. monoco/daemon/scheduler.py +207 -0
  44. monoco/daemon/services.py +27 -58
  45. monoco/daemon/triggers.py +55 -0
  46. monoco/features/agent/__init__.py +25 -7
  47. monoco/features/agent/adapter.py +17 -7
  48. monoco/features/agent/cli.py +91 -57
  49. monoco/features/agent/engines.py +31 -170
  50. monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
  51. monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  52. monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  53. monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  54. monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  55. monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
  56. monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
  57. monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
  58. monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
  59. monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
  60. monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
  61. monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
  62. monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
  63. monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
  64. monoco/features/agent/worker.py +1 -1
  65. monoco/features/artifact/__init__.py +0 -0
  66. monoco/features/artifact/adapter.py +33 -0
  67. monoco/features/artifact/resources/zh/AGENTS.md +14 -0
  68. monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
  69. monoco/features/glossary/adapter.py +18 -7
  70. monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  71. monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
  72. monoco/features/hooks/__init__.py +11 -0
  73. monoco/features/hooks/adapter.py +67 -0
  74. monoco/features/hooks/commands.py +309 -0
  75. monoco/features/hooks/core.py +441 -0
  76. monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
  77. monoco/features/i18n/adapter.py +18 -5
  78. monoco/features/i18n/core.py +482 -17
  79. monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  80. monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  81. monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
  82. monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
  83. monoco/features/issue/adapter.py +19 -6
  84. monoco/features/issue/commands.py +352 -20
  85. monoco/features/issue/core.py +475 -16
  86. monoco/features/issue/engine/machine.py +114 -4
  87. monoco/features/issue/linter.py +60 -5
  88. monoco/features/issue/models.py +2 -2
  89. monoco/features/issue/resources/en/AGENTS.md +109 -0
  90. monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
  91. monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  92. monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  93. monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  94. monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  95. monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
  96. monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
  97. monoco/features/issue/resources/hooks/pre-push.sh +35 -0
  98. monoco/features/issue/resources/zh/AGENTS.md +109 -0
  99. monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
  100. monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
  101. monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
  102. monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
  103. monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
  104. monoco/features/issue/validator.py +101 -1
  105. monoco/features/memo/adapter.py +21 -8
  106. monoco/features/memo/cli.py +103 -10
  107. monoco/features/memo/core.py +178 -92
  108. monoco/features/memo/models.py +53 -0
  109. monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  110. monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  111. monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
  112. monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
  113. monoco/features/spike/adapter.py +18 -5
  114. monoco/features/spike/commands.py +5 -3
  115. monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  116. monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  117. monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
  118. monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
  119. monoco/main.py +38 -1
  120. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +7 -1
  121. monoco_toolkit-0.3.12.dist-info/RECORD +202 -0
  122. monoco/features/agent/apoptosis.py +0 -44
  123. monoco/features/agent/manager.py +0 -91
  124. monoco/features/agent/session.py +0 -121
  125. monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
  126. /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
  127. /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
  128. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
  129. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
  130. {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
monoco/core/registry.py CHANGED
@@ -1,45 +1,54 @@
1
- from typing import Dict, List
1
+ from typing import Dict, List, Optional
2
2
  from monoco.core.feature import MonocoFeature
3
+ from monoco.core.loader import FeatureLoader, FeatureRegistry as LoaderFeatureRegistry
3
4
 
4
5
 
5
6
  class FeatureRegistry:
6
- _features: Dict[str, MonocoFeature] = {}
7
+ """
8
+ Feature registry that wraps the new unified FeatureLoader.
9
+
10
+ This class provides backward compatibility while delegating to the
11
+ new FeatureLoader for dynamic discovery and lifecycle management.
12
+ """
13
+
14
+ _loader: Optional[FeatureLoader] = None
15
+
16
+ @classmethod
17
+ def _get_loader(cls) -> FeatureLoader:
18
+ """Get or create the default feature loader."""
19
+ if cls._loader is None:
20
+ cls._loader = FeatureLoader()
21
+ # Discover and load all features
22
+ cls._loader.discover()
23
+ cls._loader.load_all()
24
+ return cls._loader
7
25
 
8
26
  @classmethod
9
27
  def register(cls, feature: MonocoFeature):
10
28
  """Register a feature instance."""
11
- cls._features[feature.name] = feature
29
+ loader = cls._get_loader()
30
+ loader.registry.register(feature) # type: ignore
12
31
 
13
32
  @classmethod
14
33
  def get_features(cls) -> List[MonocoFeature]:
15
34
  """Get all registered features."""
16
- return list(cls._features.values())
35
+ loader = cls._get_loader()
36
+ return loader.registry.get_all() # type: ignore
17
37
 
18
38
  @classmethod
19
- def get_feature(cls, name: str) -> MonocoFeature:
39
+ def get_feature(cls, name: str) -> Optional[MonocoFeature]:
20
40
  """Get a specific feature by name."""
21
- return cls._features.get(name)
41
+ loader = cls._get_loader()
42
+ return loader.registry.get(name) # type: ignore
22
43
 
23
44
  @classmethod
24
45
  def load_defaults(cls):
25
46
  """
26
- Load default core features.
27
- TODO: In the future, this could be dynamic via entry points.
28
- """
29
- # Import here to avoid circular dependencies at module level
30
- from monoco.features.issue.adapter import IssueFeature
31
- from monoco.features.spike.adapter import SpikeFeature
32
- from monoco.features.i18n.adapter import I18nFeature
33
- from monoco.features.memo.adapter import MemoFeature
34
-
35
- cls.register(IssueFeature())
36
- cls.register(SpikeFeature())
37
- cls.register(I18nFeature())
38
- cls.register(MemoFeature())
39
-
47
+ Load default core features using the unified FeatureLoader.
40
48
 
41
- from monoco.features.glossary.adapter import GlossaryFeature
42
- cls.register(GlossaryFeature())
43
-
44
- from monoco.features.agent.adapter import AgentFeature
45
- cls.register(AgentFeature())
49
+ This method discovers and loads all features from monoco/features/
50
+ automatically, replacing the manual registration approach.
51
+ """
52
+ loader = cls._get_loader()
53
+ # Features are already discovered and loaded in _get_loader
54
+ # This method is kept for backward compatibility
@@ -0,0 +1,55 @@
1
+ """
2
+ Router Module - Layer 2 of the Event Automation Framework.
3
+
4
+ This module provides event routing capabilities that map events to actions.
5
+ It is part of the three-layer architecture:
6
+ - Layer 1: File Watcher
7
+ - Layer 2: Action Router (this module)
8
+ - Layer 3: Action Executor
9
+
10
+ Example Usage:
11
+ >>> from monoco.core.router import ActionRouter, Action, ActionResult
12
+ >>> from monoco.core.scheduler import AgentEventType
13
+ >>>
14
+ >>> router = ActionRouter()
15
+ >>>
16
+ >>> # Register action for issue creation
17
+ >>> router.register(AgentEventType.ISSUE_CREATED, my_action)
18
+ >>>
19
+ >>> # Register with condition
20
+ >>> router.register(
21
+ ... AgentEventType.ISSUE_STAGE_CHANGED,
22
+ ... engineer_action,
23
+ ... condition=lambda e: e.payload.get("new_stage") == "doing"
24
+ ... )
25
+ >>>
26
+ >>> await router.start()
27
+ """
28
+
29
+ from .action import (
30
+ Action,
31
+ ActionChain,
32
+ ActionRegistry,
33
+ ActionResult,
34
+ ActionStatus,
35
+ ConditionalAction,
36
+ )
37
+ from .router import (
38
+ ActionRouter,
39
+ ConditionalRouter,
40
+ RoutingRule,
41
+ )
42
+
43
+ __all__ = [
44
+ # Actions
45
+ "Action",
46
+ "ActionChain",
47
+ "ActionRegistry",
48
+ "ActionResult",
49
+ "ActionStatus",
50
+ "ConditionalAction",
51
+ # Router
52
+ "ActionRouter",
53
+ "ConditionalRouter",
54
+ "RoutingRule",
55
+ ]
@@ -0,0 +1,341 @@
1
+ """
2
+ Action Abstractions - Layer 2 & 3 of the Event Automation Framework.
3
+
4
+ This module defines the Action ABC and related classes for the execution layer.
5
+ Actions are the units of work that respond to events.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from abc import ABC, abstractmethod
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime
14
+ from enum import Enum
15
+ from typing import Any, Callable, Dict, List, Optional, Union
16
+
17
+ from monoco.core.scheduler import AgentEvent, AgentEventType
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class ActionStatus(Enum):
23
+ """Status of an Action execution."""
24
+ PENDING = "pending"
25
+ RUNNING = "running"
26
+ SUCCESS = "success"
27
+ FAILED = "failed"
28
+ SKIPPED = "skipped"
29
+ CANCELLED = "cancelled"
30
+
31
+
32
+ @dataclass
33
+ class ActionResult:
34
+ """
35
+ Result of an Action execution.
36
+
37
+ Attributes:
38
+ success: Whether the action succeeded
39
+ status: Detailed status
40
+ output: Output data from the action
41
+ error: Error message if failed
42
+ metadata: Additional metadata
43
+ started_at: Execution start time
44
+ completed_at: Execution completion time
45
+ """
46
+ success: bool
47
+ status: ActionStatus
48
+ output: Any = None
49
+ error: Optional[str] = None
50
+ metadata: Dict[str, Any] = field(default_factory=dict)
51
+ started_at: Optional[datetime] = None
52
+ completed_at: Optional[datetime] = None
53
+
54
+ @classmethod
55
+ def success_result(
56
+ cls,
57
+ output: Any = None,
58
+ metadata: Optional[Dict[str, Any]] = None,
59
+ ) -> "ActionResult":
60
+ """Create a success result."""
61
+ return cls(
62
+ success=True,
63
+ status=ActionStatus.SUCCESS,
64
+ output=output,
65
+ metadata=metadata or {},
66
+ completed_at=datetime.now(),
67
+ )
68
+
69
+ @classmethod
70
+ def failure_result(
71
+ cls,
72
+ error: str,
73
+ metadata: Optional[Dict[str, Any]] = None,
74
+ ) -> "ActionResult":
75
+ """Create a failure result."""
76
+ return cls(
77
+ success=False,
78
+ status=ActionStatus.FAILED,
79
+ error=error,
80
+ metadata=metadata or {},
81
+ completed_at=datetime.now(),
82
+ )
83
+
84
+ @classmethod
85
+ def skipped_result(
86
+ cls,
87
+ reason: str = "",
88
+ metadata: Optional[Dict[str, Any]] = None,
89
+ ) -> "ActionResult":
90
+ """Create a skipped result."""
91
+ return cls(
92
+ success=True,
93
+ status=ActionStatus.SKIPPED,
94
+ metadata={"reason": reason, **(metadata or {})},
95
+ completed_at=datetime.now(),
96
+ )
97
+
98
+
99
+ class Action(ABC):
100
+ """
101
+ Abstract base class for Actions (Layer 3).
102
+
103
+ Actions are the units of work that respond to events.
104
+ They are executed by the ActionRouter when events match their conditions.
105
+
106
+ Responsibilities:
107
+ - Define execution conditions (can_execute)
108
+ - Implement execution logic (execute)
109
+ - Return execution results
110
+
111
+ Example:
112
+ >>> class MyAction(Action):
113
+ ... @property
114
+ ... def name(self) -> str:
115
+ ... return "MyAction"
116
+ ...
117
+ ... async def can_execute(self, event: AgentEvent) -> bool:
118
+ ... return event.type == AgentEventType.ISSUE_CREATED
119
+ ...
120
+ ... async def execute(self, event: AgentEvent) -> ActionResult:
121
+ ... # Do something
122
+ ... return ActionResult.success_result()
123
+ """
124
+
125
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
126
+ self.config = config or {}
127
+ self._execution_count = 0
128
+ self._last_execution: Optional[datetime] = None
129
+
130
+ @property
131
+ @abstractmethod
132
+ def name(self) -> str:
133
+ """Return the unique name of this action."""
134
+ pass
135
+
136
+ @abstractmethod
137
+ async def can_execute(self, event: AgentEvent) -> bool:
138
+ """
139
+ Check if this action should execute for the given event.
140
+
141
+ Args:
142
+ event: The event to check
143
+
144
+ Returns:
145
+ True if the action should execute, False otherwise
146
+ """
147
+ pass
148
+
149
+ @abstractmethod
150
+ async def execute(self, event: AgentEvent) -> ActionResult:
151
+ """
152
+ Execute the action.
153
+
154
+ Args:
155
+ event: The event that triggered this action
156
+
157
+ Returns:
158
+ ActionResult indicating success/failure
159
+ """
160
+ pass
161
+
162
+ async def __call__(self, event: AgentEvent) -> ActionResult:
163
+ """
164
+ Make action callable - checks conditions then executes.
165
+
166
+ Args:
167
+ event: The event to process
168
+
169
+ Returns:
170
+ ActionResult
171
+ """
172
+ self._last_execution = datetime.now()
173
+
174
+ try:
175
+ if not await self.can_execute(event):
176
+ return ActionResult.skipped_result(
177
+ reason="Conditions not met",
178
+ metadata={"action": self.name, "event_type": event.type.value},
179
+ )
180
+
181
+ self._execution_count += 1
182
+ result = await self.execute(event)
183
+
184
+ # Ensure result has timestamps
185
+ if result.started_at is None:
186
+ result.started_at = self._last_execution
187
+
188
+ return result
189
+
190
+ except Exception as e:
191
+ logger.error(f"Action {self.name} failed: {e}", exc_info=True)
192
+ return ActionResult.failure_result(
193
+ error=str(e),
194
+ metadata={"action": self.name, "event_type": event.type.value},
195
+ )
196
+
197
+ def get_stats(self) -> Dict[str, Any]:
198
+ """Get action statistics."""
199
+ return {
200
+ "name": self.name,
201
+ "execution_count": self._execution_count,
202
+ "last_execution": self._last_execution.isoformat() if self._last_execution else None,
203
+ }
204
+
205
+
206
+ class ConditionalAction(Action):
207
+ """
208
+ Action with configurable conditions.
209
+
210
+ Allows defining conditions declaratively without subclassing.
211
+ """
212
+
213
+ def __init__(
214
+ self,
215
+ name: str,
216
+ execute_fn: Callable[[AgentEvent], Any],
217
+ condition_fn: Optional[Callable[[AgentEvent], bool]] = None,
218
+ config: Optional[Dict[str, Any]] = None,
219
+ ):
220
+ super().__init__(config)
221
+ self._name = name
222
+ self._execute_fn = execute_fn
223
+ self._condition_fn = condition_fn
224
+
225
+ @property
226
+ def name(self) -> str:
227
+ return self._name
228
+
229
+ async def can_execute(self, event: AgentEvent) -> bool:
230
+ if self._condition_fn is None:
231
+ return True
232
+
233
+ if hasattr(self._condition_fn, '__await__'):
234
+ return await self._condition_fn(event)
235
+ return self._condition_fn(event)
236
+
237
+ async def execute(self, event: AgentEvent) -> ActionResult:
238
+ try:
239
+ if hasattr(self._execute_fn, '__await__'):
240
+ result = await self._execute_fn(event)
241
+ else:
242
+ result = self._execute_fn(event)
243
+
244
+ if isinstance(result, ActionResult):
245
+ return result
246
+ return ActionResult.success_result(output=result)
247
+
248
+ except Exception as e:
249
+ return ActionResult.failure_result(error=str(e))
250
+
251
+
252
+ class ActionChain:
253
+ """
254
+ Chain of actions that execute sequentially.
255
+
256
+ Each action in the chain receives the output of the previous action.
257
+ If any action fails, the chain stops.
258
+ """
259
+
260
+ def __init__(self, name: str, actions: Optional[List[Action]] = None):
261
+ self.name = name
262
+ self.actions = actions or []
263
+ self._results: List[ActionResult] = []
264
+
265
+ def add(self, action: Action) -> "ActionChain":
266
+ """Add an action to the chain."""
267
+ self.actions.append(action)
268
+ return self
269
+
270
+ async def execute(self, event: AgentEvent) -> List[ActionResult]:
271
+ """
272
+ Execute all actions in the chain.
273
+
274
+ Args:
275
+ event: The triggering event
276
+
277
+ Returns:
278
+ List of ActionResults for each action
279
+ """
280
+ self._results = []
281
+ context = {"event": event, "chain_name": self.name}
282
+
283
+ for action in self.actions:
284
+ # Check if previous action failed
285
+ if self._results and not self._results[-1].success:
286
+ self._results.append(ActionResult.skipped_result(
287
+ reason="Previous action in chain failed"
288
+ ))
289
+ continue
290
+
291
+ # Execute action with context
292
+ result = await action(event)
293
+ result.metadata["chain_context"] = context.copy()
294
+ self._results.append(result)
295
+
296
+ # Update context with output
297
+ if result.success and result.output is not None:
298
+ context[f"{action.name}_output"] = result.output
299
+
300
+ return self._results
301
+
302
+ def get_stats(self) -> Dict[str, Any]:
303
+ """Get chain statistics."""
304
+ return {
305
+ "name": self.name,
306
+ "actions": len(self.actions),
307
+ "action_names": [a.name for a in self.actions],
308
+ "last_results": len(self._results),
309
+ }
310
+
311
+
312
+ class ActionRegistry:
313
+ """
314
+ Registry for managing actions.
315
+
316
+ Provides action lookup and management.
317
+ """
318
+
319
+ def __init__(self):
320
+ self._actions: Dict[str, Action] = {}
321
+
322
+ def register(self, action: Action) -> None:
323
+ """Register an action."""
324
+ self._actions[action.name] = action
325
+ logger.debug(f"Registered action: {action.name}")
326
+
327
+ def unregister(self, name: str) -> Optional[Action]:
328
+ """Unregister an action."""
329
+ return self._actions.pop(name, None)
330
+
331
+ def get(self, name: str) -> Optional[Action]:
332
+ """Get an action by name."""
333
+ return self._actions.get(name)
334
+
335
+ def list_actions(self) -> List[str]:
336
+ """List all registered action names."""
337
+ return list(self._actions.keys())
338
+
339
+ def clear(self) -> None:
340
+ """Clear all registered actions."""
341
+ self._actions.clear()