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.
- monoco/core/automation/__init__.py +51 -0
- monoco/core/automation/config.py +338 -0
- monoco/core/automation/field_watcher.py +296 -0
- monoco/core/automation/handlers.py +723 -0
- monoco/core/config.py +1 -1
- monoco/core/executor/__init__.py +38 -0
- monoco/core/executor/agent_action.py +254 -0
- monoco/core/executor/git_action.py +303 -0
- monoco/core/executor/im_action.py +309 -0
- monoco/core/executor/pytest_action.py +218 -0
- monoco/core/git.py +15 -0
- monoco/core/hooks/context.py +74 -13
- monoco/core/router/__init__.py +55 -0
- monoco/core/router/action.py +341 -0
- monoco/core/router/router.py +392 -0
- monoco/core/scheduler/__init__.py +63 -0
- monoco/core/scheduler/base.py +152 -0
- monoco/core/scheduler/engines.py +175 -0
- monoco/core/scheduler/events.py +171 -0
- monoco/core/scheduler/local.py +377 -0
- monoco/core/watcher/__init__.py +57 -0
- monoco/core/watcher/base.py +365 -0
- monoco/core/watcher/dropzone.py +152 -0
- monoco/core/watcher/issue.py +303 -0
- monoco/core/watcher/memo.py +200 -0
- monoco/core/watcher/task.py +238 -0
- monoco/daemon/events.py +34 -0
- monoco/daemon/scheduler.py +172 -201
- monoco/daemon/services.py +27 -243
- monoco/features/agent/__init__.py +25 -7
- monoco/features/agent/cli.py +91 -57
- monoco/features/agent/engines.py +31 -170
- monoco/features/agent/worker.py +1 -1
- monoco/features/issue/commands.py +90 -32
- monoco/features/issue/core.py +249 -4
- monoco/features/spike/commands.py +5 -3
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +1 -1
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/RECORD +41 -20
- monoco/features/agent/apoptosis.py +0 -44
- monoco/features/agent/manager.py +0 -127
- monoco/features/agent/session.py +0 -169
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
- {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
|