gdmcode 0.1.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.
- gdmcode-0.1.0.dist-info/METADATA +240 -0
- gdmcode-0.1.0.dist-info/RECORD +131 -0
- gdmcode-0.1.0.dist-info/WHEEL +4 -0
- gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
- src/__init__.py +1 -0
- src/_internal/__init__.py +0 -0
- src/_internal/constants.py +244 -0
- src/_internal/domain_skills.py +339 -0
- src/agent/__init__.py +0 -0
- src/agent/commit_classifier.py +91 -0
- src/agent/context_budget.py +391 -0
- src/agent/daemon.py +681 -0
- src/agent/dag_validator.py +153 -0
- src/agent/debug_loop.py +473 -0
- src/agent/impact_analyzer.py +149 -0
- src/agent/impact_graph.py +117 -0
- src/agent/loop.py +1410 -0
- src/agent/orchestrator.py +141 -0
- src/agent/regression_guard.py +251 -0
- src/agent/review_gate.py +648 -0
- src/agent/risk_scorer.py +169 -0
- src/agent/self_healing.py +145 -0
- src/agent/smart_test_selector.py +89 -0
- src/agent/system_prompt.py +226 -0
- src/agent/task_tracker.py +320 -0
- src/agent/test_validator.py +210 -0
- src/agent/tool_orchestrator.py +402 -0
- src/agent/transcript.py +230 -0
- src/agent/verification_loop.py +133 -0
- src/agent/work_director.py +136 -0
- src/agent/worktree_manager.py +53 -0
- src/artifacts/__init__.py +16 -0
- src/artifacts/artifact_store.py +456 -0
- src/artifacts/verification_graph.py +75 -0
- src/auth.py +411 -0
- src/cli.py +1290 -0
- src/commands.py +1398 -0
- src/config.py +762 -0
- src/cost_tracker.py +348 -0
- src/db/__init__.py +4 -0
- src/db/migrations.py +337 -0
- src/enterprise/__init__.py +3 -0
- src/enterprise/audit_log.py +182 -0
- src/enterprise/identity.py +90 -0
- src/enterprise/rbac.py +100 -0
- src/enterprise/team_config.py +125 -0
- src/enterprise/usage_analytics.py +261 -0
- src/exceptions.py +207 -0
- src/git_workflow.py +651 -0
- src/integrations/__init__.py +6 -0
- src/integrations/github_actions.py +106 -0
- src/integrations/mcp_server.py +333 -0
- src/integrations/sentry_integration.py +100 -0
- src/integrations/sentry_server.py +82 -0
- src/integrations/webhook_security.py +19 -0
- src/main.py +27 -0
- src/memory/__init__.py +0 -0
- src/memory/code_index.py +376 -0
- src/memory/compressor.py +378 -0
- src/memory/context_memory.py +135 -0
- src/memory/continuous_memory.py +234 -0
- src/memory/conventions.py +495 -0
- src/memory/db.py +1119 -0
- src/memory/document_index.py +205 -0
- src/memory/file_cache.py +128 -0
- src/memory/project_scanner.py +178 -0
- src/memory/session_store.py +201 -0
- src/models/__init__.py +0 -0
- src/models/client.py +715 -0
- src/models/definitions.py +459 -0
- src/models/router.py +418 -0
- src/models/schemas.py +389 -0
- src/permissions.py +294 -0
- src/remote/__init__.py +5 -0
- src/remote/command_filter.py +33 -0
- src/remote/models.py +31 -0
- src/remote/permission_handler.py +79 -0
- src/remote/phone_ui.py +48 -0
- src/remote/protocol.py +59 -0
- src/remote/qr.py +65 -0
- src/remote/server.py +586 -0
- src/remote/token_manager.py +61 -0
- src/remote/tunnel.py +212 -0
- src/repl.py +475 -0
- src/runtime/__init__.py +1 -0
- src/runtime/branch_farm.py +372 -0
- src/runtime/replay.py +351 -0
- src/sandbox/__init__.py +2 -0
- src/sandbox/hermetic.py +214 -0
- src/sandbox/policy.py +44 -0
- src/sdk/__init__.py +3 -0
- src/sdk/plugin_base.py +39 -0
- src/sdk/plugin_host.py +100 -0
- src/sdk/plugin_loader.py +101 -0
- src/security.py +409 -0
- src/server/__init__.py +7 -0
- src/server/bridge.py +427 -0
- src/server/bridge_cli.py +103 -0
- src/server/bridge_client.py +170 -0
- src/server/protocol_version.py +103 -0
- src/session/__init__.py +10 -0
- src/session/event_fanout.py +46 -0
- src/session/input_broker.py +38 -0
- src/session/permission_bridge.py +100 -0
- src/tools/__init__.py +160 -0
- src/tools/_atomic.py +72 -0
- src/tools/agent_tools.py +423 -0
- src/tools/ask_user_tool.py +83 -0
- src/tools/bash_tool.py +384 -0
- src/tools/browser_tool.py +352 -0
- src/tools/browser_tools.py +179 -0
- src/tools/dep_tools.py +210 -0
- src/tools/document_reader.py +167 -0
- src/tools/document_tool.py +240 -0
- src/tools/document_writer.py +171 -0
- src/tools/impact_tools.py +240 -0
- src/tools/playwright_tool.py +172 -0
- src/tools/quality_tools.py +366 -0
- src/tools/read_tools.py +318 -0
- src/tools/result_cache.py +157 -0
- src/tools/search_tools.py +310 -0
- src/tools/shell_tools.py +311 -0
- src/tools/write_tools.py +337 -0
- src/voice/__init__.py +25 -0
- src/voice/audio_capture.py +92 -0
- src/voice/audio_playback.py +68 -0
- src/voice/errors.py +14 -0
- src/voice/models.py +35 -0
- src/voice/providers.py +143 -0
- src/voice/vad.py +55 -0
- src/voice/voice_loop.py +156 -0
src/tools/agent_tools.py
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Agent sub-tools — plan, explore, verify, fork, resume, and run sub-agents.
|
|
2
|
+
|
|
3
|
+
These tools expose the agent hierarchy to the model loop. Each tool creates a
|
|
4
|
+
child AgentLoop with an appropriate model tier and a scoped task.
|
|
5
|
+
|
|
6
|
+
Architecture mirrors Claude Code'"'"'s AgentTool system:
|
|
7
|
+
PlanAgentTool — break a task into subtasks before executing (Thinker tier)
|
|
8
|
+
ExploreAgentTool — parallel research across codebase files (Scout tier)
|
|
9
|
+
VerifyAgentTool — post-edit: re-read files + run tests + confirm (Coder tier)
|
|
10
|
+
ForkAgentTool — spawn independent agent for a sub-task (background, Scout/Coder)
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
from src.models.definitions import ModelTier
|
|
22
|
+
from src.models.schemas import TaskPlan
|
|
23
|
+
from src.tools import REGISTRY, ToolBase, ToolResult
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from src.agent.loop import AgentLoop
|
|
27
|
+
from src.config import GdmConfig
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"PlanAgentTool",
|
|
31
|
+
"ExploreAgentTool",
|
|
32
|
+
"VerifyAgentTool",
|
|
33
|
+
"ForkAgentTool",
|
|
34
|
+
"SubAgentResult",
|
|
35
|
+
"SubAgentConfig",
|
|
36
|
+
"SubAgentRunner",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
_MAX_PLAN_TURNS: int = 3
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# SubAgentResult / SubAgentConfig / SubAgentRunner
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class SubAgentResult:
|
|
50
|
+
"""Result returned by a completed sub-agent run."""
|
|
51
|
+
|
|
52
|
+
success: bool
|
|
53
|
+
output: str
|
|
54
|
+
error: str | None = None
|
|
55
|
+
elapsed_s: float = 0.0
|
|
56
|
+
tool_calls: int = 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class SubAgentConfig:
|
|
61
|
+
"""Configuration for a single sub-agent invocation."""
|
|
62
|
+
|
|
63
|
+
task: str
|
|
64
|
+
model: str = "grok-3"
|
|
65
|
+
max_tokens: int = 4096
|
|
66
|
+
timeout_s: float = 120.0
|
|
67
|
+
tools: list[str] = field(default_factory=list) # tool names to allow
|
|
68
|
+
context: str = "" # extra context to inject
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SubAgentRunner:
|
|
72
|
+
"""Spawns child agents with isolated context to handle subtasks."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, api_key: str | None = None) -> None:
|
|
75
|
+
self._api_key = api_key
|
|
76
|
+
|
|
77
|
+
async def run(self, config: SubAgentConfig) -> SubAgentResult:
|
|
78
|
+
"""Run a sub-agent for the given task. Returns result, never raises."""
|
|
79
|
+
start = time.monotonic()
|
|
80
|
+
try:
|
|
81
|
+
messages = self._build_messages(config)
|
|
82
|
+
loop = asyncio.get_running_loop()
|
|
83
|
+
output = await asyncio.wait_for(
|
|
84
|
+
loop.run_in_executor(None, self._call_model, messages, config),
|
|
85
|
+
timeout=config.timeout_s,
|
|
86
|
+
)
|
|
87
|
+
elapsed = time.monotonic() - start
|
|
88
|
+
return SubAgentResult(success=True, output=output, elapsed_s=elapsed)
|
|
89
|
+
except asyncio.TimeoutError:
|
|
90
|
+
elapsed = time.monotonic() - start
|
|
91
|
+
return SubAgentResult(
|
|
92
|
+
success=False, output="", error="timeout", elapsed_s=elapsed
|
|
93
|
+
)
|
|
94
|
+
except Exception as exc: # noqa: BLE001
|
|
95
|
+
elapsed = time.monotonic() - start
|
|
96
|
+
return SubAgentResult(
|
|
97
|
+
success=False, output="", error=str(exc), elapsed_s=elapsed
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
async def run_parallel(
|
|
101
|
+
self, configs: list[SubAgentConfig]
|
|
102
|
+
) -> list[SubAgentResult]:
|
|
103
|
+
"""Run multiple sub-agents in parallel with asyncio.gather."""
|
|
104
|
+
return list(await asyncio.gather(*(self.run(c) for c in configs)))
|
|
105
|
+
|
|
106
|
+
def run_sync(self, config: SubAgentConfig) -> SubAgentResult:
|
|
107
|
+
"""Sync wrapper for run(). Safe from both sync and async contexts."""
|
|
108
|
+
try:
|
|
109
|
+
return asyncio.run(self.run(config))
|
|
110
|
+
except RuntimeError:
|
|
111
|
+
# Already inside a running event loop — use a fresh thread
|
|
112
|
+
result_holder: list[SubAgentResult] = []
|
|
113
|
+
exc_holder: list[BaseException] = []
|
|
114
|
+
|
|
115
|
+
def _in_thread() -> None:
|
|
116
|
+
new_loop = asyncio.new_event_loop()
|
|
117
|
+
asyncio.set_event_loop(new_loop)
|
|
118
|
+
try:
|
|
119
|
+
result_holder.append(
|
|
120
|
+
new_loop.run_until_complete(self.run(config))
|
|
121
|
+
)
|
|
122
|
+
except BaseException as e: # noqa: BLE001
|
|
123
|
+
exc_holder.append(e)
|
|
124
|
+
finally:
|
|
125
|
+
new_loop.close()
|
|
126
|
+
|
|
127
|
+
t = threading.Thread(target=_in_thread, daemon=True)
|
|
128
|
+
t.start()
|
|
129
|
+
t.join()
|
|
130
|
+
if exc_holder:
|
|
131
|
+
raise exc_holder[0]
|
|
132
|
+
return result_holder[0]
|
|
133
|
+
|
|
134
|
+
def _build_messages(self, config: SubAgentConfig) -> list[dict]:
|
|
135
|
+
"""Build the messages list for the sub-agent call."""
|
|
136
|
+
system_lines = [
|
|
137
|
+
"You are a sub-agent. Complete the assigned task concisely."
|
|
138
|
+
]
|
|
139
|
+
if config.tools:
|
|
140
|
+
system_lines.append(f"Available tools: {', '.join(config.tools)}")
|
|
141
|
+
messages: list[dict] = [
|
|
142
|
+
{"role": "system", "content": " ".join(system_lines)}
|
|
143
|
+
]
|
|
144
|
+
user_content = (
|
|
145
|
+
f"{config.context}\n\n{config.task}"
|
|
146
|
+
if config.context
|
|
147
|
+
else config.task
|
|
148
|
+
)
|
|
149
|
+
messages.append({"role": "user", "content": user_content})
|
|
150
|
+
return messages
|
|
151
|
+
|
|
152
|
+
def _call_model(self, messages: list[dict], config: SubAgentConfig) -> str:
|
|
153
|
+
"""Call the model API. Mocked in tests."""
|
|
154
|
+
try:
|
|
155
|
+
from src.models.router import ModelRouter # noqa: F401
|
|
156
|
+
except ImportError:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
import openai
|
|
160
|
+
|
|
161
|
+
client = openai.OpenAI(
|
|
162
|
+
api_key=self._api_key or "dummy",
|
|
163
|
+
base_url="https://api.x.ai/v1",
|
|
164
|
+
)
|
|
165
|
+
resp = client.chat.completions.create(
|
|
166
|
+
model=config.model,
|
|
167
|
+
messages=messages, # type: ignore[arg-type]
|
|
168
|
+
max_tokens=config.max_tokens,
|
|
169
|
+
)
|
|
170
|
+
if not resp.choices:
|
|
171
|
+
raise RuntimeError("No completion choices returned")
|
|
172
|
+
return resp.choices[0].message.content or ""
|
|
173
|
+
|
|
174
|
+
_MAX_EXPLORE_TURNS: int = 5
|
|
175
|
+
_MAX_VERIFY_TURNS: int = 4
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# PlanAgentTool
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
class PlanAgentTool(ToolBase):
|
|
183
|
+
"""Break a complex task into ordered subtasks before executing.
|
|
184
|
+
|
|
185
|
+
Spawns a Thinker-tier sub-agent whose only job is to produce a TaskPlan.
|
|
186
|
+
The plan is injected back into the parent agent'"'"'s context.
|
|
187
|
+
Should be called for any task touching 3+ files or requiring >1 edit pass.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
name = "plan_agent"
|
|
191
|
+
description = (
|
|
192
|
+
"Break a complex task into ordered subtasks before executing. "
|
|
193
|
+
"Call this FIRST for any task touching 3+ files or with multiple edit passes. "
|
|
194
|
+
"Returns a structured plan with subtask list."
|
|
195
|
+
)
|
|
196
|
+
input_schema: dict[str, Any] = {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"properties": {
|
|
199
|
+
"task": {"type": "string", "description": "The task description to plan."},
|
|
200
|
+
"context": {
|
|
201
|
+
"type": "string",
|
|
202
|
+
"description": "Additional context: file paths, constraints, etc.",
|
|
203
|
+
"default": "",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
"required": ["task"],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
def __init__(self, cfg: Any, parent_loop: Any) -> None: # cfg: GdmConfig, parent_loop: AgentLoop
|
|
210
|
+
self._cfg = cfg
|
|
211
|
+
self._parent = parent_loop
|
|
212
|
+
|
|
213
|
+
def execute(self, params: dict[str, Any]) -> ToolResult:
|
|
214
|
+
"""Run a planning sub-agent. Returns the plan as formatted text."""
|
|
215
|
+
task = params.get("task", "")
|
|
216
|
+
context = params.get("context", "")
|
|
217
|
+
if not task:
|
|
218
|
+
return ToolResult(output="", error="plan_agent: 'task' param required")
|
|
219
|
+
|
|
220
|
+
plan_prompt = _build_plan_prompt(task, context)
|
|
221
|
+
result_text = self._run_sub_agent(plan_prompt, ModelTier.THINKER, _MAX_PLAN_TURNS)
|
|
222
|
+
return ToolResult(output=result_text)
|
|
223
|
+
|
|
224
|
+
def _run_sub_agent(self, prompt: str, tier: str, max_turns: int) -> str:
|
|
225
|
+
"""Run a minimal sub-loop and collect the final RESPONSE."""
|
|
226
|
+
try:
|
|
227
|
+
return _collect_response(self._parent, prompt, tier)
|
|
228
|
+
except Exception as exc: # noqa: BLE001
|
|
229
|
+
log.warning("plan_agent sub-loop failed: %s", exc)
|
|
230
|
+
return f"Planning failed: {exc}"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
# ExploreAgentTool
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
class ExploreAgentTool(ToolBase):
|
|
238
|
+
"""Research codebase files in parallel to answer multiple questions.
|
|
239
|
+
|
|
240
|
+
Spawns up to 3 Scout-tier sub-agents (one per question) and merges results.
|
|
241
|
+
Use for: understanding existing code, finding patterns, locating implementations.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
name = "explore_agent"
|
|
245
|
+
description = (
|
|
246
|
+
"Research multiple questions about the codebase in parallel. "
|
|
247
|
+
"Provide a list of questions; each is answered independently by a Scout agent. "
|
|
248
|
+
"Faster than sequential reads for 2+ unrelated questions."
|
|
249
|
+
)
|
|
250
|
+
input_schema: dict[str, Any] = {
|
|
251
|
+
"type": "object",
|
|
252
|
+
"properties": {
|
|
253
|
+
"questions": {
|
|
254
|
+
"type": "array",
|
|
255
|
+
"items": {"type": "string"},
|
|
256
|
+
"description": "List of research questions (max 3).",
|
|
257
|
+
"maxItems": 3,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
"required": ["questions"],
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
def __init__(self, cfg: Any, parent_loop: Any) -> None:
|
|
264
|
+
self._cfg = cfg
|
|
265
|
+
self._parent = parent_loop
|
|
266
|
+
|
|
267
|
+
def execute(self, params: dict[str, Any]) -> ToolResult:
|
|
268
|
+
"""Run Scout sub-agents for each question and merge results."""
|
|
269
|
+
questions: list[str] = params.get("questions", [])[:3]
|
|
270
|
+
if not questions:
|
|
271
|
+
return ToolResult(output="", error="explore_agent: at least 1 question required")
|
|
272
|
+
|
|
273
|
+
results: list[str] = []
|
|
274
|
+
for i, q in enumerate(questions, 1):
|
|
275
|
+
prompt = f"Research question {i}/{len(questions)}: {q}"
|
|
276
|
+
answer = _collect_response(self._parent, prompt, ModelTier.SCOUT)
|
|
277
|
+
results.append(f"Q{i}: {q}\nA: {answer}")
|
|
278
|
+
|
|
279
|
+
return ToolResult(output="\n\n---\n\n".join(results))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# VerifyAgentTool
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
class VerifyAgentTool(ToolBase):
|
|
287
|
+
"""Post-edit verification: re-read modified files and run related tests.
|
|
288
|
+
|
|
289
|
+
Should be called after any multi-file edit session.
|
|
290
|
+
Spawns a Coder-tier sub-agent that re-reads all modified files and
|
|
291
|
+
checks that the changes are correct and tests pass.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
name = "verify_agent"
|
|
295
|
+
description = (
|
|
296
|
+
"After editing files, verify changes are correct by re-reading and running tests. "
|
|
297
|
+
"Call after any multi-file edit. Returns pass/fail with details."
|
|
298
|
+
)
|
|
299
|
+
input_schema: dict[str, Any] = {
|
|
300
|
+
"type": "object",
|
|
301
|
+
"properties": {
|
|
302
|
+
"files_modified": {
|
|
303
|
+
"type": "array",
|
|
304
|
+
"items": {"type": "string"},
|
|
305
|
+
"description": "Paths of files that were modified.",
|
|
306
|
+
},
|
|
307
|
+
"test_command": {
|
|
308
|
+
"type": "string",
|
|
309
|
+
"description": "Command to run tests (e.g., 'pytest tests/').",
|
|
310
|
+
"default": "pytest",
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
"required": ["files_modified"],
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
def __init__(self, cfg: Any, parent_loop: Any) -> None:
|
|
317
|
+
self._cfg = cfg
|
|
318
|
+
self._parent = parent_loop
|
|
319
|
+
|
|
320
|
+
def execute(self, params: dict[str, Any]) -> ToolResult:
|
|
321
|
+
"""Verify modified files are correct and tests pass."""
|
|
322
|
+
files = params.get("files_modified", [])
|
|
323
|
+
test_cmd = params.get("test_command", "pytest")
|
|
324
|
+
if not files:
|
|
325
|
+
return ToolResult(output="", error="verify_agent: files_modified required")
|
|
326
|
+
|
|
327
|
+
prompt = _build_verify_prompt(files, test_cmd)
|
|
328
|
+
answer = _collect_response(self._parent, prompt, ModelTier.CODER)
|
|
329
|
+
return ToolResult(output=answer)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# ---------------------------------------------------------------------------
|
|
333
|
+
# ForkAgentTool
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
class ForkAgentTool(ToolBase):
|
|
337
|
+
"""Spawn an independent background agent for a long-running sub-task.
|
|
338
|
+
|
|
339
|
+
The forked agent runs the task and writes results to the outbox.
|
|
340
|
+
The parent continues working; results are delivered via /tasks or btw queue.
|
|
341
|
+
Currently runs synchronously (Phase 2); true background execution in Phase 3.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
name = "fork_agent"
|
|
345
|
+
description = (
|
|
346
|
+
"Fork an independent agent for a long-running sub-task. "
|
|
347
|
+
"The agent runs in the background while you continue. "
|
|
348
|
+
"Check /tasks for status. Use for: running full test suite, "
|
|
349
|
+
"refactoring a module, long analysis tasks."
|
|
350
|
+
)
|
|
351
|
+
input_schema: dict[str, Any] = {
|
|
352
|
+
"type": "object",
|
|
353
|
+
"properties": {
|
|
354
|
+
"task": {"type": "string", "description": "The task for the forked agent."},
|
|
355
|
+
"model_tier": {
|
|
356
|
+
"type": "string",
|
|
357
|
+
"enum": ["scout", "coder", "thinker"],
|
|
358
|
+
"description": "Model tier to use. Default: coder.",
|
|
359
|
+
"default": "coder",
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
"required": ["task"],
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
def __init__(self, cfg: Any, parent_loop: Any) -> None:
|
|
366
|
+
self._cfg = cfg
|
|
367
|
+
self._parent = parent_loop
|
|
368
|
+
|
|
369
|
+
def execute(self, params: dict[str, Any]) -> ToolResult:
|
|
370
|
+
"""Run the forked task (synchronously in Phase 2)."""
|
|
371
|
+
task = params.get("task", "")
|
|
372
|
+
tier = params.get("model_tier", ModelTier.CODER)
|
|
373
|
+
if not task:
|
|
374
|
+
return ToolResult(output="", error="fork_agent: 'task' param required")
|
|
375
|
+
|
|
376
|
+
log.info("Forking sub-agent (tier=%s): %s", tier, task[:80])
|
|
377
|
+
result = _collect_response(self._parent, task, tier)
|
|
378
|
+
return ToolResult(output=f"[Forked agent result]\n{result}")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
# Private helpers
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def _collect_response(parent_loop: Any, prompt: str, tier: str) -> str:
|
|
386
|
+
"""Run parent_loop for one prompt and collect RESPONSE events as text.
|
|
387
|
+
|
|
388
|
+
This is a simplified synchronous sub-agent invocation. Phase 3 will
|
|
389
|
+
make this truly asynchronous with isolated transcript + context.
|
|
390
|
+
"""
|
|
391
|
+
from src.agent.loop import EventType
|
|
392
|
+
|
|
393
|
+
parts: list[str] = []
|
|
394
|
+
try:
|
|
395
|
+
for event in parent_loop.run(f"[sub-agent, tier={tier}] {prompt}"):
|
|
396
|
+
if event.type == EventType.RESPONSE:
|
|
397
|
+
parts.append(event.content)
|
|
398
|
+
elif event.type == EventType.DONE:
|
|
399
|
+
break
|
|
400
|
+
except Exception as exc: # noqa: BLE001
|
|
401
|
+
log.warning("Sub-agent loop error: %s", exc)
|
|
402
|
+
return f"Sub-agent error: {exc}"
|
|
403
|
+
return "".join(parts) or "(no response)"
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _build_plan_prompt(task: str, context: str) -> str:
|
|
407
|
+
ctx_line = f"\nAdditional context: {context}" if context else ""
|
|
408
|
+
return (
|
|
409
|
+
f"Break this task into concrete, ordered subtasks:{ctx_line}\n\nTask: {task}\n\n"
|
|
410
|
+
"For each subtask specify: what to do, which file(s) to touch, "
|
|
411
|
+
"and what success looks like. Be specific and minimal."
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _build_verify_prompt(files: list[str], test_cmd: str) -> str:
|
|
416
|
+
file_list = ", ".join(files)
|
|
417
|
+
return (
|
|
418
|
+
f"Verify these recently modified files are correct: {file_list}\n"
|
|
419
|
+
f"1. Re-read each file and confirm the changes look correct\n"
|
|
420
|
+
f"2. Run: {test_cmd}\n"
|
|
421
|
+
"3. Report: PASS if all tests pass and code looks correct, "
|
|
422
|
+
"FAIL with specific issues if not."
|
|
423
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""AskUserQuestionTool — interrupt the agent loop to ask a human question.
|
|
2
|
+
|
|
3
|
+
This tool pauses the agent and presents a rich prompt to the user in the
|
|
4
|
+
terminal. The agent must not proceed until the user answers.
|
|
5
|
+
|
|
6
|
+
The tool returns the user's answer as a string. The agent should not call this
|
|
7
|
+
tool for trivial choices — only when it is genuinely uncertain about something
|
|
8
|
+
that could cause irreversible harm (e.g., "Are you sure you want to delete the
|
|
9
|
+
production database?").
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, ClassVar
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.prompt import Prompt
|
|
18
|
+
|
|
19
|
+
from src.tools import REGISTRY, ToolBase, ToolResult
|
|
20
|
+
|
|
21
|
+
__all__ = ["AskUserQuestionTool"]
|
|
22
|
+
|
|
23
|
+
log = logging.getLogger(__name__)
|
|
24
|
+
_console = Console(stderr=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AskUserQuestionTool(ToolBase):
|
|
28
|
+
"""Pause execution and ask the human user a question.
|
|
29
|
+
|
|
30
|
+
Use ONLY when:
|
|
31
|
+
1. The action is irreversible (delete, publish, send).
|
|
32
|
+
2. There is genuine ambiguity that cannot be resolved from context.
|
|
33
|
+
3. The user has not already answered via /allow-all or --yes.
|
|
34
|
+
|
|
35
|
+
Do NOT use for routine choices — make reasonable assumptions instead.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: ClassVar[str] = "ask_user"
|
|
39
|
+
description: ClassVar[str] = (
|
|
40
|
+
"Ask the user a clarifying question and wait for their answer. "
|
|
41
|
+
"Only use for irreversible actions or genuine ambiguity. "
|
|
42
|
+
"Do not use for routine choices — make a reasonable assumption instead."
|
|
43
|
+
)
|
|
44
|
+
input_schema: ClassVar[dict[str, Any]] = {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"required": ["question"],
|
|
47
|
+
"properties": {
|
|
48
|
+
"question": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "The question to ask the user.",
|
|
51
|
+
},
|
|
52
|
+
"default": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Default answer if the user presses Enter without typing.",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
"additionalProperties": False,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def execute(self, params: dict[str, Any]) -> ToolResult: # noqa: D102
|
|
61
|
+
question: str = params["question"]
|
|
62
|
+
default: str | None = params.get("default")
|
|
63
|
+
|
|
64
|
+
_console.print(f"\n[bold yellow]⚡ Agent question:[/bold yellow] {question}")
|
|
65
|
+
try:
|
|
66
|
+
answer = Prompt.ask(
|
|
67
|
+
"[bold]Your answer[/bold]",
|
|
68
|
+
default=default or "",
|
|
69
|
+
console=_console,
|
|
70
|
+
)
|
|
71
|
+
except (EOFError, KeyboardInterrupt):
|
|
72
|
+
answer = default or ""
|
|
73
|
+
_console.print("[dim]No answer received — using default.[/dim]")
|
|
74
|
+
|
|
75
|
+
log.info("AskUserQuestionTool: Q=%r A=%r", question, answer)
|
|
76
|
+
return ToolResult(output=answer, metadata={"question": question})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Auto-register
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
REGISTRY.register(AskUserQuestionTool())
|