monoco-toolkit 0.3.2__py3-none-any.whl → 0.3.5__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/config.py +35 -0
- monoco/core/integrations.py +0 -6
- monoco/core/sync.py +6 -19
- monoco/features/issue/commands.py +24 -16
- monoco/features/issue/core.py +90 -39
- monoco/features/issue/domain/models.py +1 -0
- monoco/features/issue/domain_commands.py +47 -0
- monoco/features/issue/domain_service.py +69 -0
- monoco/features/issue/linter.py +153 -50
- monoco/features/issue/resolver.py +177 -0
- monoco/features/issue/resources/en/AGENTS.md +6 -4
- monoco/features/issue/resources/zh/AGENTS.md +6 -4
- monoco/features/issue/test_priority_integration.py +102 -0
- monoco/features/issue/test_resolver.py +83 -0
- monoco/features/issue/validator.py +97 -21
- monoco/features/scheduler/__init__.py +19 -0
- monoco/features/scheduler/cli.py +285 -0
- monoco/features/scheduler/config.py +68 -0
- monoco/features/scheduler/defaults.py +54 -0
- monoco/features/scheduler/engines.py +149 -0
- monoco/features/scheduler/manager.py +49 -0
- monoco/features/scheduler/models.py +24 -0
- monoco/features/scheduler/reliability.py +106 -0
- monoco/features/scheduler/session.py +87 -0
- monoco/features/scheduler/worker.py +133 -0
- monoco/main.py +5 -0
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/METADATA +37 -46
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/RECORD +31 -21
- monoco/core/agent/__init__.py +0 -3
- monoco/core/agent/action.py +0 -168
- monoco/core/agent/adapters.py +0 -133
- monoco/core/agent/protocol.py +0 -32
- monoco/core/agent/state.py +0 -106
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.3.2.dist-info → monoco_toolkit-0.3.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Engine Adapters for Monoco Scheduler.
|
|
3
|
+
|
|
4
|
+
This module provides a unified interface for different AI agent execution engines,
|
|
5
|
+
allowing the Worker to seamlessly switch between Gemini, Claude, and future engines.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EngineAdapter(ABC):
|
|
13
|
+
"""
|
|
14
|
+
Abstract base class for agent engine adapters.
|
|
15
|
+
|
|
16
|
+
Each adapter is responsible for:
|
|
17
|
+
1. Constructing the correct CLI command for its engine
|
|
18
|
+
2. Handling engine-specific error scenarios
|
|
19
|
+
3. Providing metadata about the engine's capabilities
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
24
|
+
"""
|
|
25
|
+
Build the CLI command to execute the agent with the given prompt.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
prompt: The instruction/context to send to the agent
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
List of command arguments (e.g., ["gemini", "-y", "prompt text"])
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def name(self) -> str:
|
|
38
|
+
"""Return the canonical name of this engine."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def supports_yolo_mode(self) -> bool:
|
|
43
|
+
"""Whether this engine supports auto-approval mode."""
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GeminiAdapter(EngineAdapter):
|
|
48
|
+
"""
|
|
49
|
+
Adapter for Google Gemini CLI.
|
|
50
|
+
|
|
51
|
+
Command format: gemini -y <prompt>
|
|
52
|
+
The -y flag enables "YOLO mode" (auto-approval of actions).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
56
|
+
return ["gemini", "-y", prompt]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def name(self) -> str:
|
|
60
|
+
return "gemini"
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def supports_yolo_mode(self) -> bool:
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ClaudeAdapter(EngineAdapter):
|
|
68
|
+
"""
|
|
69
|
+
Adapter for Anthropic Claude CLI.
|
|
70
|
+
|
|
71
|
+
Command format: claude -p <prompt>
|
|
72
|
+
The -p/--print flag enables non-interactive mode (print response and exit).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
76
|
+
return ["claude", "-p", prompt]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def name(self) -> str:
|
|
80
|
+
return "claude"
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def supports_yolo_mode(self) -> bool:
|
|
84
|
+
# Claude uses -p for non-interactive mode, similar concept
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class QwenAdapter(EngineAdapter):
|
|
89
|
+
"""
|
|
90
|
+
Adapter for Qwen Code CLI.
|
|
91
|
+
|
|
92
|
+
Command format: qwen -y <prompt>
|
|
93
|
+
The -y flag enables "YOLO mode" (auto-approval of actions).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def build_command(self, prompt: str) -> List[str]:
|
|
97
|
+
return ["qwen", "-y", prompt]
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def name(self) -> str:
|
|
101
|
+
return "qwen"
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def supports_yolo_mode(self) -> bool:
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class EngineFactory:
|
|
109
|
+
"""
|
|
110
|
+
Factory for creating engine adapter instances.
|
|
111
|
+
|
|
112
|
+
Usage:
|
|
113
|
+
adapter = EngineFactory.create("gemini")
|
|
114
|
+
command = adapter.build_command("Write a test")
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
_adapters = {
|
|
118
|
+
"gemini": GeminiAdapter,
|
|
119
|
+
"claude": ClaudeAdapter,
|
|
120
|
+
"qwen": QwenAdapter,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def create(cls, engine_name: str) -> EngineAdapter:
|
|
125
|
+
"""
|
|
126
|
+
Create an adapter instance for the specified engine.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
engine_name: Name of the engine (e.g., "gemini", "claude")
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
An instance of the appropriate EngineAdapter
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If the engine is not supported
|
|
136
|
+
"""
|
|
137
|
+
adapter_class = cls._adapters.get(engine_name.lower())
|
|
138
|
+
if not adapter_class:
|
|
139
|
+
supported = ", ".join(cls._adapters.keys())
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Unsupported engine: '{engine_name}'. "
|
|
142
|
+
f"Supported engines: {supported}"
|
|
143
|
+
)
|
|
144
|
+
return adapter_class()
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def supported_engines(cls) -> List[str]:
|
|
148
|
+
"""Return a list of all supported engine names."""
|
|
149
|
+
return list(cls._adapters.keys())
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
|
+
import uuid
|
|
3
|
+
from .models import RoleTemplate
|
|
4
|
+
from .worker import Worker
|
|
5
|
+
from .session import Session, RuntimeSession
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SessionManager:
|
|
9
|
+
"""
|
|
10
|
+
Manages the lifecycle of sessions.
|
|
11
|
+
Responsible for creating, tracking, and retrieving sessions.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
# In-memory storage for now. In prod, this might be a DB or file-backed.
|
|
16
|
+
self._sessions: Dict[str, RuntimeSession] = {}
|
|
17
|
+
|
|
18
|
+
def create_session(self, issue_id: str, role: RoleTemplate) -> RuntimeSession:
|
|
19
|
+
session_id = str(uuid.uuid4())
|
|
20
|
+
branch_name = (
|
|
21
|
+
f"agent/{issue_id}/{session_id[:8]}" # Simple branch naming strategy
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
session_model = Session(
|
|
25
|
+
id=session_id,
|
|
26
|
+
issue_id=issue_id,
|
|
27
|
+
role_name=role.name,
|
|
28
|
+
branch_name=branch_name,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
worker = Worker(role, issue_id)
|
|
32
|
+
runtime = RuntimeSession(session_model, worker)
|
|
33
|
+
self._sessions[session_id] = runtime
|
|
34
|
+
return runtime
|
|
35
|
+
|
|
36
|
+
def get_session(self, session_id: str) -> Optional[RuntimeSession]:
|
|
37
|
+
return self._sessions.get(session_id)
|
|
38
|
+
|
|
39
|
+
def list_sessions(self, issue_id: Optional[str] = None) -> List[RuntimeSession]:
|
|
40
|
+
if issue_id:
|
|
41
|
+
return [s for s in self._sessions.values() if s.model.issue_id == issue_id]
|
|
42
|
+
return list(self._sessions.values())
|
|
43
|
+
|
|
44
|
+
def terminate_session(self, session_id: str):
|
|
45
|
+
session = self.get_session(session_id)
|
|
46
|
+
if session:
|
|
47
|
+
session.terminate()
|
|
48
|
+
# We might want to keep the record for a while, so don't delete immediately
|
|
49
|
+
# del self._sessions[session_id]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RoleTemplate(BaseModel):
|
|
6
|
+
name: str = Field(
|
|
7
|
+
..., description="Unique identifier for the role (e.g., 'crafter')"
|
|
8
|
+
)
|
|
9
|
+
description: str = Field(..., description="Human-readable description of the role")
|
|
10
|
+
trigger: str = Field(
|
|
11
|
+
..., description="Event that triggers this agent (e.g., 'issue.created')"
|
|
12
|
+
)
|
|
13
|
+
goal: str = Field(..., description="The primary goal/output of this agent")
|
|
14
|
+
tools: List[str] = Field(default_factory=list, description="List of allowed tools")
|
|
15
|
+
system_prompt: str = Field(
|
|
16
|
+
..., description="The system prompt template for this agent"
|
|
17
|
+
)
|
|
18
|
+
engine: str = Field(
|
|
19
|
+
default="gemini", description="CLI agent engine (gemini/claude)"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SchedulerConfig(BaseModel):
|
|
24
|
+
roles: List[RoleTemplate] = Field(default_factory=list)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from monoco.core.config import get_config
|
|
3
|
+
from .manager import SessionManager
|
|
4
|
+
from .session import RuntimeSession
|
|
5
|
+
from .config import load_scheduler_config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApoptosisManager:
|
|
9
|
+
"""
|
|
10
|
+
Handles the 'Apoptosis' (Programmed Cell Death) lifecycle for agents.
|
|
11
|
+
Ensures that failing agents are killed, analyzed, and the environment is reset.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, session_manager: SessionManager):
|
|
15
|
+
self.session_manager = session_manager
|
|
16
|
+
|
|
17
|
+
# Load roles dynamically based on current project context
|
|
18
|
+
settings = get_config()
|
|
19
|
+
project_root = Path(settings.paths.root).resolve()
|
|
20
|
+
roles = load_scheduler_config(project_root)
|
|
21
|
+
|
|
22
|
+
# Find coroner role
|
|
23
|
+
self.coroner_role = roles.get("coroner")
|
|
24
|
+
|
|
25
|
+
if not self.coroner_role:
|
|
26
|
+
raise ValueError("Coroner role not defined!")
|
|
27
|
+
|
|
28
|
+
def check_health(self, session: RuntimeSession) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Check if a session is healthy.
|
|
31
|
+
In a real implementation, this would check heartbeat, CPU usage, or token limits.
|
|
32
|
+
"""
|
|
33
|
+
# Placeholder logic: Random failure or external flag?
|
|
34
|
+
# For now, always healthy unless explicitly marked 'crashed' (which we can simulate)
|
|
35
|
+
if hasattr(session, "simulate_crash") and session.simulate_crash:
|
|
36
|
+
return False
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
def trigger_apoptosis(self, session_id: str):
|
|
40
|
+
"""
|
|
41
|
+
Execute the full death and rebirth cycle.
|
|
42
|
+
"""
|
|
43
|
+
session = self.session_manager.get_session(session_id)
|
|
44
|
+
if not session:
|
|
45
|
+
print(f"Session {session_id} not found for apoptosis.")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
print(f"💀 [Apoptosis] Starting lifecycle for Session {session_id}")
|
|
49
|
+
|
|
50
|
+
# 1. Kill
|
|
51
|
+
self._kill(session)
|
|
52
|
+
|
|
53
|
+
# 2. Autopsy
|
|
54
|
+
try:
|
|
55
|
+
self._perform_autopsy(session)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"⚠️ Autopsy failed: {e}")
|
|
58
|
+
|
|
59
|
+
# 3. Reset
|
|
60
|
+
self._reset_environment(session)
|
|
61
|
+
|
|
62
|
+
print(
|
|
63
|
+
f"✅ [Apoptosis] Task {session.model.issue_id} has been reset and analyzed."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _kill(self, session: RuntimeSession):
|
|
67
|
+
print(f"🔪 Killing worker process for {session.model.id}...")
|
|
68
|
+
session.terminate()
|
|
69
|
+
session.model.status = "crashed"
|
|
70
|
+
|
|
71
|
+
def _perform_autopsy(self, victim_session: RuntimeSession):
|
|
72
|
+
print(
|
|
73
|
+
f"🔍 Performing autopsy on {victim_session.model.id} via Coroner agent..."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Start a Coroner session
|
|
77
|
+
coroner_session = self.session_manager.create_session(
|
|
78
|
+
victim_session.model.issue_id, self.coroner_role
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Context for the coroner
|
|
82
|
+
context = {
|
|
83
|
+
"description": f"The previous agent session ({victim_session.model.id}) for role '{victim_session.model.role_name}' crashed. Please analyze the environment and the Issue {victim_session.model.issue_id}, then write a ## Post-mortem section in the issue file."
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
coroner_session.start(context=context)
|
|
87
|
+
print("📄 Coroner agent finished analysis.")
|
|
88
|
+
|
|
89
|
+
def _reset_environment(self, session: RuntimeSession):
|
|
90
|
+
print("🧹 Resetting environment (simulated git reset --hard)...")
|
|
91
|
+
# In real impl:
|
|
92
|
+
# import subprocess
|
|
93
|
+
# subprocess.run(["git", "reset", "--hard"], check=True)
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def _retry(self, session: RuntimeSession):
|
|
97
|
+
print("🔄 Reincarnating session...")
|
|
98
|
+
# Create a new session with the same role and issue
|
|
99
|
+
new_session = self.session_manager.create_session(
|
|
100
|
+
session.model.issue_id,
|
|
101
|
+
# We need to find the original role object.
|
|
102
|
+
# Simplified: assuming we can find it by name or pass it.
|
|
103
|
+
# For now, just placeholder.
|
|
104
|
+
session.worker.role,
|
|
105
|
+
)
|
|
106
|
+
new_session.start()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from .worker import Worker
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Session(BaseModel):
|
|
8
|
+
"""
|
|
9
|
+
Represents a runtime session of a worker.
|
|
10
|
+
Persisted state of the session.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
id: str = Field(..., description="Unique session ID (likely UUID)")
|
|
14
|
+
issue_id: str = Field(..., description="The Issue ID this session is working on")
|
|
15
|
+
role_name: str = Field(..., description="Name of the role employed")
|
|
16
|
+
status: str = Field(
|
|
17
|
+
default="pending", description="pending, running, suspended, terminated"
|
|
18
|
+
)
|
|
19
|
+
branch_name: str = Field(
|
|
20
|
+
..., description="Git branch name associated with this session"
|
|
21
|
+
)
|
|
22
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
23
|
+
updated_at: datetime = Field(default_factory=datetime.now)
|
|
24
|
+
# History could be a list of logs or pointers to git commits
|
|
25
|
+
# For now, let's keep it simple. The git log IS the history.
|
|
26
|
+
|
|
27
|
+
class Config:
|
|
28
|
+
arbitrary_types_allowed = True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RuntimeSession:
|
|
32
|
+
"""
|
|
33
|
+
The in-memory wrapper around the Session model and the active Worker.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, session_model: Session, worker: Worker):
|
|
37
|
+
self.model = session_model
|
|
38
|
+
self.worker = worker
|
|
39
|
+
|
|
40
|
+
def start(self, context: Optional[dict] = None):
|
|
41
|
+
print(
|
|
42
|
+
f"Session {self.model.id}: Starting worker on branch {self.model.branch_name}"
|
|
43
|
+
)
|
|
44
|
+
# In real impl, checking out branch happening here
|
|
45
|
+
self.model.status = "running"
|
|
46
|
+
self.model.updated_at = datetime.now()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
self.worker.start(context)
|
|
50
|
+
# Async mode: we assume it started running.
|
|
51
|
+
# Use poll or refresh_status to check later.
|
|
52
|
+
self.model.status = "running"
|
|
53
|
+
except Exception:
|
|
54
|
+
self.model.status = "failed"
|
|
55
|
+
raise
|
|
56
|
+
finally:
|
|
57
|
+
self.model.updated_at = datetime.now()
|
|
58
|
+
|
|
59
|
+
def refresh_status(self) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Polls the worker and updates the session model status.
|
|
62
|
+
"""
|
|
63
|
+
worker_status = self.worker.poll()
|
|
64
|
+
self.model.status = worker_status
|
|
65
|
+
self.model.updated_at = datetime.now()
|
|
66
|
+
return worker_status
|
|
67
|
+
|
|
68
|
+
def suspend(self):
|
|
69
|
+
print(f"Session {self.model.id}: Suspending worker")
|
|
70
|
+
self.worker.stop()
|
|
71
|
+
self.model.status = "suspended"
|
|
72
|
+
self.model.updated_at = datetime.now()
|
|
73
|
+
# In real impl, ensure git commit of current state?
|
|
74
|
+
|
|
75
|
+
def resume(self):
|
|
76
|
+
print(f"Session {self.model.id}: Resuming worker")
|
|
77
|
+
self.worker.start() # In real impl, might need to re-init process
|
|
78
|
+
|
|
79
|
+
# Async mode: assume running
|
|
80
|
+
self.model.status = "running"
|
|
81
|
+
self.model.updated_at = datetime.now()
|
|
82
|
+
|
|
83
|
+
def terminate(self):
|
|
84
|
+
print(f"Session {self.model.id}: Terminating")
|
|
85
|
+
self.worker.stop()
|
|
86
|
+
self.model.status = "terminated"
|
|
87
|
+
self.model.updated_at = datetime.now()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from .models import RoleTemplate
|
|
3
|
+
from .engines import EngineFactory
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Worker:
|
|
7
|
+
"""
|
|
8
|
+
Represents an active or pending agent session assigned to a specific role and issue.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, role: RoleTemplate, issue_id: str):
|
|
12
|
+
self.role = role
|
|
13
|
+
self.issue_id = issue_id
|
|
14
|
+
self.status = "pending" # pending, running, suspended, terminated
|
|
15
|
+
self.process_id: Optional[int] = None
|
|
16
|
+
self._process = None
|
|
17
|
+
|
|
18
|
+
def start(self, context: Optional[dict] = None):
|
|
19
|
+
"""
|
|
20
|
+
Start the worker session asynchronously.
|
|
21
|
+
"""
|
|
22
|
+
# Allow restart if not currently running
|
|
23
|
+
if self.status == "running":
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
print(f"Starting worker {self.role.name} for issue {self.issue_id}")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
self._execute_work(context)
|
|
30
|
+
self.status = "running"
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print(f"Worker failed to start: {e}")
|
|
33
|
+
self.status = "failed"
|
|
34
|
+
raise
|
|
35
|
+
|
|
36
|
+
def _execute_work(self, context: Optional[dict] = None):
|
|
37
|
+
import subprocess
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
# Prepare the prompt
|
|
41
|
+
# We treat 'crafter' as a drafter when context is provided (Draft Mode)
|
|
42
|
+
if (self.role.name == "drafter" or self.role.name == "crafter") and context:
|
|
43
|
+
issue_type = context.get("type", "feature")
|
|
44
|
+
description = context.get("description", "No description")
|
|
45
|
+
prompt = (
|
|
46
|
+
f"You are a Drafter in the Monoco project.\n\n"
|
|
47
|
+
f"Task: Create a new {issue_type} issue based on this request: {description}\n\n"
|
|
48
|
+
"Constraints:\n"
|
|
49
|
+
"1. Use 'monoco issue create' to generate the file.\n"
|
|
50
|
+
"2. Use 'monoco issue update' or direct file editing to enrich Objective and Tasks.\n"
|
|
51
|
+
"3. IMPORTANT: Once the issue file is created and filled with high-quality content, EXIT search or interactive mode immediately.\n"
|
|
52
|
+
"4. Do not perform any other development tasks."
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
prompt = (
|
|
56
|
+
f"{self.role.system_prompt}\n\n"
|
|
57
|
+
f"Issue context: {self.issue_id}\n"
|
|
58
|
+
f"Goal: {self.role.goal}\n"
|
|
59
|
+
)
|
|
60
|
+
if context and "description" in context:
|
|
61
|
+
prompt += f"Specific Task: {context['description']}"
|
|
62
|
+
|
|
63
|
+
engine = self.role.engine
|
|
64
|
+
|
|
65
|
+
print(f"[{self.role.name}] Engine: {engine}")
|
|
66
|
+
print(f"[{self.role.name}] Goal: {self.role.goal}")
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Use factory to get the appropriate engine adapter
|
|
70
|
+
adapter = EngineFactory.create(engine)
|
|
71
|
+
engine_args = adapter.build_command(prompt)
|
|
72
|
+
|
|
73
|
+
self._process = subprocess.Popen(
|
|
74
|
+
engine_args, stdout=sys.stdout, stderr=sys.stderr, text=True
|
|
75
|
+
)
|
|
76
|
+
self.process_id = self._process.pid
|
|
77
|
+
|
|
78
|
+
# DO NOT WAIT HERE.
|
|
79
|
+
# The scheduler/monitoring loop is responsible for checking status.
|
|
80
|
+
|
|
81
|
+
except ValueError as e:
|
|
82
|
+
# Engine not supported by factory
|
|
83
|
+
raise RuntimeError(f"Unsupported engine '{engine}'. {str(e)}")
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
raise RuntimeError(
|
|
86
|
+
f"Agent engine '{engine}' not found. Please ensure it is installed and in PATH."
|
|
87
|
+
)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"[{self.role.name}] Process Error: {e}")
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
def poll(self) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Check process status. Returns current worker status.
|
|
95
|
+
Updates self.status if process has finished.
|
|
96
|
+
"""
|
|
97
|
+
if not self._process:
|
|
98
|
+
return self.status
|
|
99
|
+
|
|
100
|
+
returncode = self._process.poll()
|
|
101
|
+
if returncode is None:
|
|
102
|
+
return "running"
|
|
103
|
+
|
|
104
|
+
if returncode == 0:
|
|
105
|
+
self.status = "completed"
|
|
106
|
+
else:
|
|
107
|
+
self.status = "failed"
|
|
108
|
+
|
|
109
|
+
return self.status
|
|
110
|
+
|
|
111
|
+
def wait(self):
|
|
112
|
+
"""
|
|
113
|
+
Block until process finishes.
|
|
114
|
+
"""
|
|
115
|
+
if self._process:
|
|
116
|
+
self._process.wait()
|
|
117
|
+
self.poll() # Update status
|
|
118
|
+
|
|
119
|
+
def stop(self):
|
|
120
|
+
"""
|
|
121
|
+
Stop the worker session.
|
|
122
|
+
"""
|
|
123
|
+
if self.status == "terminated":
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
print(f"Stopping worker {self.role.name} for issue {self.issue_id}")
|
|
127
|
+
self.status = "terminated"
|
|
128
|
+
self.process_id = None
|
|
129
|
+
|
|
130
|
+
def __repr__(self):
|
|
131
|
+
return (
|
|
132
|
+
f"<Worker role={self.role.name} issue={self.issue_id} status={self.status}>"
|
|
133
|
+
)
|
monoco/main.py
CHANGED
|
@@ -166,6 +166,11 @@ app.add_typer(config_cmd.app, name="config", help="Manage configuration")
|
|
|
166
166
|
app.add_typer(project_cmd.app, name="project", help="Manage projects")
|
|
167
167
|
app.add_typer(workspace_cmd.app, name="workspace", help="Manage workspace")
|
|
168
168
|
|
|
169
|
+
from monoco.features.scheduler import cli as scheduler_cmd
|
|
170
|
+
|
|
171
|
+
app.add_typer(scheduler_cmd.app, name="agent", help="Manage agent sessions")
|
|
172
|
+
app.add_typer(scheduler_cmd.role_app, name="role", help="Manage agent roles")
|
|
173
|
+
|
|
169
174
|
|
|
170
175
|
from monoco.daemon.commands import serve
|
|
171
176
|
|