ata-coder 2.4.2__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.
- ata_coder/__init__.py +1 -0
- ata_coder/agent.py +874 -0
- ata_coder/agent_compact.py +190 -0
- ata_coder/agent_controller.py +218 -0
- ata_coder/agent_extension.py +69 -0
- ata_coder/agent_routing.py +105 -0
- ata_coder/agent_subsystems.py +72 -0
- ata_coder/agent_tools.py +318 -0
- ata_coder/agent_undo.py +63 -0
- ata_coder/anthropic_client.py +465 -0
- ata_coder/change_tracker.py +368 -0
- ata_coder/clawd_integration.py +574 -0
- ata_coder/commands/__init__.py +128 -0
- ata_coder/commands/_core.py +184 -0
- ata_coder/commands/_safety.py +95 -0
- ata_coder/commands/_settings.py +241 -0
- ata_coder/commands/_workflow.py +451 -0
- ata_coder/commands.py +974 -0
- ata_coder/config.py +257 -0
- ata_coder/core/__init__.py +35 -0
- ata_coder/core/events.py +73 -0
- ata_coder/core/queue.py +85 -0
- ata_coder/core/state.py +17 -0
- ata_coder/event_queue.py +5 -0
- ata_coder/extension.py +654 -0
- ata_coder/extensions/__init__.py +1 -0
- ata_coder/extensions/hello_skill.py +47 -0
- ata_coder/fool_proof.py +295 -0
- ata_coder/git_workflow.py +371 -0
- ata_coder/gui.py +511 -0
- ata_coder/llm_client.py +543 -0
- ata_coder/main.py +814 -0
- ata_coder/mcp_client.py +1095 -0
- ata_coder/memory.py +539 -0
- ata_coder/model_registry.py +134 -0
- ata_coder/model_router.py +105 -0
- ata_coder/permissions.py +274 -0
- ata_coder/privilege.py +464 -0
- ata_coder/project.py +273 -0
- ata_coder/prompt_template.py +423 -0
- ata_coder/prompts/auto-mode.md +7 -0
- ata_coder/prompts/coding-rules.md +40 -0
- ata_coder/prompts/execution-guardrails.md +14 -0
- ata_coder/prompts/memory-system.md +24 -0
- ata_coder/prompts/output-style.md +23 -0
- ata_coder/prompts/safety.md +17 -0
- ata_coder/prompts/slash-commands.md +24 -0
- ata_coder/prompts/sub-agents.md +38 -0
- ata_coder/prompts/system-reminders.md +17 -0
- ata_coder/prompts/system.md +105 -0
- ata_coder/prompts/tool-policy.md +46 -0
- ata_coder/repl_theme.py +99 -0
- ata_coder/repl_tracker.py +89 -0
- ata_coder/repl_ui.py +1214 -0
- ata_coder/safety_guard.py +434 -0
- ata_coder/self_correct.py +346 -0
- ata_coder/server.py +882 -0
- ata_coder/server_session.py +159 -0
- ata_coder/server_shell.py +129 -0
- ata_coder/session.py +431 -0
- ata_coder/settings.py +439 -0
- ata_coder/setup_wizard.py +136 -0
- ata_coder/skill_extension.py +92 -0
- ata_coder/skills/architect/SKILL.md +42 -0
- ata_coder/skills/code-reviewer/SKILL.md +37 -0
- ata_coder/skills/codecraft/SKILL.md +452 -0
- ata_coder/skills/debugger/SKILL.md +45 -0
- ata_coder/skills/doc-writer/SKILL.md +36 -0
- ata_coder/skills/general-coder/SKILL.md +76 -0
- ata_coder/skills/math-calculator/README.md +40 -0
- ata_coder/skills/math-calculator/SKILL.md +59 -0
- ata_coder/skills/math-calculator/handler.py +103 -0
- ata_coder/skills/math-calculator/prompts/system.md +8 -0
- ata_coder/skills/math-calculator/requirements.txt +2 -0
- ata_coder/skills/math-calculator/resources/constants.json +8 -0
- ata_coder/skills/math-calculator/tests/test_handler.py +53 -0
- ata_coder/skills/security-auditor/SKILL.md +40 -0
- ata_coder/skills/test-writer/SKILL.md +36 -0
- ata_coder/skills/weather-skill/README.md +45 -0
- ata_coder/skills/weather-skill/handler.py +76 -0
- ata_coder/skills/weather-skill/manifest.json +48 -0
- ata_coder/skills/weather-skill/prompts/system_prompt.txt +9 -0
- ata_coder/skills/weather-skill/prompts/user_prompt_template.txt +3 -0
- ata_coder/skills/weather-skill/requirements.txt +1 -0
- ata_coder/skills/weather-skill/resources/city_list.json +17 -0
- ata_coder/skills/weather-skill/resources/error_messages.json +7 -0
- ata_coder/skills/weather-skill/tests/test_handler.py +28 -0
- ata_coder/skills/weather-skill/weather_utils.py +50 -0
- ata_coder/skills.py +1014 -0
- ata_coder/sub_agent.py +273 -0
- ata_coder/sub_agent_manager.py +203 -0
- ata_coder/system_prompt_builder.py +146 -0
- ata_coder/task_planner.py +391 -0
- ata_coder/terminal.py +318 -0
- ata_coder/test_runner.py +219 -0
- ata_coder/thread_supervisor.py +195 -0
- ata_coder/tool_defs.py +335 -0
- ata_coder/tools/__init__.py +11 -0
- ata_coder/tools/definitions.py +335 -0
- ata_coder/tools/executor.py +1036 -0
- ata_coder/tools/result.py +26 -0
- ata_coder/tools/subagent.py +332 -0
- ata_coder/tools/web.py +361 -0
- ata_coder/tools.py +1576 -0
- ata_coder/types.py +92 -0
- ata_coder/utils.py +113 -0
- ata_coder/web/css/style.css +180 -0
- ata_coder/web/index.html +84 -0
- ata_coder/web/js/app.js +489 -0
- ata_coder/web/package-lock.json +25 -0
- ata_coder/web/package.json +10 -0
- ata_coder/web/tsconfig.json +13 -0
- ata_coder-2.4.2.dist-info/METADATA +799 -0
- ata_coder-2.4.2.dist-info/RECORD +118 -0
- ata_coder-2.4.2.dist-info/WHEEL +5 -0
- ata_coder-2.4.2.dist-info/entry_points.txt +2 -0
- ata_coder-2.4.2.dist-info/licenses/LICENSE +21 -0
- ata_coder-2.4.2.dist-info/top_level.txt +1 -0
ata_coder/sub_agent.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Sub-Agent — independent agent with isolated context window.
|
|
4
|
+
|
|
5
|
+
Each SubAgent runs as an asyncio.Task and has:
|
|
6
|
+
- Independent LLM client (separate httpx session)
|
|
7
|
+
- Independent message history (no context leakage)
|
|
8
|
+
- Independent tool executor (own file cache)
|
|
9
|
+
- Cancel support via asyncio.Task.cancel()
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import uuid
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Callable, Optional
|
|
19
|
+
|
|
20
|
+
from .config import AppConfig, LLMConfig
|
|
21
|
+
from .llm_client import LLMClient
|
|
22
|
+
from .tools import ToolExecutor, TOOL_DEFINITIONS, ToolResult
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
__all__ = ["SubAgent", "SubAgentResult"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class SubAgentResult:
|
|
31
|
+
"""Result returned when a sub-agent completes."""
|
|
32
|
+
agent_id: str
|
|
33
|
+
result: Optional[str] = None
|
|
34
|
+
error: Optional[str] = None
|
|
35
|
+
messages: list[dict] = field(default_factory=list)
|
|
36
|
+
success: bool = True
|
|
37
|
+
tool_call_count: int = 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SubAgent:
|
|
41
|
+
"""
|
|
42
|
+
Independent sub-agent with isolated context window.
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
sub = SubAgent(config=config, skill_prompt="You are a debugger...")
|
|
46
|
+
await sub.run("Find the bug in this code: ...")
|
|
47
|
+
result = await sub.wait(timeout=300)
|
|
48
|
+
print(result.result)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
config: AppConfig,
|
|
54
|
+
skill_prompt: str = "",
|
|
55
|
+
model: Optional[str] = None,
|
|
56
|
+
tools: Optional[list[dict]] = None,
|
|
57
|
+
event_callback: Optional[Callable[[str, str, ToolResult], None]] = None,
|
|
58
|
+
agent_id: Optional[str] = None,
|
|
59
|
+
):
|
|
60
|
+
self.id = agent_id or f"sub_{uuid.uuid4().hex[:8]}"
|
|
61
|
+
|
|
62
|
+
# Independent LLM client — support both OpenAI and Anthropic formats
|
|
63
|
+
llm_config = LLMConfig(
|
|
64
|
+
api_key=config.llm.api_key,
|
|
65
|
+
base_url=config.llm.base_url,
|
|
66
|
+
model=model or config.llm.model,
|
|
67
|
+
temperature=config.llm.temperature,
|
|
68
|
+
max_tokens=config.llm.max_tokens,
|
|
69
|
+
thinking_strength=config.llm.thinking_strength,
|
|
70
|
+
use_anthropic=config.llm.use_anthropic,
|
|
71
|
+
)
|
|
72
|
+
if llm_config.use_anthropic:
|
|
73
|
+
from .anthropic_client import AnthropicClient
|
|
74
|
+
self._llm = AnthropicClient(llm_config)
|
|
75
|
+
self._use_anthropic = True
|
|
76
|
+
else:
|
|
77
|
+
self._llm = LLMClient(llm_config)
|
|
78
|
+
self._use_anthropic = False
|
|
79
|
+
|
|
80
|
+
# Independent tool executor
|
|
81
|
+
self._tools = ToolExecutor(config.agent)
|
|
82
|
+
self._tool_defs = tools or list(TOOL_DEFINITIONS)
|
|
83
|
+
self._llm.register_tools(self._tool_defs)
|
|
84
|
+
|
|
85
|
+
self._messages: list[dict] = []
|
|
86
|
+
self._result: Optional[str] = None
|
|
87
|
+
self._error: Optional[str] = None
|
|
88
|
+
self._tool_call_count = 0
|
|
89
|
+
self._status = "idle" # idle | running | done | failed | cancelled
|
|
90
|
+
self._event_callback = event_callback
|
|
91
|
+
self._skill_prompt = skill_prompt
|
|
92
|
+
self._task: Optional[asyncio.Task] = None
|
|
93
|
+
self._done = asyncio.Event()
|
|
94
|
+
|
|
95
|
+
# ── Public API ───────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
async def run(self, task: str) -> None:
|
|
98
|
+
"""Start sub-agent execution as an asyncio.Task."""
|
|
99
|
+
if self._status == "running":
|
|
100
|
+
raise RuntimeError(f"Sub-agent {self.id} is already running")
|
|
101
|
+
self._done.clear()
|
|
102
|
+
self._status = "running"
|
|
103
|
+
self._task = asyncio.create_task(self._run(task))
|
|
104
|
+
|
|
105
|
+
async def wait(self, timeout: Optional[float] = 300.0) -> SubAgentResult:
|
|
106
|
+
"""Wait until the sub-agent completes or times out."""
|
|
107
|
+
if self._status == "idle":
|
|
108
|
+
return SubAgentResult(agent_id=self.id, result=None,
|
|
109
|
+
error="Sub-agent was never started", success=False)
|
|
110
|
+
if not self._task:
|
|
111
|
+
return SubAgentResult(agent_id=self.id, result=self._result,
|
|
112
|
+
error=self._error, success=(self._status == "done"),
|
|
113
|
+
messages=list(self._messages),
|
|
114
|
+
tool_call_count=self._tool_call_count)
|
|
115
|
+
try:
|
|
116
|
+
await asyncio.wait_for(self._done.wait(), timeout=timeout)
|
|
117
|
+
except asyncio.TimeoutError:
|
|
118
|
+
await self.cancel()
|
|
119
|
+
return SubAgentResult(
|
|
120
|
+
agent_id=self.id,
|
|
121
|
+
result=self._result,
|
|
122
|
+
error=f"Timeout after {timeout}s",
|
|
123
|
+
success=False,
|
|
124
|
+
tool_call_count=self._tool_call_count,
|
|
125
|
+
)
|
|
126
|
+
return SubAgentResult(
|
|
127
|
+
agent_id=self.id,
|
|
128
|
+
result=self._result,
|
|
129
|
+
error=self._error,
|
|
130
|
+
messages=list(self._messages),
|
|
131
|
+
success=(self._status == "done"),
|
|
132
|
+
tool_call_count=self._tool_call_count,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def cancel(self) -> None:
|
|
136
|
+
"""Cancel the sub-agent execution."""
|
|
137
|
+
if self._task and not self._task.done():
|
|
138
|
+
self._task.cancel()
|
|
139
|
+
try:
|
|
140
|
+
await self._task
|
|
141
|
+
except asyncio.CancelledError:
|
|
142
|
+
pass
|
|
143
|
+
if self._status in ("running", "idle"):
|
|
144
|
+
self._status = "cancelled"
|
|
145
|
+
self._done.set()
|
|
146
|
+
|
|
147
|
+
def is_running(self) -> bool:
|
|
148
|
+
return self._status == "running"
|
|
149
|
+
|
|
150
|
+
def is_done(self) -> bool:
|
|
151
|
+
return self._status in ("done", "failed", "cancelled")
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def status(self) -> str:
|
|
155
|
+
return self._status
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def result(self) -> Optional[str]:
|
|
159
|
+
return self._result
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def messages(self) -> list[dict]:
|
|
163
|
+
return list(self._messages)
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def tool_call_count(self) -> int:
|
|
167
|
+
return self._tool_call_count
|
|
168
|
+
|
|
169
|
+
# ── Internal ─────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
async def _run(self, task: str) -> None:
|
|
172
|
+
"""Internal async target."""
|
|
173
|
+
try:
|
|
174
|
+
self._messages = [
|
|
175
|
+
{"role": "system", "content": self._build_prompt()},
|
|
176
|
+
{"role": "user", "content": task},
|
|
177
|
+
]
|
|
178
|
+
self._result = await self._loop()
|
|
179
|
+
if self._status == "running":
|
|
180
|
+
self._status = "done"
|
|
181
|
+
except asyncio.CancelledError:
|
|
182
|
+
self._status = "cancelled"
|
|
183
|
+
raise
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.exception("Sub-agent %s failed", self.id)
|
|
186
|
+
self._error = str(e)
|
|
187
|
+
self._status = "failed"
|
|
188
|
+
finally:
|
|
189
|
+
try:
|
|
190
|
+
await self._llm.close()
|
|
191
|
+
except Exception:
|
|
192
|
+
logger.debug(
|
|
193
|
+
"Error closing LLM for sub-agent %s", self.id, exc_info=True
|
|
194
|
+
)
|
|
195
|
+
self._done.set()
|
|
196
|
+
|
|
197
|
+
async def _loop(self) -> str:
|
|
198
|
+
"""Internal tool-call loop (similar to CoderAgent.run() but simplified)."""
|
|
199
|
+
SAFETY_LIMIT = 50
|
|
200
|
+
last_text = ""
|
|
201
|
+
|
|
202
|
+
while self._status == "running":
|
|
203
|
+
if self._tool_call_count >= SAFETY_LIMIT:
|
|
204
|
+
logger.warning("Sub-agent %s reached safety limit", self.id)
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
# Anthropic client takes system prompt separately
|
|
208
|
+
if self._use_anthropic:
|
|
209
|
+
sys_msg = ""
|
|
210
|
+
for m in self._messages:
|
|
211
|
+
if m.get("role") == "system":
|
|
212
|
+
sys_msg = m.get("content", "")
|
|
213
|
+
break
|
|
214
|
+
response = await self._llm.chat(
|
|
215
|
+
self._messages, system_prompt=sys_msg, tools=self._tool_defs
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
response = await self._llm.chat(self._messages, tools=self._tool_defs)
|
|
219
|
+
tool_calls = response.get("tool_calls", [])
|
|
220
|
+
text = response.get("content", "")
|
|
221
|
+
|
|
222
|
+
if text:
|
|
223
|
+
last_text = text
|
|
224
|
+
|
|
225
|
+
if not tool_calls:
|
|
226
|
+
return text or last_text or "Done."
|
|
227
|
+
|
|
228
|
+
# Execute tools serially
|
|
229
|
+
for tc in tool_calls:
|
|
230
|
+
if self._status != "running":
|
|
231
|
+
return last_text
|
|
232
|
+
self._tool_call_count += 1
|
|
233
|
+
tool_name = tc["function"]["name"]
|
|
234
|
+
try:
|
|
235
|
+
arguments = json.loads(tc["function"]["arguments"])
|
|
236
|
+
except json.JSONDecodeError:
|
|
237
|
+
arguments = {}
|
|
238
|
+
|
|
239
|
+
result = await self._tools.execute(tool_name, arguments)
|
|
240
|
+
|
|
241
|
+
self._messages.append({
|
|
242
|
+
"role": "assistant",
|
|
243
|
+
"content": text or None,
|
|
244
|
+
"tool_calls": [tc],
|
|
245
|
+
})
|
|
246
|
+
self._messages.append({
|
|
247
|
+
"role": "tool",
|
|
248
|
+
"tool_call_id": tc["id"],
|
|
249
|
+
"content": result.to_message(),
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
if self._event_callback:
|
|
253
|
+
try:
|
|
254
|
+
self._event_callback(self.id, tool_name, result)
|
|
255
|
+
except Exception:
|
|
256
|
+
logger.debug(
|
|
257
|
+
"Event callback failed for sub-agent %s", self.id,
|
|
258
|
+
exc_info=True,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return last_text
|
|
262
|
+
|
|
263
|
+
def _build_prompt(self) -> str:
|
|
264
|
+
"""Build the sub-agent's system prompt."""
|
|
265
|
+
parts = []
|
|
266
|
+
if self._skill_prompt:
|
|
267
|
+
parts.append(self._skill_prompt)
|
|
268
|
+
parts.append(
|
|
269
|
+
"You are a sub-agent working on a delegated task. "
|
|
270
|
+
"Complete your task independently and return a clear result. "
|
|
271
|
+
"Do not ask follow-up questions — the main agent cannot see them."
|
|
272
|
+
)
|
|
273
|
+
return "\n\n".join(parts)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
SubAgentManager — lifecycle management for concurrent sub-agents.
|
|
4
|
+
|
|
5
|
+
Handles spawning, collecting, cancelling, and listing sub-agents.
|
|
6
|
+
Uses asyncio.Semaphore for concurrency limits.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Callable, Optional
|
|
13
|
+
|
|
14
|
+
from .config import AppConfig
|
|
15
|
+
from .sub_agent import SubAgent, SubAgentResult
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
__all__ = ["SubAgentManager"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SubAgentManager:
|
|
23
|
+
"""
|
|
24
|
+
Manages sub-agent lifecycle: spawn, collect, cancel, list.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
mgr = SubAgentManager(config, max_concurrent=5)
|
|
28
|
+
aid = await mgr.spawn("Search for all TODO comments", skill_prompt="...")
|
|
29
|
+
# ... do other work ...
|
|
30
|
+
result = await mgr.collect(aid, timeout=300)
|
|
31
|
+
print(result.result)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
config: AppConfig,
|
|
37
|
+
max_concurrent: int = 5,
|
|
38
|
+
default_timeout: float = 300.0,
|
|
39
|
+
):
|
|
40
|
+
self._config = config
|
|
41
|
+
self._default_timeout = default_timeout
|
|
42
|
+
self._agents: dict[str, SubAgent] = {}
|
|
43
|
+
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
44
|
+
self._collected: set[str] = set() # track agents already released via collect()
|
|
45
|
+
|
|
46
|
+
# ── Spawn ──────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
async def spawn(
|
|
49
|
+
self,
|
|
50
|
+
task: str,
|
|
51
|
+
skill_prompt: str = "",
|
|
52
|
+
model: Optional[str] = None,
|
|
53
|
+
tools: Optional[list[dict]] = None,
|
|
54
|
+
event_callback: Optional[Callable] = None,
|
|
55
|
+
) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Spawn a sub-agent. Returns the agent_id.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
task: The task to delegate (must be self-contained)
|
|
61
|
+
skill_prompt: Optional skill system prompt for the sub-agent
|
|
62
|
+
model: Optional model override
|
|
63
|
+
tools: Optional tool list (defaults to built-in tools)
|
|
64
|
+
event_callback: Optional callback for tool results
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
agent_id string
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
RuntimeError: if max concurrent agents reached
|
|
71
|
+
"""
|
|
72
|
+
# Acquire semaphore slot with timeout (avoids TOCTOU + private attr access)
|
|
73
|
+
try:
|
|
74
|
+
await asyncio.wait_for(self._semaphore.acquire(), timeout=30.0)
|
|
75
|
+
except asyncio.TimeoutError:
|
|
76
|
+
running = sum(1 for a in self._agents.values() if a.is_running())
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
f"Max concurrent sub-agents reached ({running} running). "
|
|
79
|
+
f"Wait for some to complete or increase the limit."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
agent_id = f"sub_{uuid.uuid4().hex[:8]}"
|
|
83
|
+
sub = SubAgent(
|
|
84
|
+
config=self._config,
|
|
85
|
+
skill_prompt=skill_prompt,
|
|
86
|
+
model=model,
|
|
87
|
+
tools=tools,
|
|
88
|
+
event_callback=event_callback,
|
|
89
|
+
agent_id=agent_id,
|
|
90
|
+
)
|
|
91
|
+
self._agents[agent_id] = sub
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
await sub.run(task)
|
|
95
|
+
except Exception:
|
|
96
|
+
# Release semaphore on failure — caller may never call collect()
|
|
97
|
+
self._semaphore.release()
|
|
98
|
+
raise
|
|
99
|
+
running = sum(1 for a in self._agents.values() if a.is_running())
|
|
100
|
+
logger.info("Sub-agent spawned: %s (running=%d)", agent_id, running)
|
|
101
|
+
return agent_id
|
|
102
|
+
|
|
103
|
+
# ── Collect ────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
async def collect(self, agent_id: str, timeout: Optional[float] = None) -> SubAgentResult:
|
|
106
|
+
"""Wait for and collect a sub-agent's result."""
|
|
107
|
+
if timeout is None:
|
|
108
|
+
timeout = self._default_timeout
|
|
109
|
+
sub = self._agents.get(agent_id)
|
|
110
|
+
if not sub:
|
|
111
|
+
return SubAgentResult(
|
|
112
|
+
agent_id=agent_id, result=None,
|
|
113
|
+
error=f"Unknown agent: {agent_id}", success=False,
|
|
114
|
+
)
|
|
115
|
+
result = await sub.wait(timeout=timeout)
|
|
116
|
+
# Release semaphore slot (track to avoid double-release in clear_finished)
|
|
117
|
+
self._semaphore.release()
|
|
118
|
+
self._collected.add(agent_id)
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
async def collect_all(self, timeout: Optional[float] = None) -> list[SubAgentResult]:
|
|
122
|
+
"""Collect results from all sub-agents."""
|
|
123
|
+
results = []
|
|
124
|
+
for aid in list(self._agents.keys()):
|
|
125
|
+
results.append(await self.collect(aid, timeout))
|
|
126
|
+
return results
|
|
127
|
+
|
|
128
|
+
# ── Cancel ─────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
async def cancel(self, agent_id: str) -> bool:
|
|
131
|
+
"""Cancel a specific sub-agent."""
|
|
132
|
+
sub = self._agents.get(agent_id)
|
|
133
|
+
if sub and sub.is_running():
|
|
134
|
+
await sub.cancel()
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
async def cancel_all(self) -> None:
|
|
139
|
+
"""Cancel all running sub-agents."""
|
|
140
|
+
for sub in list(self._agents.values()):
|
|
141
|
+
if sub.is_running():
|
|
142
|
+
await sub.cancel()
|
|
143
|
+
|
|
144
|
+
# ── Queries ────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
def get(self, agent_id: str) -> Optional[SubAgent]:
|
|
147
|
+
"""Get a sub-agent by ID."""
|
|
148
|
+
return self._agents.get(agent_id)
|
|
149
|
+
|
|
150
|
+
def list_all(self) -> list[SubAgent]:
|
|
151
|
+
"""List all sub-agents."""
|
|
152
|
+
return list(self._agents.values())
|
|
153
|
+
|
|
154
|
+
def list_active(self) -> list[SubAgent]:
|
|
155
|
+
"""List only running sub-agents."""
|
|
156
|
+
return [a for a in self._agents.values() if a.is_running()]
|
|
157
|
+
|
|
158
|
+
def list_finished(self) -> list[SubAgent]:
|
|
159
|
+
"""List only completed/failed/cancelled sub-agents."""
|
|
160
|
+
return [a for a in self._agents.values() if a.is_done()]
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def active_count(self) -> int:
|
|
164
|
+
return sum(1 for a in self._agents.values() if a.is_running())
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def total_count(self) -> int:
|
|
168
|
+
return len(self._agents)
|
|
169
|
+
|
|
170
|
+
# ── Cleanup ────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
def clear_finished(self) -> int:
|
|
173
|
+
"""Remove finished agents from tracking. Returns count removed.
|
|
174
|
+
|
|
175
|
+
Releases one semaphore slot per removed agent that was NOT already
|
|
176
|
+
collected via :meth:`collect` — without this, slots belonging to
|
|
177
|
+
agents cleaned up without :meth:`collect` would leak permanently.
|
|
178
|
+
"""
|
|
179
|
+
to_remove = [
|
|
180
|
+
aid for aid, a in self._agents.items()
|
|
181
|
+
if a.is_done()
|
|
182
|
+
]
|
|
183
|
+
for aid in to_remove:
|
|
184
|
+
del self._agents[aid]
|
|
185
|
+
if aid not in self._collected:
|
|
186
|
+
self._semaphore.release()
|
|
187
|
+
else:
|
|
188
|
+
self._collected.discard(aid)
|
|
189
|
+
if to_remove:
|
|
190
|
+
logger.debug("Cleared %d finished sub-agent(s) (slots released)", len(to_remove))
|
|
191
|
+
return len(to_remove)
|
|
192
|
+
|
|
193
|
+
async def shutdown(self) -> None:
|
|
194
|
+
"""Cancel all agents and wait for them to finish."""
|
|
195
|
+
await self.cancel_all()
|
|
196
|
+
# Wait for all tasks to finish
|
|
197
|
+
for agent in list(self._agents.values()):
|
|
198
|
+
if agent._task and not agent._task.done():
|
|
199
|
+
try:
|
|
200
|
+
await asyncio.wait_for(agent._task, timeout=5.0)
|
|
201
|
+
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
202
|
+
pass
|
|
203
|
+
self._agents.clear()
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System prompt builder — composes the full system prompt for the coding agent.
|
|
3
|
+
|
|
4
|
+
The prompt is structured to put the skill persona FIRST (the agent's identity),
|
|
5
|
+
followed by supporting context in descending order of relevance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import platform
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SystemPromptBuilder:
|
|
17
|
+
"""Constructs the full system prompt for the coding agent.
|
|
18
|
+
|
|
19
|
+
Structure (in order):
|
|
20
|
+
1. Skill persona (who the agent IS — dominant)
|
|
21
|
+
2. Memory (relevant context from past sessions)
|
|
22
|
+
3. Environment (OS, workspace, model — 2 lines)
|
|
23
|
+
4. Project (language, framework — only if detected)
|
|
24
|
+
5. Tools (compact name-only list)
|
|
25
|
+
6. MCP (external servers — only if connected)
|
|
26
|
+
7. Rules (5 concise operational rules)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, subsystems: Any, workspace_dir: str | Path,
|
|
30
|
+
model: str = "", default_prompt: str = ""):
|
|
31
|
+
self.subsys = subsystems
|
|
32
|
+
self.workspace = str(workspace_dir)
|
|
33
|
+
self.model = model
|
|
34
|
+
self.default_prompt = default_prompt
|
|
35
|
+
|
|
36
|
+
def build(self, tool_definitions: list[dict], user_input: str = "") -> str:
|
|
37
|
+
"""Build the complete system prompt string."""
|
|
38
|
+
parts: list[str] = []
|
|
39
|
+
|
|
40
|
+
# 1. Skill persona — ALWAYS first
|
|
41
|
+
parts.append(self._skill_section())
|
|
42
|
+
|
|
43
|
+
# 2. Memory — relevant past context
|
|
44
|
+
memory = self._memory_section(user_input)
|
|
45
|
+
if memory:
|
|
46
|
+
parts.append(memory)
|
|
47
|
+
|
|
48
|
+
# 3. Environment — compact
|
|
49
|
+
parts.append(self._environment_section())
|
|
50
|
+
|
|
51
|
+
# 4. Project — only if detected
|
|
52
|
+
project = self._project_section()
|
|
53
|
+
if project:
|
|
54
|
+
parts.append(project)
|
|
55
|
+
|
|
56
|
+
# 5. Tools — compact name list
|
|
57
|
+
parts.append(self._tools_section(tool_definitions))
|
|
58
|
+
|
|
59
|
+
# 6. MCP — only if connected
|
|
60
|
+
mcp = self._mcp_section()
|
|
61
|
+
if mcp:
|
|
62
|
+
parts.append(mcp)
|
|
63
|
+
|
|
64
|
+
# 7. Rules — always last
|
|
65
|
+
parts.append(self._rules_section())
|
|
66
|
+
|
|
67
|
+
return "\n\n".join(parts)
|
|
68
|
+
|
|
69
|
+
# ── Sections ──────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
def _skill_section(self) -> str:
|
|
72
|
+
"""The agent's persona — from active skills or default prompt."""
|
|
73
|
+
# Primary: ExtensionManager aggregates all active skill extensions
|
|
74
|
+
if self.subsys.has_extensions and self.subsys.extensions:
|
|
75
|
+
aggregated = self.subsys.extensions.aggregate_prompts(base_prompt="")
|
|
76
|
+
if aggregated.strip():
|
|
77
|
+
return aggregated
|
|
78
|
+
|
|
79
|
+
# Fallback: direct skill manager
|
|
80
|
+
if self.subsys.has_skills:
|
|
81
|
+
try:
|
|
82
|
+
prompt = self.subsys.skills.get_system_prompt()
|
|
83
|
+
if prompt.strip():
|
|
84
|
+
return prompt
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
return self.default_prompt
|
|
89
|
+
|
|
90
|
+
def _memory_section(self, user_input: str = "") -> str:
|
|
91
|
+
"""Targeted memory recall (max 5, score >= 3)."""
|
|
92
|
+
if not self.subsys.has_memory:
|
|
93
|
+
return ""
|
|
94
|
+
mem = self.subsys.memory
|
|
95
|
+
if user_input.strip():
|
|
96
|
+
ctx = mem.recall_context(user_input, max_memories=5)
|
|
97
|
+
else:
|
|
98
|
+
ctx = mem.get_memory_context(max_total=5)
|
|
99
|
+
return ctx if ctx else ""
|
|
100
|
+
|
|
101
|
+
def _environment_section(self) -> str:
|
|
102
|
+
"""Single-line environment summary."""
|
|
103
|
+
model_part = f" | Model: {self.model}" if self.model else ""
|
|
104
|
+
return (
|
|
105
|
+
f"OS: {platform.system()} {platform.release()} "
|
|
106
|
+
f"| Python: {platform.python_version()} "
|
|
107
|
+
f"| Workspace: {self.workspace}"
|
|
108
|
+
f"{model_part}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _project_section(self) -> str:
|
|
112
|
+
if self.subsys.has_project_info and self.subsys.project_info:
|
|
113
|
+
return self.subsys.project_info.to_prompt()
|
|
114
|
+
return ""
|
|
115
|
+
|
|
116
|
+
def _tools_section(self, tool_definitions: list[dict]) -> str:
|
|
117
|
+
"""Compact tool list — names only, no descriptions."""
|
|
118
|
+
names: list[str] = []
|
|
119
|
+
for t in tool_definitions:
|
|
120
|
+
fn = t.get("function", t)
|
|
121
|
+
name = fn.get("name", "")
|
|
122
|
+
if name:
|
|
123
|
+
names.append(name)
|
|
124
|
+
return f"Tools ({len(names)}): " + ", ".join(sorted(names))
|
|
125
|
+
|
|
126
|
+
def _mcp_section(self) -> str:
|
|
127
|
+
if not self.subsys.has_mcp:
|
|
128
|
+
return ""
|
|
129
|
+
mcp = self.subsys.mcp
|
|
130
|
+
if not mcp.connected_servers:
|
|
131
|
+
return ""
|
|
132
|
+
servers = ", ".join(mcp.connected_servers)
|
|
133
|
+
return f"MCP servers ({mcp.tool_count} tools): {servers}"
|
|
134
|
+
|
|
135
|
+
def _rules_section(self) -> str:
|
|
136
|
+
"""Concise operational rules — replaces the old verbose ops + formatting sections."""
|
|
137
|
+
return (
|
|
138
|
+
"Rules:\n"
|
|
139
|
+
"- Shell cwd is already the workspace. Use compound commands (cd X && do_Y) if needed.\n"
|
|
140
|
+
"- Shell command blocked? Use: python -c \"import subprocess; subprocess.run([...], cwd='...')\"\n"
|
|
141
|
+
"- Read error messages carefully — diagnose before retrying. Auto-correction handles common failures.\n"
|
|
142
|
+
"- File reads are cached — re-reading the same file returns a [cached] note. Use offset/limit to page.\n"
|
|
143
|
+
"- Context auto-compacts at ~200k tokens. When you see the compaction notice, trust the summary.\n"
|
|
144
|
+
"- Respond clearly: use **bold** for key terms, ## headings for structure, code blocks with language tags."
|
|
145
|
+
)
|
|
146
|
+
|