monoco-toolkit 0.3.11__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 (44) hide show
  1. monoco/core/automation/__init__.py +51 -0
  2. monoco/core/automation/config.py +338 -0
  3. monoco/core/automation/field_watcher.py +296 -0
  4. monoco/core/automation/handlers.py +723 -0
  5. monoco/core/config.py +1 -1
  6. monoco/core/executor/__init__.py +38 -0
  7. monoco/core/executor/agent_action.py +254 -0
  8. monoco/core/executor/git_action.py +303 -0
  9. monoco/core/executor/im_action.py +309 -0
  10. monoco/core/executor/pytest_action.py +218 -0
  11. monoco/core/git.py +15 -0
  12. monoco/core/hooks/context.py +74 -13
  13. monoco/core/router/__init__.py +55 -0
  14. monoco/core/router/action.py +341 -0
  15. monoco/core/router/router.py +392 -0
  16. monoco/core/scheduler/__init__.py +63 -0
  17. monoco/core/scheduler/base.py +152 -0
  18. monoco/core/scheduler/engines.py +175 -0
  19. monoco/core/scheduler/events.py +171 -0
  20. monoco/core/scheduler/local.py +377 -0
  21. monoco/core/watcher/__init__.py +57 -0
  22. monoco/core/watcher/base.py +365 -0
  23. monoco/core/watcher/dropzone.py +152 -0
  24. monoco/core/watcher/issue.py +303 -0
  25. monoco/core/watcher/memo.py +200 -0
  26. monoco/core/watcher/task.py +238 -0
  27. monoco/daemon/events.py +34 -0
  28. monoco/daemon/scheduler.py +172 -201
  29. monoco/daemon/services.py +27 -243
  30. monoco/features/agent/__init__.py +25 -7
  31. monoco/features/agent/cli.py +91 -57
  32. monoco/features/agent/engines.py +31 -170
  33. monoco/features/agent/worker.py +1 -1
  34. monoco/features/issue/commands.py +90 -32
  35. monoco/features/issue/core.py +249 -4
  36. monoco/features/spike/commands.py +5 -3
  37. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +1 -1
  38. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/RECORD +41 -20
  39. monoco/features/agent/apoptosis.py +0 -44
  40. monoco/features/agent/manager.py +0 -127
  41. monoco/features/agent/session.py +0 -169
  42. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
  43. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
  44. {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
@@ -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()