hanzo-mcp 0.6.13__py3-none-any.whl → 0.7.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.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (62) hide show
  1. hanzo_mcp/analytics/__init__.py +5 -0
  2. hanzo_mcp/analytics/posthog_analytics.py +364 -0
  3. hanzo_mcp/cli.py +3 -3
  4. hanzo_mcp/cli_enhanced.py +3 -3
  5. hanzo_mcp/config/settings.py +1 -1
  6. hanzo_mcp/config/tool_config.py +18 -4
  7. hanzo_mcp/server.py +34 -1
  8. hanzo_mcp/tools/__init__.py +65 -2
  9. hanzo_mcp/tools/agent/__init__.py +84 -3
  10. hanzo_mcp/tools/agent/agent_tool.py +102 -4
  11. hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
  12. hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
  13. hanzo_mcp/tools/agent/clarification_tool.py +68 -0
  14. hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
  15. hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
  16. hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
  17. hanzo_mcp/tools/agent/code_auth.py +436 -0
  18. hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
  19. hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
  20. hanzo_mcp/tools/agent/critic_tool.py +376 -0
  21. hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
  22. hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
  23. hanzo_mcp/tools/agent/iching_tool.py +380 -0
  24. hanzo_mcp/tools/agent/network_tool.py +273 -0
  25. hanzo_mcp/tools/agent/prompt.py +62 -20
  26. hanzo_mcp/tools/agent/review_tool.py +433 -0
  27. hanzo_mcp/tools/agent/swarm_tool.py +535 -0
  28. hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
  29. hanzo_mcp/tools/common/base.py +1 -0
  30. hanzo_mcp/tools/common/batch_tool.py +102 -10
  31. hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
  32. hanzo_mcp/tools/common/forgiving_edit.py +243 -0
  33. hanzo_mcp/tools/common/paginated_base.py +230 -0
  34. hanzo_mcp/tools/common/paginated_response.py +307 -0
  35. hanzo_mcp/tools/common/pagination.py +226 -0
  36. hanzo_mcp/tools/common/tool_list.py +3 -0
  37. hanzo_mcp/tools/common/truncate.py +101 -0
  38. hanzo_mcp/tools/filesystem/__init__.py +29 -0
  39. hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
  40. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
  41. hanzo_mcp/tools/lsp/__init__.py +5 -0
  42. hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
  43. hanzo_mcp/tools/memory/__init__.py +76 -0
  44. hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
  45. hanzo_mcp/tools/memory/memory_tools.py +456 -0
  46. hanzo_mcp/tools/search/__init__.py +6 -0
  47. hanzo_mcp/tools/search/find_tool.py +581 -0
  48. hanzo_mcp/tools/search/unified_search.py +953 -0
  49. hanzo_mcp/tools/shell/__init__.py +5 -0
  50. hanzo_mcp/tools/shell/auto_background.py +203 -0
  51. hanzo_mcp/tools/shell/base_process.py +53 -27
  52. hanzo_mcp/tools/shell/bash_tool.py +17 -33
  53. hanzo_mcp/tools/shell/npx_tool.py +15 -32
  54. hanzo_mcp/tools/shell/streaming_command.py +594 -0
  55. hanzo_mcp/tools/shell/uvx_tool.py +15 -32
  56. hanzo_mcp/types.py +23 -0
  57. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.0.dist-info}/METADATA +228 -71
  58. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.0.dist-info}/RECORD +61 -24
  59. hanzo_mcp-0.6.13.dist-info/licenses/LICENSE +0 -21
  60. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
  61. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
  62. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,459 @@
