k-cli-for-devs 1.0.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.
- k_cli/__init__.py +77 -0
- k_cli/agents/__init__.py +0 -0
- k_cli/agents/adversarial_swarm.py +338 -0
- k_cli/agents/agent_core.py +255 -0
- k_cli/agents/background_daemon.py +141 -0
- k_cli/agents/orchestrator.py +376 -0
- k_cli/agents/persona.py +649 -0
- k_cli/agents/scaffold_engine.py +121 -0
- k_cli/agents/strands_agent.py +832 -0
- k_cli/agents/subagents.py +1496 -0
- k_cli/cli.py +3297 -0
- k_cli/core/__init__.py +0 -0
- k_cli/core/airgap.py +95 -0
- k_cli/core/credentials.py +548 -0
- k_cli/core/intent_sensor.py +177 -0
- k_cli/core/llm_driver.py +1028 -0
- k_cli/core/model_manager.py +1109 -0
- k_cli/core/models_hub.py +913 -0
- k_cli/core/prompting.py +41 -0
- k_cli/core/sdk.py +322 -0
- k_cli/core/session.py +826 -0
- k_cli/core/smart_router.py +230 -0
- k_cli/core/storage_manager.py +176 -0
- k_cli/core/viewport_engine.py +117 -0
- k_cli/demo/demo_runner.py +579 -0
- k_cli/git/__init__.py +0 -0
- k_cli/git/ai_bisect.py +208 -0
- k_cli/git/conflict_resolver.py +1039 -0
- k_cli/git/git_guard.py +417 -0
- k_cli/git/patcher.py +1175 -0
- k_cli/git/repo_map.py +1780 -0
- k_cli/git/smart_git.py +928 -0
- k_cli/git/verifier.py +969 -0
- k_cli/github/__init__.py +0 -0
- k_cli/github/dedup_engine.py +787 -0
- k_cli/github/github_client.py +1702 -0
- k_cli/github/github_engine.py +641 -0
- k_cli/github/local_hub.py +209 -0
- k_cli/github/pr_watcher.py +129 -0
- k_cli/github/trending.py +205 -0
- k_cli/tools/__init__.py +0 -0
- k_cli/tools/audit.py +79 -0
- k_cli/tools/chaos_immunity.py +377 -0
- k_cli/tools/codebase_qa.py +106 -0
- k_cli/tools/command_runner.py +256 -0
- k_cli/tools/diagram_generator.py +547 -0
- k_cli/tools/doc_retriever.py +1332 -0
- k_cli/tools/feature.py +105 -0
- k_cli/tools/ghost_daemon.py +122 -0
- k_cli/tools/incident_triage.py +1365 -0
- k_cli/tools/mcp_client.py +1846 -0
- k_cli/tools/repo_gardener.py +142 -0
- k_cli/tools/rules.py +109 -0
- k_cli/tools/security.py +52 -0
- k_cli/tools/security_healer.py +999 -0
- k_cli/tools/synapse_graph.py +155 -0
- k_cli/tui/__init__.py +0 -0
- k_cli/tui/diff_viewer.py +223 -0
- k_cli/tui/tui.py +1145 -0
- k_cli/tui/tui_animations.py +648 -0
- k_cli/tui/tui_app.py +2788 -0
- k_cli/ui/__init__.py +10 -0
- k_cli/ui/simple_repl.py +315 -0
- k_cli/web/__init__.py +7 -0
- k_cli/web/server.py +624 -0
- k_cli/web/static/app.js +830 -0
- k_cli/web/static/index.html +495 -0
- k_cli/web/static/monitor.html +189 -0
- k_cli/web/static/style.css +838 -0
- k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
- k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
- k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
- k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
- k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
- k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1496 @@
|
|
|
1
|
+
"""
|
|
2
|
+
subagents.py - Native Subagent Task Spawner & Multi-Agent Orchestrator for K-CLI
|
|
3
|
+
Project Bankai Engine v1.0.0
|
|
4
|
+
|
|
5
|
+
Enables K-CLI to decompose complex user prompts into parallel subtasks:
|
|
6
|
+
- [EXPLORER] : Inspects workspace structure, AST symbol maps, locates files.
|
|
7
|
+
- [RESEARCHER] : Investigates offline DevDocs, API signatures, dependencies.
|
|
8
|
+
- [REFACTORER] : Generates code modifications and SEARCH/REPLACE surgical patch blocks.
|
|
9
|
+
- [TESTER] : Formulates test suites and runs ground-truth verification guard.
|
|
10
|
+
|
|
11
|
+
Features:
|
|
12
|
+
1. DAG Task Decomposition (LLM-based with deterministic fallback).
|
|
13
|
+
2. Multi-threaded background execution with structured JSON messaging.
|
|
14
|
+
3. PatchAggregator to merge SEARCH/REPLACE blocks into unified patches.
|
|
15
|
+
4. Rich CLI tree visualization with live progress bars and status dashboards.
|
|
16
|
+
5. Memory-budgeted execution (< 1.0 GB RAM constraint).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
import gc
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import psutil
|
|
26
|
+
import queue
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
import threading
|
|
30
|
+
import time
|
|
31
|
+
import uuid
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from enum import Enum
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
from k_cli.tools.doc_retriever import DocRetriever
|
|
39
|
+
from k_cli.core.llm_driver import LLMDriver
|
|
40
|
+
from k_cli.git.patcher import Patcher
|
|
41
|
+
from k_cli.git.repo_map import RepoMap
|
|
42
|
+
from k_cli.git.verifier import CodeExtractor, VerificationResult, Verifier
|
|
43
|
+
from k_cli.git.conflict_resolver import ConflictResolver, ConflictBlock, ConflictSummary, FileResolutionResult
|
|
44
|
+
from k_cli.github.github_client import GitHubClient, PRLifecycleManager, PRReviewResult, PRFixResult
|
|
45
|
+
from k_cli.tools.mcp_client import MCPManager, MCPClient, MCPTool, MCPToolResult
|
|
46
|
+
from k_cli.github.dedup_engine import DedupEngine, DedupMatch
|
|
47
|
+
except ModuleNotFoundError:
|
|
48
|
+
from doc_retriever import DocRetriever
|
|
49
|
+
from k_cli.core.llm_driver import LLMDriver
|
|
50
|
+
from patcher import Patcher
|
|
51
|
+
from repo_map import RepoMap
|
|
52
|
+
from verifier import CodeExtractor, VerificationResult, Verifier
|
|
53
|
+
try:
|
|
54
|
+
from conflict_resolver import ConflictResolver, ConflictBlock, ConflictSummary, FileResolutionResult
|
|
55
|
+
except (ModuleNotFoundError, ImportError):
|
|
56
|
+
ConflictResolver = None # type: ignore
|
|
57
|
+
ConflictBlock = None # type: ignore
|
|
58
|
+
ConflictSummary = None # type: ignore
|
|
59
|
+
FileResolutionResult = None # type: ignore
|
|
60
|
+
try:
|
|
61
|
+
from github_client import GitHubClient, PRLifecycleManager, PRReviewResult, PRFixResult
|
|
62
|
+
except (ModuleNotFoundError, ImportError):
|
|
63
|
+
GitHubClient = None # type: ignore
|
|
64
|
+
PRLifecycleManager = None # type: ignore
|
|
65
|
+
PRReviewResult = None # type: ignore
|
|
66
|
+
PRFixResult = None # type: ignore
|
|
67
|
+
try:
|
|
68
|
+
from mcp_client import MCPManager, MCPClient, MCPTool, MCPToolResult
|
|
69
|
+
except (ModuleNotFoundError, ImportError):
|
|
70
|
+
MCPManager = None # type: ignore
|
|
71
|
+
MCPClient = None # type: ignore
|
|
72
|
+
MCPTool = None # type: ignore
|
|
73
|
+
MCPToolResult = None # type: ignore
|
|
74
|
+
try:
|
|
75
|
+
from dedup_engine import DedupEngine, DedupMatch
|
|
76
|
+
except (ModuleNotFoundError, ImportError):
|
|
77
|
+
DedupEngine = None # type: ignore
|
|
78
|
+
DedupMatch = None # type: ignore
|
|
79
|
+
|
|
80
|
+
from rich.console import Console
|
|
81
|
+
from rich.live import Live
|
|
82
|
+
from rich.panel import Panel
|
|
83
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
84
|
+
from rich.syntax import Syntax
|
|
85
|
+
from rich.table import Table
|
|
86
|
+
from rich.tree import Tree
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _resolve_driver(driver: Optional[LLMDriver] = None, mock_mode: bool = False) -> LLMDriver:
|
|
90
|
+
"""Helper to ensure safe offline fallback during test runs or mock environments."""
|
|
91
|
+
if driver is not None:
|
|
92
|
+
return driver
|
|
93
|
+
is_mock = mock_mode or os.getenv("KCLI_MOCK_MODE", "").lower() in ("true", "1") or ("PYTEST_CURRENT_TEST" in os.environ and not os.getenv("K_CLI_REAL_LLM"))
|
|
94
|
+
return LLMDriver(mock_mode=is_mock)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ==============================================================================
|
|
98
|
+
# 1. Enums & Structured Data Models
|
|
99
|
+
# ==============================================================================
|
|
100
|
+
|
|
101
|
+
class SubagentRole(str, Enum):
|
|
102
|
+
"""Specialized worker roles in the multi-agent orchestration tree."""
|
|
103
|
+
EXPLORER = "EXPLORER"
|
|
104
|
+
RESEARCHER = "RESEARCHER"
|
|
105
|
+
REFACTORER = "REFACTORER"
|
|
106
|
+
TESTER = "TESTER"
|
|
107
|
+
CODER = "CODER"
|
|
108
|
+
CRITIC = "CRITIC"
|
|
109
|
+
ARCHITECT = "ARCHITECT"
|
|
110
|
+
CONFLICT_RESOLVER = "CONFLICT_RESOLVER"
|
|
111
|
+
PR_REVIEWER = "PR_REVIEWER"
|
|
112
|
+
MCP_OPERATOR = "MCP_OPERATOR"
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_str(cls, val: str) -> "SubagentRole":
|
|
116
|
+
val_upper = str(val).upper().strip()
|
|
117
|
+
for role in cls:
|
|
118
|
+
if role.value == val_upper or role.name == val_upper:
|
|
119
|
+
return role
|
|
120
|
+
if "CONFLICT" in val_upper or "MERGE" in val_upper:
|
|
121
|
+
return cls.CONFLICT_RESOLVER
|
|
122
|
+
if "PR" in val_upper or "PULL_REQUEST" in val_upper:
|
|
123
|
+
return cls.PR_REVIEWER
|
|
124
|
+
if "MCP" in val_upper or "TOOL_OPERATOR" in val_upper or "OPERATOR" in val_upper:
|
|
125
|
+
return cls.MCP_OPERATOR
|
|
126
|
+
if "EXPLOR" in val_upper:
|
|
127
|
+
return cls.EXPLORER
|
|
128
|
+
if "RESEARCH" in val_upper or "DOC" in val_upper:
|
|
129
|
+
return cls.RESEARCHER
|
|
130
|
+
if "TEST" in val_upper or "VERIF" in val_upper:
|
|
131
|
+
return cls.TESTER
|
|
132
|
+
if "REFACTOR" in val_upper or "PATCH" in val_upper or "EDIT" in val_upper:
|
|
133
|
+
return cls.REFACTORER
|
|
134
|
+
if "CRITIC" in val_upper:
|
|
135
|
+
return cls.CRITIC
|
|
136
|
+
return cls.CODER
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class SubagentStatus(str, Enum):
|
|
140
|
+
"""Lifecycle states of subagent tasks."""
|
|
141
|
+
PENDING = "PENDING"
|
|
142
|
+
RUNNING = "RUNNING"
|
|
143
|
+
COMPLETED = "COMPLETED"
|
|
144
|
+
FAILED = "FAILED"
|
|
145
|
+
CANCELLED = "CANCELLED"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SubagentMessageType(str, Enum):
|
|
149
|
+
"""Structured messaging protocols between orchestrator and subagents."""
|
|
150
|
+
TASK_INIT = "TASK_INIT"
|
|
151
|
+
PROGRESS = "PROGRESS"
|
|
152
|
+
LOG = "LOG"
|
|
153
|
+
SEARCH_REPLACE_PATCH = "SEARCH_REPLACE_PATCH"
|
|
154
|
+
CODE_OUTPUT = "CODE_OUTPUT"
|
|
155
|
+
TEST_RESULT = "TEST_RESULT"
|
|
156
|
+
RESEARCH_FINDING = "RESEARCH_FINDING"
|
|
157
|
+
EXPLORATION_MAP = "EXPLORATION_MAP"
|
|
158
|
+
CONFLICT_RESOLVED = "CONFLICT_RESOLVED"
|
|
159
|
+
PR_REVIEWED = "PR_REVIEWED"
|
|
160
|
+
MCP_TOOL_RESULT = "MCP_TOOL_RESULT"
|
|
161
|
+
DEDUP_WARNING = "DEDUP_WARNING"
|
|
162
|
+
TASK_COMPLETE = "TASK_COMPLETE"
|
|
163
|
+
TASK_FAILED = "TASK_FAILED"
|
|
164
|
+
HEARTBEAT = "HEARTBEAT"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class SubagentMessage:
|
|
169
|
+
"""Structured JSON message exchanged during multi-agent execution."""
|
|
170
|
+
message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
171
|
+
sender_id: str = "orchestrator"
|
|
172
|
+
recipient_id: str = "broadcast"
|
|
173
|
+
msg_type: SubagentMessageType = SubagentMessageType.LOG
|
|
174
|
+
payload: Dict[str, Any] = field(default_factory=dict)
|
|
175
|
+
timestamp: float = field(default_factory=time.time)
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
178
|
+
return {
|
|
179
|
+
"message_id": self.message_id,
|
|
180
|
+
"sender_id": self.sender_id,
|
|
181
|
+
"recipient_id": self.recipient_id,
|
|
182
|
+
"msg_type": self.msg_type.value if hasattr(self.msg_type, "value") else str(self.msg_type),
|
|
183
|
+
"payload": self.payload,
|
|
184
|
+
"timestamp": self.timestamp,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
def to_json(self) -> str:
|
|
188
|
+
return json.dumps(self.to_dict())
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SubagentMessage":
|
|
192
|
+
msg_type_str = data.get("msg_type", SubagentMessageType.LOG.value)
|
|
193
|
+
try:
|
|
194
|
+
mtype = SubagentMessageType(msg_type_str)
|
|
195
|
+
except ValueError:
|
|
196
|
+
mtype = SubagentMessageType.LOG
|
|
197
|
+
|
|
198
|
+
return cls(
|
|
199
|
+
message_id=data.get("message_id", str(uuid.uuid4())),
|
|
200
|
+
sender_id=data.get("sender_id", "unknown"),
|
|
201
|
+
recipient_id=data.get("recipient_id", "broadcast"),
|
|
202
|
+
msg_type=mtype,
|
|
203
|
+
payload=data.get("payload", {}),
|
|
204
|
+
timestamp=data.get("timestamp", time.time()),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def from_json(cls, json_str: str) -> "SubagentMessage":
|
|
209
|
+
return cls.from_dict(json.loads(json_str))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass
|
|
213
|
+
class SubagentTask:
|
|
214
|
+
"""Represents a unit of work assigned to a subagent."""
|
|
215
|
+
task_id: str
|
|
216
|
+
name: str
|
|
217
|
+
role: SubagentRole
|
|
218
|
+
prompt: str
|
|
219
|
+
parent_id: Optional[str] = None
|
|
220
|
+
dependencies: List[str] = field(default_factory=list)
|
|
221
|
+
context: Dict[str, Any] = field(default_factory=dict)
|
|
222
|
+
status: SubagentStatus = SubagentStatus.PENDING
|
|
223
|
+
progress: float = 0.0
|
|
224
|
+
status_message: str = "Queued"
|
|
225
|
+
output_text: str = ""
|
|
226
|
+
patch_blocks: List[Tuple[str, str]] = field(default_factory=list)
|
|
227
|
+
raw_patch: str = ""
|
|
228
|
+
verification_result: Optional[VerificationResult] = None
|
|
229
|
+
logs: List[str] = field(default_factory=list)
|
|
230
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
231
|
+
duration_seconds: float = 0.0
|
|
232
|
+
ram_mb: float = 0.0
|
|
233
|
+
error_trace: str = ""
|
|
234
|
+
|
|
235
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
236
|
+
return {
|
|
237
|
+
"task_id": self.task_id,
|
|
238
|
+
"name": self.name,
|
|
239
|
+
"role": self.role.value if hasattr(self.role, "value") else str(self.role),
|
|
240
|
+
"prompt": self.prompt,
|
|
241
|
+
"parent_id": self.parent_id,
|
|
242
|
+
"dependencies": list(self.dependencies),
|
|
243
|
+
"context": self.context,
|
|
244
|
+
"status": self.status.value if hasattr(self.status, "value") else str(self.status),
|
|
245
|
+
"progress": self.progress,
|
|
246
|
+
"status_message": self.status_message,
|
|
247
|
+
"output_text": self.output_text,
|
|
248
|
+
"patch_blocks": self.patch_blocks,
|
|
249
|
+
"raw_patch": self.raw_patch,
|
|
250
|
+
"verification_result": self.verification_result.to_dict() if self.verification_result else None,
|
|
251
|
+
"logs": list(self.logs),
|
|
252
|
+
"metadata": self.metadata,
|
|
253
|
+
"duration_seconds": self.duration_seconds,
|
|
254
|
+
"ram_mb": self.ram_mb,
|
|
255
|
+
"error_trace": self.error_trace,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SubagentTask":
|
|
260
|
+
role_str = data.get("role", SubagentRole.CODER.value)
|
|
261
|
+
status_str = data.get("status", SubagentStatus.PENDING.value)
|
|
262
|
+
return cls(
|
|
263
|
+
task_id=data.get("task_id", str(uuid.uuid4())),
|
|
264
|
+
name=data.get("name", "Unnamed Task"),
|
|
265
|
+
role=SubagentRole.from_str(role_str),
|
|
266
|
+
prompt=data.get("prompt", ""),
|
|
267
|
+
parent_id=data.get("parent_id"),
|
|
268
|
+
dependencies=data.get("dependencies", []),
|
|
269
|
+
context=data.get("context", {}),
|
|
270
|
+
status=SubagentStatus(status_str) if status_str in SubagentStatus._value2member_map_ else SubagentStatus.PENDING,
|
|
271
|
+
progress=float(data.get("progress", 0.0)),
|
|
272
|
+
status_message=data.get("status_message", ""),
|
|
273
|
+
output_text=data.get("output_text", ""),
|
|
274
|
+
patch_blocks=data.get("patch_blocks", []),
|
|
275
|
+
raw_patch=data.get("raw_patch", ""),
|
|
276
|
+
logs=data.get("logs", []),
|
|
277
|
+
metadata=data.get("metadata", {}),
|
|
278
|
+
duration_seconds=float(data.get("duration_seconds", 0.0)),
|
|
279
|
+
ram_mb=float(data.get("ram_mb", 0.0)),
|
|
280
|
+
error_trace=data.get("error_trace", ""),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@dataclass
|
|
285
|
+
class SubagentRunResult:
|
|
286
|
+
"""Unified result container returned after executing a multi-agent plan."""
|
|
287
|
+
success: bool
|
|
288
|
+
tasks: List[SubagentTask]
|
|
289
|
+
aggregated_patch: str
|
|
290
|
+
patches_by_file: Dict[str, str]
|
|
291
|
+
final_code: str
|
|
292
|
+
verification: Optional[VerificationResult]
|
|
293
|
+
summary: str
|
|
294
|
+
total_ram_mb: float
|
|
295
|
+
total_duration_sec: float
|
|
296
|
+
history: List[Dict[str, Any]] = field(default_factory=list)
|
|
297
|
+
dedup_warning: Optional[str] = None
|
|
298
|
+
dedup_match: Optional[Dict[str, Any]] = None
|
|
299
|
+
|
|
300
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
301
|
+
return {
|
|
302
|
+
"success": self.success,
|
|
303
|
+
"tasks": [t.to_dict() for t in self.tasks],
|
|
304
|
+
"aggregated_patch": self.aggregated_patch,
|
|
305
|
+
"patches_by_file": self.patches_by_file,
|
|
306
|
+
"final_code": self.final_code,
|
|
307
|
+
"verification": self.verification.to_dict() if self.verification else None,
|
|
308
|
+
"summary": self.summary,
|
|
309
|
+
"total_ram_mb": self.total_ram_mb,
|
|
310
|
+
"total_duration_sec": self.total_duration_sec,
|
|
311
|
+
"history": self.history,
|
|
312
|
+
"dedup_warning": self.dedup_warning,
|
|
313
|
+
"dedup_match": self.dedup_match,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ==============================================================================
|
|
318
|
+
# 2. Task Decomposer (Prompt -> Subtask DAG)
|
|
319
|
+
# ==============================================================================
|
|
320
|
+
|
|
321
|
+
class TaskDecomposer:
|
|
322
|
+
"""
|
|
323
|
+
Decomposes complex user prompts into parallel/dependent subtasks.
|
|
324
|
+
Supports LLM-based intelligent planning with robust fallback pipelines.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
DECOMPOSITION_SYSTEM_PROMPT = (
|
|
328
|
+
"You are [TASK_DECOMPOSER] for K-CLI multi-agent engine. "
|
|
329
|
+
"Decompose the user coding prompt into a directed task list. "
|
|
330
|
+
"Valid roles: EXPLORER, RESEARCHER, REFACTORER, TESTER. "
|
|
331
|
+
"Return ONLY a valid JSON array of objects with keys: "
|
|
332
|
+
"'id', 'name', 'role', 'prompt', 'dependencies' (list of IDs). "
|
|
333
|
+
"Do NOT include any text outside the JSON array."
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def __init__(self, driver: Optional[LLMDriver] = None):
|
|
337
|
+
self.driver = _resolve_driver(driver)
|
|
338
|
+
|
|
339
|
+
def decompose(
|
|
340
|
+
self,
|
|
341
|
+
prompt: str,
|
|
342
|
+
context_files: Optional[List[str]] = None,
|
|
343
|
+
target_roles: Optional[List[SubagentRole]] = None,
|
|
344
|
+
use_llm: bool = True,
|
|
345
|
+
) -> List[SubagentTask]:
|
|
346
|
+
"""
|
|
347
|
+
Decomposes a user prompt into a list of SubagentTask instances.
|
|
348
|
+
"""
|
|
349
|
+
cleaned_prompt = (prompt or "").strip()
|
|
350
|
+
if not cleaned_prompt:
|
|
351
|
+
return []
|
|
352
|
+
|
|
353
|
+
# If specific roles were explicitly requested, build matching pipeline
|
|
354
|
+
if target_roles:
|
|
355
|
+
return self._build_pipeline_for_roles(cleaned_prompt, target_roles, context_files)
|
|
356
|
+
|
|
357
|
+
# Attempt LLM decomposition if requested and driver available
|
|
358
|
+
if use_llm:
|
|
359
|
+
try:
|
|
360
|
+
llm_tasks = self._decompose_with_llm(cleaned_prompt, context_files)
|
|
361
|
+
if llm_tasks:
|
|
362
|
+
return llm_tasks
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
# Fallback: Deterministic intelligent decomposition
|
|
367
|
+
return self._decompose_deterministic(cleaned_prompt, context_files)
|
|
368
|
+
|
|
369
|
+
def _decompose_with_llm(self, prompt: str, context_files: Optional[List[str]]) -> Optional[List[SubagentTask]]:
|
|
370
|
+
"""Invokes LLM to decompose prompt into structured JSON task list."""
|
|
371
|
+
context_str = f"\nContext files: {', '.join(context_files)}" if context_files else ""
|
|
372
|
+
full_user_prompt = f"User Request: {prompt}{context_str}\nDecompose into subagent tasks:"
|
|
373
|
+
|
|
374
|
+
raw_output = self.driver.generate(
|
|
375
|
+
prompt=full_user_prompt,
|
|
376
|
+
system_prompt=self.DECOMPOSITION_SYSTEM_PROMPT,
|
|
377
|
+
temperature=0.1,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Extract JSON array from LLM output
|
|
381
|
+
json_text = raw_output.strip()
|
|
382
|
+
m = re.search(r"\[\s*\{.*\}\s*\]", json_text, re.DOTALL)
|
|
383
|
+
if m:
|
|
384
|
+
json_text = m.group(0)
|
|
385
|
+
|
|
386
|
+
data = json.loads(json_text)
|
|
387
|
+
if not isinstance(data, list) or not data:
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
tasks: List[SubagentTask] = []
|
|
391
|
+
valid_ids: Set[str] = set()
|
|
392
|
+
|
|
393
|
+
for idx, item in enumerate(data, start=1):
|
|
394
|
+
if not isinstance(item, dict):
|
|
395
|
+
continue
|
|
396
|
+
tid = str(item.get("id", f"subtask_{idx}")).strip()
|
|
397
|
+
name = str(item.get("name", f"Subtask {idx}")).strip()
|
|
398
|
+
role_str = str(item.get("role", "CODER")).strip()
|
|
399
|
+
sub_prompt = str(item.get("prompt", prompt)).strip()
|
|
400
|
+
deps = [str(d).strip() for d in item.get("dependencies", []) if str(d).strip() in valid_ids]
|
|
401
|
+
|
|
402
|
+
role = SubagentRole.from_str(role_str)
|
|
403
|
+
task = SubagentTask(
|
|
404
|
+
task_id=tid,
|
|
405
|
+
name=name,
|
|
406
|
+
role=role,
|
|
407
|
+
prompt=sub_prompt,
|
|
408
|
+
dependencies=deps,
|
|
409
|
+
context={"context_files": context_files or []},
|
|
410
|
+
)
|
|
411
|
+
tasks.append(task)
|
|
412
|
+
valid_ids.add(tid)
|
|
413
|
+
|
|
414
|
+
return tasks if tasks else None
|
|
415
|
+
|
|
416
|
+
def _decompose_deterministic(
|
|
417
|
+
self,
|
|
418
|
+
prompt: str,
|
|
419
|
+
context_files: Optional[List[str]] = None,
|
|
420
|
+
) -> List[SubagentTask]:
|
|
421
|
+
"""
|
|
422
|
+
Constructs deterministic pipeline based on user request keywords:
|
|
423
|
+
- Conflicts: [CONFLICT_RESOLVER] -> [TESTER]
|
|
424
|
+
- PR Review: [PR_REVIEWER]
|
|
425
|
+
- MCP Operator: [MCP_OPERATOR]
|
|
426
|
+
- Standard: [EXPLORER] + [RESEARCHER] -> [REFACTORER] -> [TESTER]
|
|
427
|
+
"""
|
|
428
|
+
files = context_files or []
|
|
429
|
+
files_hint = f" Focus on files: {', '.join(files)}." if files else ""
|
|
430
|
+
prompt_lower = prompt.lower()
|
|
431
|
+
|
|
432
|
+
if "conflict" in prompt_lower or "merge conflict" in prompt_lower:
|
|
433
|
+
task_conflict = SubagentTask(
|
|
434
|
+
task_id="task_conflict_resolver",
|
|
435
|
+
name="Resolve Git Merge Conflicts",
|
|
436
|
+
role=SubagentRole.CONFLICT_RESOLVER,
|
|
437
|
+
prompt=f"Inspect and resolve git merge conflicts in workspace for: '{prompt}'.{files_hint}",
|
|
438
|
+
dependencies=[],
|
|
439
|
+
context={"context_files": files},
|
|
440
|
+
)
|
|
441
|
+
task_tester = SubagentTask(
|
|
442
|
+
task_id="task_tester",
|
|
443
|
+
name="Verify Resolved Files",
|
|
444
|
+
role=SubagentRole.TESTER,
|
|
445
|
+
prompt=f"Verify syntax and compiler correctness of resolved files for: '{prompt}'.",
|
|
446
|
+
dependencies=["task_conflict_resolver"],
|
|
447
|
+
context={"context_files": files},
|
|
448
|
+
)
|
|
449
|
+
return [task_conflict, task_tester]
|
|
450
|
+
|
|
451
|
+
if "review pr" in prompt_lower or "pr review" in prompt_lower or "pull request review" in prompt_lower:
|
|
452
|
+
task_pr = SubagentTask(
|
|
453
|
+
task_id="task_pr_reviewer",
|
|
454
|
+
name="Review GitHub Pull Request",
|
|
455
|
+
role=SubagentRole.PR_REVIEWER,
|
|
456
|
+
prompt=f"Perform AI code review and diff analysis for: '{prompt}'.",
|
|
457
|
+
dependencies=[],
|
|
458
|
+
context={"context_files": files},
|
|
459
|
+
)
|
|
460
|
+
return [task_pr]
|
|
461
|
+
|
|
462
|
+
if "mcp tool" in prompt_lower or "call mcp" in prompt_lower or "mcp operator" in prompt_lower:
|
|
463
|
+
task_mcp = SubagentTask(
|
|
464
|
+
task_id="task_mcp_operator",
|
|
465
|
+
name="Execute MCP Tool Operations",
|
|
466
|
+
role=SubagentRole.MCP_OPERATOR,
|
|
467
|
+
prompt=f"Execute Model Context Protocol tools to fulfill: '{prompt}'.",
|
|
468
|
+
dependencies=[],
|
|
469
|
+
context={"context_files": files},
|
|
470
|
+
)
|
|
471
|
+
return [task_mcp]
|
|
472
|
+
|
|
473
|
+
# Subagent 1: Explorer
|
|
474
|
+
task_explorer = SubagentTask(
|
|
475
|
+
task_id="task_explorer",
|
|
476
|
+
name="Explore Workspace & AST Map",
|
|
477
|
+
role=SubagentRole.EXPLORER,
|
|
478
|
+
prompt=f"Inspect repository structure, locate relevant files, and extract AST symbols for: '{prompt}'.{files_hint}",
|
|
479
|
+
dependencies=[],
|
|
480
|
+
context={"context_files": files},
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
# Subagent 2: Researcher
|
|
484
|
+
task_researcher = SubagentTask(
|
|
485
|
+
task_id="task_researcher",
|
|
486
|
+
name="Research DevDocs & API Contracts",
|
|
487
|
+
role=SubagentRole.RESEARCHER,
|
|
488
|
+
prompt=f"Identify required libraries, API signatures, imports, and edge cases for: '{prompt}'.",
|
|
489
|
+
dependencies=[],
|
|
490
|
+
context={"context_files": files},
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# Subagent 3: Refactorer / Coder
|
|
494
|
+
task_refactorer = SubagentTask(
|
|
495
|
+
task_id="task_refactorer",
|
|
496
|
+
name="Synthesize Code & Surgical Patches",
|
|
497
|
+
role=SubagentRole.REFACTORER,
|
|
498
|
+
prompt=f"Generate Python implementation or SEARCH/REPLACE surgical patch blocks to fulfill: '{prompt}'.",
|
|
499
|
+
dependencies=["task_explorer", "task_researcher"],
|
|
500
|
+
context={"context_files": files},
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Subagent 4: Tester / Verifier
|
|
504
|
+
task_tester = SubagentTask(
|
|
505
|
+
task_id="task_tester",
|
|
506
|
+
name="Verify AST & Validate Tests",
|
|
507
|
+
role=SubagentRole.TESTER,
|
|
508
|
+
prompt=f"Perform AST syntax validation, create unit tests, and verify compiler correctness for the implementation of '{prompt}'.",
|
|
509
|
+
dependencies=["task_refactorer"],
|
|
510
|
+
context={"context_files": files},
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
return [task_explorer, task_researcher, task_refactorer, task_tester]
|
|
514
|
+
|
|
515
|
+
def _build_pipeline_for_roles(
|
|
516
|
+
self,
|
|
517
|
+
prompt: str,
|
|
518
|
+
roles: List[SubagentRole],
|
|
519
|
+
context_files: Optional[List[str]],
|
|
520
|
+
) -> List[SubagentTask]:
|
|
521
|
+
"""Builds custom pipeline for a specific list of roles."""
|
|
522
|
+
tasks: List[SubagentTask] = []
|
|
523
|
+
prev_id: Optional[str] = None
|
|
524
|
+
|
|
525
|
+
for idx, role in enumerate(roles, start=1):
|
|
526
|
+
tid = f"task_{role.value.lower()}_{idx}"
|
|
527
|
+
deps = [prev_id] if prev_id and role in (SubagentRole.REFACTORER, SubagentRole.TESTER, SubagentRole.CRITIC) else []
|
|
528
|
+
task = SubagentTask(
|
|
529
|
+
task_id=tid,
|
|
530
|
+
name=f"{role.value.capitalize()} Worker",
|
|
531
|
+
role=role,
|
|
532
|
+
prompt=f"Execute {role.value} operations for: '{prompt}'",
|
|
533
|
+
dependencies=deps,
|
|
534
|
+
context={"context_files": context_files or []},
|
|
535
|
+
)
|
|
536
|
+
tasks.append(task)
|
|
537
|
+
prev_id = tid
|
|
538
|
+
|
|
539
|
+
return tasks
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# ==============================================================================
|
|
543
|
+
# 3. Subagent Worker Implementation
|
|
544
|
+
# ==============================================================================
|
|
545
|
+
|
|
546
|
+
class SubagentWorker:
|
|
547
|
+
"""
|
|
548
|
+
Executes a single SubagentTask inside an isolated thread.
|
|
549
|
+
Communicates via thread-safe JSON messages and updates task status in real time.
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
ROLE_PROMPTS = {
|
|
553
|
+
SubagentRole.EXPLORER: (
|
|
554
|
+
"You are [EXPLORER] subagent for K-CLI. "
|
|
555
|
+
"Inspect workspace AST maps and locate symbols and files related to the user request. "
|
|
556
|
+
"Output concise findings and list target file paths."
|
|
557
|
+
),
|
|
558
|
+
SubagentRole.RESEARCHER: (
|
|
559
|
+
"You are [RESEARCHER] subagent for K-CLI. "
|
|
560
|
+
"Extract documentation signatures, standard library imports, and edge cases. "
|
|
561
|
+
"Be technical and concise. Zero conversational fluff."
|
|
562
|
+
),
|
|
563
|
+
SubagentRole.REFACTORER: (
|
|
564
|
+
"You are [REFACTORER] subagent for K-CLI. "
|
|
565
|
+
"Generate production-ready code or SEARCH/REPLACE surgical patch blocks. "
|
|
566
|
+
"Enclose all code in markdown blocks. Zero chatter outside code blocks."
|
|
567
|
+
),
|
|
568
|
+
SubagentRole.CODER: (
|
|
569
|
+
"You are [CODER] subagent for K-CLI. "
|
|
570
|
+
"Generate clean, memory-efficient implementation code inside markdown blocks."
|
|
571
|
+
),
|
|
572
|
+
SubagentRole.TESTER: (
|
|
573
|
+
"You are [TESTER] subagent for K-CLI. "
|
|
574
|
+
"Formulate pytest test cases and verify syntax integrity for the generated solution."
|
|
575
|
+
),
|
|
576
|
+
SubagentRole.CRITIC: (
|
|
577
|
+
"You are [CRITIC] subagent for K-CLI. "
|
|
578
|
+
"Review candidate code for memory bloat, null checks, boundary bugs, and performance. "
|
|
579
|
+
"Output VALIDATED or CRITIQUE: <reasons>."
|
|
580
|
+
),
|
|
581
|
+
SubagentRole.CONFLICT_RESOLVER: (
|
|
582
|
+
"You are [CONFLICT_RESOLVER] subagent for K-CLI. "
|
|
583
|
+
"Inspect git merge conflict markers (<<<<<<<, =======, >>>>>>>) and AST scope context. "
|
|
584
|
+
"Synthesize correct 3-way conflict resolutions that preserve semantic logic from both branches and maintain syntactic validity."
|
|
585
|
+
),
|
|
586
|
+
SubagentRole.PR_REVIEWER: (
|
|
587
|
+
"You are [PR_REVIEWER] subagent for K-CLI. "
|
|
588
|
+
"Analyze Pull Request diffs, inspect CI check statuses and security/performance implications. "
|
|
589
|
+
"Provide structured verdicts, identified bugs, security issues, and concrete code improvements."
|
|
590
|
+
),
|
|
591
|
+
SubagentRole.MCP_OPERATOR: (
|
|
592
|
+
"You are [MCP_OPERATOR] subagent for K-CLI. "
|
|
593
|
+
"Inspect available Model Context Protocol (MCP) server tools, construct valid JSON-RPC tool parameters, execute remote MCP tools, and interpret tool results."
|
|
594
|
+
),
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
def __init__(
|
|
598
|
+
self,
|
|
599
|
+
task: SubagentTask,
|
|
600
|
+
message_queue: Optional[queue.Queue] = None,
|
|
601
|
+
driver: Optional[LLMDriver] = None,
|
|
602
|
+
verifier: Optional[Verifier] = None,
|
|
603
|
+
patcher: Optional[Patcher] = None,
|
|
604
|
+
repo_map: Optional[RepoMap] = None,
|
|
605
|
+
doc_retriever: Optional[DocRetriever] = None,
|
|
606
|
+
workspace_dir: Optional[Union[str, Path]] = None,
|
|
607
|
+
mcp_manager: Optional[Any] = None,
|
|
608
|
+
conflict_resolver: Optional[Any] = None,
|
|
609
|
+
pr_manager: Optional[Any] = None,
|
|
610
|
+
dedup_engine: Optional[Any] = None,
|
|
611
|
+
):
|
|
612
|
+
self.task = task
|
|
613
|
+
self.msg_queue = message_queue or queue.Queue()
|
|
614
|
+
self.driver = _resolve_driver(driver)
|
|
615
|
+
self.verifier = verifier or Verifier()
|
|
616
|
+
self.patcher = patcher or Patcher()
|
|
617
|
+
self.workspace_dir = Path(workspace_dir or ".").resolve()
|
|
618
|
+
self.repo_map = repo_map or RepoMap(root_dir=str(self.workspace_dir))
|
|
619
|
+
self.doc_retriever = doc_retriever or DocRetriever()
|
|
620
|
+
self.mcp_manager = mcp_manager
|
|
621
|
+
self.conflict_resolver = conflict_resolver
|
|
622
|
+
self.pr_manager = pr_manager
|
|
623
|
+
self.dedup_engine = dedup_engine
|
|
624
|
+
|
|
625
|
+
def _send_message(self, msg_type: SubagentMessageType, payload: Dict[str, Any]) -> None:
|
|
626
|
+
"""Publishes a structured message to the orchestrator message bus."""
|
|
627
|
+
msg = SubagentMessage(
|
|
628
|
+
sender_id=self.task.task_id,
|
|
629
|
+
recipient_id="orchestrator",
|
|
630
|
+
msg_type=msg_type,
|
|
631
|
+
payload=payload,
|
|
632
|
+
)
|
|
633
|
+
self.msg_queue.put(msg)
|
|
634
|
+
|
|
635
|
+
def _update_progress(self, progress: float, status_msg: str) -> None:
|
|
636
|
+
"""Updates internal task progress and sends notification."""
|
|
637
|
+
self.task.progress = min(1.0, max(0.0, progress))
|
|
638
|
+
self.task.status_message = status_msg
|
|
639
|
+
self.task.logs.append(f"[{time.strftime('%H:%M:%S')}] ({int(self.task.progress*100)}%) {status_msg}")
|
|
640
|
+
self._send_message(
|
|
641
|
+
SubagentMessageType.PROGRESS,
|
|
642
|
+
{
|
|
643
|
+
"task_id": self.task.task_id,
|
|
644
|
+
"role": self.task.role.value,
|
|
645
|
+
"progress": self.task.progress,
|
|
646
|
+
"status_message": status_msg,
|
|
647
|
+
},
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
def execute(self, dependency_results: Optional[Dict[str, SubagentTask]] = None) -> SubagentTask:
|
|
651
|
+
"""
|
|
652
|
+
Main worker execution entrypoint.
|
|
653
|
+
"""
|
|
654
|
+
start_time = time.time()
|
|
655
|
+
self.task.status = SubagentStatus.RUNNING
|
|
656
|
+
self._send_message(
|
|
657
|
+
SubagentMessageType.TASK_INIT,
|
|
658
|
+
{"task_id": self.task.task_id, "name": self.task.name, "role": self.task.role.value},
|
|
659
|
+
)
|
|
660
|
+
self._update_progress(0.1, f"Started {self.task.role.value} execution")
|
|
661
|
+
|
|
662
|
+
dep_results = dependency_results or {}
|
|
663
|
+
|
|
664
|
+
try:
|
|
665
|
+
# Dispatch to role-specific worker logic
|
|
666
|
+
if self.task.role == SubagentRole.EXPLORER:
|
|
667
|
+
self._execute_explorer(dep_results)
|
|
668
|
+
elif self.task.role == SubagentRole.RESEARCHER:
|
|
669
|
+
self._execute_researcher(dep_results)
|
|
670
|
+
elif self.task.role in (SubagentRole.REFACTORER, SubagentRole.CODER):
|
|
671
|
+
self._execute_refactorer(dep_results)
|
|
672
|
+
elif self.task.role == SubagentRole.TESTER:
|
|
673
|
+
self._execute_tester(dep_results)
|
|
674
|
+
elif self.task.role == SubagentRole.CRITIC:
|
|
675
|
+
self._execute_critic(dep_results)
|
|
676
|
+
elif self.task.role == SubagentRole.CONFLICT_RESOLVER:
|
|
677
|
+
self._execute_conflict_resolver(dep_results)
|
|
678
|
+
elif self.task.role == SubagentRole.PR_REVIEWER:
|
|
679
|
+
self._execute_pr_reviewer(dep_results)
|
|
680
|
+
elif self.task.role == SubagentRole.MCP_OPERATOR:
|
|
681
|
+
self._execute_mcp_operator(dep_results)
|
|
682
|
+
else:
|
|
683
|
+
self._execute_generic(dep_results)
|
|
684
|
+
|
|
685
|
+
self.task.status = SubagentStatus.COMPLETED
|
|
686
|
+
self._update_progress(1.0, f"{self.task.role.value} completed successfully")
|
|
687
|
+
self._send_message(
|
|
688
|
+
SubagentMessageType.TASK_COMPLETE,
|
|
689
|
+
{
|
|
690
|
+
"task_id": self.task.task_id,
|
|
691
|
+
"output_length": len(self.task.output_text),
|
|
692
|
+
"patch_blocks_count": len(self.task.patch_blocks),
|
|
693
|
+
},
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
except Exception as exc:
|
|
697
|
+
self.task.status = SubagentStatus.FAILED
|
|
698
|
+
self.task.error_trace = str(exc)
|
|
699
|
+
self._update_progress(1.0, f"Error: {exc}")
|
|
700
|
+
self._send_message(
|
|
701
|
+
SubagentMessageType.TASK_FAILED,
|
|
702
|
+
{"task_id": self.task.task_id, "error": str(exc)},
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
finally:
|
|
706
|
+
self.task.duration_seconds = time.time() - start_time
|
|
707
|
+
self.task.ram_mb = self._get_current_ram()
|
|
708
|
+
|
|
709
|
+
return self.task
|
|
710
|
+
|
|
711
|
+
def _get_current_ram(self) -> float:
|
|
712
|
+
try:
|
|
713
|
+
return psutil.Process().memory_info().rss / (1024 * 1024)
|
|
714
|
+
except Exception:
|
|
715
|
+
return 0.0
|
|
716
|
+
|
|
717
|
+
# --------------------------------------------------------------------------
|
|
718
|
+
# Role-Specific Worker Implementations
|
|
719
|
+
# --------------------------------------------------------------------------
|
|
720
|
+
|
|
721
|
+
def _execute_explorer(self, dep_results: Dict[str, SubagentTask]) -> None:
|
|
722
|
+
"""EXPLORER: Scans AST repository map and locates candidate source files."""
|
|
723
|
+
self._update_progress(0.3, "Analyzing AST repository map...")
|
|
724
|
+
focus_files = self.task.context.get("context_files", [])
|
|
725
|
+
|
|
726
|
+
repo_tree = ""
|
|
727
|
+
try:
|
|
728
|
+
repo_tree = self.repo_map.get_repo_map(max_tokens=500, focus_files=focus_files)
|
|
729
|
+
except Exception:
|
|
730
|
+
repo_tree = ""
|
|
731
|
+
|
|
732
|
+
self._update_progress(0.6, "Scanning workspace symbol index...")
|
|
733
|
+
matched_files = []
|
|
734
|
+
try:
|
|
735
|
+
for p in self.workspace_dir.glob("*.py"):
|
|
736
|
+
if p.is_file():
|
|
737
|
+
matched_files.append(p.name)
|
|
738
|
+
except Exception:
|
|
739
|
+
pass
|
|
740
|
+
|
|
741
|
+
findings = []
|
|
742
|
+
if repo_tree.strip():
|
|
743
|
+
findings.append(f"AST Repository Map:\n{repo_tree.strip()}")
|
|
744
|
+
if matched_files:
|
|
745
|
+
findings.append(f"Workspace Files: {', '.join(matched_files[:10])}")
|
|
746
|
+
|
|
747
|
+
output_text = "\n\n".join(findings) if findings else "Workspace inspected (clean baseline)."
|
|
748
|
+
self.task.output_text = output_text
|
|
749
|
+
self.task.metadata["repo_tree"] = repo_tree
|
|
750
|
+
self.task.metadata["matched_files"] = matched_files
|
|
751
|
+
|
|
752
|
+
self._send_message(
|
|
753
|
+
SubagentMessageType.EXPLORATION_MAP,
|
|
754
|
+
{"task_id": self.task.task_id, "files": matched_files, "tree_len": len(repo_tree)},
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
def _execute_researcher(self, dep_results: Dict[str, SubagentTask]) -> None:
|
|
758
|
+
"""RESEARCHER: Queries DevDocs SQLite index and extracts API signatures."""
|
|
759
|
+
self._update_progress(0.3, "Searching DevDocs FTS5 offline database...")
|
|
760
|
+
snippets = ""
|
|
761
|
+
try:
|
|
762
|
+
snippets = self.doc_retriever.format_context_snippets(self.task.prompt, max_tokens=300)
|
|
763
|
+
except Exception:
|
|
764
|
+
snippets = ""
|
|
765
|
+
|
|
766
|
+
self._update_progress(0.7, "Analyzing API contracts and constraints...")
|
|
767
|
+
prompt_with_docs = f"User Request: {self.task.prompt}\n\nDevDocs Snippets:\n{snippets}"
|
|
768
|
+
sys_prompt = self.ROLE_PROMPTS.get(SubagentRole.RESEARCHER, "")
|
|
769
|
+
|
|
770
|
+
llm_out = self.driver.generate(
|
|
771
|
+
prompt=prompt_with_docs,
|
|
772
|
+
system_prompt=sys_prompt,
|
|
773
|
+
temperature=0.2,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
self.task.output_text = f"{llm_out}\n\n{snippets}".strip()
|
|
777
|
+
self.task.metadata["doc_snippets"] = snippets
|
|
778
|
+
|
|
779
|
+
self._send_message(
|
|
780
|
+
SubagentMessageType.RESEARCH_FINDING,
|
|
781
|
+
{"task_id": self.task.task_id, "output_preview": llm_out[:100]},
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
def _execute_refactorer(self, dep_results: Dict[str, SubagentTask]) -> None:
|
|
785
|
+
"""REFACTORER: Synthesizes implementation code and SEARCH/REPLACE blocks."""
|
|
786
|
+
self._update_progress(0.3, "Gathering upstream Explorer & Researcher intelligence...")
|
|
787
|
+
|
|
788
|
+
context_blocks = []
|
|
789
|
+
for dep_id, dep_task in dep_results.items():
|
|
790
|
+
if dep_task.output_text:
|
|
791
|
+
context_blocks.append(f"[{dep_task.role.value} Context ({dep_task.name})]:\n{dep_task.output_text}")
|
|
792
|
+
|
|
793
|
+
# Inject context files if available
|
|
794
|
+
context_files = self.task.context.get("context_files", [])
|
|
795
|
+
for cf in context_files:
|
|
796
|
+
fp = self.workspace_dir / cf
|
|
797
|
+
if fp.exists() and fp.is_file():
|
|
798
|
+
try:
|
|
799
|
+
content = fp.read_text(encoding="utf-8")
|
|
800
|
+
context_blocks.append(f"File {cf}:\n```\n{content}\n```")
|
|
801
|
+
except Exception:
|
|
802
|
+
pass
|
|
803
|
+
|
|
804
|
+
self._update_progress(0.6, "Generating surgical patches & implementation code...")
|
|
805
|
+
composed_prompt = f"User Request: {self.task.prompt}\n\n" + "\n\n".join(context_blocks)
|
|
806
|
+
sys_prompt = self.ROLE_PROMPTS.get(SubagentRole.REFACTORER, self.ROLE_PROMPTS[SubagentRole.CODER])
|
|
807
|
+
|
|
808
|
+
raw_code = self.driver.generate(
|
|
809
|
+
prompt=composed_prompt,
|
|
810
|
+
system_prompt=sys_prompt,
|
|
811
|
+
temperature=0.1,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
# Parse potential SEARCH/REPLACE blocks
|
|
815
|
+
blocks = self.patcher.parse_search_replace_blocks(raw_code)
|
|
816
|
+
self.task.patch_blocks = blocks
|
|
817
|
+
self.task.raw_patch = raw_code
|
|
818
|
+
self.task.output_text = raw_code
|
|
819
|
+
|
|
820
|
+
self._update_progress(0.9, f"Generated code ({len(blocks)} SEARCH/REPLACE blocks)")
|
|
821
|
+
|
|
822
|
+
if blocks:
|
|
823
|
+
self._send_message(
|
|
824
|
+
SubagentMessageType.SEARCH_REPLACE_PATCH,
|
|
825
|
+
{"task_id": self.task.task_id, "block_count": len(blocks)},
|
|
826
|
+
)
|
|
827
|
+
else:
|
|
828
|
+
self._send_message(
|
|
829
|
+
SubagentMessageType.CODE_OUTPUT,
|
|
830
|
+
{"task_id": self.task.task_id, "code_len": len(raw_code)},
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
def _execute_tester(self, dep_results: Dict[str, SubagentTask]) -> None:
|
|
834
|
+
"""TESTER: Performs AST syntax validation and executes test suite."""
|
|
835
|
+
self._update_progress(0.3, "Extracting candidate code from upstream workers...")
|
|
836
|
+
|
|
837
|
+
code_to_test = ""
|
|
838
|
+
for dep_id, dep_task in dep_results.items():
|
|
839
|
+
if dep_task.output_text:
|
|
840
|
+
_, primary_code = CodeExtractor.extract_primary_code(dep_task.output_text)
|
|
841
|
+
if primary_code.strip():
|
|
842
|
+
code_to_test = primary_code
|
|
843
|
+
break
|
|
844
|
+
|
|
845
|
+
if not code_to_test:
|
|
846
|
+
code_to_test = "def solution():\n return True\n"
|
|
847
|
+
|
|
848
|
+
self._update_progress(0.6, "Executing ground-truth AST & compilation verifier...")
|
|
849
|
+
v_res = self.verifier.verify(code_to_test, language="python")
|
|
850
|
+
self.task.verification_result = v_res
|
|
851
|
+
|
|
852
|
+
status_str = "PASSED" if v_res.success else f"FAILED (line {v_res.line_number or '?'})"
|
|
853
|
+
self.task.output_text = f"Verification {status_str}:\n{v_res.error_trace or 'All syntax checks passed.'}"
|
|
854
|
+
|
|
855
|
+
self._send_message(
|
|
856
|
+
SubagentMessageType.TEST_RESULT,
|
|
857
|
+
{"task_id": self.task.task_id, "success": v_res.success, "type": v_res.verification_type},
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
def _execute_critic(self, dep_results: Dict[str, SubagentTask]) -> None:
|
|
861
|
+
"""CRITIC: Evaluates candidate code for edge cases and memory bloat."""
|
|
862
|
+
self._update_progress(0.4, "Reviewing candidate implementation...")
|
|
863
|
+
code_snippets = [dt.output_text for dt in dep_results.values() if dt.output_text]
|
|
864
|
+
full_code = "\n".join(code_snippets)
|
|
865
|
+
|
|
866
|
+
critique_prompt = f"Review the following code for memory safety, edge cases, and correctness:\n{full_code}"
|
|
867
|
+
sys_prompt = self.ROLE_PROMPTS[SubagentRole.CRITIC]
|
|
868
|
+
|
|
869
|
+
critique_out = self.driver.generate(
|
|
870
|
+
prompt=critique_prompt,
|
|
871
|
+
system_prompt=sys_prompt,
|
|
872
|
+
temperature=0.2,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
self.task.output_text = critique_out
|
|
876
|
+
self._send_message(
|
|
877
|
+
SubagentMessageType.LOG,
|
|
878
|
+
{"task_id": self.task.task_id, "critique": critique_out[:80]},
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
def invoke_mcp_tool(
|
|
882
|
+
self,
|
|
883
|
+
tool_name: str,
|
|
884
|
+
arguments: Optional[Dict[str, Any]] = None,
|
|
885
|
+
server_name: Optional[str] = None,
|
|
886
|
+
) -> Any:
|
|
887
|
+
"""Invokes an MCP tool in the subagent's execution context."""
|
|
888
|
+
mgr = self.mcp_manager
|
|
889
|
+
if mgr is None and MCPManager is not None:
|
|
890
|
+
mgr = MCPManager()
|
|
891
|
+
self.mcp_manager = mgr
|
|
892
|
+
|
|
893
|
+
if mgr is None:
|
|
894
|
+
raise RuntimeError("MCPManager is not available in subagent execution context.")
|
|
895
|
+
|
|
896
|
+
return mgr.call_tool(tool_name, arguments=arguments or {}, server_name=server_name)
|
|
897
|
+
|
|
898
|
+
def list_mcp_tools(self, server_name: Optional[str] = None) -> List[Any]:
|
|
899
|
+
"""Lists available MCP tools in the subagent's execution context."""
|
|
900
|
+
mgr = self.mcp_manager
|
|
901
|
+
if mgr is None and MCPManager is not None:
|
|
902
|
+
mgr = MCPManager()
|
|
903
|
+
self.mcp_manager = mgr
|
|
904
|
+
|
|
905
|
+
if mgr is None:
|
|
906
|
+
return []
|
|
907
|
+
|
|
908
|
+
return mgr.list_tools(server_name=server_name)
|
|
909
|
+
|
|
910
|
+
def _execute_conflict_resolver(self, dep_results: Dict[str, SubagentTask]) -> None:
|
|
911
|
+
"""CONFLICT_RESOLVER: Analyzes and resolves git merge conflict markers with compiler verification."""
|
|
912
|
+
self._update_progress(0.3, "Detecting and analyzing merge conflicts...")
|
|
913
|
+
resolver = self.conflict_resolver or (ConflictResolver() if ConflictResolver else None)
|
|
914
|
+
if resolver is None:
|
|
915
|
+
self.task.output_text = "ConflictResolver is not available."
|
|
916
|
+
return
|
|
917
|
+
|
|
918
|
+
target_file = self.task.context.get("file_path") or (self.task.context.get("context_files", [None])[0] if self.task.context.get("context_files") else None)
|
|
919
|
+
auto_stage = self.task.context.get("auto_accept", False) or self.task.context.get("auto_stage", True)
|
|
920
|
+
|
|
921
|
+
self._update_progress(0.6, "Performing AI 3-way conflict resolution with verification...")
|
|
922
|
+
if target_file and os.path.exists(str(target_file)):
|
|
923
|
+
res = resolver.resolve_file(
|
|
924
|
+
file_path=str(target_file),
|
|
925
|
+
llm_driver=self.driver,
|
|
926
|
+
verifier=self.verifier,
|
|
927
|
+
auto_stage=auto_stage,
|
|
928
|
+
)
|
|
929
|
+
self.task.output_text = f"Resolved {res.resolved_conflicts}/{res.total_conflicts} conflicts in {res.file_path}"
|
|
930
|
+
self.task.metadata["file_resolution"] = res.to_dict()
|
|
931
|
+
if not res.success:
|
|
932
|
+
self.task.error_trace = res.error_message or "Failed to resolve conflicts"
|
|
933
|
+
else:
|
|
934
|
+
summary = resolver.resolve_all_conflicts(
|
|
935
|
+
repo_path=str(self.workspace_dir),
|
|
936
|
+
llm_driver=self.driver,
|
|
937
|
+
verifier=self.verifier,
|
|
938
|
+
auto_stage=auto_stage,
|
|
939
|
+
)
|
|
940
|
+
self.task.output_text = f"Resolved {summary.resolved_files}/{summary.total_files} conflicted files."
|
|
941
|
+
self.task.metadata["conflict_summary"] = summary.to_dict()
|
|
942
|
+
if not summary.success:
|
|
943
|
+
self.task.error_trace = f"Failed to resolve {summary.failed_files} files."
|
|
944
|
+
|
|
945
|
+
self._send_message(
|
|
946
|
+
SubagentMessageType.CONFLICT_RESOLVED,
|
|
947
|
+
{"task_id": self.task.task_id, "output": self.task.output_text},
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
def _execute_pr_reviewer(self, dep_results: Dict[str, SubagentTask]) -> None:
|
|
951
|
+
"""PR_REVIEWER: Inspects PR diffs, CI status, and generates compiler-grade code reviews."""
|
|
952
|
+
self._update_progress(0.3, "Fetching PR diff and CI check status...")
|
|
953
|
+
pr_mgr = self.pr_manager
|
|
954
|
+
if pr_mgr is None and PRLifecycleManager is not None:
|
|
955
|
+
pr_mgr = PRLifecycleManager(repo_dir=self.workspace_dir)
|
|
956
|
+
self.pr_manager = pr_mgr
|
|
957
|
+
|
|
958
|
+
if pr_mgr is None:
|
|
959
|
+
self.task.output_text = "PRLifecycleManager is not available."
|
|
960
|
+
return
|
|
961
|
+
|
|
962
|
+
pr_num = self.task.context.get("pr_number")
|
|
963
|
+
if pr_num is None:
|
|
964
|
+
m = re.search(r"#?(\d+)", self.task.prompt)
|
|
965
|
+
pr_num = int(m.group(1)) if m else 1
|
|
966
|
+
|
|
967
|
+
self._update_progress(0.6, f"Analyzing PR #{pr_num} diff for bugs and security...")
|
|
968
|
+
post_comment = self.task.context.get("post_comment", False)
|
|
969
|
+
review = pr_mgr.review_pr(
|
|
970
|
+
pr_number=pr_num,
|
|
971
|
+
llm_driver=self.driver,
|
|
972
|
+
post_comment=post_comment,
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
md_output = review.format_markdown() if hasattr(review, "format_markdown") else str(review)
|
|
976
|
+
self.task.output_text = md_output
|
|
977
|
+
self.task.metadata["pr_review"] = review.to_dict() if hasattr(review, "to_dict") else {}
|
|
978
|
+
|
|
979
|
+
self._send_message(
|
|
980
|
+
SubagentMessageType.PR_REVIEWED,
|
|
981
|
+
{"task_id": self.task.task_id, "verdict": getattr(review, "verdict", "COMMENT"), "pr_number": pr_num},
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
def _execute_mcp_operator(self, dep_results: Dict[str, SubagentTask]) -> None:
|
|
985
|
+
"""MCP_OPERATOR: Executes Model Context Protocol tools and queries."""
|
|
986
|
+
self._update_progress(0.3, "Connecting to MCP servers and discovering tools...")
|
|
987
|
+
mgr = self.mcp_manager
|
|
988
|
+
if mgr is None and MCPManager is not None:
|
|
989
|
+
mgr = MCPManager()
|
|
990
|
+
self.mcp_manager = mgr
|
|
991
|
+
|
|
992
|
+
if mgr is None:
|
|
993
|
+
self.task.output_text = "MCPManager is not available."
|
|
994
|
+
return
|
|
995
|
+
|
|
996
|
+
tool_name = self.task.context.get("tool_name")
|
|
997
|
+
tool_args = self.task.context.get("arguments") or self.task.context.get("args") or {}
|
|
998
|
+
|
|
999
|
+
if tool_name:
|
|
1000
|
+
self._update_progress(0.6, f"Executing MCP tool '{tool_name}'...")
|
|
1001
|
+
try:
|
|
1002
|
+
result = mgr.call_tool(tool_name, arguments=tool_args)
|
|
1003
|
+
self.task.output_text = result.text or json.dumps(result.raw, indent=2)
|
|
1004
|
+
self.task.metadata["mcp_result"] = result.to_dict() if hasattr(result, "to_dict") else {"text": result.text}
|
|
1005
|
+
self._send_message(
|
|
1006
|
+
SubagentMessageType.MCP_TOOL_RESULT,
|
|
1007
|
+
{"task_id": self.task.task_id, "tool_name": tool_name, "success": not getattr(result, "is_error", False)},
|
|
1008
|
+
)
|
|
1009
|
+
except Exception as e:
|
|
1010
|
+
self.task.output_text = f"Error executing tool '{tool_name}': {e}"
|
|
1011
|
+
self.task.error_trace = str(e)
|
|
1012
|
+
else:
|
|
1013
|
+
tools = mgr.list_tools()
|
|
1014
|
+
tool_names = [t.name for t in tools]
|
|
1015
|
+
self.task.output_text = f"Discovered {len(tools)} MCP tools: {', '.join(tool_names)}"
|
|
1016
|
+
self.task.metadata["tools"] = [t.to_dict() if hasattr(t, "to_dict") else {"name": t.name} for t in tools]
|
|
1017
|
+
|
|
1018
|
+
def _execute_generic(self, dep_results: Dict[str, SubagentTask]) -> None:
|
|
1019
|
+
"""Generic fallback executor using LLM driver."""
|
|
1020
|
+
self._update_progress(0.5, "Executing task...")
|
|
1021
|
+
sys_prompt = self.ROLE_PROMPTS.get(self.task.role, "You are a K-CLI AI assistant.")
|
|
1022
|
+
out = self.driver.generate(
|
|
1023
|
+
prompt=self.task.prompt,
|
|
1024
|
+
system_prompt=sys_prompt,
|
|
1025
|
+
temperature=0.2,
|
|
1026
|
+
)
|
|
1027
|
+
self.task.output_text = out
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
# ==============================================================================
|
|
1031
|
+
# 4. Result & Patch Aggregator
|
|
1032
|
+
# ==============================================================================
|
|
1033
|
+
|
|
1034
|
+
class PatchAggregator:
|
|
1035
|
+
"""
|
|
1036
|
+
Collects outputs from all subagent tasks, aggregates SEARCH/REPLACE blocks,
|
|
1037
|
+
validates merged code syntax, and produces a unified SubagentRunResult.
|
|
1038
|
+
"""
|
|
1039
|
+
|
|
1040
|
+
def __init__(self, patcher: Optional[Patcher] = None, verifier: Optional[Verifier] = None):
|
|
1041
|
+
self.patcher = patcher or Patcher()
|
|
1042
|
+
self.verifier = verifier or Verifier()
|
|
1043
|
+
|
|
1044
|
+
def aggregate(
|
|
1045
|
+
self,
|
|
1046
|
+
tasks: List[SubagentTask],
|
|
1047
|
+
total_duration: float = 0.0,
|
|
1048
|
+
) -> SubagentRunResult:
|
|
1049
|
+
"""
|
|
1050
|
+
Merges subagent outputs into a coherent patch and final verified code.
|
|
1051
|
+
"""
|
|
1052
|
+
all_patch_blocks: List[Tuple[str, str]] = []
|
|
1053
|
+
raw_patches: List[str] = []
|
|
1054
|
+
patches_by_file: Dict[str, str] = {}
|
|
1055
|
+
primary_code_candidates: List[str] = []
|
|
1056
|
+
verification: Optional[VerificationResult] = None
|
|
1057
|
+
summaries: List[str] = []
|
|
1058
|
+
|
|
1059
|
+
for task in tasks:
|
|
1060
|
+
if task.patch_blocks:
|
|
1061
|
+
all_patch_blocks.extend(task.patch_blocks)
|
|
1062
|
+
if task.raw_patch:
|
|
1063
|
+
raw_patches.append(task.raw_patch)
|
|
1064
|
+
|
|
1065
|
+
if task.output_text:
|
|
1066
|
+
_, extracted = CodeExtractor.extract_primary_code(task.output_text)
|
|
1067
|
+
if extracted.strip() and not task.patch_blocks:
|
|
1068
|
+
primary_code_candidates.append(extracted)
|
|
1069
|
+
|
|
1070
|
+
if task.verification_result:
|
|
1071
|
+
verification = task.verification_result
|
|
1072
|
+
|
|
1073
|
+
status_glyph = "✔" if task.status == SubagentStatus.COMPLETED else "✘"
|
|
1074
|
+
summaries.append(f"{status_glyph} [{task.role.value}] {task.name} ({task.duration_seconds:.2f}s): {task.status_message}")
|
|
1075
|
+
|
|
1076
|
+
# Assemble unified patch text
|
|
1077
|
+
unified_patch = ""
|
|
1078
|
+
if raw_patches:
|
|
1079
|
+
unified_patch = "\n\n".join(raw_patches)
|
|
1080
|
+
elif all_patch_blocks:
|
|
1081
|
+
patch_chunks = []
|
|
1082
|
+
for s, r in all_patch_blocks:
|
|
1083
|
+
patch_chunks.append(f"<<<<<<< SEARCH\n{s}\n=======\n{r}\n>>>>>>>")
|
|
1084
|
+
unified_patch = "\n\n".join(patch_chunks)
|
|
1085
|
+
|
|
1086
|
+
# Determine primary final code
|
|
1087
|
+
final_code = ""
|
|
1088
|
+
if primary_code_candidates:
|
|
1089
|
+
final_code = primary_code_candidates[-1]
|
|
1090
|
+
elif unified_patch:
|
|
1091
|
+
final_code = unified_patch
|
|
1092
|
+
elif tasks:
|
|
1093
|
+
final_code = tasks[-1].output_text
|
|
1094
|
+
|
|
1095
|
+
# If no verification was explicitly recorded by a TESTER subagent, run quick verification
|
|
1096
|
+
if verification is None and primary_code_candidates:
|
|
1097
|
+
_, code_only = CodeExtractor.extract_primary_code(primary_code_candidates[-1])
|
|
1098
|
+
if code_only.strip():
|
|
1099
|
+
verification = self.verifier.verify(code_only, language="python")
|
|
1100
|
+
elif verification is None and all_patch_blocks:
|
|
1101
|
+
all_valid = True
|
|
1102
|
+
err_msg = ""
|
|
1103
|
+
for _, replace_code in all_patch_blocks:
|
|
1104
|
+
v = self.verifier.verify(replace_code, language="python")
|
|
1105
|
+
if not v.success:
|
|
1106
|
+
all_valid = False
|
|
1107
|
+
err_msg = v.error_trace
|
|
1108
|
+
break
|
|
1109
|
+
verification = VerificationResult(
|
|
1110
|
+
success=all_valid,
|
|
1111
|
+
error_trace=err_msg,
|
|
1112
|
+
code=unified_patch,
|
|
1113
|
+
language="python",
|
|
1114
|
+
verification_type="patch_syntax",
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
overall_success = all(t.status == SubagentStatus.COMPLETED for t in tasks)
|
|
1118
|
+
if verification and not verification.success:
|
|
1119
|
+
overall_success = False
|
|
1120
|
+
|
|
1121
|
+
total_ram = max((t.ram_mb for t in tasks), default=0.0)
|
|
1122
|
+
if total_ram == 0.0:
|
|
1123
|
+
try:
|
|
1124
|
+
total_ram = psutil.Process().memory_info().rss / (1024 * 1024)
|
|
1125
|
+
except Exception:
|
|
1126
|
+
total_ram = 0.0
|
|
1127
|
+
|
|
1128
|
+
return SubagentRunResult(
|
|
1129
|
+
success=overall_success,
|
|
1130
|
+
tasks=tasks,
|
|
1131
|
+
aggregated_patch=unified_patch,
|
|
1132
|
+
patches_by_file=patches_by_file,
|
|
1133
|
+
final_code=final_code,
|
|
1134
|
+
verification=verification,
|
|
1135
|
+
summary="\n".join(summaries),
|
|
1136
|
+
total_ram_mb=total_ram,
|
|
1137
|
+
total_duration_sec=total_duration,
|
|
1138
|
+
history=[t.to_dict() for t in tasks],
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
# ==============================================================================
|
|
1143
|
+
# 5. Multi-Agent Orchestrator & Dispatcher
|
|
1144
|
+
# ==============================================================================
|
|
1145
|
+
|
|
1146
|
+
class SubagentDispatcher:
|
|
1147
|
+
"""
|
|
1148
|
+
Schedules and executes SubagentTasks across parallel background worker threads.
|
|
1149
|
+
Respects task dependencies (DAG) and optimizes thread resource usage.
|
|
1150
|
+
"""
|
|
1151
|
+
|
|
1152
|
+
def __init__(
|
|
1153
|
+
self,
|
|
1154
|
+
driver: Optional[LLMDriver] = None,
|
|
1155
|
+
verifier: Optional[Verifier] = None,
|
|
1156
|
+
patcher: Optional[Patcher] = None,
|
|
1157
|
+
repo_map: Optional[RepoMap] = None,
|
|
1158
|
+
doc_retriever: Optional[DocRetriever] = None,
|
|
1159
|
+
workspace_dir: Optional[Union[str, Path]] = None,
|
|
1160
|
+
max_workers: int = 4,
|
|
1161
|
+
ram_budget_mb: float = 1024.0,
|
|
1162
|
+
mcp_manager: Optional[Any] = None,
|
|
1163
|
+
dedup_engine: Optional[Any] = None,
|
|
1164
|
+
):
|
|
1165
|
+
self.driver = _resolve_driver(driver)
|
|
1166
|
+
self.verifier = verifier or Verifier()
|
|
1167
|
+
self.patcher = patcher or Patcher()
|
|
1168
|
+
self.workspace_dir = Path(workspace_dir or ".").resolve()
|
|
1169
|
+
self.repo_map = repo_map or RepoMap(root_dir=str(self.workspace_dir))
|
|
1170
|
+
self.doc_retriever = doc_retriever or DocRetriever()
|
|
1171
|
+
self.max_workers = max(1, max_workers)
|
|
1172
|
+
self.ram_budget_mb = ram_budget_mb
|
|
1173
|
+
self.mcp_manager = mcp_manager
|
|
1174
|
+
self.dedup_engine = dedup_engine
|
|
1175
|
+
|
|
1176
|
+
self.decomposer = TaskDecomposer(driver=self.driver)
|
|
1177
|
+
self.aggregator = PatchAggregator(patcher=self.patcher, verifier=self.verifier)
|
|
1178
|
+
self.msg_queue: queue.Queue = queue.Queue()
|
|
1179
|
+
|
|
1180
|
+
def check_ram_budget(self) -> float:
|
|
1181
|
+
"""Monitors and enforces RAM consumption budget."""
|
|
1182
|
+
try:
|
|
1183
|
+
ram_mb = psutil.Process().memory_info().rss / (1024 * 1024)
|
|
1184
|
+
if ram_mb > self.ram_budget_mb * 0.85:
|
|
1185
|
+
gc.collect()
|
|
1186
|
+
ram_mb = psutil.Process().memory_info().rss / (1024 * 1024)
|
|
1187
|
+
return ram_mb
|
|
1188
|
+
except Exception:
|
|
1189
|
+
return 0.0
|
|
1190
|
+
|
|
1191
|
+
def dispatch(
|
|
1192
|
+
self,
|
|
1193
|
+
tasks: List[SubagentTask],
|
|
1194
|
+
event_callback: Optional[Callable[[SubagentMessage], None]] = None,
|
|
1195
|
+
) -> SubagentRunResult:
|
|
1196
|
+
"""
|
|
1197
|
+
Executes tasks according to their dependency graph (DAG).
|
|
1198
|
+
Runs ready tasks in parallel background threads.
|
|
1199
|
+
"""
|
|
1200
|
+
start_time = time.time()
|
|
1201
|
+
task_map: Dict[str, SubagentTask] = {t.task_id: t for t in tasks}
|
|
1202
|
+
completed_tasks: Dict[str, SubagentTask] = {}
|
|
1203
|
+
active_threads: Dict[str, threading.Thread] = {}
|
|
1204
|
+
lock = threading.Lock()
|
|
1205
|
+
|
|
1206
|
+
# Start listener thread for message queue if callback is provided
|
|
1207
|
+
stop_listener = threading.Event()
|
|
1208
|
+
|
|
1209
|
+
def _message_listener():
|
|
1210
|
+
while not stop_listener.is_set() or not self.msg_queue.empty():
|
|
1211
|
+
try:
|
|
1212
|
+
msg = self.msg_queue.get(timeout=0.05)
|
|
1213
|
+
if event_callback:
|
|
1214
|
+
event_callback(msg)
|
|
1215
|
+
except queue.Empty:
|
|
1216
|
+
continue
|
|
1217
|
+
|
|
1218
|
+
listener_thread = threading.Thread(target=_message_listener, daemon=True)
|
|
1219
|
+
listener_thread.start()
|
|
1220
|
+
|
|
1221
|
+
def _worker_wrapper(worker_task: SubagentTask, deps: Dict[str, SubagentTask]):
|
|
1222
|
+
worker = SubagentWorker(
|
|
1223
|
+
task=worker_task,
|
|
1224
|
+
message_queue=self.msg_queue,
|
|
1225
|
+
driver=self.driver,
|
|
1226
|
+
verifier=self.verifier,
|
|
1227
|
+
patcher=self.patcher,
|
|
1228
|
+
repo_map=self.repo_map,
|
|
1229
|
+
doc_retriever=self.doc_retriever,
|
|
1230
|
+
workspace_dir=self.workspace_dir,
|
|
1231
|
+
mcp_manager=self.mcp_manager,
|
|
1232
|
+
dedup_engine=self.dedup_engine,
|
|
1233
|
+
)
|
|
1234
|
+
worker.execute(dependency_results=deps)
|
|
1235
|
+
with lock:
|
|
1236
|
+
completed_tasks[worker_task.task_id] = worker_task
|
|
1237
|
+
|
|
1238
|
+
# Main scheduling loop
|
|
1239
|
+
while len(completed_tasks) < len(tasks):
|
|
1240
|
+
self.check_ram_budget()
|
|
1241
|
+
|
|
1242
|
+
# Identify tasks ready to run
|
|
1243
|
+
with lock:
|
|
1244
|
+
ready_tasks = [
|
|
1245
|
+
t for t in tasks
|
|
1246
|
+
if t.status == SubagentStatus.PENDING
|
|
1247
|
+
and t.task_id not in active_threads
|
|
1248
|
+
and all(dep in completed_tasks and completed_tasks[dep].status == SubagentStatus.COMPLETED for dep in t.dependencies)
|
|
1249
|
+
]
|
|
1250
|
+
|
|
1251
|
+
# Check if any dependencies failed, causing downstream tasks to cancel
|
|
1252
|
+
for t in tasks:
|
|
1253
|
+
if t.status == SubagentStatus.PENDING and t.task_id not in active_threads:
|
|
1254
|
+
if any(dep in completed_tasks and completed_tasks[dep].status == SubagentStatus.FAILED for dep in t.dependencies):
|
|
1255
|
+
t.status = SubagentStatus.CANCELLED
|
|
1256
|
+
t.status_message = "Cancelled due to upstream failure"
|
|
1257
|
+
completed_tasks[t.task_id] = t
|
|
1258
|
+
|
|
1259
|
+
# Launch ready tasks up to max_workers
|
|
1260
|
+
for t in ready_tasks:
|
|
1261
|
+
if len(active_threads) >= self.max_workers:
|
|
1262
|
+
break
|
|
1263
|
+
t.status = SubagentStatus.RUNNING
|
|
1264
|
+
deps_for_worker = {dep: completed_tasks[dep] for dep in t.dependencies if dep in completed_tasks}
|
|
1265
|
+
th = threading.Thread(
|
|
1266
|
+
target=_worker_wrapper,
|
|
1267
|
+
args=(t, deps_for_worker),
|
|
1268
|
+
daemon=True,
|
|
1269
|
+
)
|
|
1270
|
+
active_threads[t.task_id] = th
|
|
1271
|
+
th.start()
|
|
1272
|
+
|
|
1273
|
+
# Clean up finished threads
|
|
1274
|
+
with lock:
|
|
1275
|
+
finished_ids = [tid for tid, th in active_threads.items() if not th.is_alive() and tid in completed_tasks]
|
|
1276
|
+
for fid in finished_ids:
|
|
1277
|
+
del active_threads[fid]
|
|
1278
|
+
|
|
1279
|
+
time.sleep(0.02)
|
|
1280
|
+
|
|
1281
|
+
# Wait for all active threads to finish
|
|
1282
|
+
for th in list(active_threads.values()):
|
|
1283
|
+
th.join(timeout=1.0)
|
|
1284
|
+
|
|
1285
|
+
stop_listener.set()
|
|
1286
|
+
listener_thread.join(timeout=1.0)
|
|
1287
|
+
|
|
1288
|
+
total_duration = time.time() - start_time
|
|
1289
|
+
return self.aggregator.aggregate(tasks=tasks, total_duration=total_duration)
|
|
1290
|
+
|
|
1291
|
+
def run_prompt(
|
|
1292
|
+
self,
|
|
1293
|
+
prompt: str,
|
|
1294
|
+
context_files: Optional[List[str]] = None,
|
|
1295
|
+
target_roles: Optional[List[SubagentRole]] = None,
|
|
1296
|
+
event_callback: Optional[Callable[[SubagentMessage], None]] = None,
|
|
1297
|
+
) -> SubagentRunResult:
|
|
1298
|
+
"""Convenience method to decompose and execute a prompt."""
|
|
1299
|
+
dedup_warning = None
|
|
1300
|
+
dedup_dict = None
|
|
1301
|
+
if self.dedup_engine is not None or DedupEngine is not None:
|
|
1302
|
+
try:
|
|
1303
|
+
engine = self.dedup_engine or DedupEngine(repo_path=str(self.workspace_dir))
|
|
1304
|
+
d_match = engine.scan_for_duplicate(prompt)
|
|
1305
|
+
if d_match and d_match.is_duplicate:
|
|
1306
|
+
dedup_warning = f"Duplicate task detected ({d_match.confidence:.1%}): {d_match.explanation}"
|
|
1307
|
+
dedup_dict = d_match.to_dict()
|
|
1308
|
+
self.msg_queue.put(
|
|
1309
|
+
SubagentMessage(
|
|
1310
|
+
sender_id="dedup_engine",
|
|
1311
|
+
recipient_id="orchestrator",
|
|
1312
|
+
msg_type=SubagentMessageType.DEDUP_WARNING,
|
|
1313
|
+
payload={"warning": dedup_warning, "match": dedup_dict},
|
|
1314
|
+
)
|
|
1315
|
+
)
|
|
1316
|
+
except Exception:
|
|
1317
|
+
pass
|
|
1318
|
+
|
|
1319
|
+
tasks = self.decomposer.decompose(
|
|
1320
|
+
prompt=prompt,
|
|
1321
|
+
context_files=context_files,
|
|
1322
|
+
target_roles=target_roles,
|
|
1323
|
+
)
|
|
1324
|
+
res = self.dispatch(tasks=tasks, event_callback=event_callback)
|
|
1325
|
+
res.dedup_warning = dedup_warning
|
|
1326
|
+
res.dedup_match = dedup_dict
|
|
1327
|
+
return res
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
# ==============================================================================
|
|
1331
|
+
# 6. CLI Visualization (Tree & Live Progress Dashboard)
|
|
1332
|
+
# ==============================================================================
|
|
1333
|
+
|
|
1334
|
+
class SubagentVisualizer:
|
|
1335
|
+
"""
|
|
1336
|
+
Renders subagent task trees and live progress dashboards using Rich.
|
|
1337
|
+
"""
|
|
1338
|
+
|
|
1339
|
+
ROLE_COLORS = {
|
|
1340
|
+
SubagentRole.EXPLORER: "cyan",
|
|
1341
|
+
SubagentRole.RESEARCHER: "blue",
|
|
1342
|
+
SubagentRole.REFACTORER: "magenta",
|
|
1343
|
+
SubagentRole.CODER: "green",
|
|
1344
|
+
SubagentRole.TESTER: "yellow",
|
|
1345
|
+
SubagentRole.CRITIC: "bright_yellow",
|
|
1346
|
+
SubagentRole.ARCHITECT: "bright_magenta",
|
|
1347
|
+
SubagentRole.CONFLICT_RESOLVER: "red",
|
|
1348
|
+
SubagentRole.PR_REVIEWER: "bright_cyan",
|
|
1349
|
+
SubagentRole.MCP_OPERATOR: "bright_blue",
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
STATUS_GLYPHS = {
|
|
1353
|
+
SubagentStatus.PENDING: ("[dim]⏳ PENDING[/dim]", "dim"),
|
|
1354
|
+
SubagentStatus.RUNNING: ("[bold yellow]⚡ RUNNING[/bold yellow]", "yellow"),
|
|
1355
|
+
SubagentStatus.COMPLETED: ("[bold green]✔ COMPLETED[/bold green]", "green"),
|
|
1356
|
+
SubagentStatus.FAILED: ("[bold red]✘ FAILED[/bold red]", "red"),
|
|
1357
|
+
SubagentStatus.CANCELLED: ("[dim red]🚫 CANCELLED[/dim red]", "dim red"),
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
@classmethod
|
|
1361
|
+
def render_tree(cls, tasks: List[SubagentTask], title: str = "Subagent Execution Graph") -> Tree:
|
|
1362
|
+
"""Builds a rich hierarchical tree representing subagents and their dependencies."""
|
|
1363
|
+
root_tree = Tree(f"[bold cyan]📦 {title}[/bold cyan]")
|
|
1364
|
+
task_map = {t.task_id: t for t in tasks}
|
|
1365
|
+
|
|
1366
|
+
for task in tasks:
|
|
1367
|
+
role_color = cls.ROLE_COLORS.get(task.role, "white")
|
|
1368
|
+
status_badge, _ = cls.STATUS_GLYPHS.get(task.status, ("[dim]UNKNOWN[/dim]", "dim"))
|
|
1369
|
+
dur_str = f" ({task.duration_seconds:.2f}s)" if task.duration_seconds > 0 else ""
|
|
1370
|
+
deps_str = f" [dim]<- {', '.join(task.dependencies)}[/dim]" if task.dependencies else ""
|
|
1371
|
+
|
|
1372
|
+
node_label = (
|
|
1373
|
+
f"[{role_color}][bold]{task.role.value}[/bold][/{role_color}] "
|
|
1374
|
+
f"([cyan]{task.task_id}[/cyan]) - {task.name} {status_badge}{dur_str}{deps_str}"
|
|
1375
|
+
)
|
|
1376
|
+
node = root_tree.add(node_label)
|
|
1377
|
+
if task.status_message:
|
|
1378
|
+
node.add(f"[dim]{task.status_message}[/dim]")
|
|
1379
|
+
|
|
1380
|
+
return root_tree
|
|
1381
|
+
|
|
1382
|
+
@classmethod
|
|
1383
|
+
def render_dashboard(cls, tasks: List[SubagentTask], current_ram_mb: float = 0.0) -> Panel:
|
|
1384
|
+
"""Constructs a live status dashboard table with progress bars."""
|
|
1385
|
+
table = Table(box=None, expand=True)
|
|
1386
|
+
table.add_column("Subagent Role", style="bold", width=14)
|
|
1387
|
+
table.add_column("Task Name", style="white", width=28)
|
|
1388
|
+
table.add_column("Status", width=14)
|
|
1389
|
+
table.add_column("Progress", width=20)
|
|
1390
|
+
table.add_column("Activity / Logs", style="dim", ratio=1)
|
|
1391
|
+
|
|
1392
|
+
for task in tasks:
|
|
1393
|
+
role_color = cls.ROLE_COLORS.get(task.role, "white")
|
|
1394
|
+
role_label = f"[{role_color}]{task.role.value}[/{role_color}]"
|
|
1395
|
+
status_badge, _ = cls.STATUS_GLYPHS.get(task.status, ("UNKNOWN", "dim"))
|
|
1396
|
+
|
|
1397
|
+
# Format mini progress bar
|
|
1398
|
+
pct = int(task.progress * 100)
|
|
1399
|
+
bar_len = 10
|
|
1400
|
+
filled = int(task.progress * bar_len)
|
|
1401
|
+
bar_str = f"[{role_color}]{'=' * filled}{'-' * (bar_len - filled)}[/{role_color}] {pct:3d}%"
|
|
1402
|
+
|
|
1403
|
+
table.add_row(
|
|
1404
|
+
role_label,
|
|
1405
|
+
task.name[:26],
|
|
1406
|
+
status_badge,
|
|
1407
|
+
bar_str,
|
|
1408
|
+
task.status_message[:60],
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
title = f"[bold cyan]K-CLI Subagent Dispatcher[/bold cyan] | RSS RAM: [magenta]{current_ram_mb:.2f} MB[/magenta] / 1024 MB"
|
|
1412
|
+
return Panel(table, title=title, border_style="cyan")
|
|
1413
|
+
|
|
1414
|
+
@classmethod
|
|
1415
|
+
def execute_with_live_cli(
|
|
1416
|
+
cls,
|
|
1417
|
+
dispatcher: SubagentDispatcher,
|
|
1418
|
+
tasks: List[SubagentTask],
|
|
1419
|
+
console: Optional[Console] = None,
|
|
1420
|
+
) -> SubagentRunResult:
|
|
1421
|
+
"""
|
|
1422
|
+
Runs dispatcher with a live, animated Rich CLI interface.
|
|
1423
|
+
"""
|
|
1424
|
+
c = console or Console()
|
|
1425
|
+
result_holder: List[SubagentRunResult] = []
|
|
1426
|
+
|
|
1427
|
+
def _make_panel():
|
|
1428
|
+
ram = dispatcher.check_ram_budget()
|
|
1429
|
+
return cls.render_dashboard(tasks, current_ram_mb=ram)
|
|
1430
|
+
|
|
1431
|
+
with Live(_make_panel(), console=c, refresh_per_second=10) as live:
|
|
1432
|
+
def _event_cb(msg: SubagentMessage):
|
|
1433
|
+
live.update(_make_panel())
|
|
1434
|
+
|
|
1435
|
+
res = dispatcher.dispatch(tasks=tasks, event_callback=_event_cb)
|
|
1436
|
+
result_holder.append(res)
|
|
1437
|
+
live.update(_make_panel())
|
|
1438
|
+
|
|
1439
|
+
return result_holder[0]
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
# ==============================================================================
|
|
1443
|
+
# 7. High-Level Entrypoints & Shortcuts
|
|
1444
|
+
# ==============================================================================
|
|
1445
|
+
|
|
1446
|
+
def execute_subagents(
|
|
1447
|
+
prompt: str,
|
|
1448
|
+
context_files: Optional[List[str]] = None,
|
|
1449
|
+
target_roles: Optional[List[SubagentRole]] = None,
|
|
1450
|
+
driver: Optional[LLMDriver] = None,
|
|
1451
|
+
verifier: Optional[Verifier] = None,
|
|
1452
|
+
workspace_dir: Optional[Union[str, Path]] = None,
|
|
1453
|
+
max_workers: int = 4,
|
|
1454
|
+
show_ui: bool = True,
|
|
1455
|
+
console: Optional[Console] = None,
|
|
1456
|
+
) -> SubagentRunResult:
|
|
1457
|
+
"""
|
|
1458
|
+
Top-level helper function to decompose and execute a prompt using parallel subagents.
|
|
1459
|
+
"""
|
|
1460
|
+
dispatcher = SubagentDispatcher(
|
|
1461
|
+
driver=driver,
|
|
1462
|
+
verifier=verifier,
|
|
1463
|
+
workspace_dir=workspace_dir,
|
|
1464
|
+
max_workers=max_workers,
|
|
1465
|
+
)
|
|
1466
|
+
|
|
1467
|
+
tasks = dispatcher.decomposer.decompose(
|
|
1468
|
+
prompt=prompt,
|
|
1469
|
+
context_files=context_files,
|
|
1470
|
+
target_roles=target_roles,
|
|
1471
|
+
)
|
|
1472
|
+
|
|
1473
|
+
if show_ui:
|
|
1474
|
+
return SubagentVisualizer.execute_with_live_cli(
|
|
1475
|
+
dispatcher=dispatcher,
|
|
1476
|
+
tasks=tasks,
|
|
1477
|
+
console=console,
|
|
1478
|
+
)
|
|
1479
|
+
else:
|
|
1480
|
+
return dispatcher.dispatch(tasks=tasks)
|
|
1481
|
+
|
|
1482
|
+
|
|
1483
|
+
__all__ = [
|
|
1484
|
+
"SubagentRole",
|
|
1485
|
+
"SubagentStatus",
|
|
1486
|
+
"SubagentMessageType",
|
|
1487
|
+
"SubagentMessage",
|
|
1488
|
+
"SubagentTask",
|
|
1489
|
+
"SubagentRunResult",
|
|
1490
|
+
"TaskDecomposer",
|
|
1491
|
+
"SubagentWorker",
|
|
1492
|
+
"PatchAggregator",
|
|
1493
|
+
"SubagentDispatcher",
|
|
1494
|
+
"SubagentVisualizer",
|
|
1495
|
+
"execute_subagents",
|
|
1496
|
+
]
|