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.
- monoco/__main__.py +8 -0
- monoco/core/artifacts/__init__.py +16 -0
- monoco/core/artifacts/manager.py +575 -0
- monoco/core/artifacts/models.py +161 -0
- 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 +31 -4
- 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 +38 -0
- monoco/core/hooks/context.py +74 -13
- monoco/core/ingestion/__init__.py +20 -0
- monoco/core/ingestion/discovery.py +248 -0
- monoco/core/ingestion/watcher.py +343 -0
- monoco/core/ingestion/worker.py +436 -0
- monoco/core/loader.py +633 -0
- monoco/core/registry.py +34 -25
- 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/skills.py +119 -80
- 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/app.py +77 -1
- monoco/daemon/commands.py +10 -0
- monoco/daemon/events.py +34 -0
- monoco/daemon/mailroom_service.py +196 -0
- monoco/daemon/models.py +1 -0
- monoco/daemon/scheduler.py +207 -0
- monoco/daemon/services.py +27 -58
- monoco/daemon/triggers.py +55 -0
- monoco/features/agent/__init__.py +25 -7
- monoco/features/agent/adapter.py +17 -7
- monoco/features/agent/cli.py +91 -57
- monoco/features/agent/engines.py +31 -170
- monoco/{core/resources/en/skills/monoco_core → features/agent/resources/en/skills/monoco_atom_core}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
- monoco/features/agent/resources/en/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
- monoco/features/agent/resources/{roles/role-engineer.yaml → zh/roles/monoco_role_engineer.yaml} +3 -3
- monoco/features/agent/resources/{roles/role-manager.yaml → zh/roles/monoco_role_manager.yaml} +8 -8
- monoco/features/agent/resources/{roles/role-planner.yaml → zh/roles/monoco_role_planner.yaml} +8 -8
- monoco/features/agent/resources/{roles/role-reviewer.yaml → zh/roles/monoco_role_reviewer.yaml} +8 -8
- monoco/{core/resources/zh/skills/monoco_core → features/agent/resources/zh/skills/monoco_atom_core}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_engineer → monoco_workflow_agent_engineer}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_manager → monoco_workflow_agent_manager}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_planner → monoco_workflow_agent_planner}/SKILL.md +2 -2
- monoco/features/agent/resources/zh/skills/{flow_reviewer → monoco_workflow_agent_reviewer}/SKILL.md +2 -2
- monoco/features/agent/worker.py +1 -1
- monoco/features/artifact/__init__.py +0 -0
- monoco/features/artifact/adapter.py +33 -0
- monoco/features/artifact/resources/zh/AGENTS.md +14 -0
- monoco/features/artifact/resources/zh/skills/monoco_atom_artifact/SKILL.md +278 -0
- monoco/features/glossary/adapter.py +18 -7
- monoco/features/glossary/resources/en/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
- monoco/features/glossary/resources/zh/skills/{monoco_glossary → monoco_atom_glossary}/SKILL.md +2 -2
- monoco/features/hooks/__init__.py +11 -0
- monoco/features/hooks/adapter.py +67 -0
- monoco/features/hooks/commands.py +309 -0
- monoco/features/hooks/core.py +441 -0
- monoco/features/hooks/resources/ADDING_HOOKS.md +234 -0
- monoco/features/i18n/adapter.py +18 -5
- monoco/features/i18n/core.py +482 -17
- monoco/features/i18n/resources/en/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
- monoco/features/i18n/resources/en/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/i18n/resources/zh/skills/{monoco_i18n → monoco_atom_i18n}/SKILL.md +2 -2
- monoco/features/i18n/resources/zh/skills/{i18n_scan_workflow → monoco_workflow_i18n_scan}/SKILL.md +2 -2
- monoco/features/issue/adapter.py +19 -6
- monoco/features/issue/commands.py +352 -20
- monoco/features/issue/core.py +475 -16
- monoco/features/issue/engine/machine.py +114 -4
- monoco/features/issue/linter.py +60 -5
- monoco/features/issue/models.py +2 -2
- monoco/features/issue/resources/en/AGENTS.md +109 -0
- monoco/features/issue/resources/en/skills/{monoco_issue → monoco_atom_issue}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/en/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
- monoco/features/issue/resources/hooks/post-checkout.sh +39 -0
- monoco/features/issue/resources/hooks/pre-commit.sh +41 -0
- monoco/features/issue/resources/hooks/pre-push.sh +35 -0
- monoco/features/issue/resources/zh/AGENTS.md +109 -0
- monoco/features/issue/resources/zh/skills/{monoco_issue → monoco_atom_issue_lifecycle}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_create_workflow → monoco_workflow_issue_creation}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_develop_workflow → monoco_workflow_issue_development}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_lifecycle_workflow → monoco_workflow_issue_management}/SKILL.md +2 -2
- monoco/features/issue/resources/zh/skills/{issue_refine_workflow → monoco_workflow_issue_refinement}/SKILL.md +2 -2
- monoco/features/issue/validator.py +101 -1
- monoco/features/memo/adapter.py +21 -8
- monoco/features/memo/cli.py +103 -10
- monoco/features/memo/core.py +178 -92
- monoco/features/memo/models.py +53 -0
- monoco/features/memo/resources/en/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
- monoco/features/memo/resources/en/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/memo/resources/zh/skills/{monoco_memo → monoco_atom_memo}/SKILL.md +2 -2
- monoco/features/memo/resources/zh/skills/{note_processing_workflow → monoco_workflow_note_processing}/SKILL.md +2 -2
- monoco/features/spike/adapter.py +18 -5
- monoco/features/spike/commands.py +5 -3
- monoco/features/spike/resources/en/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
- monoco/features/spike/resources/en/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
- monoco/features/spike/resources/zh/skills/{monoco_spike → monoco_atom_spike}/SKILL.md +2 -2
- monoco/features/spike/resources/zh/skills/{research_workflow → monoco_workflow_research}/SKILL.md +2 -2
- monoco/main.py +38 -1
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/METADATA +7 -1
- monoco_toolkit-0.3.12.dist-info/RECORD +202 -0
- monoco/features/agent/apoptosis.py +0 -44
- monoco/features/agent/manager.py +0 -91
- monoco/features/agent/session.py +0 -121
- monoco_toolkit-0.3.10.dist-info/RECORD +0 -156
- /monoco/{core → features/agent}/resources/en/AGENTS.md +0 -0
- /monoco/{core → features/agent}/resources/zh/AGENTS.md +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.10.dist-info → monoco_toolkit-0.3.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -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/skills.py
CHANGED
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
Skill Manager for Monoco Toolkit.
|
|
3
3
|
|
|
4
4
|
This module provides centralized management and distribution of Agent Skills
|
|
5
|
-
following the
|
|
6
|
-
- Atom Skills: Atomic capabilities
|
|
7
|
-
- Workflow Skills: Orchestration of atoms
|
|
8
|
-
- Role Skills: Configuration layer
|
|
5
|
+
following the standardized architecture:
|
|
6
|
+
- Atom Skills: monoco_atom_{name} - Atomic capabilities
|
|
7
|
+
- Workflow Skills: monoco_workflow_{name} - Orchestration of atoms
|
|
8
|
+
- Role Skills: monoco_role_{name} - Configuration layer
|
|
9
9
|
|
|
10
10
|
Key Responsibilities:
|
|
11
|
-
1. Discover skills from
|
|
12
|
-
2.
|
|
13
|
-
3.
|
|
14
|
-
4.
|
|
15
|
-
|
|
11
|
+
1. Discover skills from features (monoco/features/{feature}/resources/)
|
|
12
|
+
2. Validate skill structure and metadata
|
|
13
|
+
3. Distribute skills to target agent framework directories
|
|
14
|
+
4. Support i18n for skill content
|
|
15
|
+
|
|
16
|
+
Architecture Principle:
|
|
17
|
+
- Core is framework-only, no skills
|
|
18
|
+
- All skills are defined in Features (value delivery atoms)
|
|
19
|
+
- All skills follow naming convention: monoco_{type}_{name}
|
|
16
20
|
"""
|
|
17
21
|
|
|
18
22
|
import shutil
|
|
@@ -165,22 +169,17 @@ class Skill:
|
|
|
165
169
|
|
|
166
170
|
class SkillManager:
|
|
167
171
|
"""
|
|
168
|
-
Central manager for Monoco skills
|
|
172
|
+
Central manager for Monoco skills.
|
|
169
173
|
|
|
170
|
-
|
|
171
|
-
- Atom Skills: resources/atoms/*.yaml
|
|
172
|
-
- Workflow Skills: resources/workflows/*.yaml
|
|
173
|
-
- Role Skills: resources/roles
|
|
174
|
+
Architecture:
|
|
175
|
+
- Atom Skills: resources/{lang}/skills/monoco_atom_*/SKILL.md or resources/atoms/*.yaml
|
|
176
|
+
- Workflow Skills: resources/{lang}/skills/monoco_workflow_*/SKILL.md or resources/workflows/*.yaml
|
|
177
|
+
- Role Skills: resources/{lang}/roles/monoco_role_*.yaml
|
|
174
178
|
|
|
175
|
-
|
|
176
|
-
- Standard Skills: resources/{lang}/skills/{name}/SKILL.md
|
|
177
|
-
- Flow Skills: resources/{lang}/skills/flow_*/SKILL.md
|
|
179
|
+
All skills follow the naming convention: monoco_{type}_{name}
|
|
178
180
|
"""
|
|
179
181
|
|
|
180
|
-
#
|
|
181
|
-
FLOW_SKILL_PREFIX = "monoco_flow_"
|
|
182
|
-
|
|
183
|
-
# Prefix for three-level architecture skills
|
|
182
|
+
# Prefix for standardized skill naming
|
|
184
183
|
ATOM_PREFIX = "monoco_atom_"
|
|
185
184
|
WORKFLOW_PREFIX = "monoco_workflow_"
|
|
186
185
|
ROLE_PREFIX = "monoco_role_"
|
|
@@ -189,37 +188,24 @@ class SkillManager:
|
|
|
189
188
|
self,
|
|
190
189
|
root: Path,
|
|
191
190
|
features: Optional[List] = None,
|
|
192
|
-
flow_skill_prefix: str = FLOW_SKILL_PREFIX,
|
|
193
191
|
):
|
|
194
192
|
self.root = root
|
|
195
193
|
self.features = features or []
|
|
196
|
-
self.flow_skill_prefix = flow_skill_prefix
|
|
197
194
|
|
|
198
|
-
#
|
|
195
|
+
# Skills discovered from resources/{lang}/skills/monoco_*/SKILL.md
|
|
199
196
|
self.skills: Dict[str, Skill] = {}
|
|
200
197
|
|
|
201
|
-
#
|
|
198
|
+
# Three-level architecture skills
|
|
202
199
|
self._skill_loaders: Dict[str, SkillLoader] = {}
|
|
203
200
|
self._atoms: Dict[str, AtomSkillMetadata] = {}
|
|
204
201
|
self._workflows: Dict[str, WorkflowSkillMetadata] = {}
|
|
205
202
|
self._roles: Dict[str, RoleSkillMetadata] = {}
|
|
206
203
|
|
|
207
|
-
#
|
|
204
|
+
# Discover skills from features only (core is framework-only, no skills)
|
|
208
205
|
if self.features:
|
|
209
206
|
self._discover_skills_from_features()
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
# Load new three-level skills
|
|
213
|
-
self._discover_three_level_skills()
|
|
214
|
-
|
|
215
|
-
def _discover_core_skills(self) -> None:
|
|
216
|
-
"""Discover skills from monoco/core/resources/{lang}/skills/."""
|
|
217
|
-
core_resources_dir = self.root / "monoco" / "core" / "resources"
|
|
218
|
-
|
|
219
|
-
if not core_resources_dir.exists():
|
|
220
|
-
return
|
|
207
|
+
self._discover_three_level_skills()
|
|
221
208
|
|
|
222
|
-
self._discover_skills_in_resources(core_resources_dir, "monoco_core")
|
|
223
209
|
|
|
224
210
|
def _discover_skills_from_features(self) -> None:
|
|
225
211
|
"""Discover skills from Feature resources."""
|
|
@@ -250,7 +236,7 @@ class SkillManager:
|
|
|
250
236
|
if not resources_dir.exists():
|
|
251
237
|
return
|
|
252
238
|
|
|
253
|
-
|
|
239
|
+
skill_folders: Set[Path] = set()
|
|
254
240
|
|
|
255
241
|
for lang_dir in resources_dir.iterdir():
|
|
256
242
|
if not lang_dir.is_dir() or len(lang_dir.name) != 2:
|
|
@@ -262,9 +248,11 @@ class SkillManager:
|
|
|
262
248
|
|
|
263
249
|
for skill_subdir in skills_dir.iterdir():
|
|
264
250
|
if skill_subdir.is_dir() and (skill_subdir / "SKILL.md").exists():
|
|
265
|
-
|
|
251
|
+
# print(f"DEBUG: Found skill folder {skill_subdir.name} in feature {feature_name}")
|
|
252
|
+
skill_folders.add(skill_subdir)
|
|
266
253
|
|
|
267
|
-
for
|
|
254
|
+
for skill_dir in skill_folders:
|
|
255
|
+
skill_name = skill_dir.name
|
|
268
256
|
skill = Skill(
|
|
269
257
|
root_dir=self.root,
|
|
270
258
|
skill_name=skill_name,
|
|
@@ -277,40 +265,96 @@ class SkillManager:
|
|
|
277
265
|
)
|
|
278
266
|
continue
|
|
279
267
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
name = name[5:]
|
|
285
|
-
skill_key = f"{self.flow_skill_prefix}{name}"
|
|
268
|
+
# Naming Logic: All skills must follow monoco_{type}_{name} convention
|
|
269
|
+
# The skill_key is the folder name (which should match the metadata name)
|
|
270
|
+
if skill_name.startswith("monoco_"):
|
|
271
|
+
skill_key = skill_name
|
|
286
272
|
else:
|
|
287
|
-
|
|
273
|
+
# Non-compliant skills are skipped (should not happen after standardization)
|
|
274
|
+
console.print(
|
|
275
|
+
f"[yellow]Warning: Skill {skill_name} does not follow monoco_{{type}}_{{name}} naming, skipping[/yellow]"
|
|
276
|
+
)
|
|
277
|
+
continue
|
|
288
278
|
|
|
289
279
|
skill.name = skill_key
|
|
290
280
|
self.skills[skill_key] = skill
|
|
291
281
|
|
|
292
282
|
def _discover_three_level_skills(self) -> None:
|
|
293
|
-
"""Discover skills from the new three-level architecture
|
|
294
|
-
|
|
295
|
-
agent_resources_dir = self.root / "monoco" / "features" / "agent" / "resources"
|
|
296
|
-
|
|
297
|
-
if not agent_resources_dir.exists():
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
loader = SkillLoader(agent_resources_dir)
|
|
301
|
-
loader.load_all()
|
|
302
|
-
|
|
303
|
-
self._skill_loaders["agent"] = loader
|
|
283
|
+
"""Discover skills from the new three-level architecture in resources/{atoms,workflows,roles}/."""
|
|
284
|
+
from monoco.core.feature import MonocoFeature
|
|
304
285
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
286
|
+
for feature in self.features:
|
|
287
|
+
if not isinstance(feature, MonocoFeature):
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
module_parts = feature.__class__.__module__.split(".")
|
|
291
|
+
if (
|
|
292
|
+
len(module_parts) >= 3
|
|
293
|
+
and module_parts[0] == "monoco"
|
|
294
|
+
and module_parts[1] == "features"
|
|
295
|
+
):
|
|
296
|
+
feature_name = module_parts[2]
|
|
297
|
+
resources_dir = self.root / "monoco" / "features" / feature_name / "resources"
|
|
298
|
+
|
|
299
|
+
if not resources_dir.exists():
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
# Discover atoms from resources/atoms/*.yaml
|
|
303
|
+
atoms_dir = resources_dir / "atoms"
|
|
304
|
+
if atoms_dir.exists():
|
|
305
|
+
for atom_file in atoms_dir.glob("*.yaml"):
|
|
306
|
+
try:
|
|
307
|
+
data = yaml.safe_load(atom_file.read_text())
|
|
308
|
+
atom = AtomSkillMetadata(**data)
|
|
309
|
+
|
|
310
|
+
# Ensure name follows monoco_atom_ prefix
|
|
311
|
+
atom_key = atom.name
|
|
312
|
+
if not atom_key.startswith(self.ATOM_PREFIX):
|
|
313
|
+
atom_key = f"{self.ATOM_PREFIX}{atom_key}"
|
|
314
|
+
|
|
315
|
+
self._atoms[atom_key] = atom
|
|
316
|
+
except Exception as e:
|
|
317
|
+
console.print(f"[red]Failed to load atom skill {atom_file}: {e}[/red]")
|
|
318
|
+
|
|
319
|
+
# Discover workflows from resources/workflows/*.yaml
|
|
320
|
+
workflows_dir = resources_dir / "workflows"
|
|
321
|
+
if workflows_dir.exists():
|
|
322
|
+
for workflow_file in workflows_dir.glob("*.yaml"):
|
|
323
|
+
try:
|
|
324
|
+
data = yaml.safe_load(workflow_file.read_text())
|
|
325
|
+
workflow = WorkflowSkillMetadata(**data)
|
|
326
|
+
|
|
327
|
+
# Ensure name follows monoco_workflow_ prefix
|
|
328
|
+
workflow_key = workflow.name
|
|
329
|
+
if not workflow_key.startswith(self.WORKFLOW_PREFIX):
|
|
330
|
+
workflow_key = f"{self.WORKFLOW_PREFIX}{workflow_key}"
|
|
331
|
+
|
|
332
|
+
self._workflows[workflow_key] = workflow
|
|
333
|
+
except Exception as e:
|
|
334
|
+
console.print(f"[red]Failed to load workflow skill {workflow_file}: {e}[/red]")
|
|
335
|
+
|
|
336
|
+
# Discover roles from resources/{lang}/roles/*.yaml
|
|
337
|
+
for lang_dir in resources_dir.iterdir():
|
|
338
|
+
if not lang_dir.is_dir() or len(lang_dir.name) != 2:
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
roles_dir = lang_dir / "roles"
|
|
342
|
+
if not roles_dir.exists():
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
for role_file in roles_dir.glob("*.yaml"):
|
|
346
|
+
try:
|
|
347
|
+
data = yaml.safe_load(role_file.read_text())
|
|
348
|
+
role = RoleSkillMetadata(**data)
|
|
349
|
+
|
|
350
|
+
# Ensure name follows monoco_role_ prefix
|
|
351
|
+
role_key = role.name
|
|
352
|
+
if not role_key.startswith(self.ROLE_PREFIX):
|
|
353
|
+
role_key = f"{self.ROLE_PREFIX}{role_key}"
|
|
354
|
+
|
|
355
|
+
self._roles[role_key] = role
|
|
356
|
+
except Exception as e:
|
|
357
|
+
console.print(f"[red]Failed to load role skill {role_file}: {e}[/red]")
|
|
314
358
|
|
|
315
359
|
# ========================================================================
|
|
316
360
|
# Legacy Skill API (backward compatible)
|
|
@@ -688,19 +732,14 @@ class SkillManager:
|
|
|
688
732
|
"""Get list of available flow skill commands."""
|
|
689
733
|
commands = []
|
|
690
734
|
|
|
691
|
-
#
|
|
692
|
-
for skill in self.
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
name = skill.name
|
|
698
|
-
if name.startswith(self.flow_skill_prefix):
|
|
699
|
-
role = name[len(self.flow_skill_prefix):]
|
|
700
|
-
if role:
|
|
701
|
-
commands.append(f"/flow:{role}")
|
|
735
|
+
# Workflow/Flow skills with role attribute from legacy skills
|
|
736
|
+
for skill in self.skills.values():
|
|
737
|
+
if skill.get_type() in ["flow", "workflow"]:
|
|
738
|
+
role = skill.get_role()
|
|
739
|
+
if role:
|
|
740
|
+
commands.append(f"/flow:{role}")
|
|
702
741
|
|
|
703
|
-
#
|
|
742
|
+
# Role skills from three-level architecture
|
|
704
743
|
for role_name in self._roles.keys():
|
|
705
744
|
short_name = role_name.replace(self.ROLE_PREFIX, "")
|
|
706
745
|
commands.append(f"/flow:{short_name}")
|