monoco-toolkit 0.3.11__py3-none-any.whl → 0.4.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.
- monoco/core/automation/__init__.py +40 -0
- monoco/core/automation/field_watcher.py +296 -0
- monoco/core/automation/handlers.py +805 -0
- monoco/core/config.py +29 -11
- monoco/core/daemon/__init__.py +5 -0
- monoco/core/daemon/pid.py +290 -0
- monoco/core/git.py +15 -0
- monoco/core/hooks/context.py +74 -13
- monoco/core/injection.py +86 -8
- monoco/core/integrations.py +0 -24
- monoco/core/router/__init__.py +17 -0
- monoco/core/router/action.py +202 -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 +197 -0
- monoco/core/scheduler/local.py +377 -0
- monoco/core/setup.py +9 -0
- monoco/core/sync.py +199 -4
- monoco/core/watcher/__init__.py +63 -0
- monoco/core/watcher/base.py +382 -0
- monoco/core/watcher/dropzone.py +152 -0
- monoco/core/watcher/im.py +460 -0
- monoco/core/watcher/issue.py +303 -0
- monoco/core/watcher/memo.py +192 -0
- monoco/core/watcher/task.py +238 -0
- monoco/daemon/app.py +3 -60
- monoco/daemon/commands.py +459 -25
- monoco/daemon/events.py +34 -0
- monoco/daemon/scheduler.py +157 -201
- monoco/daemon/services.py +42 -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/resources/en/AGENTS.md +14 -14
- monoco/features/agent/resources/en/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/en/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/en/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/en/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/resources/zh/skills/monoco_role_engineer/SKILL.md +101 -0
- monoco/features/agent/resources/zh/skills/monoco_role_manager/SKILL.md +95 -0
- monoco/features/agent/resources/zh/skills/monoco_role_planner/SKILL.md +177 -0
- monoco/features/agent/resources/zh/skills/monoco_role_reviewer/SKILL.md +139 -0
- monoco/features/agent/worker.py +1 -1
- monoco/features/hooks/__init__.py +61 -6
- monoco/features/hooks/commands.py +281 -271
- monoco/features/hooks/dispatchers/__init__.py +23 -0
- monoco/features/hooks/dispatchers/agent_dispatcher.py +486 -0
- monoco/features/hooks/dispatchers/git_dispatcher.py +478 -0
- monoco/features/hooks/manager.py +357 -0
- monoco/features/hooks/models.py +262 -0
- monoco/features/hooks/parser.py +322 -0
- monoco/features/hooks/universal_interceptor.py +503 -0
- monoco/features/im/__init__.py +67 -0
- monoco/features/im/core.py +782 -0
- monoco/features/im/models.py +311 -0
- monoco/features/issue/commands.py +133 -60
- monoco/features/issue/core.py +385 -40
- monoco/features/issue/domain_commands.py +0 -19
- monoco/features/issue/resources/en/AGENTS.md +17 -122
- monoco/features/issue/resources/hooks/agent/before-tool.sh +102 -0
- monoco/features/issue/resources/hooks/agent/session-start.sh +88 -0
- monoco/features/issue/resources/hooks/{post-checkout.sh → git/git-post-checkout.sh} +10 -9
- monoco/features/issue/resources/hooks/git/git-pre-commit.sh +31 -0
- monoco/features/issue/resources/hooks/{pre-push.sh → git/git-pre-push.sh} +7 -13
- monoco/features/issue/resources/zh/AGENTS.md +18 -123
- monoco/features/memo/cli.py +15 -64
- monoco/features/memo/core.py +6 -34
- monoco/features/memo/models.py +24 -15
- monoco/features/memo/resources/en/AGENTS.md +31 -0
- monoco/features/memo/resources/zh/AGENTS.md +28 -5
- monoco/features/spike/commands.py +5 -3
- monoco/main.py +5 -3
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.4.0.dist-info/RECORD +170 -0
- monoco/core/execution.py +0 -67
- monoco/features/agent/apoptosis.py +0 -44
- monoco/features/agent/manager.py +0 -127
- monoco/features/agent/resources/atoms/atom-code-dev.yaml +0 -61
- monoco/features/agent/resources/atoms/atom-issue-lifecycle.yaml +0 -73
- monoco/features/agent/resources/atoms/atom-knowledge.yaml +0 -55
- monoco/features/agent/resources/atoms/atom-review.yaml +0 -60
- monoco/features/agent/resources/en/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_manager/SKILL.md +0 -93
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_planner/SKILL.md +0 -85
- monoco/features/agent/resources/en/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -114
- monoco/features/agent/resources/workflows/workflow-dev.yaml +0 -83
- monoco/features/agent/resources/workflows/workflow-issue-create.yaml +0 -72
- monoco/features/agent/resources/workflows/workflow-review.yaml +0 -94
- monoco/features/agent/resources/zh/roles/monoco_role_engineer.yaml +0 -49
- monoco/features/agent/resources/zh/roles/monoco_role_manager.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_planner.yaml +0 -46
- monoco/features/agent/resources/zh/roles/monoco_role_reviewer.yaml +0 -47
- monoco/features/agent/resources/zh/skills/monoco_atom_core/SKILL.md +0 -99
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_engineer/SKILL.md +0 -94
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_manager/SKILL.md +0 -88
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_planner/SKILL.md +0 -259
- monoco/features/agent/resources/zh/skills/monoco_workflow_agent_reviewer/SKILL.md +0 -137
- monoco/features/agent/session.py +0 -169
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +0 -278
- monoco/features/glossary/resources/en/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/glossary/resources/zh/skills/monoco_atom_glossary/SKILL.md +0 -35
- monoco/features/hooks/adapter.py +0 -67
- monoco/features/hooks/core.py +0 -441
- monoco/features/i18n/resources/en/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/en/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/i18n/resources/zh/skills/monoco_atom_i18n/SKILL.md +0 -96
- monoco/features/i18n/resources/zh/skills/monoco_workflow_i18n_scan/SKILL.md +0 -105
- monoco/features/issue/resources/en/skills/monoco_atom_issue/SKILL.md +0 -165
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/en/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/issue/resources/hooks/pre-commit.sh +0 -41
- monoco/features/issue/resources/zh/skills/monoco_atom_issue_lifecycle/SKILL.md +0 -190
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_creation/SKILL.md +0 -167
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_development/SKILL.md +0 -224
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_management/SKILL.md +0 -159
- monoco/features/issue/resources/zh/skills/monoco_workflow_issue_refinement/SKILL.md +0 -203
- monoco/features/memo/resources/en/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/en/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/memo/resources/zh/skills/monoco_atom_memo/SKILL.md +0 -77
- monoco/features/memo/resources/zh/skills/monoco_workflow_note_processing/SKILL.md +0 -140
- monoco/features/spike/resources/en/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/en/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco/features/spike/resources/zh/skills/monoco_atom_spike/SKILL.md +0 -76
- monoco/features/spike/resources/zh/skills/monoco_workflow_research/SKILL.md +0 -121
- monoco_toolkit-0.3.11.dist-info/RECORD +0 -181
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.11.dist-info → monoco_toolkit-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EventBus - Central event system for Agent scheduling (FEAT-0155).
|
|
3
|
+
|
|
4
|
+
Provides async event publishing/subscription mechanism for decoupled
|
|
5
|
+
Agent lifecycle management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import inspect
|
|
10
|
+
import logging
|
|
11
|
+
from enum import Enum, auto
|
|
12
|
+
from typing import Dict, List, Callable, Any, Optional, Union
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_async_handler(handler: Callable) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Check if a handler is async (coroutine function or has async __call__).
|
|
20
|
+
|
|
21
|
+
Handles both:
|
|
22
|
+
- Regular async functions: async def func(): ...
|
|
23
|
+
- Callable objects with async __call__: class Handler: async def __call__(self, ...): ...
|
|
24
|
+
"""
|
|
25
|
+
# Direct check for coroutine function
|
|
26
|
+
if inspect.iscoroutinefunction(handler):
|
|
27
|
+
return True
|
|
28
|
+
# Check for callable object with async __call__ method
|
|
29
|
+
if hasattr(handler, "__call__") and inspect.iscoroutinefunction(handler.__call__):
|
|
30
|
+
return True
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger("monoco.core.scheduler.events")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AgentEventType(Enum):
|
|
37
|
+
"""Event types for Agent lifecycle and triggers."""
|
|
38
|
+
# Memo events
|
|
39
|
+
MEMO_CREATED = "memo.created"
|
|
40
|
+
MEMO_THRESHOLD = "memo.threshold"
|
|
41
|
+
|
|
42
|
+
# Issue events
|
|
43
|
+
ISSUE_CREATED = "issue.created"
|
|
44
|
+
ISSUE_UPDATED = "issue.updated"
|
|
45
|
+
ISSUE_STAGE_CHANGED = "issue.stage_changed"
|
|
46
|
+
ISSUE_STATUS_CHANGED = "issue.status_changed"
|
|
47
|
+
|
|
48
|
+
# Session events
|
|
49
|
+
SESSION_STARTED = "session.started"
|
|
50
|
+
SESSION_COMPLETED = "session.completed"
|
|
51
|
+
SESSION_FAILED = "session.failed"
|
|
52
|
+
SESSION_CRASHED = "session.crashed"
|
|
53
|
+
SESSION_TERMINATED = "session.terminated"
|
|
54
|
+
|
|
55
|
+
# PR events (for Reviewer trigger)
|
|
56
|
+
PR_CREATED = "pr.created"
|
|
57
|
+
PR_UPDATED = "pr.updated"
|
|
58
|
+
|
|
59
|
+
# IM events (FEAT-0167)
|
|
60
|
+
IM_MESSAGE_RECEIVED = "im.message.received"
|
|
61
|
+
IM_MESSAGE_REPLIED = "im.message.replied"
|
|
62
|
+
IM_AGENT_TRIGGER = "im.agent.trigger"
|
|
63
|
+
IM_SESSION_STARTED = "im.session.started"
|
|
64
|
+
IM_SESSION_CLOSED = "im.session.closed"
|
|
65
|
+
IM_CHANNEL_CREATED = "im.channel.created"
|
|
66
|
+
IM_CHANNEL_UPDATED = "im.channel.updated"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class AgentEvent:
|
|
71
|
+
"""Event data structure."""
|
|
72
|
+
type: AgentEventType
|
|
73
|
+
payload: Dict[str, Any]
|
|
74
|
+
timestamp: datetime = None
|
|
75
|
+
source: str = None
|
|
76
|
+
|
|
77
|
+
def __post_init__(self):
|
|
78
|
+
if self.timestamp is None:
|
|
79
|
+
self.timestamp = datetime.now()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
EventHandler = Callable[[AgentEvent], Any]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class EventBus:
|
|
86
|
+
"""
|
|
87
|
+
Central async event bus for Agent scheduling.
|
|
88
|
+
|
|
89
|
+
Supports:
|
|
90
|
+
- Subscribe/unsubscribe handlers for specific event types
|
|
91
|
+
- Publish events to all subscribed handlers
|
|
92
|
+
- Async handler execution
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self):
|
|
96
|
+
self._handlers: Dict[AgentEventType, List[EventHandler]] = {
|
|
97
|
+
event_type: [] for event_type in AgentEventType
|
|
98
|
+
}
|
|
99
|
+
self._lock = asyncio.Lock()
|
|
100
|
+
self._event_queue: asyncio.Queue = asyncio.Queue()
|
|
101
|
+
self._dispatch_task: Optional[asyncio.Task] = None
|
|
102
|
+
self._running = False
|
|
103
|
+
|
|
104
|
+
async def start(self):
|
|
105
|
+
"""Start the event dispatch loop."""
|
|
106
|
+
if self._running:
|
|
107
|
+
return
|
|
108
|
+
self._running = True
|
|
109
|
+
self._dispatch_task = asyncio.create_task(self._dispatch_loop())
|
|
110
|
+
logger.info("EventBus started")
|
|
111
|
+
|
|
112
|
+
async def stop(self):
|
|
113
|
+
"""Stop the event dispatch loop."""
|
|
114
|
+
if not self._running:
|
|
115
|
+
return
|
|
116
|
+
self._running = False
|
|
117
|
+
if self._dispatch_task:
|
|
118
|
+
self._dispatch_task.cancel()
|
|
119
|
+
try:
|
|
120
|
+
await self._dispatch_task
|
|
121
|
+
except asyncio.CancelledError:
|
|
122
|
+
pass
|
|
123
|
+
logger.info("EventBus stopped")
|
|
124
|
+
|
|
125
|
+
async def _dispatch_loop(self):
|
|
126
|
+
"""Background loop to dispatch events."""
|
|
127
|
+
while self._running:
|
|
128
|
+
try:
|
|
129
|
+
event = await asyncio.wait_for(self._event_queue.get(), timeout=1.0)
|
|
130
|
+
await self._dispatch_event(event)
|
|
131
|
+
except asyncio.TimeoutError:
|
|
132
|
+
continue
|
|
133
|
+
except asyncio.CancelledError:
|
|
134
|
+
break
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error(f"Error dispatching event: {e}")
|
|
137
|
+
|
|
138
|
+
async def _dispatch_event(self, event: AgentEvent):
|
|
139
|
+
"""Dispatch event to all subscribed handlers."""
|
|
140
|
+
handlers = self._handlers.get(event.type, [])
|
|
141
|
+
if not handlers:
|
|
142
|
+
logger.debug(f"No handlers for event {event.type.value}")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
logger.debug(f"Dispatching {event.type.value} to {len(handlers)} handlers")
|
|
146
|
+
|
|
147
|
+
# Execute handlers concurrently
|
|
148
|
+
tasks = []
|
|
149
|
+
for handler in handlers:
|
|
150
|
+
try:
|
|
151
|
+
if _is_async_handler(handler):
|
|
152
|
+
tasks.append(asyncio.create_task(handler(event)))
|
|
153
|
+
else:
|
|
154
|
+
handler(event)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.error(f"Handler error for {event.type.value}: {e}")
|
|
157
|
+
|
|
158
|
+
if tasks:
|
|
159
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
160
|
+
|
|
161
|
+
def subscribe(self, event_type: AgentEventType, handler: EventHandler):
|
|
162
|
+
"""Subscribe a handler to an event type."""
|
|
163
|
+
if handler not in self._handlers[event_type]:
|
|
164
|
+
self._handlers[event_type].append(handler)
|
|
165
|
+
logger.debug(f"Handler subscribed to {event_type.value}")
|
|
166
|
+
|
|
167
|
+
def unsubscribe(self, event_type: AgentEventType, handler: EventHandler):
|
|
168
|
+
"""Unsubscribe a handler from an event type."""
|
|
169
|
+
if handler in self._handlers[event_type]:
|
|
170
|
+
self._handlers[event_type].remove(handler)
|
|
171
|
+
logger.debug(f"Handler unsubscribed from {event_type.value}")
|
|
172
|
+
|
|
173
|
+
async def publish(self, event_type: AgentEventType, payload: Dict[str, Any], source: str = None):
|
|
174
|
+
"""Publish an event to the bus."""
|
|
175
|
+
event = AgentEvent(type=event_type, payload=payload, source=source)
|
|
176
|
+
await self._event_queue.put(event)
|
|
177
|
+
logger.debug(f"Published event {event_type.value}")
|
|
178
|
+
|
|
179
|
+
def get_subscriber_count(self, event_type: AgentEventType) -> int:
|
|
180
|
+
"""Get number of subscribers for an event type."""
|
|
181
|
+
return len(self._handlers.get(event_type, []))
|
|
182
|
+
|
|
183
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
184
|
+
"""Get event bus statistics."""
|
|
185
|
+
return {
|
|
186
|
+
"running": self._running,
|
|
187
|
+
"queue_size": self._event_queue.qsize(),
|
|
188
|
+
"subscribers": {
|
|
189
|
+
event_type.value: len(handlers)
|
|
190
|
+
for event_type, handlers in self._handlers.items()
|
|
191
|
+
if handlers
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Global event bus instance
|
|
197
|
+
event_bus = EventBus()
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LocalProcessScheduler - Local process-based agent scheduler.
|
|
3
|
+
|
|
4
|
+
Implements the AgentScheduler ABC using local subprocess execution.
|
|
5
|
+
Integrates with SessionManager and Worker for process lifecycle management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, Optional, Any
|
|
16
|
+
|
|
17
|
+
from .base import AgentScheduler, AgentTask, AgentStatus
|
|
18
|
+
from .engines import EngineFactory
|
|
19
|
+
from .events import AgentEventType, event_bus
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("monoco.core.scheduler.local")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LocalProcessScheduler(AgentScheduler):
|
|
25
|
+
"""
|
|
26
|
+
Local process-based scheduler for agent execution.
|
|
27
|
+
|
|
28
|
+
This scheduler manages agent tasks as local subprocesses, providing:
|
|
29
|
+
- Process lifecycle management (spawn, monitor, terminate)
|
|
30
|
+
- Concurrency quota control via semaphore
|
|
31
|
+
- Timeout handling
|
|
32
|
+
- Session tracking and status reporting
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
max_concurrent: Maximum number of concurrent agent processes
|
|
36
|
+
project_root: Root path of the Monoco project
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> scheduler = LocalProcessScheduler(max_concurrent=5)
|
|
40
|
+
>>> session_id = await scheduler.schedule(task)
|
|
41
|
+
>>> status = scheduler.get_status(session_id)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
max_concurrent: int = 5,
|
|
47
|
+
project_root: Optional[Path] = None,
|
|
48
|
+
):
|
|
49
|
+
self.max_concurrent = max_concurrent
|
|
50
|
+
self.project_root = project_root or Path.cwd()
|
|
51
|
+
|
|
52
|
+
# Session tracking: session_id -> process info
|
|
53
|
+
self._sessions: Dict[str, Dict[str, Any]] = {}
|
|
54
|
+
|
|
55
|
+
# Concurrency control
|
|
56
|
+
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
57
|
+
|
|
58
|
+
# Background monitoring task
|
|
59
|
+
self._monitor_task: Optional[asyncio.Task] = None
|
|
60
|
+
self._running = False
|
|
61
|
+
|
|
62
|
+
async def start(self):
|
|
63
|
+
"""Start the scheduler and monitoring loop."""
|
|
64
|
+
if self._running:
|
|
65
|
+
return
|
|
66
|
+
self._running = True
|
|
67
|
+
self._monitor_task = asyncio.create_task(self._monitor_loop())
|
|
68
|
+
logger.info(f"LocalProcessScheduler started (max_concurrent={self.max_concurrent})")
|
|
69
|
+
|
|
70
|
+
async def stop(self):
|
|
71
|
+
"""Stop the scheduler and terminate all sessions."""
|
|
72
|
+
if not self._running:
|
|
73
|
+
return
|
|
74
|
+
self._running = False
|
|
75
|
+
|
|
76
|
+
# Cancel monitor loop
|
|
77
|
+
if self._monitor_task:
|
|
78
|
+
self._monitor_task.cancel()
|
|
79
|
+
try:
|
|
80
|
+
await self._monitor_task
|
|
81
|
+
except asyncio.CancelledError:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
# Terminate all active sessions
|
|
85
|
+
for session_id in list(self._sessions.keys()):
|
|
86
|
+
await self.terminate(session_id)
|
|
87
|
+
|
|
88
|
+
logger.info("LocalProcessScheduler stopped")
|
|
89
|
+
|
|
90
|
+
async def schedule(self, task: AgentTask) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Schedule a task for execution as a local subprocess.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
task: The task to schedule
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
session_id: Unique identifier for the scheduled session
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
RuntimeError: If scheduling fails or engine is not supported
|
|
102
|
+
"""
|
|
103
|
+
session_id = str(uuid.uuid4())
|
|
104
|
+
|
|
105
|
+
# Acquire semaphore slot
|
|
106
|
+
acquired = await self._semaphore.acquire()
|
|
107
|
+
if not acquired:
|
|
108
|
+
# This shouldn't happen with asyncio.Semaphore, but just in case
|
|
109
|
+
raise RuntimeError("Failed to acquire concurrency slot")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
# Get engine adapter
|
|
113
|
+
adapter = EngineFactory.create(task.engine)
|
|
114
|
+
command = adapter.build_command(task.prompt)
|
|
115
|
+
|
|
116
|
+
logger.info(f"[{session_id}] Starting {task.role_name} with {task.engine} engine")
|
|
117
|
+
|
|
118
|
+
# Start subprocess
|
|
119
|
+
process = subprocess.Popen(
|
|
120
|
+
command,
|
|
121
|
+
stdout=sys.stdout,
|
|
122
|
+
stderr=sys.stderr,
|
|
123
|
+
text=True,
|
|
124
|
+
cwd=self.project_root,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Track session
|
|
128
|
+
self._sessions[session_id] = {
|
|
129
|
+
"task": task,
|
|
130
|
+
"process": process,
|
|
131
|
+
"status": AgentStatus.RUNNING,
|
|
132
|
+
"started_at": time.time(),
|
|
133
|
+
"role_name": task.role_name,
|
|
134
|
+
"issue_id": task.issue_id,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Publish session started event
|
|
138
|
+
await event_bus.publish(
|
|
139
|
+
AgentEventType.SESSION_STARTED,
|
|
140
|
+
{
|
|
141
|
+
"session_id": session_id,
|
|
142
|
+
"issue_id": task.issue_id,
|
|
143
|
+
"role_name": task.role_name,
|
|
144
|
+
"engine": task.engine,
|
|
145
|
+
},
|
|
146
|
+
source="LocalProcessScheduler"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return session_id
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
# Release semaphore on failure
|
|
153
|
+
self._semaphore.release()
|
|
154
|
+
logger.error(f"[{session_id}] Failed to start task: {e}")
|
|
155
|
+
raise RuntimeError(f"Failed to schedule task: {e}")
|
|
156
|
+
|
|
157
|
+
async def terminate(self, session_id: str) -> bool:
|
|
158
|
+
"""
|
|
159
|
+
Terminate a running or pending session.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
session_id: The session ID to terminate
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
True if termination was successful, False otherwise
|
|
166
|
+
"""
|
|
167
|
+
session = self._sessions.get(session_id)
|
|
168
|
+
if not session:
|
|
169
|
+
logger.warning(f"[{session_id}] Session not found for termination")
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
process = session.get("process")
|
|
173
|
+
if not process:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
# Try graceful termination
|
|
178
|
+
process.terminate()
|
|
179
|
+
|
|
180
|
+
# Wait a bit for graceful shutdown
|
|
181
|
+
try:
|
|
182
|
+
process.wait(timeout=2)
|
|
183
|
+
except subprocess.TimeoutExpired:
|
|
184
|
+
# Force kill if still running
|
|
185
|
+
process.kill()
|
|
186
|
+
process.wait()
|
|
187
|
+
|
|
188
|
+
session["status"] = AgentStatus.TERMINATED
|
|
189
|
+
|
|
190
|
+
# Publish session terminated event
|
|
191
|
+
await event_bus.publish(
|
|
192
|
+
AgentEventType.SESSION_TERMINATED,
|
|
193
|
+
{
|
|
194
|
+
"session_id": session_id,
|
|
195
|
+
"issue_id": session.get("issue_id"),
|
|
196
|
+
"role_name": session.get("role_name"),
|
|
197
|
+
},
|
|
198
|
+
source="LocalProcessScheduler"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Release semaphore
|
|
202
|
+
self._semaphore.release()
|
|
203
|
+
|
|
204
|
+
logger.info(f"[{session_id}] Session terminated")
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"[{session_id}] Error terminating session: {e}")
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
def get_status(self, session_id: str) -> Optional[AgentStatus]:
|
|
212
|
+
"""
|
|
213
|
+
Get the current status of a session.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
session_id: The session ID to query
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The current AgentStatus, or None if session not found
|
|
220
|
+
"""
|
|
221
|
+
session = self._sessions.get(session_id)
|
|
222
|
+
if not session:
|
|
223
|
+
return None
|
|
224
|
+
return session.get("status")
|
|
225
|
+
|
|
226
|
+
def list_active(self) -> Dict[str, AgentStatus]:
|
|
227
|
+
"""
|
|
228
|
+
List all active (pending or running) sessions.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Dictionary mapping session_id to AgentStatus
|
|
232
|
+
"""
|
|
233
|
+
return {
|
|
234
|
+
session_id: session["status"]
|
|
235
|
+
for session_id, session in self._sessions.items()
|
|
236
|
+
if session["status"] in (AgentStatus.PENDING, AgentStatus.RUNNING)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
240
|
+
"""
|
|
241
|
+
Get scheduler statistics.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Dictionary containing scheduler metrics
|
|
245
|
+
"""
|
|
246
|
+
active_count = len(self.list_active())
|
|
247
|
+
total_count = len(self._sessions)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
"running": self._running,
|
|
251
|
+
"max_concurrent": self.max_concurrent,
|
|
252
|
+
"active_sessions": active_count,
|
|
253
|
+
"total_sessions": total_count,
|
|
254
|
+
"available_slots": self.max_concurrent - active_count,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async def _monitor_loop(self):
|
|
258
|
+
"""Background loop to monitor session statuses."""
|
|
259
|
+
logger.info("Starting session monitor loop")
|
|
260
|
+
|
|
261
|
+
while self._running:
|
|
262
|
+
try:
|
|
263
|
+
await self._check_sessions()
|
|
264
|
+
await asyncio.sleep(2) # Check every 2 seconds
|
|
265
|
+
except asyncio.CancelledError:
|
|
266
|
+
break
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f"Error in monitor loop: {e}")
|
|
269
|
+
await asyncio.sleep(2)
|
|
270
|
+
|
|
271
|
+
async def _check_sessions(self):
|
|
272
|
+
"""Check all sessions and update statuses."""
|
|
273
|
+
for session_id, session in list(self._sessions.items()):
|
|
274
|
+
process = session.get("process")
|
|
275
|
+
if not process:
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
current_status = session["status"]
|
|
279
|
+
|
|
280
|
+
# Skip if already in terminal state
|
|
281
|
+
if current_status in (
|
|
282
|
+
AgentStatus.COMPLETED,
|
|
283
|
+
AgentStatus.FAILED,
|
|
284
|
+
AgentStatus.TERMINATED,
|
|
285
|
+
AgentStatus.TIMEOUT,
|
|
286
|
+
):
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
# Check timeout
|
|
290
|
+
task = session.get("task")
|
|
291
|
+
started_at = session.get("started_at", 0)
|
|
292
|
+
if task and task.timeout and (time.time() - started_at) > task.timeout:
|
|
293
|
+
logger.warning(f"[{session_id}] Task timeout exceeded ({task.timeout}s)")
|
|
294
|
+
await self._handle_timeout(session_id, session)
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# Check process status
|
|
298
|
+
returncode = process.poll()
|
|
299
|
+
if returncode is None:
|
|
300
|
+
# Still running
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# Process finished
|
|
304
|
+
if returncode == 0:
|
|
305
|
+
await self._handle_completion(session_id, session)
|
|
306
|
+
else:
|
|
307
|
+
await self._handle_failure(session_id, session, returncode)
|
|
308
|
+
|
|
309
|
+
async def _handle_completion(self, session_id: str, session: Dict[str, Any]):
|
|
310
|
+
"""Handle successful session completion."""
|
|
311
|
+
session["status"] = AgentStatus.COMPLETED
|
|
312
|
+
|
|
313
|
+
# Publish completion event
|
|
314
|
+
await event_bus.publish(
|
|
315
|
+
AgentEventType.SESSION_COMPLETED,
|
|
316
|
+
{
|
|
317
|
+
"session_id": session_id,
|
|
318
|
+
"issue_id": session.get("issue_id"),
|
|
319
|
+
"role_name": session.get("role_name"),
|
|
320
|
+
},
|
|
321
|
+
source="LocalProcessScheduler"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Release semaphore
|
|
325
|
+
self._semaphore.release()
|
|
326
|
+
|
|
327
|
+
logger.info(f"[{session_id}] Session completed successfully")
|
|
328
|
+
|
|
329
|
+
async def _handle_failure(self, session_id: str, session: Dict[str, Any], returncode: int):
|
|
330
|
+
"""Handle session failure."""
|
|
331
|
+
session["status"] = AgentStatus.FAILED
|
|
332
|
+
|
|
333
|
+
# Publish failure event
|
|
334
|
+
await event_bus.publish(
|
|
335
|
+
AgentEventType.SESSION_FAILED,
|
|
336
|
+
{
|
|
337
|
+
"session_id": session_id,
|
|
338
|
+
"issue_id": session.get("issue_id"),
|
|
339
|
+
"role_name": session.get("role_name"),
|
|
340
|
+
"reason": f"Process exited with code {returncode}",
|
|
341
|
+
},
|
|
342
|
+
source="LocalProcessScheduler"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Release semaphore
|
|
346
|
+
self._semaphore.release()
|
|
347
|
+
|
|
348
|
+
logger.error(f"[{session_id}] Session failed with exit code {returncode}")
|
|
349
|
+
|
|
350
|
+
async def _handle_timeout(self, session_id: str, session: Dict[str, Any]):
|
|
351
|
+
"""Handle session timeout."""
|
|
352
|
+
process = session.get("process")
|
|
353
|
+
|
|
354
|
+
# Kill the process
|
|
355
|
+
if process:
|
|
356
|
+
try:
|
|
357
|
+
process.kill()
|
|
358
|
+
process.wait()
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.error(f"[{session_id}] Error killing timed out process: {e}")
|
|
361
|
+
|
|
362
|
+
session["status"] = AgentStatus.TIMEOUT
|
|
363
|
+
|
|
364
|
+
# Publish failure event (timeout is a type of failure)
|
|
365
|
+
await event_bus.publish(
|
|
366
|
+
AgentEventType.SESSION_FAILED,
|
|
367
|
+
{
|
|
368
|
+
"session_id": session_id,
|
|
369
|
+
"issue_id": session.get("issue_id"),
|
|
370
|
+
"role_name": session.get("role_name"),
|
|
371
|
+
"reason": "Timeout exceeded",
|
|
372
|
+
},
|
|
373
|
+
source="LocalProcessScheduler"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Release semaphore
|
|
377
|
+
self._semaphore.release()
|
monoco/core/setup.py
CHANGED
|
@@ -211,6 +211,8 @@ def init_cli(
|
|
|
211
211
|
project_config_path = project_config_dir / "project.yaml"
|
|
212
212
|
|
|
213
213
|
project_initialized = False
|
|
214
|
+
workspace_config = {}
|
|
215
|
+
project_key = "MON"
|
|
214
216
|
|
|
215
217
|
# Check if we should init project
|
|
216
218
|
if workspace_config_path.exists() or project_config_path.exists():
|
|
@@ -323,6 +325,13 @@ def init_cli(
|
|
|
323
325
|
try:
|
|
324
326
|
from monoco.core.githooks import install_hooks
|
|
325
327
|
|
|
328
|
+
# Check if git initialized, if not, do it
|
|
329
|
+
if not (cwd / ".git").exists():
|
|
330
|
+
console.print("[dim]Git repository not found. Initializing...[/dim]")
|
|
331
|
+
# Set global default branch to main
|
|
332
|
+
subprocess.run(["git", "config", "--global", "init.defaultBranch", "main"], check=False)
|
|
333
|
+
subprocess.run(["git", "init"], cwd=cwd, check=False)
|
|
334
|
+
|
|
326
335
|
# Re-load config to get the just-written hooks (or default ones)
|
|
327
336
|
# Actually we have the dict right here in workspace_config['hooks']
|
|
328
337
|
hooks_config = workspace_config.get("hooks", {})
|