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.
Files changed (131) hide show
  1. gdmcode-0.1.0.dist-info/METADATA +240 -0
  2. gdmcode-0.1.0.dist-info/RECORD +131 -0
  3. gdmcode-0.1.0.dist-info/WHEEL +4 -0
  4. gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. src/__init__.py +1 -0
  6. src/_internal/__init__.py +0 -0
  7. src/_internal/constants.py +244 -0
  8. src/_internal/domain_skills.py +339 -0
  9. src/agent/__init__.py +0 -0
  10. src/agent/commit_classifier.py +91 -0
  11. src/agent/context_budget.py +391 -0
  12. src/agent/daemon.py +681 -0
  13. src/agent/dag_validator.py +153 -0
  14. src/agent/debug_loop.py +473 -0
  15. src/agent/impact_analyzer.py +149 -0
  16. src/agent/impact_graph.py +117 -0
  17. src/agent/loop.py +1410 -0
  18. src/agent/orchestrator.py +141 -0
  19. src/agent/regression_guard.py +251 -0
  20. src/agent/review_gate.py +648 -0
  21. src/agent/risk_scorer.py +169 -0
  22. src/agent/self_healing.py +145 -0
  23. src/agent/smart_test_selector.py +89 -0
  24. src/agent/system_prompt.py +226 -0
  25. src/agent/task_tracker.py +320 -0
  26. src/agent/test_validator.py +210 -0
  27. src/agent/tool_orchestrator.py +402 -0
  28. src/agent/transcript.py +230 -0
  29. src/agent/verification_loop.py +133 -0
  30. src/agent/work_director.py +136 -0
  31. src/agent/worktree_manager.py +53 -0
  32. src/artifacts/__init__.py +16 -0
  33. src/artifacts/artifact_store.py +456 -0
  34. src/artifacts/verification_graph.py +75 -0
  35. src/auth.py +411 -0
  36. src/cli.py +1290 -0
  37. src/commands.py +1398 -0
  38. src/config.py +762 -0
  39. src/cost_tracker.py +348 -0
  40. src/db/__init__.py +4 -0
  41. src/db/migrations.py +337 -0
  42. src/enterprise/__init__.py +3 -0
  43. src/enterprise/audit_log.py +182 -0
  44. src/enterprise/identity.py +90 -0
  45. src/enterprise/rbac.py +100 -0
  46. src/enterprise/team_config.py +125 -0
  47. src/enterprise/usage_analytics.py +261 -0
  48. src/exceptions.py +207 -0
  49. src/git_workflow.py +651 -0
  50. src/integrations/__init__.py +6 -0
  51. src/integrations/github_actions.py +106 -0
  52. src/integrations/mcp_server.py +333 -0
  53. src/integrations/sentry_integration.py +100 -0
  54. src/integrations/sentry_server.py +82 -0
  55. src/integrations/webhook_security.py +19 -0
  56. src/main.py +27 -0
  57. src/memory/__init__.py +0 -0
  58. src/memory/code_index.py +376 -0
  59. src/memory/compressor.py +378 -0
  60. src/memory/context_memory.py +135 -0
  61. src/memory/continuous_memory.py +234 -0
  62. src/memory/conventions.py +495 -0
  63. src/memory/db.py +1119 -0
  64. src/memory/document_index.py +205 -0
  65. src/memory/file_cache.py +128 -0
  66. src/memory/project_scanner.py +178 -0
  67. src/memory/session_store.py +201 -0
  68. src/models/__init__.py +0 -0
  69. src/models/client.py +715 -0
  70. src/models/definitions.py +459 -0
  71. src/models/router.py +418 -0
  72. src/models/schemas.py +389 -0
  73. src/permissions.py +294 -0
  74. src/remote/__init__.py +5 -0
  75. src/remote/command_filter.py +33 -0
  76. src/remote/models.py +31 -0
  77. src/remote/permission_handler.py +79 -0
  78. src/remote/phone_ui.py +48 -0
  79. src/remote/protocol.py +59 -0
  80. src/remote/qr.py +65 -0
  81. src/remote/server.py +586 -0
  82. src/remote/token_manager.py +61 -0
  83. src/remote/tunnel.py +212 -0
  84. src/repl.py +475 -0
  85. src/runtime/__init__.py +1 -0
  86. src/runtime/branch_farm.py +372 -0
  87. src/runtime/replay.py +351 -0
  88. src/sandbox/__init__.py +2 -0
  89. src/sandbox/hermetic.py +214 -0
  90. src/sandbox/policy.py +44 -0
  91. src/sdk/__init__.py +3 -0
  92. src/sdk/plugin_base.py +39 -0
  93. src/sdk/plugin_host.py +100 -0
  94. src/sdk/plugin_loader.py +101 -0
  95. src/security.py +409 -0
  96. src/server/__init__.py +7 -0
  97. src/server/bridge.py +427 -0
  98. src/server/bridge_cli.py +103 -0
  99. src/server/bridge_client.py +170 -0
  100. src/server/protocol_version.py +103 -0
  101. src/session/__init__.py +10 -0
  102. src/session/event_fanout.py +46 -0
  103. src/session/input_broker.py +38 -0
  104. src/session/permission_bridge.py +100 -0
  105. src/tools/__init__.py +160 -0
  106. src/tools/_atomic.py +72 -0
  107. src/tools/agent_tools.py +423 -0
  108. src/tools/ask_user_tool.py +83 -0
  109. src/tools/bash_tool.py +384 -0
  110. src/tools/browser_tool.py +352 -0
  111. src/tools/browser_tools.py +179 -0
  112. src/tools/dep_tools.py +210 -0
  113. src/tools/document_reader.py +167 -0
  114. src/tools/document_tool.py +240 -0
  115. src/tools/document_writer.py +171 -0
  116. src/tools/impact_tools.py +240 -0
  117. src/tools/playwright_tool.py +172 -0
  118. src/tools/quality_tools.py +366 -0
  119. src/tools/read_tools.py +318 -0
  120. src/tools/result_cache.py +157 -0
  121. src/tools/search_tools.py +310 -0
  122. src/tools/shell_tools.py +311 -0
  123. src/tools/write_tools.py +337 -0
  124. src/voice/__init__.py +25 -0
  125. src/voice/audio_capture.py +92 -0
  126. src/voice/audio_playback.py +68 -0
  127. src/voice/errors.py +14 -0
  128. src/voice/models.py +35 -0
  129. src/voice/providers.py +143 -0
  130. src/voice/vad.py +55 -0
  131. src/voice/voice_loop.py +156 -0
@@ -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())