hanzo-mcp 0.6.12__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.
- hanzo_mcp/__init__.py +2 -2
- hanzo_mcp/analytics/__init__.py +5 -0
- hanzo_mcp/analytics/posthog_analytics.py +364 -0
- hanzo_mcp/cli.py +5 -5
- hanzo_mcp/cli_enhanced.py +7 -7
- hanzo_mcp/cli_plugin.py +91 -0
- hanzo_mcp/config/__init__.py +1 -1
- hanzo_mcp/config/settings.py +70 -7
- hanzo_mcp/config/tool_config.py +20 -6
- hanzo_mcp/dev_server.py +3 -3
- hanzo_mcp/prompts/project_system.py +1 -1
- hanzo_mcp/server.py +40 -3
- hanzo_mcp/server_enhanced.py +69 -0
- hanzo_mcp/tools/__init__.py +140 -31
- hanzo_mcp/tools/agent/__init__.py +85 -4
- hanzo_mcp/tools/agent/agent_tool.py +104 -6
- hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
- hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
- hanzo_mcp/tools/agent/clarification_tool.py +68 -0
- hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
- hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
- hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
- hanzo_mcp/tools/agent/code_auth.py +436 -0
- hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
- hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
- hanzo_mcp/tools/agent/critic_tool.py +376 -0
- hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
- hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
- hanzo_mcp/tools/agent/iching_tool.py +380 -0
- hanzo_mcp/tools/agent/network_tool.py +273 -0
- hanzo_mcp/tools/agent/prompt.py +62 -20
- hanzo_mcp/tools/agent/review_tool.py +433 -0
- hanzo_mcp/tools/agent/swarm_tool.py +535 -0
- hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
- hanzo_mcp/tools/common/__init__.py +15 -1
- hanzo_mcp/tools/common/base.py +5 -4
- hanzo_mcp/tools/common/batch_tool.py +103 -11
- hanzo_mcp/tools/common/config_tool.py +2 -2
- hanzo_mcp/tools/common/context.py +2 -2
- hanzo_mcp/tools/common/context_fix.py +26 -0
- hanzo_mcp/tools/common/critic_tool.py +196 -0
- hanzo_mcp/tools/common/decorators.py +208 -0
- hanzo_mcp/tools/common/enhanced_base.py +106 -0
- hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
- hanzo_mcp/tools/common/forgiving_edit.py +243 -0
- hanzo_mcp/tools/common/mode.py +116 -0
- hanzo_mcp/tools/common/mode_loader.py +105 -0
- hanzo_mcp/tools/common/paginated_base.py +230 -0
- hanzo_mcp/tools/common/paginated_response.py +307 -0
- hanzo_mcp/tools/common/pagination.py +226 -0
- hanzo_mcp/tools/common/permissions.py +1 -1
- hanzo_mcp/tools/common/personality.py +936 -0
- hanzo_mcp/tools/common/plugin_loader.py +287 -0
- hanzo_mcp/tools/common/stats.py +4 -4
- hanzo_mcp/tools/common/tool_list.py +4 -1
- hanzo_mcp/tools/common/truncate.py +101 -0
- hanzo_mcp/tools/common/validation.py +1 -1
- hanzo_mcp/tools/config/__init__.py +3 -1
- hanzo_mcp/tools/config/config_tool.py +1 -1
- hanzo_mcp/tools/config/mode_tool.py +209 -0
- hanzo_mcp/tools/database/__init__.py +1 -1
- hanzo_mcp/tools/editor/__init__.py +1 -1
- hanzo_mcp/tools/filesystem/__init__.py +48 -14
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
- hanzo_mcp/tools/filesystem/batch_search.py +3 -3
- hanzo_mcp/tools/filesystem/diff.py +2 -2
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
- hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
- hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
- hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
- hanzo_mcp/tools/filesystem/watch.py +3 -2
- hanzo_mcp/tools/jupyter/__init__.py +2 -2
- hanzo_mcp/tools/jupyter/jupyter.py +1 -1
- hanzo_mcp/tools/llm/__init__.py +3 -3
- hanzo_mcp/tools/llm/llm_tool.py +648 -143
- hanzo_mcp/tools/lsp/__init__.py +5 -0
- hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
- hanzo_mcp/tools/mcp/__init__.py +2 -2
- hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
- hanzo_mcp/tools/memory/__init__.py +76 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
- hanzo_mcp/tools/memory/memory_tools.py +456 -0
- hanzo_mcp/tools/search/__init__.py +6 -0
- hanzo_mcp/tools/search/find_tool.py +581 -0
- hanzo_mcp/tools/search/unified_search.py +953 -0
- hanzo_mcp/tools/shell/__init__.py +11 -6
- hanzo_mcp/tools/shell/auto_background.py +203 -0
- hanzo_mcp/tools/shell/base_process.py +57 -29
- hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
- hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +18 -34
- hanzo_mcp/tools/shell/command_executor.py +2 -2
- hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +16 -33
- hanzo_mcp/tools/shell/open.py +2 -2
- hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
- hanzo_mcp/tools/shell/run_command_windows.py +1 -1
- hanzo_mcp/tools/shell/streaming_command.py +594 -0
- hanzo_mcp/tools/shell/uvx.py +47 -2
- hanzo_mcp/tools/shell/uvx_background.py +47 -2
- hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +16 -33
- hanzo_mcp/tools/todo/__init__.py +14 -19
- hanzo_mcp/tools/todo/todo.py +22 -1
- hanzo_mcp/tools/vector/__init__.py +1 -1
- hanzo_mcp/tools/vector/infinity_store.py +2 -2
- hanzo_mcp/tools/vector/project_manager.py +1 -1
- hanzo_mcp/types.py +23 -0
- hanzo_mcp-0.7.0.dist-info/METADATA +516 -0
- hanzo_mcp-0.7.0.dist-info/RECORD +180 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/entry_points.txt +1 -0
- hanzo_mcp/tools/common/palette.py +0 -344
- hanzo_mcp/tools/common/palette_loader.py +0 -108
- hanzo_mcp/tools/config/palette_tool.py +0 -179
- hanzo_mcp/tools/llm/llm_unified.py +0 -851
- hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
- hanzo_mcp-0.6.12.dist-info/RECORD +0 -135
- hanzo_mcp-0.6.12.dist-info/licenses/LICENSE +0 -21
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.6.12.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"
|