1
+ """Agent tool implementation using hanzo-agents SDK.
2
+
3
+ This module implements the AgentTool that leverages the hanzo-agents SDK
4
+ for sophisticated agent orchestration and execution.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import os
10
+ import re
11
+ import time
12
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional, Dict, Any, List
13
+
14
+ from mcp.server.fastmcp import Context as MCPContext
15
+ from mcp.server import FastMCP
16
+ from pydantic import Field
17
+
18
+ # Import hanzo-agents SDK
19
+ try:
20
+ from hanzo_agents import (
21
+ Agent, State, Network, Tool, History,
22
+ ModelRegistry, InferenceResult, ToolCall,
23
+ create_memory_kv, create_memory_vector,
24
+ sequential_router, state_based_router,
25
+ )
26
+ from hanzo_agents.core.cli_agent import (
27
+ ClaudeCodeAgent, OpenAICodexAgent,
28
+ GeminiAgent, GrokAgent
29
+ )
30
+ HANZO_AGENTS_AVAILABLE = True
31
+ except ImportError:
32
+ HANZO_AGENTS_AVAILABLE = False
33
+
34
+ from hanzo_mcp.tools.common.base import BaseTool
35
+ from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
36
+ from hanzo_mcp.tools.common.permissions import PermissionManager
37
+ from hanzo_mcp.tools.filesystem import get_read_only_filesystem_tools, Edit, MultiEdit
38
+ from hanzo_mcp.tools.jupyter import get_read_only_jupyter_tools
39
+ from hanzo_mcp.tools.common.batch_tool import BatchTool
40
+ from hanzo_mcp.tools.agent.clarification_protocol import AgentClarificationMixin, ClarificationType
41
+ from hanzo_mcp.tools.agent.clarification_tool import ClarificationTool
42
+ from hanzo_mcp.tools.agent.critic_tool import CriticTool
43
+ from hanzo_mcp.tools.agent.review_tool import ReviewTool
44
+ from hanzo_mcp.tools.agent.iching_tool import IChingTool
45
+
46
+
47
+ class AgentToolParams(TypedDict, total=False):
48
+ """Parameters for the AgentTool."""
49
+ prompts: str | list[str]
50
+ model: Optional[str]
51
+ use_memory: Optional[bool]
52
+ memory_backend: Optional[str]
53
+
54
+
55
+ class MCPAgentState(State):
56
+ """State for MCP agents."""
57
+
58
+ def __init__(self, prompts: List[str], context: Dict[str, Any]):
59
+ """Initialize agent state."""
60
+ super().__init__()
61
+ self.prompts = prompts
62
+ self.context = context
63
+ self.current_prompt_index = 0
64
+ self.results = []
65
+
66
+ def to_dict(self) -> Dict[str, Any]:
67
+ """Convert to dictionary."""
68
+ base_dict = super().to_dict()
69
+ base_dict.update({
70
+ "prompts": self.prompts,
71
+ "context": self.context,
72
+ "current_prompt_index": self.current_prompt_index,
73
+ "results": self.results
74
+ })
75
+ return base_dict
76
+
77
+ @classmethod
78
+ def from_dict(cls, data: Dict[str, Any]) -> "MCPAgentState":
79
+ """Create from dictionary."""
80
+ state = cls(
81
+ prompts=data.get("prompts", []),
82
+ context=data.get("context", {})
83
+ )
84
+ state.current_prompt_index = data.get("current_prompt_index", 0)
85
+ state.results = data.get("results", [])
86
+ for k, v in data.items():
87
+ if k not in ["prompts", "context", "current_prompt_index", "results"]:
88
+ state[k] = v
89
+ return state
90
+
91
+
92
+ class MCPToolAdapter(Tool):
93
+ """Adapter to wrap MCP tools for hanzo-agents."""
94
+
95
+ def __init__(self, mcp_tool: BaseTool, ctx: MCPContext):
96
+ """Initialize adapter."""
97
+ self.mcp_tool = mcp_tool
98
+ self.ctx = ctx
99
+
100
+ @property
101
+ def name(self) -> str:
102
+ """Get tool name."""
103
+ return self.mcp_tool.name
104
+
105
+ @property
106
+ def description(self) -> str:
107
+ """Get tool description."""
108
+ return self.mcp_tool.description
109
+
110
+ async def execute(self, **kwargs) -> str:
111
+ """Execute the MCP tool."""
112
+ return await self.mcp_tool.call(self.ctx, **kwargs)
113
+
114
+
115
+ class MCPAgent(Agent):
116
+ """Agent that executes MCP tasks."""
117
+
118
+ name = "mcp_agent"
119
+ description = "Agent for executing MCP tasks"
120
+
121
+ def __init__(self,
122
+ available_tools: List[BaseTool],
123
+ permission_manager: PermissionManager,
124
+ ctx: MCPContext,
125
+ model: str = "model://anthropic/claude-3-5-sonnet-20241022",
126
+ **kwargs):
127
+ """Initialize MCP agent."""
128
+ super().__init__(model=model, **kwargs)
129
+
130
+ self.available_tools = available_tools
131
+ self.permission_manager = permission_manager
132
+ self.ctx = ctx
133
+
134
+ # Register MCP tools as agent tools
135
+ for mcp_tool in available_tools:
136
+ adapter = MCPToolAdapter(mcp_tool, ctx)
137
+ self.register_tool(adapter)
138
+
139
+ async def run(self, state: MCPAgentState, history: History, network: Network) -> InferenceResult:
140
+ """Execute the agent."""
141
+ # Get current prompt
142
+ if state.current_prompt_index >= len(state.prompts):
143
+ return InferenceResult(
144
+ agent=self.name,
145
+ content="All prompts completed",
146
+ metadata={"completed": True}
147
+ )
148
+
149
+ prompt = state.prompts[state.current_prompt_index]
150
+
151
+ # Execute with tools
152
+ messages = [
153
+ {"role": "system", "content": self._get_system_prompt()},
154
+ {"role": "user", "content": prompt}
155
+ ]
156
+
157
+ # Add history context
158
+ for entry in history[-10:]:
159
+ if entry.role == "assistant":
160
+ messages.append({
161
+ "role": "assistant",
162
+ "content": entry.content
163
+ })
164
+ elif entry.role == "user":
165
+ messages.append({
166
+ "role": "user",
167
+ "content": entry.content
168
+ })
169
+
170
+ # Call model
171
+ from hanzo_agents import ModelRegistry
172
+ adapter = ModelRegistry.get_adapter(self.model)
173
+ response = await adapter.chat(messages)
174
+
175
+ # Update state
176
+ state.current_prompt_index += 1
177
+ state.results.append(response)
178
+
179
+ # Return result
180
+ return InferenceResult(
181
+ agent=self.name,
182
+ content=response,
183
+ metadata={
184
+ "prompt_index": state.current_prompt_index - 1,
185
+ "total_prompts": len(state.prompts)
186
+ }
187
+ )
188
+
189
+ def _get_system_prompt(self) -> str:
190
+ """Get system prompt for the agent."""
191
+ tool_descriptions = []
192
+ for tool in self.tools.values():
193
+ tool_descriptions.append(f"- {tool.name}: {tool.description}")
194
+
195
+ return f"""You are an AI assistant with access to the following tools:
196
+
197
+ {chr(10).join(tool_descriptions)}
198
+
199
+ When you need to use a tool, respond with:
200
+ TOOL: tool_name(arg1="value1", arg2="value2")
201
+
202
+ Important guidelines:
203
+ - Always include absolute paths starting with / when working with files
204
+ - Be thorough in your searches and analysis
205
+ - Provide clear, actionable results
206
+ - Edit files when requested to make changes
207
+ """
208
+
209
+
210
+ @final
211
+ class AgentTool(AgentClarificationMixin, BaseTool):
212
+ """Tool for delegating tasks to sub-agents using hanzo-agents SDK."""
213
+
214
+ @property
215
+ @override
216
+ def name(self) -> str:
217
+ """Get the tool name."""
218
+ return "agent"
219
+
220
+ @property
221
+ @override
222
+ def description(self) -> str:
223
+ """Get the tool description."""
224
+ if not HANZO_AGENTS_AVAILABLE:
225
+ return "Agent tool (hanzo-agents SDK not available - using fallback)"
226
+
227
+ at = [t.name for t in self.available_tools]
228
+ return f"""Launch a new agent that has access to the following tools: {at}.
229
+
230
+ When to use the Agent tool:
231
+ - If you are searching for a keyword like "config" or "logger"
232
+ - When you need to perform edits across multiple files
233
+ - When you need to delegate complex file modification tasks
234
+
235
+ When NOT to use the Agent tool:
236
+ - If you want to read a specific file path
237
+ - If you are searching for a specific class definition
238
+ - Writing code and running bash commands
239
+ - Other tasks that are not related to searching
240
+
241
+ Usage notes:
242
+ 1. Launch multiple agents concurrently whenever possible
243
+ 2. Agent results are not visible to the user - summarize them
244
+ 3. Each agent invocation is stateless
245
+ 4. The agent's outputs should generally be trusted
246
+ 5. Clearly tell the agent whether you expect it to write code or just do research"""
247
+
248
+ def __init__(
249
+ self,
250
+ permission_manager: PermissionManager,
251
+ model: str | None = None,
252
+ api_key: str | None = None,
253
+ base_url: str | None = None,
254
+ max_tokens: int | None = None,
255
+ max_iterations: int = 10,
256
+ max_tool_uses: int = 30,
257
+ ) -> None:
258
+ """Initialize the agent tool."""
259
+ self.permission_manager = permission_manager
260
+ self.model_override = model
261
+ self.api_key_override = api_key
262
+ self.base_url_override = base_url
263
+ self.max_tokens_override = max_tokens
264
+ self.max_iterations = max_iterations
265
+ self.max_tool_uses = max_tool_uses
266
+
267
+ # Set up available tools
268
+ self.available_tools: list[BaseTool] = []
269
+ self.available_tools.extend(
270
+ get_read_only_filesystem_tools(self.permission_manager)
271
+ )
272
+ self.available_tools.extend(
273
+ get_read_only_jupyter_tools(self.permission_manager)
274
+ )
275
+
276
+ # Add edit tools
277
+ self.available_tools.append(Edit(self.permission_manager))
278
+ self.available_tools.append(MultiEdit(self.permission_manager))
279
+
280
+ # Add special tools
281
+ self.available_tools.append(ClarificationTool())
282
+ self.available_tools.append(CriticTool())
283
+ self.available_tools.append(ReviewTool())
284
+ self.available_tools.append(IChingTool())
285
+
286
+ self.available_tools.append(
287
+ BatchTool({t.name: t for t in self.available_tools})
288
+ )
289
+
290
+ @override
291
+ async def call(
292
+ self,
293
+ ctx: MCPContext,
294
+ **params: Unpack[AgentToolParams],
295
+ ) -> str:
296
+ """Execute the tool with the given parameters."""
297
+ start_time = time.time()
298
+
299
+ # Create tool context
300
+ tool_ctx = create_tool_context(ctx)
301
+ await tool_ctx.set_tool_info(self.name)
302
+
303
+ # Extract parameters
304
+ prompts = params.get("prompts")
305
+ if prompts is None:
306
+ await tool_ctx.error("No prompts provided")
307
+ return "Error: At least one prompt must be provided."
308
+
309
+ # Handle both string and list inputs
310
+ if isinstance(prompts, str):
311
+ prompt_list = [prompts]
312
+ elif isinstance(prompts, list):
313
+ if not prompts:
314
+ await tool_ctx.error("Empty prompts list provided")
315
+ return "Error: At least one prompt must be provided."
316
+ prompt_list = prompts
317
+ else:
318
+ await tool_ctx.error("Invalid prompts parameter type")
319
+ return "Error: Parameter 'prompts' must be a string or list of strings."
320
+
321
+ # Validate absolute paths
322
+ absolute_path_pattern = r"/(?:[^/\s]+/)*[^/\s]+"
323
+ for prompt in prompt_list:
324
+ if not re.search(absolute_path_pattern, prompt):
325
+ await tool_ctx.error(f"Prompt missing absolute path: {prompt[:50]}...")
326
+ return "Error: All prompts must contain at least one absolute path."
327
+
328
+ # Check if hanzo-agents is available
329
+ if not HANZO_AGENTS_AVAILABLE:
330
+ # Fall back to original implementation
331
+ await tool_ctx.warning("hanzo-agents SDK not available, using fallback")
332
+ from hanzo_mcp.tools.agent.agent_tool import AgentTool as OriginalAgentTool
333
+ original_tool = OriginalAgentTool(
334
+ permission_manager=self.permission_manager,
335
+ model=self.model_override,
336
+ api_key=self.api_key_override,
337
+ base_url=self.base_url_override,
338
+ max_tokens=self.max_tokens_override,
339
+ max_iterations=self.max_iterations,
340
+ max_tool_uses=self.max_tool_uses,
341
+ )
342
+ return await original_tool.call(ctx, prompts=prompts)
343
+
344
+ # Use hanzo-agents SDK
345
+ await tool_ctx.info(f"Launching {len(prompt_list)} agent(s) using hanzo-agents SDK")
346
+
347
+ # Determine model and agent type
348
+ model = params.get("model", self.model_override)
349
+ use_memory = params.get("use_memory", False)
350
+ memory_backend = params.get("memory_backend", "sqlite")
351
+
352
+ # Get appropriate agent class
353
+ agent_class = self._get_agent_class(model)
354
+
355
+ # Create state
356
+ state = MCPAgentState(
357
+ prompts=prompt_list,
358
+ context={
359
+ "permission_manager": self.permission_manager,
360
+ "api_key": self.api_key_override,
361
+ "base_url": self.base_url_override,
362
+ "max_tokens": self.max_tokens_override,
363
+ }
364
+ )
365
+
366
+ # Create memory if requested
367
+ memory_kv = None
368
+ memory_vector = None
369
+ if use_memory:
370
+ memory_kv = create_memory_kv(memory_backend)
371
+ memory_vector = create_memory_vector("simple")
372
+
373
+ # Create network
374
+ network = Network(
375
+ state=state,
376
+ agents=[agent_class],
377
+ router=sequential_router([agent_class] * len(prompt_list)),
378
+ memory_kv=memory_kv,
379
+ memory_vector=memory_vector,
380
+ max_steps=self.max_iterations * len(prompt_list),
381
+ )
382
+
383
+ # Execute
384
+ try:
385
+ final_state = await network.run()
386
+ execution_time = time.time() - start_time
387
+
388
+ # Format results
389
+ results = final_state.results
390
+ if len(results) == 1:
391
+ formatted_result = f"""Agent execution completed in {execution_time:.2f} seconds.
392
+
393
+ AGENT RESPONSE:
394
+ {results[0]}"""
395
+ else:
396
+ formatted_results = []
397
+ for i, result in enumerate(results):
398
+ formatted_results.append(f"Agent {i+1} Result:\n{result}")
399
+
400
+ formatted_result = f"""Multi-agent execution completed in {execution_time:.2f} seconds ({len(results)} agents).
401
+
402
+ AGENT RESPONSES:
403
+ {chr(10).join(formatted_results)}"""
404
+
405
+ await tool_ctx.info(f"Execution completed in {execution_time:.2f}s")
406
+ return formatted_result
407
+
408
+ except Exception as e:
409
+ await tool_ctx.error(f"Agent execution failed: {str(e)}")
410
+ return f"Error: {str(e)}"
411
+
412
+ def _get_agent_class(self, model: Optional[str]) -> type[Agent]:
413
+ """Get appropriate agent class based on model."""
414
+ if not model:
415
+ model = "model://anthropic/claude-3-5-sonnet-20241022"
416
+
417
+ # Check for CLI agents
418
+ cli_agents = {
419
+ "claude_cli": ClaudeCodeAgent,
420
+ "codex_cli": OpenAICodexAgent,
421
+ "gemini_cli": GeminiAgent,
422
+ "grok_cli": GrokAgent,
423
+ }
424
+
425
+ if model in cli_agents:
426
+ return cli_agents[model]
427
+
428
+ # Return generic MCP agent
429
+ return type("DynamicMCPAgent", (MCPAgent,), {
430
+ "model": model,
431
+ "__init__": lambda self: MCPAgent.__init__(
432
+ self,
433
+ available_tools=self.available_tools,
434
+ permission_manager=self.permission_manager,
435
+ ctx=self.ctx,
436
+ model=model
437
+ )
438
+ })
439
+
440
+ @override
441
+ def register(self, mcp_server: FastMCP) -> None:
442
+ """Register this agent tool with the MCP server."""
443
+ tool_self = self
444
+
445
+ @mcp_server.tool(name=self.name, description=self.description)
446
+ async def dispatch_agent(
447
+ prompts: str | list[str],
448
+ ctx: MCPContext,
449
+ model: Optional[str] = None,
450
+ use_memory: bool = False,
451
+ memory_backend: str = "sqlite"
452
+ ) -> str:
453
+ return await tool_self.call(
454
+ ctx,
455
+ prompts=prompts,
456
+ model=model,
457
+ use_memory=use_memory,
458
+ memory_backend=memory_backend
459
+ )
@@ -0,0 +1,220 @@
1
+ """Clarification protocol for agent-to-mainloop communication.
2
+
3
+ This module provides a protocol for agents to request clarification
4
+ from the main loop without human intervention.
5
+ """
6
+
7
+ import json
8
+ from dataclasses import dataclass
9
+ from typing import Any, Dict, List, Optional
10
+ from enum import Enum
11
+
12
+
13
+ class ClarificationType(Enum):
14
+ """Types of clarification requests."""
15
+ AMBIGUOUS_INSTRUCTION = "ambiguous_instruction"
16
+ MISSING_CONTEXT = "missing_context"
17
+ MULTIPLE_OPTIONS = "multiple_options"
18
+ CONFIRMATION_NEEDED = "confirmation_needed"
19
+ ADDITIONAL_INFO = "additional_info"
20
+
21
+
22
+ @dataclass
23
+ class ClarificationRequest:
24
+ """A request for clarification from an agent."""
25
+
26
+ agent_id: str
27
+ request_type: ClarificationType
28
+ question: str
29
+ context: Dict[str, Any]
30
+ options: Optional[List[str]] = None
31
+
32
+ def to_json(self) -> str:
33
+ """Convert to JSON for transport."""
34
+ return json.dumps({
35
+ "agent_id": self.agent_id,
36
+ "request_type": self.request_type.value,
37
+ "question": self.question,
38
+ "context": self.context,
39
+ "options": self.options
40
+ })
41
+
42
+ @classmethod
43
+ def from_json(cls, data: str) -> "ClarificationRequest":
44
+ """Create from JSON string."""
45
+ obj = json.loads(data)
46
+ return cls(
47
+ agent_id=obj["agent_id"],
48
+ request_type=ClarificationType(obj["request_type"]),
49
+ question=obj["question"],
50
+ context=obj["context"],
51
+ options=obj.get("options")
52
+ )
53
+
54
+
55
+ @dataclass
56
+ class ClarificationResponse:
57
+ """A response to a clarification request."""
58
+
59
+ request_id: str
60
+ answer: str
61
+ additional_context: Optional[Dict[str, Any]] = None
62
+
63
+ def to_json(self) -> str:
64
+ """Convert to JSON for transport."""
65
+ return json.dumps({
66
+ "request_id": self.request_id,
67
+ "answer": self.answer,
68
+ "additional_context": self.additional_context
69
+ })
70
+
71
+
72
+ class ClarificationHandler:
73
+ """Handles clarification requests from agents."""
74
+
75
+ def __init__(self):
76
+ self.pending_requests: Dict[str, ClarificationRequest] = {}
77
+ self.request_counter = 0
78
+
79
+ def create_request(
80
+ self,
81
+ agent_id: str,
82
+ request_type: ClarificationType,
83
+ question: str,
84
+ context: Dict[str, Any],
85
+ options: Optional[List[str]] = None
86
+ ) -> str:
87
+ """Create a new clarification request.
88
+
89
+ Returns:
90
+ Request ID for tracking
91
+ """
92
+ request = ClarificationRequest(
93
+ agent_id=agent_id,
94
+ request_type=request_type,
95
+ question=question,
96
+ context=context,
97
+ options=options
98
+ )
99
+
100
+ request_id = f"clarify_{self.request_counter}"
101
+ self.request_counter += 1
102
+ self.pending_requests[request_id] = request
103
+
104
+ return request_id
105
+
106
+ def handle_request(self, request: ClarificationRequest) -> ClarificationResponse:
107
+ """Handle a clarification request automatically.
108
+
109
+ This method implements automatic clarification resolution
110
+ based on context and common patterns.
111
+ """
112
+ request_id = f"clarify_{len(self.pending_requests)}"
113
+
114
+ # Handle different types of clarification
115
+ if request.request_type == ClarificationType.AMBIGUOUS_INSTRUCTION:
116
+ # Try to clarify based on context
117
+ if "file_path" in request.context:
118
+ if request.context["file_path"].endswith(".go"):
119
+ answer = "For Go files, ensure you add imports in the correct format and handle both single import and import block cases."
120
+ elif request.context["file_path"].endswith(".py"):
121
+ answer = "For Python files, add imports at the top of the file after any module docstring."
122
+ else:
123
+ answer = "Add imports according to the language's conventions."
124
+ else:
125
+ answer = "Proceed with the most reasonable interpretation based on the context."
126
+
127
+ elif request.request_type == ClarificationType.MISSING_CONTEXT:
128
+ # Provide additional context based on what's missing
129
+ if "import_path" in request.question.lower():
130
+ answer = "Use the standard import path based on the project structure. Check existing imports in similar files for patterns."
131
+ elif "format" in request.question.lower():
132
+ answer = "Match the existing code style in the file. Use the same indentation and formatting patterns."
133
+ else:
134
+ answer = "Analyze the surrounding code and project structure to infer the missing information."
135
+
136
+ elif request.request_type == ClarificationType.MULTIPLE_OPTIONS:
137
+ # Choose the best option based on context
138
+ if request.options:
139
+ # Simple heuristic: choose the first option that seems most standard
140
+ for option in request.options:
141
+ if "common" in option or "standard" in option:
142
+ answer = f"Choose option: {option}"
143
+ break
144
+ else:
145
+ answer = f"Choose option: {request.options[0]}"
146
+ else:
147
+ answer = "Choose the most conventional approach based on the codebase patterns."
148
+
149
+ elif request.request_type == ClarificationType.CONFIRMATION_NEEDED:
150
+ # Auto-confirm safe operations
151
+ if "add import" in request.question.lower():
152
+ answer = "Yes, proceed with adding the import."
153
+ elif "multi_edit" in request.question.lower():
154
+ answer = "Yes, use multi_edit for efficiency."
155
+ else:
156
+ answer = "Proceed if the operation is safe and reversible."
157
+
158
+ else: # ADDITIONAL_INFO
159
+ answer = "Continue with available information and make reasonable assumptions based on context."
160
+
161
+ return ClarificationResponse(
162
+ request_id=request_id,
163
+ answer=answer,
164
+ additional_context={"auto_resolved": True}
165
+ )
166
+
167
+
168
+ class AgentClarificationMixin:
169
+ """Mixin for agents to request clarification."""
170
+
171
+ def __init__(self, *args, **kwargs):
172
+ super().__init__(*args, **kwargs)
173
+ self.clarification_handler = ClarificationHandler()
174
+ self.clarification_count = 0
175
+ self.max_clarifications = 1 # Allow up to 1 clarification per task
176
+
177
+ async def request_clarification(
178
+ self,
179
+ request_type: ClarificationType,
180
+ question: str,
181
+ context: Dict[str, Any],
182
+ options: Optional[List[str]] = None
183
+ ) -> str:
184
+ """Request clarification from the main loop.
185
+
186
+ Args:
187
+ request_type: Type of clarification needed
188
+ question: The question to ask
189
+ context: Relevant context for the question
190
+ options: Optional list of choices
191
+
192
+ Returns:
193
+ The clarification response
194
+
195
+ Raises:
196
+ RuntimeError: If clarification limit exceeded
197
+ """
198
+ if self.clarification_count >= self.max_clarifications:
199
+ raise RuntimeError("Clarification limit exceeded")
200
+
201
+ self.clarification_count += 1
202
+
203
+ # Create request
204
+ request = ClarificationRequest(
205
+ agent_id=getattr(self, 'agent_id', 'unknown'),
206
+ request_type=request_type,
207
+ question=question,
208
+ context=context,
209
+ options=options
210
+ )
211
+
212
+ # In real implementation, this would communicate with main loop
213
+ # For now, use the automatic handler
214
+ response = self.clarification_handler.handle_request(request)
215
+
216
+ return response.answer
217
+
218
+ def format_clarification_in_output(self, question: str, answer: str) -> str:
219
+ """Format clarification exchange for output."""
220
+ return f"\n🤔 Clarification needed: {question}\n✅ Resolved: {answer}\n"