hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.1__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 +1 -1
- hanzo_mcp/analytics/posthog_analytics.py +14 -1
- hanzo_mcp/cli.py +108 -4
- hanzo_mcp/server.py +11 -0
- hanzo_mcp/tools/__init__.py +3 -16
- hanzo_mcp/tools/agent/__init__.py +5 -0
- hanzo_mcp/tools/agent/agent.py +5 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -17
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
- hanzo_mcp/tools/agent/clarification_tool.py +7 -1
- hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
- hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
- hanzo_mcp/tools/agent/cli_tools.py +26 -0
- hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
- hanzo_mcp/tools/agent/critic_tool.py +7 -1
- hanzo_mcp/tools/agent/iching_tool.py +5 -0
- hanzo_mcp/tools/agent/network_tool.py +5 -0
- hanzo_mcp/tools/agent/review_tool.py +7 -1
- hanzo_mcp/tools/agent/swarm_alias.py +5 -0
- hanzo_mcp/tools/agent/swarm_tool.py +701 -0
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
- hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
- hanzo_mcp/tools/common/auto_timeout.py +234 -0
- hanzo_mcp/tools/common/base.py +4 -0
- hanzo_mcp/tools/common/batch_tool.py +5 -0
- hanzo_mcp/tools/common/config_tool.py +5 -0
- hanzo_mcp/tools/common/critic_tool.py +5 -0
- hanzo_mcp/tools/common/paginated_base.py +4 -0
- hanzo_mcp/tools/common/permissions.py +38 -12
- hanzo_mcp/tools/common/personality.py +673 -980
- hanzo_mcp/tools/common/stats.py +5 -0
- hanzo_mcp/tools/common/thinking_tool.py +5 -0
- hanzo_mcp/tools/common/timeout_parser.py +103 -0
- hanzo_mcp/tools/common/tool_disable.py +5 -0
- hanzo_mcp/tools/common/tool_enable.py +5 -0
- hanzo_mcp/tools/common/tool_list.py +5 -0
- hanzo_mcp/tools/config/config_tool.py +5 -0
- hanzo_mcp/tools/config/mode_tool.py +5 -0
- hanzo_mcp/tools/database/graph.py +5 -0
- hanzo_mcp/tools/database/graph_add.py +5 -0
- hanzo_mcp/tools/database/graph_query.py +5 -0
- hanzo_mcp/tools/database/graph_remove.py +5 -0
- hanzo_mcp/tools/database/graph_search.py +5 -0
- hanzo_mcp/tools/database/graph_stats.py +5 -0
- hanzo_mcp/tools/database/sql.py +5 -0
- hanzo_mcp/tools/database/sql_query.py +2 -0
- hanzo_mcp/tools/database/sql_search.py +5 -0
- hanzo_mcp/tools/database/sql_stats.py +5 -0
- hanzo_mcp/tools/editor/neovim_command.py +5 -0
- hanzo_mcp/tools/editor/neovim_edit.py +7 -2
- hanzo_mcp/tools/editor/neovim_session.py +5 -0
- hanzo_mcp/tools/filesystem/__init__.py +23 -26
- hanzo_mcp/tools/filesystem/ast_tool.py +2 -3
- hanzo_mcp/tools/filesystem/base.py +0 -16
- hanzo_mcp/tools/filesystem/batch_search.py +825 -0
- hanzo_mcp/tools/filesystem/content_replace.py +5 -3
- hanzo_mcp/tools/filesystem/diff.py +5 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
- hanzo_mcp/tools/filesystem/edit.py +5 -4
- hanzo_mcp/tools/filesystem/find.py +177 -311
- hanzo_mcp/tools/filesystem/find_files.py +370 -0
- hanzo_mcp/tools/filesystem/git_search.py +5 -3
- hanzo_mcp/tools/filesystem/grep.py +454 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +5 -4
- hanzo_mcp/tools/filesystem/read.py +11 -8
- hanzo_mcp/tools/filesystem/rules_tool.py +5 -3
- hanzo_mcp/tools/filesystem/search_tool.py +728 -0
- hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
- hanzo_mcp/tools/filesystem/tree.py +273 -0
- hanzo_mcp/tools/filesystem/watch.py +6 -1
- hanzo_mcp/tools/filesystem/write.py +12 -6
- hanzo_mcp/tools/jupyter/jupyter.py +30 -2
- hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
- hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
- hanzo_mcp/tools/llm/consensus_tool.py +8 -6
- hanzo_mcp/tools/llm/llm_manage.py +5 -0
- hanzo_mcp/tools/llm/llm_tool.py +2 -0
- hanzo_mcp/tools/llm/llm_unified.py +5 -0
- hanzo_mcp/tools/llm/provider_tools.py +5 -0
- hanzo_mcp/tools/lsp/lsp_tool.py +475 -622
- hanzo_mcp/tools/mcp/mcp_add.py +7 -2
- hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
- hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
- hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
- hanzo_mcp/tools/memory/memory_tools.py +17 -0
- hanzo_mcp/tools/search/find_tool.py +5 -3
- hanzo_mcp/tools/search/unified_search.py +3 -1
- hanzo_mcp/tools/shell/__init__.py +2 -14
- hanzo_mcp/tools/shell/base_process.py +4 -2
- hanzo_mcp/tools/shell/bash_tool.py +2 -0
- hanzo_mcp/tools/shell/command_executor.py +7 -7
- hanzo_mcp/tools/shell/logs.py +5 -0
- hanzo_mcp/tools/shell/npx.py +5 -0
- hanzo_mcp/tools/shell/npx_background.py +5 -0
- hanzo_mcp/tools/shell/npx_tool.py +5 -0
- hanzo_mcp/tools/shell/open.py +5 -0
- hanzo_mcp/tools/shell/pkill.py +5 -0
- hanzo_mcp/tools/shell/process_tool.py +5 -0
- hanzo_mcp/tools/shell/processes.py +5 -0
- hanzo_mcp/tools/shell/run_background.py +5 -0
- hanzo_mcp/tools/shell/run_command.py +2 -0
- hanzo_mcp/tools/shell/run_command_windows.py +5 -0
- hanzo_mcp/tools/shell/streaming_command.py +5 -0
- hanzo_mcp/tools/shell/uvx.py +5 -0
- hanzo_mcp/tools/shell/uvx_background.py +5 -0
- hanzo_mcp/tools/shell/uvx_tool.py +5 -0
- hanzo_mcp/tools/shell/zsh_tool.py +3 -0
- hanzo_mcp/tools/todo/todo.py +5 -0
- hanzo_mcp/tools/todo/todo_read.py +142 -0
- hanzo_mcp/tools/todo/todo_write.py +367 -0
- hanzo_mcp/tools/vector/__init__.py +42 -95
- hanzo_mcp/tools/vector/index_tool.py +5 -0
- hanzo_mcp/tools/vector/vector.py +5 -0
- hanzo_mcp/tools/vector/vector_index.py +5 -0
- hanzo_mcp/tools/vector/vector_search.py +5 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/METADATA +1 -1
- hanzo_mcp-0.9.1.dist-info/RECORD +195 -0
- hanzo_mcp/tools/common/path_utils.py +0 -34
- hanzo_mcp/tools/compiler/__init__.py +0 -8
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
- hanzo_mcp/tools/environment/__init__.py +0 -8
- hanzo_mcp/tools/environment/environment_detector.py +0 -594
- hanzo_mcp/tools/filesystem/search.py +0 -1160
- hanzo_mcp/tools/framework/__init__.py +0 -8
- hanzo_mcp/tools/framework/framework_modes.py +0 -714
- hanzo_mcp/tools/memory/conversation_memory.py +0 -636
- hanzo_mcp/tools/shell/run_tool.py +0 -56
- hanzo_mcp/tools/vector/node_tool.py +0 -538
- hanzo_mcp/tools/vector/unified_vector.py +0 -384
- hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
"""Agent tool implementation for Hanzo AI.
|
|
2
|
+
|
|
3
|
+
This module implements the AgentTool that allows Claude to delegate tasks to sub-agents,
|
|
4
|
+
enabling concurrent execution of multiple operations and specialized processing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
# Import litellm with warnings suppressed
|
|
13
|
+
import warnings
|
|
14
|
+
from typing import Unpack, Annotated, TypedDict, final, override
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
|
|
17
|
+
with warnings.catch_warnings():
|
|
18
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
|
19
|
+
import litellm
|
|
20
|
+
from pydantic import Field
|
|
21
|
+
from mcp.server import FastMCP
|
|
22
|
+
|
|
23
|
+
from hanzo_mcp.tools.common.auto_timeout import auto_timeout
|
|
24
|
+
from openai.types.chat import ChatCompletionToolParam, ChatCompletionMessageParam
|
|
25
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
26
|
+
|
|
27
|
+
from hanzo_mcp.tools.jupyter import get_read_only_jupyter_tools
|
|
28
|
+
from hanzo_mcp.tools.filesystem import Edit, MultiEdit, get_read_only_filesystem_tools
|
|
29
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
30
|
+
from hanzo_mcp.tools.agent.prompt import (
|
|
31
|
+
get_default_model,
|
|
32
|
+
get_system_prompt,
|
|
33
|
+
get_model_parameters,
|
|
34
|
+
get_allowed_agent_tools,
|
|
35
|
+
)
|
|
36
|
+
from hanzo_mcp.tools.common.context import (
|
|
37
|
+
ToolContext,
|
|
38
|
+
create_tool_context,
|
|
39
|
+
)
|
|
40
|
+
from hanzo_mcp.tools.agent.critic_tool import CriticTool, CriticProtocol
|
|
41
|
+
from hanzo_mcp.tools.agent.iching_tool import IChingTool
|
|
42
|
+
from hanzo_mcp.tools.agent.review_tool import ReviewTool, ReviewProtocol
|
|
43
|
+
from hanzo_mcp.tools.common.batch_tool import BatchTool
|
|
44
|
+
from hanzo_mcp.tools.agent.tool_adapter import (
|
|
45
|
+
convert_tools_to_openai_functions,
|
|
46
|
+
)
|
|
47
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
48
|
+
from hanzo_mcp.tools.agent.clarification_tool import ClarificationTool
|
|
49
|
+
from hanzo_mcp.tools.agent.clarification_protocol import (
|
|
50
|
+
ClarificationType,
|
|
51
|
+
AgentClarificationMixin,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
Prompt = Annotated[
|
|
55
|
+
str,
|
|
56
|
+
Field(
|
|
57
|
+
description="Task for the agent to perform (must include absolute paths starting with /)",
|
|
58
|
+
min_length=1,
|
|
59
|
+
),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AgentToolParams(TypedDict, total=False):
|
|
64
|
+
"""Parameters for the AgentTool.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
prompts: Task(s) for the agent to perform (must include absolute paths starting with /)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
prompts: str | list[str]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@final
|
|
74
|
+
class AgentTool(AgentClarificationMixin, BaseTool):
|
|
75
|
+
"""Tool for delegating tasks to sub-agents.
|
|
76
|
+
|
|
77
|
+
The AgentTool allows Claude to create and manage sub-agents for performing
|
|
78
|
+
specialized tasks concurrently, such as code search, analysis, and more.
|
|
79
|
+
|
|
80
|
+
Agents can request clarification from the main loop up to once per task.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
@override
|
|
85
|
+
def name(self) -> str:
|
|
86
|
+
"""Get the tool name.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Tool name
|
|
90
|
+
"""
|
|
91
|
+
return "agent"
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
@override
|
|
95
|
+
def description(self) -> str:
|
|
96
|
+
"""Get the tool description.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Tool description
|
|
100
|
+
"""
|
|
101
|
+
# TODO: Add glob when it is implemented
|
|
102
|
+
at = [t.name for t in self.available_tools]
|
|
103
|
+
|
|
104
|
+
return f"""Launch a new agent that has access to the following tools: {at}. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use the Agent tool to perform the search for you.
|
|
105
|
+
|
|
106
|
+
When to use the Agent tool:
|
|
107
|
+
- If you are searching for a keyword like \"config\" or \"logger\", or for questions like \"which file does X?\", the Agent tool is strongly recommended
|
|
108
|
+
- When you need to perform edits across multiple files based on search results
|
|
109
|
+
- When you need to delegate complex file modification tasks
|
|
110
|
+
|
|
111
|
+
When NOT to use the Agent tool:
|
|
112
|
+
- If you want to read a specific file path, use the read or glob tool instead of the Agent tool, to find the match more quickly
|
|
113
|
+
- If you are searching for a specific class definition like \"class Foo\", use the glob tool instead, to find the match more quickly
|
|
114
|
+
- If you are searching for code within a specific file or set of 2-3 files, use the read tool instead of the Agent tool, to find the match more quickly
|
|
115
|
+
- Writing code and running bash commands (use other tools for that)
|
|
116
|
+
- Other tasks that are not related to searching for a keyword or file
|
|
117
|
+
|
|
118
|
+
Usage notes:
|
|
119
|
+
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
|
120
|
+
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
|
|
121
|
+
3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
|
122
|
+
4. The agent's outputs should generally be trusted
|
|
123
|
+
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent"""
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
permission_manager: PermissionManager,
|
|
128
|
+
model: str | None = None,
|
|
129
|
+
api_key: str | None = None,
|
|
130
|
+
base_url: str | None = None,
|
|
131
|
+
max_tokens: int | None = None,
|
|
132
|
+
max_iterations: int = 10,
|
|
133
|
+
max_tool_uses: int = 30,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Initialize the agent tool.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
|
|
139
|
+
permission_manager: Permission manager for access control
|
|
140
|
+
model: Optional model name override in LiteLLM format (e.g., "openai/gpt-4o")
|
|
141
|
+
api_key: Optional API key for the model provider
|
|
142
|
+
base_url: Optional base URL for the model provider API endpoint
|
|
143
|
+
max_tokens: Optional maximum tokens for model responses
|
|
144
|
+
max_iterations: Maximum number of iterations for agent (default: 10)
|
|
145
|
+
max_tool_uses: Maximum number of total tool uses for agent (default: 30)
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
self.permission_manager = permission_manager
|
|
149
|
+
self.model_override = model
|
|
150
|
+
self.api_key_override = api_key
|
|
151
|
+
self.base_url_override = base_url
|
|
152
|
+
self.max_tokens_override = max_tokens
|
|
153
|
+
self.max_iterations = max_iterations
|
|
154
|
+
self.max_tool_uses = max_tool_uses
|
|
155
|
+
self.available_tools: list[BaseTool] = []
|
|
156
|
+
self.available_tools.extend(get_read_only_filesystem_tools(self.permission_manager))
|
|
157
|
+
self.available_tools.extend(get_read_only_jupyter_tools(self.permission_manager))
|
|
158
|
+
|
|
159
|
+
# Always add edit tools - agents should have edit access
|
|
160
|
+
self.available_tools.append(Edit(self.permission_manager))
|
|
161
|
+
self.available_tools.append(MultiEdit(self.permission_manager))
|
|
162
|
+
|
|
163
|
+
# Add clarification tool for agents
|
|
164
|
+
self.available_tools.append(ClarificationTool())
|
|
165
|
+
|
|
166
|
+
# Add critic tool for agents (devil's advocate)
|
|
167
|
+
self.available_tools.append(CriticTool())
|
|
168
|
+
|
|
169
|
+
# Add review tool for agents (balanced review)
|
|
170
|
+
self.available_tools.append(ReviewTool())
|
|
171
|
+
|
|
172
|
+
# Add I Ching tool for creative guidance
|
|
173
|
+
self.available_tools.append(IChingTool())
|
|
174
|
+
|
|
175
|
+
self.available_tools.append(BatchTool({t.name: t for t in self.available_tools}))
|
|
176
|
+
|
|
177
|
+
# Initialize protocols
|
|
178
|
+
self.critic_protocol = CriticProtocol()
|
|
179
|
+
self.review_protocol = ReviewProtocol()
|
|
180
|
+
|
|
181
|
+
@override
|
|
182
|
+
@auto_timeout("agent_tool_v1_deprecated")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def call(
|
|
186
|
+
self,
|
|
187
|
+
ctx: MCPContext,
|
|
188
|
+
**params: Unpack[AgentToolParams],
|
|
189
|
+
) -> str:
|
|
190
|
+
"""Execute the tool with the given parameters.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
ctx: MCP context
|
|
194
|
+
**params: Tool parameters
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Tool execution result
|
|
198
|
+
"""
|
|
199
|
+
start_time = time.time()
|
|
200
|
+
|
|
201
|
+
# Create tool context
|
|
202
|
+
tool_ctx = create_tool_context(ctx)
|
|
203
|
+
await tool_ctx.set_tool_info(self.name)
|
|
204
|
+
|
|
205
|
+
# Extract and validate parameters
|
|
206
|
+
prompts = params.get("prompts")
|
|
207
|
+
|
|
208
|
+
if prompts is None:
|
|
209
|
+
await tool_ctx.error("No prompts provided")
|
|
210
|
+
return """Error: At least one prompt must be provided.
|
|
211
|
+
|
|
212
|
+
IMPORTANT REMINDER FOR CLAUDE:
|
|
213
|
+
The dispatch_agent tool requires prompts parameter. Please provide either:
|
|
214
|
+
- A single prompt as a string
|
|
215
|
+
- Multiple prompts as an array of strings
|
|
216
|
+
|
|
217
|
+
Each prompt must contain absolute paths starting with /.
|
|
218
|
+
Example of correct usage:
|
|
219
|
+
- prompts: "Search for all instances of the 'config' variable in /Users/bytedance/project/hanzo-mcp"
|
|
220
|
+
- prompts: ["Find files in /path/to/project", "Search code in /path/to/src"]"""
|
|
221
|
+
|
|
222
|
+
# Handle both string and list inputs
|
|
223
|
+
if isinstance(prompts, str):
|
|
224
|
+
prompt_list = [prompts]
|
|
225
|
+
elif isinstance(prompts, list):
|
|
226
|
+
if not prompts:
|
|
227
|
+
await tool_ctx.error("Empty prompts list provided")
|
|
228
|
+
return "Error: At least one prompt must be provided when using a list."
|
|
229
|
+
if not all(isinstance(p, str) for p in prompts):
|
|
230
|
+
await tool_ctx.error("All prompts must be strings")
|
|
231
|
+
return "Error: All prompts in the list must be strings."
|
|
232
|
+
prompt_list = prompts
|
|
233
|
+
else:
|
|
234
|
+
await tool_ctx.error("Invalid prompts parameter type")
|
|
235
|
+
return "Error: Parameter 'prompts' must be a string or an array of strings."
|
|
236
|
+
|
|
237
|
+
# Validate absolute paths in all prompts
|
|
238
|
+
absolute_path_pattern = r"/(?:[^/\s]+/)*[^/\s]+"
|
|
239
|
+
for prompt in prompt_list:
|
|
240
|
+
if not re.search(absolute_path_pattern, prompt):
|
|
241
|
+
await tool_ctx.error(f"Prompt does not contain absolute path: {prompt[:50]}...")
|
|
242
|
+
return """Error: All prompts must contain at least one absolute path.
|
|
243
|
+
|
|
244
|
+
IMPORTANT REMINDER FOR CLAUDE:
|
|
245
|
+
When using the dispatch_agent tool, always include absolute paths in your prompts.
|
|
246
|
+
Example of correct usage:
|
|
247
|
+
- "Search for all instances of the 'config' variable in /Users/bytedance/project/hanzo-mcp"
|
|
248
|
+
- "Find files that import the database module in /Users/bytedance/project/hanzo-mcp/src"
|
|
249
|
+
|
|
250
|
+
The agent cannot access files without knowing their absolute locations."""
|
|
251
|
+
|
|
252
|
+
# Execute agent(s) - always use _execute_multiple_agents for list inputs
|
|
253
|
+
if len(prompt_list) == 1:
|
|
254
|
+
await tool_ctx.info("Launching agent")
|
|
255
|
+
result = await self._execute_multiple_agents(prompt_list, tool_ctx)
|
|
256
|
+
execution_time = time.time() - start_time
|
|
257
|
+
formatted_result = f"""Agent execution completed in {execution_time:.2f} seconds.
|
|
258
|
+
|
|
259
|
+
AGENT RESPONSE:
|
|
260
|
+
{result}"""
|
|
261
|
+
await tool_ctx.info(f"Agent execution completed in {execution_time:.2f}s")
|
|
262
|
+
return formatted_result
|
|
263
|
+
else:
|
|
264
|
+
await tool_ctx.info(f"Launching {len(prompt_list)} agents in parallel")
|
|
265
|
+
result = await self._execute_multiple_agents(prompt_list, tool_ctx)
|
|
266
|
+
execution_time = time.time() - start_time
|
|
267
|
+
formatted_result = f"""Multi-agent execution completed in {execution_time:.2f} seconds ({len(prompt_list)} agents).
|
|
268
|
+
|
|
269
|
+
AGENT RESPONSES:
|
|
270
|
+
{result}"""
|
|
271
|
+
await tool_ctx.info(f"Multi-agent execution completed in {execution_time:.2f}s")
|
|
272
|
+
return formatted_result
|
|
273
|
+
|
|
274
|
+
async def _execute_agent(self, prompt: str, tool_ctx: ToolContext) -> str:
|
|
275
|
+
"""Execute a single agent with the given prompt.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
prompt: The task prompt for the agent
|
|
279
|
+
tool_ctx: Tool context for logging
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Agent execution result
|
|
283
|
+
"""
|
|
284
|
+
# Get available tools for the agent
|
|
285
|
+
agent_tools = get_allowed_agent_tools(
|
|
286
|
+
self.available_tools,
|
|
287
|
+
self.permission_manager,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Convert tools to OpenAI format
|
|
291
|
+
openai_tools = convert_tools_to_openai_functions(agent_tools)
|
|
292
|
+
|
|
293
|
+
# Log execution start
|
|
294
|
+
await tool_ctx.info("Starting agent execution")
|
|
295
|
+
|
|
296
|
+
# Create a result container
|
|
297
|
+
result = ""
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
# Create system prompt for this agent
|
|
301
|
+
system_prompt = get_system_prompt(
|
|
302
|
+
agent_tools,
|
|
303
|
+
self.permission_manager,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Execute agent
|
|
307
|
+
await tool_ctx.info(f"Executing agent task: {prompt[:50]}...")
|
|
308
|
+
result = await self._execute_agent_with_tools(system_prompt, prompt, agent_tools, openai_tools, tool_ctx)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
# Log and return error result
|
|
311
|
+
error_message = f"Error executing agent: {str(e)}"
|
|
312
|
+
await tool_ctx.error(error_message)
|
|
313
|
+
return f"Error: {error_message}"
|
|
314
|
+
|
|
315
|
+
return result if result else "No results returned from agent"
|
|
316
|
+
|
|
317
|
+
async def _execute_multiple_agents(self, prompts: list[str], tool_ctx: ToolContext) -> str:
|
|
318
|
+
"""Execute multiple agents concurrently.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
prompts: List of prompts for the agents
|
|
322
|
+
tool_ctx: Tool context for logging
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Combined results from all agents
|
|
326
|
+
"""
|
|
327
|
+
# Get available tools for the agents
|
|
328
|
+
agent_tools = get_allowed_agent_tools(
|
|
329
|
+
self.available_tools,
|
|
330
|
+
self.permission_manager,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Convert tools to OpenAI format
|
|
334
|
+
openai_tools = convert_tools_to_openai_functions(agent_tools)
|
|
335
|
+
|
|
336
|
+
# Create system prompt for the agents
|
|
337
|
+
system_prompt = get_system_prompt(
|
|
338
|
+
agent_tools,
|
|
339
|
+
self.permission_manager,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Create tasks for parallel execution
|
|
343
|
+
tasks = []
|
|
344
|
+
for i, prompt in enumerate(prompts):
|
|
345
|
+
await tool_ctx.info(f"Creating agent task {i + 1}: {prompt[:50]}...")
|
|
346
|
+
task = self._execute_agent_with_tools(system_prompt, prompt, agent_tools, openai_tools, tool_ctx)
|
|
347
|
+
tasks.append(task)
|
|
348
|
+
|
|
349
|
+
# Execute all agents concurrently
|
|
350
|
+
await tool_ctx.info(f"Executing {len(tasks)} agents in parallel")
|
|
351
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
352
|
+
|
|
353
|
+
# Handle single agent case
|
|
354
|
+
if len(results) == 1:
|
|
355
|
+
if isinstance(results[0], Exception):
|
|
356
|
+
await tool_ctx.error(f"Agent execution failed: {str(results[0])}")
|
|
357
|
+
return f"Error: {str(results[0])}"
|
|
358
|
+
return results[0]
|
|
359
|
+
|
|
360
|
+
# Format results for multiple agents
|
|
361
|
+
formatted_results = []
|
|
362
|
+
for i, result in enumerate(results):
|
|
363
|
+
if isinstance(result, Exception):
|
|
364
|
+
formatted_results.append(f"Agent {i + 1} Error:\n{str(result)}")
|
|
365
|
+
await tool_ctx.error(f"Agent {i + 1} failed: {str(result)}")
|
|
366
|
+
else:
|
|
367
|
+
formatted_results.append(f"Agent {i + 1} Result:\n{result}")
|
|
368
|
+
|
|
369
|
+
return "\n\n---\n\n".join(formatted_results)
|
|
370
|
+
|
|
371
|
+
async def _execute_agent_with_tools(
|
|
372
|
+
self,
|
|
373
|
+
system_prompt: str,
|
|
374
|
+
user_prompt: str,
|
|
375
|
+
available_tools: list[BaseTool],
|
|
376
|
+
openai_tools: list[ChatCompletionToolParam],
|
|
377
|
+
tool_ctx: ToolContext,
|
|
378
|
+
) -> str:
|
|
379
|
+
"""Execute agent with tool handling.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
system_prompt: System prompt for the agent
|
|
383
|
+
user_prompt: User prompt for the agent
|
|
384
|
+
available_tools: List of available tools
|
|
385
|
+
openai_tools: List of tools in OpenAI format
|
|
386
|
+
tool_ctx: Tool context for logging
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Agent execution result
|
|
390
|
+
"""
|
|
391
|
+
# Get model parameters and name
|
|
392
|
+
model = get_default_model(self.model_override)
|
|
393
|
+
params = get_model_parameters(max_tokens=self.max_tokens_override)
|
|
394
|
+
|
|
395
|
+
# Initialize messages
|
|
396
|
+
messages: Iterable[ChatCompletionMessageParam] = []
|
|
397
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
398
|
+
messages.append({"role": "user", "content": user_prompt})
|
|
399
|
+
|
|
400
|
+
# Track tool usage for metrics
|
|
401
|
+
tool_usage = {}
|
|
402
|
+
total_tool_use_count = 0
|
|
403
|
+
iteration_count = 0
|
|
404
|
+
max_tool_uses = self.max_tool_uses # Safety limit to prevent infinite loops
|
|
405
|
+
max_iterations = self.max_iterations # Add a maximum number of iterations for safety
|
|
406
|
+
|
|
407
|
+
# Execute until the agent completes or reaches the limit
|
|
408
|
+
while total_tool_use_count < max_tool_uses and iteration_count < max_iterations:
|
|
409
|
+
iteration_count += 1
|
|
410
|
+
await tool_ctx.info(f"Calling model (iteration {iteration_count})...")
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
# Configure model parameters based on capabilities
|
|
414
|
+
completion_params = {
|
|
415
|
+
"model": model,
|
|
416
|
+
"messages": messages,
|
|
417
|
+
"tools": openai_tools,
|
|
418
|
+
"tool_choice": "auto",
|
|
419
|
+
"temperature": params["temperature"],
|
|
420
|
+
"timeout": params["timeout"],
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if self.api_key_override:
|
|
424
|
+
completion_params["api_key"] = self.api_key_override
|
|
425
|
+
|
|
426
|
+
# Add max_tokens if provided
|
|
427
|
+
if params.get("max_tokens"):
|
|
428
|
+
completion_params["max_tokens"] = params.get("max_tokens")
|
|
429
|
+
|
|
430
|
+
# Add base_url if provided
|
|
431
|
+
if self.base_url_override:
|
|
432
|
+
completion_params["base_url"] = self.base_url_override
|
|
433
|
+
|
|
434
|
+
# Make the model call
|
|
435
|
+
response = litellm.completion(**completion_params) # pyright: ignore
|
|
436
|
+
|
|
437
|
+
if len(response.choices) == 0: # pyright: ignore
|
|
438
|
+
raise ValueError("No response choices returned")
|
|
439
|
+
|
|
440
|
+
message = response.choices[0].message # pyright: ignore
|
|
441
|
+
|
|
442
|
+
# Add message to conversation history
|
|
443
|
+
messages.append(message) # pyright: ignore
|
|
444
|
+
|
|
445
|
+
# If no tool calls, we're done
|
|
446
|
+
if not message.tool_calls:
|
|
447
|
+
return message.content or "Agent completed with no response."
|
|
448
|
+
|
|
449
|
+
# Process tool calls
|
|
450
|
+
tool_call_count = len(message.tool_calls)
|
|
451
|
+
await tool_ctx.info(f"Processing {tool_call_count} tool calls")
|
|
452
|
+
|
|
453
|
+
for tool_call in message.tool_calls:
|
|
454
|
+
total_tool_use_count += 1
|
|
455
|
+
function_name = tool_call.function.name
|
|
456
|
+
|
|
457
|
+
# Track usage
|
|
458
|
+
tool_usage[function_name] = tool_usage.get(function_name, 0) + 1
|
|
459
|
+
|
|
460
|
+
# Log tool usage
|
|
461
|
+
await tool_ctx.info(f"Agent using tool: {function_name}")
|
|
462
|
+
|
|
463
|
+
# Parse the arguments
|
|
464
|
+
try:
|
|
465
|
+
function_args = json.loads(tool_call.function.arguments)
|
|
466
|
+
except json.JSONDecodeError:
|
|
467
|
+
function_args = {}
|
|
468
|
+
|
|
469
|
+
# Find the matching tool
|
|
470
|
+
tool = next((t for t in available_tools if t.name == function_name), None)
|
|
471
|
+
if not tool:
|
|
472
|
+
tool_result = f"Error: Tool '{function_name}' not found"
|
|
473
|
+
# Special handling for clarification requests
|
|
474
|
+
elif function_name == "request_clarification":
|
|
475
|
+
try:
|
|
476
|
+
# Extract clarification parameters
|
|
477
|
+
request_type = function_args.get("type", "ADDITIONAL_INFO")
|
|
478
|
+
question = function_args.get("question", "")
|
|
479
|
+
context = function_args.get("context", {})
|
|
480
|
+
options = function_args.get("options")
|
|
481
|
+
|
|
482
|
+
# Convert string type to enum
|
|
483
|
+
clarification_type = ClarificationType[request_type]
|
|
484
|
+
|
|
485
|
+
# Request clarification
|
|
486
|
+
answer = await self.request_clarification(
|
|
487
|
+
request_type=clarification_type,
|
|
488
|
+
question=question,
|
|
489
|
+
context=context,
|
|
490
|
+
options=options,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
tool_result = self.format_clarification_in_output(question, answer)
|
|
494
|
+
except Exception as e:
|
|
495
|
+
tool_result = f"Error processing clarification: {str(e)}"
|
|
496
|
+
# Special handling for critic requests
|
|
497
|
+
elif function_name == "critic":
|
|
498
|
+
try:
|
|
499
|
+
# Extract critic parameters
|
|
500
|
+
review_type = function_args.get("review_type", "GENERAL")
|
|
501
|
+
work_description = function_args.get("work_description", "")
|
|
502
|
+
code_snippets = function_args.get("code_snippets")
|
|
503
|
+
file_paths = function_args.get("file_paths")
|
|
504
|
+
specific_concerns = function_args.get("specific_concerns")
|
|
505
|
+
|
|
506
|
+
# Request critical review
|
|
507
|
+
tool_result = self.critic_protocol.request_review(
|
|
508
|
+
review_type=review_type,
|
|
509
|
+
work_description=work_description,
|
|
510
|
+
code_snippets=code_snippets,
|
|
511
|
+
file_paths=file_paths,
|
|
512
|
+
specific_concerns=specific_concerns,
|
|
513
|
+
)
|
|
514
|
+
except Exception as e:
|
|
515
|
+
tool_result = f"Error processing critic review: {str(e)}"
|
|
516
|
+
# Special handling for review requests
|
|
517
|
+
elif function_name == "review":
|
|
518
|
+
try:
|
|
519
|
+
# Extract review parameters
|
|
520
|
+
focus = function_args.get("focus", "GENERAL")
|
|
521
|
+
work_description = function_args.get("work_description", "")
|
|
522
|
+
code_snippets = function_args.get("code_snippets")
|
|
523
|
+
file_paths = function_args.get("file_paths")
|
|
524
|
+
context = function_args.get("context")
|
|
525
|
+
|
|
526
|
+
# Request balanced review
|
|
527
|
+
tool_result = self.review_protocol.request_review(
|
|
528
|
+
focus=focus,
|
|
529
|
+
work_description=work_description,
|
|
530
|
+
code_snippets=code_snippets,
|
|
531
|
+
file_paths=file_paths,
|
|
532
|
+
context=context,
|
|
533
|
+
)
|
|
534
|
+
except Exception as e:
|
|
535
|
+
tool_result = f"Error processing review: {str(e)}"
|
|
536
|
+
else:
|
|
537
|
+
try:
|
|
538
|
+
tool_result = await tool.call(ctx=tool_ctx.mcp_context, **function_args)
|
|
539
|
+
except Exception as e:
|
|
540
|
+
tool_result = f"Error executing {function_name}: {str(e)}"
|
|
541
|
+
|
|
542
|
+
await tool_ctx.info(
|
|
543
|
+
f"tool {function_name} run with args {function_args} and return {tool_result[: min(100, len(tool_result))]}"
|
|
544
|
+
)
|
|
545
|
+
# Add the tool result to messages
|
|
546
|
+
messages.append(
|
|
547
|
+
{
|
|
548
|
+
"role": "tool",
|
|
549
|
+
"tool_call_id": tool_call.id,
|
|
550
|
+
"name": function_name,
|
|
551
|
+
"content": tool_result,
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Log progress
|
|
556
|
+
await tool_ctx.info(f"Processed {len(message.tool_calls)} tool calls. Total: {total_tool_use_count}")
|
|
557
|
+
|
|
558
|
+
except Exception as e:
|
|
559
|
+
await tool_ctx.error(f"Error in model call: {str(e)}")
|
|
560
|
+
# Avoid trying to JSON serialize message objects
|
|
561
|
+
await tool_ctx.error(f"Message count: {len(messages)}")
|
|
562
|
+
return f"Error in agent execution: {str(e)}"
|
|
563
|
+
|
|
564
|
+
# If we've reached the limit, add a warning and get final response
|
|
565
|
+
if total_tool_use_count >= max_tool_uses or iteration_count >= max_iterations:
|
|
566
|
+
messages.append(
|
|
567
|
+
{
|
|
568
|
+
"role": "system",
|
|
569
|
+
"content": "You have reached the maximum iteration. Please provide your final response.",
|
|
570
|
+
}
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
# Make a final call to get the result
|
|
575
|
+
final_response = litellm.completion(
|
|
576
|
+
model=model,
|
|
577
|
+
messages=messages,
|
|
578
|
+
temperature=params["temperature"],
|
|
579
|
+
timeout=params["timeout"],
|
|
580
|
+
max_tokens=params.get("max_tokens"),
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
return (
|
|
584
|
+
final_response.choices[0].message.content or "Agent reached max iteration limit without a response."
|
|
585
|
+
) # pyright: ignore
|
|
586
|
+
except Exception as e:
|
|
587
|
+
await tool_ctx.error(f"Error in final model call: {str(e)}")
|
|
588
|
+
return f"Error in final response: {str(e)}"
|
|
589
|
+
|
|
590
|
+
# Should not reach here but just in case
|
|
591
|
+
return "Agent execution completed after maximum iterations."
|
|
592
|
+
|
|
593
|
+
def _format_result(self, result: str, execution_time: float) -> str:
|
|
594
|
+
"""Format agent result with metrics.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
result: Raw result from agent
|
|
598
|
+
execution_time: Execution time in seconds
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
Formatted result with metrics
|
|
602
|
+
"""
|
|
603
|
+
return f"""Agent execution completed in {execution_time:.2f} seconds.
|
|
604
|
+
|
|
605
|
+
AGENT RESPONSE:
|
|
606
|
+
{result}
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
@override
|
|
610
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
611
|
+
"""Register this agent tool with the MCP server.
|
|
612
|
+
|
|
613
|
+
Creates a wrapper function with explicitly defined parameters that match
|
|
614
|
+
the tool's parameter schema and registers it with the MCP server.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
mcp_server: The FastMCP server instance
|
|
618
|
+
"""
|
|
619
|
+
tool_self = self # Create a reference to self for use in the closure
|
|
620
|
+
|
|
621
|
+
@mcp_server.tool(name=self.name, description=self.description)
|
|
622
|
+
async def dispatch_agent(prompts: str | list[str], ctx: MCPContext) -> str:
|
|
623
|
+
return await tool_self.call(ctx, prompts=prompts)
|
|
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, override
|
|
|
5
5
|
from mcp.server import FastMCP
|
|
6
6
|
from mcp.server.fastmcp import Context as MCPContext
|
|
7
7
|
|
|
8
|
+
from hanzo_mcp.tools.common.auto_timeout import auto_timeout
|
|
8
9
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
9
10
|
|
|
10
11
|
|
|
@@ -42,6 +43,7 @@ request_clarification(
|
|
|
42
43
|
options=["github.com/luxfi/node/common", "github.com/project/common"]
|
|
43
44
|
)"""
|
|
44
45
|
|
|
46
|
+
@auto_timeout("clarification")
|
|
45
47
|
async def call(
|
|
46
48
|
self,
|
|
47
49
|
ctx: MCPContext,
|
|
@@ -50,7 +52,11 @@ request_clarification(
|
|
|
50
52
|
context: Dict[str, Any],
|
|
51
53
|
options: Optional[List[str]] = None,
|
|
52
54
|
) -> str:
|
|
53
|
-
"""
|
|
55
|
+
"""Delegate to AgentTool for actual implementation.
|
|
56
|
+
|
|
57
|
+
This method provides the interface, but the actual clarification logic
|
|
58
|
+
is handled by the AgentTool's execution framework.
|
|
59
|
+
"""
|
|
54
60
|
# This tool is handled specially in the agent execution
|
|
55
61
|
return f"Clarification request: {question}"
|
|
56
62
|
|