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,392 @@
1
+ """
2
+ ActionRouter - Layer 2 of the Event Automation Framework.
3
+
4
+ This module implements the ActionRouter which routes events to appropriate actions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import inspect
11
+ import logging
12
+ from collections import defaultdict
13
+ from typing import Any, Callable, Dict, List, Optional, Set, Union
14
+
15
+ from monoco.core.scheduler import AgentEvent, AgentEventType, EventBus, event_bus
16
+
17
+ from .action import Action, ActionChain, ActionRegistry, ActionResult, ActionStatus
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class RoutingRule:
23
+ """
24
+ A routing rule that maps events to actions.
25
+
26
+ Attributes:
27
+ event_types: Event types this rule matches
28
+ action: Action to execute
29
+ condition: Optional additional condition
30
+ priority: Rule priority (higher = executed first)
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ event_types: List[AgentEventType],
36
+ action: Union[Action, ActionChain],
37
+ condition: Optional[Callable[[AgentEvent], bool]] = None,
38
+ priority: int = 0,
39
+ ):
40
+ self.event_types = event_types
41
+ self.action = action
42
+ self.condition = condition
43
+ self.priority = priority
44
+
45
+ def matches(self, event: AgentEvent) -> bool:
46
+ """Check if this rule matches the event."""
47
+ if event.type not in self.event_types:
48
+ return False
49
+
50
+ if self.condition is not None:
51
+ if inspect.iscoroutinefunction(self.condition):
52
+ # Note: This is called from sync context, so we can't await
53
+ # For async conditions, use Action.can_execute instead
54
+ logger.warning("Async conditions in RoutingRule are not supported")
55
+ return False
56
+ return self.condition(event)
57
+
58
+ return True
59
+
60
+
61
+ class ActionRouter:
62
+ """
63
+ Event router that maps events to actions (Layer 2).
64
+
65
+ Responsibilities:
66
+ - Subscribe to EventBus events
67
+ - Route events to registered actions
68
+ - Support conditional routing
69
+ - Support action chains
70
+ - Manage action lifecycle
71
+
72
+ Example:
73
+ >>> router = ActionRouter()
74
+ >>>
75
+ >>> # Register simple action
76
+ >>> router.register(AgentEventType.ISSUE_CREATED, my_action)
77
+ >>>
78
+ >>> # Register with condition
79
+ >>> router.register(
80
+ ... AgentEventType.ISSUE_STAGE_CHANGED,
81
+ ... engineer_action,
82
+ ... condition=lambda e: e.payload.get("new_stage") == "doing"
83
+ ... )
84
+ >>>
85
+ >>> await router.start()
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ event_bus: Optional[EventBus] = None,
91
+ name: str = "ActionRouter",
92
+ ):
93
+ self.event_bus = event_bus or event_bus
94
+ self.name = name
95
+ self._rules: List[RoutingRule] = []
96
+ self._registry = ActionRegistry()
97
+ self._running = False
98
+ self._event_handler: Optional[Callable[[AgentEvent], Any]] = None
99
+
100
+ # Statistics
101
+ self._event_count = 0
102
+ self._routed_count = 0
103
+ self._execution_results: List[ActionResult] = []
104
+ self._max_results_history = 100
105
+
106
+ def register(
107
+ self,
108
+ event_types: Union[AgentEventType, List[AgentEventType]],
109
+ action: Union[Action, ActionChain],
110
+ condition: Optional[Callable[[AgentEvent], bool]] = None,
111
+ priority: int = 0,
112
+ ) -> "ActionRouter":
113
+ """
114
+ Register an action for event types.
115
+
116
+ Args:
117
+ event_types: Event type(s) to subscribe to
118
+ action: Action or ActionChain to execute
119
+ condition: Optional condition function
120
+ priority: Rule priority (higher = executed first)
121
+
122
+ Returns:
123
+ Self for chaining
124
+ """
125
+ if isinstance(event_types, AgentEventType):
126
+ event_types = [event_types]
127
+
128
+ rule = RoutingRule(
129
+ event_types=event_types,
130
+ action=action,
131
+ condition=condition,
132
+ priority=priority,
133
+ )
134
+
135
+ self._rules.append(rule)
136
+
137
+ # Sort rules by priority (descending)
138
+ self._rules.sort(key=lambda r: r.priority, reverse=True)
139
+
140
+ # Register action in registry
141
+ if isinstance(action, Action):
142
+ self._registry.register(action)
143
+ elif isinstance(action, ActionChain):
144
+ for a in action.actions:
145
+ self._registry.register(a)
146
+
147
+ logger.info(
148
+ f"Registered {action.name if hasattr(action, 'name') else type(action).__name__} "
149
+ f"for events: {[t.value for t in event_types]}"
150
+ )
151
+
152
+ return self
153
+
154
+ def unregister(self, action_name: str) -> bool:
155
+ """
156
+ Unregister an action by name.
157
+
158
+ Args:
159
+ action_name: Name of the action to unregister
160
+
161
+ Returns:
162
+ True if action was found and removed
163
+ """
164
+ # Remove from rules
165
+ original_count = len(self._rules)
166
+ self._rules = [
167
+ r for r in self._rules
168
+ if not (
169
+ (hasattr(r.action, 'name') and r.action.name == action_name) or
170
+ (isinstance(r.action, ActionChain) and action_name in [a.name for a in r.action.actions])
171
+ )
172
+ ]
173
+
174
+ # Remove from registry
175
+ self._registry.unregister(action_name)
176
+
177
+ removed = len(self._rules) < original_count
178
+ if removed:
179
+ logger.info(f"Unregistered action: {action_name}")
180
+
181
+ return removed
182
+
183
+ async def start(self) -> None:
184
+ """Start the router and subscribe to events."""
185
+ if self._running:
186
+ return
187
+
188
+ self._running = True
189
+
190
+ # Create event handler
191
+ self._event_handler = self._handle_event
192
+
193
+ # Subscribe to all event types mentioned in rules
194
+ event_types = set()
195
+ for rule in self._rules:
196
+ event_types.update(rule.event_types)
197
+
198
+ for event_type in event_types:
199
+ self.event_bus.subscribe(event_type, self._event_handler)
200
+
201
+ logger.info(f"Started ActionRouter with {len(self._rules)} rules")
202
+
203
+ async def stop(self) -> None:
204
+ """Stop the router and unsubscribe from events."""
205
+ if not self._running:
206
+ return
207
+
208
+ self._running = False
209
+
210
+ # Unsubscribe from all event types
211
+ if self._event_handler:
212
+ for event_type in AgentEventType:
213
+ self.event_bus.unsubscribe(event_type, self._event_handler)
214
+
215
+ logger.info("Stopped ActionRouter")
216
+
217
+ async def _handle_event(self, event: AgentEvent) -> None:
218
+ """
219
+ Handle incoming events.
220
+
221
+ Args:
222
+ event: The event to route
223
+ """
224
+ self._event_count += 1
225
+
226
+ logger.debug(f"Routing event: {event.type.value}")
227
+
228
+ # Find matching rules
229
+ matching_rules = [r for r in self._rules if r.matches(event)]
230
+
231
+ if not matching_rules:
232
+ logger.debug(f"No matching rules for event: {event.type.value}")
233
+ return
234
+
235
+ # Execute actions
236
+ for rule in matching_rules:
237
+ try:
238
+ if isinstance(rule.action, ActionChain):
239
+ results = await rule.action.execute(event)
240
+ for result in results:
241
+ self._record_result(result)
242
+ else:
243
+ result = await rule.action(event)
244
+ self._record_result(result)
245
+
246
+ self._routed_count += 1
247
+
248
+ except Exception as e:
249
+ logger.error(f"Error executing action for {event.type.value}: {e}")
250
+ self._record_result(ActionResult.failure_result(error=str(e)))
251
+
252
+ def _record_result(self, result: ActionResult) -> None:
253
+ """Record execution result."""
254
+ self._execution_results.append(result)
255
+
256
+ # Trim history
257
+ if len(self._execution_results) > self._max_results_history:
258
+ self._execution_results = self._execution_results[-self._max_results_history:]
259
+
260
+ def route(self, event: AgentEvent) -> List[ActionResult]:
261
+ """
262
+ Manually route an event (synchronous version).
263
+
264
+ Args:
265
+ event: The event to route
266
+
267
+ Returns:
268
+ List of action results
269
+ """
270
+ results = []
271
+ matching_rules = [r for r in self._rules if r.matches(event)]
272
+
273
+ for rule in matching_rules:
274
+ try:
275
+ if isinstance(rule.action, ActionChain):
276
+ # For chains, we need to run async
277
+ loop = asyncio.get_event_loop()
278
+ chain_results = loop.run_until_complete(rule.action.execute(event))
279
+ results.extend(chain_results)
280
+ else:
281
+ loop = asyncio.get_event_loop()
282
+ result = loop.run_until_complete(rule.action(event))
283
+ results.append(result)
284
+
285
+ except Exception as e:
286
+ logger.error(f"Error in manual route: {e}")
287
+ results.append(ActionResult.failure_result(error=str(e)))
288
+
289
+ return results
290
+
291
+ def get_rules(self) -> List[Dict[str, Any]]:
292
+ """Get all routing rules as dicts."""
293
+ return [
294
+ {
295
+ "event_types": [t.value for t in r.event_types],
296
+ "action": r.action.name if hasattr(r.action, 'name') else str(type(r.action)),
297
+ "priority": r.priority,
298
+ "has_condition": r.condition is not None,
299
+ }
300
+ for r in self._rules
301
+ ]
302
+
303
+ def get_stats(self) -> Dict[str, Any]:
304
+ """Get router statistics."""
305
+ success_count = sum(
306
+ 1 for r in self._execution_results
307
+ if r.success and r.status == ActionStatus.SUCCESS
308
+ )
309
+ failed_count = sum(
310
+ 1 for r in self._execution_results
311
+ if not r.success
312
+ )
313
+ skipped_count = sum(
314
+ 1 for r in self._execution_results
315
+ if r.status == ActionStatus.SKIPPED
316
+ )
317
+
318
+ return {
319
+ "name": self.name,
320
+ "running": self._running,
321
+ "rules": len(self._rules),
322
+ "registered_actions": self._registry.list_actions(),
323
+ "events_received": self._event_count,
324
+ "events_routed": self._routed_count,
325
+ "results": {
326
+ "total": len(self._execution_results),
327
+ "success": success_count,
328
+ "failed": failed_count,
329
+ "skipped": skipped_count,
330
+ },
331
+ }
332
+
333
+
334
+ class ConditionalRouter(ActionRouter):
335
+ """
336
+ Router with advanced conditional routing capabilities.
337
+
338
+ Supports complex routing logic based on event payload.
339
+ """
340
+
341
+ def register_field_condition(
342
+ self,
343
+ event_types: Union[AgentEventType, List[AgentEventType]],
344
+ action: Union[Action, ActionChain],
345
+ field: str,
346
+ expected_value: Any,
347
+ priority: int = 0,
348
+ ) -> "ConditionalRouter":
349
+ """
350
+ Register action with a field value condition.
351
+
352
+ Args:
353
+ event_types: Event type(s) to subscribe to
354
+ action: Action to execute
355
+ field: Payload field to check
356
+ expected_value: Expected value of the field
357
+ priority: Rule priority
358
+
359
+ Returns:
360
+ Self for chaining
361
+ """
362
+ def condition(event: AgentEvent) -> bool:
363
+ return event.payload.get(field) == expected_value
364
+
365
+ return self.register(event_types, action, condition, priority)
366
+
367
+ def register_payload_condition(
368
+ self,
369
+ event_types: Union[AgentEventType, List[AgentEventType]],
370
+ action: Union[Action, ActionChain],
371
+ payload_matcher: Dict[str, Any],
372
+ priority: int = 0,
373
+ ) -> "ConditionalRouter":
374
+ """
375
+ Register action with a payload matching condition.
376
+
377
+ Args:
378
+ event_types: Event type(s) to subscribe to
379
+ action: Action to execute
380
+ payload_matcher: Dict of field -> expected value
381
+ priority: Rule priority
382
+
383
+ Returns:
384
+ Self for chaining
385
+ """
386
+ def condition(event: AgentEvent) -> bool:
387
+ return all(
388
+ event.payload.get(k) == v
389
+ for k, v in payload_matcher.items()
390
+ )
391
+
392
+ return self.register(event_types, action, condition, priority)
@@ -0,0 +1,63 @@
1
+ """
2
+ AgentScheduler - Core scheduling abstraction layer for Monoco.
3
+
4
+ This module provides the high-level AgentScheduler abstraction that decouples
5
+ scheduling policies from specific Agent Provider implementations.
6
+
7
+ Architecture:
8
+ - AgentScheduler: Abstract base class for all schedulers
9
+ - AgentTask: Data class representing a task to be scheduled
10
+ - AgentStatus: Enum for task lifecycle states
11
+ - EngineAdapter: Abstract base for agent engine adapters
12
+ - EngineFactory: Factory for creating engine adapters
13
+ - EventBus: Central event system for agent scheduling
14
+ - AgentEventType: Event types for agent lifecycle
15
+
16
+ Implementations:
17
+ - LocalProcessScheduler: Local process-based scheduler (default)
18
+ - Future: DockerScheduler, RemoteScheduler, etc.
19
+ """
20
+
21
+ from .base import (
22
+ AgentStatus,
23
+ AgentTask,
24
+ AgentScheduler,
25
+ )
26
+ from .engines import (
27
+ EngineAdapter,
28
+ EngineFactory,
29
+ GeminiAdapter,
30
+ ClaudeAdapter,
31
+ QwenAdapter,
32
+ KimiAdapter,
33
+ )
34
+ from .events import (
35
+ AgentEventType,
36
+ AgentEvent,
37
+ EventBus,
38
+ EventHandler,
39
+ event_bus,
40
+ )
41
+ from .local import LocalProcessScheduler
42
+
43
+ __all__ = [
44
+ # Base abstractions
45
+ "AgentStatus",
46
+ "AgentTask",
47
+ "AgentScheduler",
48
+ # Engine adapters
49
+ "EngineAdapter",
50
+ "EngineFactory",
51
+ "GeminiAdapter",
52
+ "ClaudeAdapter",
53
+ "QwenAdapter",
54
+ "KimiAdapter",
55
+ # Events
56
+ "AgentEventType",
57
+ "AgentEvent",
58
+ "EventBus",
59
+ "EventHandler",
60
+ "event_bus",
61
+ # Implementations
62
+ "LocalProcessScheduler",
63
+ ]
@@ -0,0 +1,152 @@
1
+ """
2
+ Base abstractions for AgentScheduler.
3
+
4
+ Defines the core AgentScheduler ABC, AgentTask dataclass, and AgentStatus enum.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime
10
+ from enum import Enum
11
+ from typing import Dict, Any, Optional
12
+
13
+
14
+ class AgentStatus(Enum):
15
+ """
16
+ Lifecycle states for an agent task.
17
+
18
+ States:
19
+ PENDING: Task is queued, waiting for resources
20
+ RUNNING: Task is actively executing
21
+ COMPLETED: Task finished successfully
22
+ FAILED: Task failed with an error
23
+ TERMINATED: Task was manually terminated
24
+ TIMEOUT: Task exceeded its time limit
25
+ """
26
+ PENDING = "pending"
27
+ RUNNING = "running"
28
+ COMPLETED = "completed"
29
+ FAILED = "failed"
30
+ TERMINATED = "terminated"
31
+ TIMEOUT = "timeout"
32
+
33
+
34
+ @dataclass
35
+ class AgentTask:
36
+ """
37
+ Data class representing a task to be scheduled.
38
+
39
+ Attributes:
40
+ task_id: Unique identifier for the task
41
+ role_name: Name of the agent role (e.g., "Engineer", "Architect")
42
+ issue_id: Associated issue ID
43
+ prompt: The instruction/context to send to the agent
44
+ engine: Agent engine to use (e.g., "gemini", "claude")
45
+ timeout: Maximum execution time in seconds
46
+ metadata: Additional task metadata
47
+ created_at: Task creation timestamp
48
+ """
49
+ task_id: str
50
+ role_name: str
51
+ issue_id: str
52
+ prompt: str
53
+ engine: str = "gemini"
54
+ timeout: int = 900
55
+ metadata: Dict[str, Any] = field(default_factory=dict)
56
+ created_at: datetime = field(default_factory=datetime.now)
57
+
58
+ def __post_init__(self):
59
+ """Ensure created_at is set."""
60
+ if self.created_at is None:
61
+ self.created_at = datetime.now()
62
+
63
+
64
+ class AgentScheduler(ABC):
65
+ """
66
+ High-level scheduling abstraction that decouples scheduling policies
67
+ from specific Agent Provider implementations.
68
+
69
+ Responsibilities:
70
+ - Task scheduling and lifecycle management
71
+ - Resource quota control (concurrency limits)
72
+ - Status monitoring and event publishing
73
+
74
+ Implementations:
75
+ - LocalProcessScheduler: Local process mode (current)
76
+ - DockerScheduler: Container mode (future)
77
+ - RemoteScheduler: Remote service mode (future)
78
+
79
+ Example:
80
+ >>> scheduler = LocalProcessScheduler(max_concurrent=5)
81
+ >>> task = AgentTask(
82
+ ... task_id="uuid-123",
83
+ ... role_name="Engineer",
84
+ ... issue_id="FEAT-123",
85
+ ... prompt="Implement feature X",
86
+ ... engine="gemini"
87
+ ... )
88
+ >>> session_id = await scheduler.schedule(task)
89
+ >>> status = scheduler.get_status(session_id)
90
+ """
91
+
92
+ @abstractmethod
93
+ async def schedule(self, task: AgentTask) -> str:
94
+ """
95
+ Schedule a task for execution.
96
+
97
+ Args:
98
+ task: The task to schedule
99
+
100
+ Returns:
101
+ session_id: Unique identifier for the scheduled session
102
+
103
+ Raises:
104
+ RuntimeError: If scheduling fails
105
+ """
106
+ pass
107
+
108
+ @abstractmethod
109
+ async def terminate(self, session_id: str) -> bool:
110
+ """
111
+ Terminate a running or pending task.
112
+
113
+ Args:
114
+ session_id: The session ID to terminate
115
+
116
+ Returns:
117
+ True if termination was successful, False otherwise
118
+ """
119
+ pass
120
+
121
+ @abstractmethod
122
+ def get_status(self, session_id: str) -> Optional[AgentStatus]:
123
+ """
124
+ Get the current status of a task.
125
+
126
+ Args:
127
+ session_id: The session ID to query
128
+
129
+ Returns:
130
+ The current AgentStatus, or None if session not found
131
+ """
132
+ pass
133
+
134
+ @abstractmethod
135
+ def list_active(self) -> Dict[str, AgentStatus]:
136
+ """
137
+ List all active (pending or running) tasks.
138
+
139
+ Returns:
140
+ Dictionary mapping session_id to AgentStatus
141
+ """
142
+ pass
143
+
144
+ @abstractmethod
145
+ def get_stats(self) -> Dict[str, Any]:
146
+ """
147
+ Get scheduler statistics.
148
+
149
+ Returns:
150
+ Dictionary containing scheduler metrics
151
+ """
152
+ pass