hanzo-mcp 0.7.3__py3-none-any.whl → 0.7.7__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 (36) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +10 -0
  3. hanzo_mcp/prompts/__init__.py +43 -0
  4. hanzo_mcp/prompts/example_custom_prompt.py +40 -0
  5. hanzo_mcp/prompts/tool_explorer.py +603 -0
  6. hanzo_mcp/tools/__init__.py +52 -51
  7. hanzo_mcp/tools/agent/__init__.py +3 -16
  8. hanzo_mcp/tools/agent/agent_tool.py +365 -525
  9. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +641 -0
  10. hanzo_mcp/tools/agent/network_tool.py +3 -5
  11. hanzo_mcp/tools/agent/swarm_tool.py +447 -349
  12. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +535 -0
  13. hanzo_mcp/tools/agent/tool_adapter.py +21 -2
  14. hanzo_mcp/tools/common/forgiving_edit.py +24 -14
  15. hanzo_mcp/tools/common/permissions.py +8 -0
  16. hanzo_mcp/tools/filesystem/__init__.py +5 -5
  17. hanzo_mcp/tools/filesystem/{symbols.py → ast_tool.py} +8 -8
  18. hanzo_mcp/tools/filesystem/batch_search.py +2 -2
  19. hanzo_mcp/tools/filesystem/directory_tree.py +8 -1
  20. hanzo_mcp/tools/filesystem/find.py +1 -0
  21. hanzo_mcp/tools/filesystem/grep.py +11 -2
  22. hanzo_mcp/tools/filesystem/read.py +8 -1
  23. hanzo_mcp/tools/filesystem/search_tool.py +1 -1
  24. hanzo_mcp/tools/jupyter/__init__.py +5 -1
  25. hanzo_mcp/tools/jupyter/base.py +2 -2
  26. hanzo_mcp/tools/jupyter/jupyter.py +89 -18
  27. hanzo_mcp/tools/search/find_tool.py +49 -8
  28. hanzo_mcp/tools/shell/base_process.py +7 -1
  29. hanzo_mcp/tools/shell/streaming_command.py +34 -1
  30. {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.7.dist-info}/METADATA +7 -1
  31. {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.7.dist-info}/RECORD +34 -32
  32. hanzo_mcp/tools/agent/agent_tool_v2.py +0 -492
  33. hanzo_mcp/tools/agent/swarm_tool_v2.py +0 -654
  34. {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.7.dist-info}/WHEEL +0 -0
  35. {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.7.dist-info}/entry_points.txt +0 -0
  36. {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.7.dist-info}/top_level.txt +0 -0
@@ -1,124 +1,283 @@
1
- """Agent tool implementation for Hanzo AI.
1
+ """Agent tool implementation using hanzo-agents SDK.
2
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.
3
+ This module implements the AgentTool that leverages the hanzo-agents SDK
4
+ for sophisticated agent orchestration and execution.
5
5
  """
6
6
 
7
7
  import asyncio
8
8
  import json
9
+ import os
9
10
  import re
10
11
  import time
11
- from collections.abc import Iterable
12
- from typing import Annotated, TypedDict, Unpack, final, override
12
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional, Dict, Any, List
13
13
 
14
- # Import litellm with warnings suppressed
15
- import warnings
16
- with warnings.catch_warnings():
17
- warnings.simplefilter("ignore", DeprecationWarning)
18
- import litellm
19
14
  from mcp.server.fastmcp import Context as MCPContext
20
15
  from mcp.server import FastMCP
21
- from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolParam
22
16
  from pydantic import Field
23
17
 
24
- from hanzo_mcp.tools.agent.prompt import (
25
- get_allowed_agent_tools,
26
- get_default_model,
27
- get_model_parameters,
28
- get_system_prompt,
29
- )
30
- from hanzo_mcp.tools.agent.tool_adapter import (
31
- convert_tools_to_openai_functions,
32
- )
33
- from hanzo_mcp.tools.agent.clarification_protocol import (
34
- AgentClarificationMixin,
35
- ClarificationType,
36
- )
37
- from hanzo_mcp.tools.agent.clarification_tool import ClarificationTool
38
- from hanzo_mcp.tools.agent.critic_tool import CriticTool, CriticProtocol
39
- from hanzo_mcp.tools.agent.review_tool import ReviewTool, ReviewProtocol
40
- from hanzo_mcp.tools.agent.iching_tool import IChingTool
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
+ # Define stub classes when hanzo-agents is not available
34
+ class State:
35
+ """Stub State class when hanzo-agents is not available."""
36
+ def __init__(self):
37
+ pass
38
+ def to_dict(self):
39
+ return {}
40
+ @classmethod
41
+ def from_dict(cls, data):
42
+ return cls()
43
+
44
+ class Tool:
45
+ """Stub Tool class when hanzo-agents is not available."""
46
+ pass
47
+
48
+ class Agent:
49
+ """Stub Agent class when hanzo-agents is not available."""
50
+ pass
51
+
52
+ class Network:
53
+ """Stub Network class when hanzo-agents is not available."""
54
+ pass
55
+
56
+ class History:
57
+ """Stub History class when hanzo-agents is not available."""
58
+ pass
59
+
60
+ class InferenceResult:
61
+ """Stub InferenceResult class when hanzo-agents is not available."""
62
+ def __init__(self, agent=None, content=None, metadata=None):
63
+ self.agent = agent
64
+ self.content = content
65
+ self.metadata = metadata or {}
66
+
41
67
  from hanzo_mcp.tools.common.base import BaseTool
42
- from hanzo_mcp.tools.common.batch_tool import BatchTool
43
- from hanzo_mcp.tools.common.context import (
44
- ToolContext,
45
- create_tool_context,
46
- )
68
+ from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
47
69
  from hanzo_mcp.tools.common.permissions import PermissionManager
48
70
  from hanzo_mcp.tools.filesystem import get_read_only_filesystem_tools, Edit, MultiEdit
49
71
  from hanzo_mcp.tools.jupyter import get_read_only_jupyter_tools
50
-
51
- Prompt = Annotated[
52
- str,
53
- Field(
54
- description="Task for the agent to perform (must include absolute paths starting with /)",
55
- min_length=1,
56
- ),
57
- ]
72
+ from hanzo_mcp.tools.common.batch_tool import BatchTool
73
+ from hanzo_mcp.tools.agent.clarification_protocol import AgentClarificationMixin, ClarificationType
74
+ from hanzo_mcp.tools.agent.clarification_tool import ClarificationTool
75
+ from hanzo_mcp.tools.agent.critic_tool import CriticTool
76
+ from hanzo_mcp.tools.agent.review_tool import ReviewTool
77
+ from hanzo_mcp.tools.agent.iching_tool import IChingTool
58
78
 
59
79
 
60
80
  class AgentToolParams(TypedDict, total=False):
61
- """Parameters for the AgentTool.
81
+ """Parameters for the AgentTool."""
82
+ prompts: str | list[str]
83
+ model: Optional[str]
84
+ use_memory: Optional[bool]
85
+ memory_backend: Optional[str]
62
86
 
63
- Attributes:
64
- prompts: Task(s) for the agent to perform (must include absolute paths starting with /)
65
- """
66
87
 
67
- prompts: str | list[str]
88
+ class MCPAgentState(State):
89
+ """State for MCP agents."""
90
+
91
+ def __init__(self, prompts: List[str], context: Dict[str, Any]):
92
+ """Initialize agent state."""
93
+ super().__init__()
94
+ self.prompts = prompts
95
+ self.context = context
96
+ self.current_prompt_index = 0
97
+ self.results = []
98
+
99
+ def to_dict(self) -> Dict[str, Any]:
100
+ """Convert to dictionary."""
101
+ base_dict = super().to_dict()
102
+ base_dict.update({
103
+ "prompts": self.prompts,
104
+ "context": self.context,
105
+ "current_prompt_index": self.current_prompt_index,
106
+ "results": self.results
107
+ })
108
+ return base_dict
109
+
110
+ @classmethod
111
+ def from_dict(cls, data: Dict[str, Any]) -> "MCPAgentState":
112
+ """Create from dictionary."""
113
+ state = cls(
114
+ prompts=data.get("prompts", []),
115
+ context=data.get("context", {})
116
+ )
117
+ state.current_prompt_index = data.get("current_prompt_index", 0)
118
+ state.results = data.get("results", [])
119
+ for k, v in data.items():
120
+ if k not in ["prompts", "context", "current_prompt_index", "results"]:
121
+ state[k] = v
122
+ return state
68
123
 
69
124
 
70
- @final
71
- class AgentTool(AgentClarificationMixin, BaseTool):
72
- """Tool for delegating tasks to sub-agents.
125
+ class MCPToolAdapter(Tool):
126
+ """Adapter to wrap MCP tools for hanzo-agents."""
127
+
128
+ def __init__(self, mcp_tool: BaseTool, ctx: MCPContext):
129
+ """Initialize adapter."""
130
+ self.mcp_tool = mcp_tool
131
+ self.ctx = ctx
132
+
133
+ @property
134
+ def name(self) -> str:
135
+ """Get tool name."""
136
+ return self.mcp_tool.name
137
+
138
+ @property
139
+ def description(self) -> str:
140
+ """Get tool description."""
141
+ return self.mcp_tool.description
142
+
143
+ async def execute(self, **kwargs) -> str:
144
+ """Execute the MCP tool."""
145
+ return await self.mcp_tool.call(self.ctx, **kwargs)
146
+
73
147
 
74
- The AgentTool allows Claude to create and manage sub-agents for performing
75
- specialized tasks concurrently, such as code search, analysis, and more.
148
+ class MCPAgent(Agent):
149
+ """Agent that executes MCP tasks."""
150
+
151
+ name = "mcp_agent"
152
+ description = "Agent for executing MCP tasks"
153
+
154
+ def __init__(self,
155
+ available_tools: List[BaseTool],
156
+ permission_manager: PermissionManager,
157
+ ctx: MCPContext,
158
+ model: str = "model://anthropic/claude-3-5-sonnet-20241022",
159
+ **kwargs):
160
+ """Initialize MCP agent."""
161
+ super().__init__(model=model, **kwargs)
162
+
163
+ self.available_tools = available_tools
164
+ self.permission_manager = permission_manager
165
+ self.ctx = ctx
166
+
167
+ # Register MCP tools as agent tools
168
+ for mcp_tool in available_tools:
169
+ adapter = MCPToolAdapter(mcp_tool, ctx)
170
+ self.register_tool(adapter)
171
+
172
+ async def run(self, state: MCPAgentState, history: History, network: Network) -> InferenceResult:
173
+ """Execute the agent."""
174
+ # Get current prompt
175
+ if state.current_prompt_index >= len(state.prompts):
176
+ return InferenceResult(
177
+ agent=self.name,
178
+ content="All prompts completed",
179
+ metadata={"completed": True}
180
+ )
181
+
182
+ prompt = state.prompts[state.current_prompt_index]
183
+
184
+ # Execute with tools
185
+ messages = [
186
+ {"role": "system", "content": self._get_system_prompt()},
187
+ {"role": "user", "content": prompt}
188
+ ]
189
+
190
+ # Add history context
191
+ for entry in history[-10:]:
192
+ if entry.role == "assistant":
193
+ messages.append({
194
+ "role": "assistant",
195
+ "content": entry.content
196
+ })
197
+ elif entry.role == "user":
198
+ messages.append({
199
+ "role": "user",
200
+ "content": entry.content
201
+ })
202
+
203
+ # Call model
204
+ from hanzo_agents import ModelRegistry
205
+ adapter = ModelRegistry.get_adapter(self.model)
206
+ response = await adapter.chat(messages)
207
+
208
+ # Update state
209
+ state.current_prompt_index += 1
210
+ state.results.append(response)
211
+
212
+ # Return result
213
+ return InferenceResult(
214
+ agent=self.name,
215
+ content=response,
216
+ metadata={
217
+ "prompt_index": state.current_prompt_index - 1,
218
+ "total_prompts": len(state.prompts)
219
+ }
220
+ )
76
221
 
77
- Agents can request clarification from the main loop up to once per task.
78
- """
222
+ def _get_system_prompt(self) -> str:
223
+ """Get system prompt for the agent."""
224
+ tool_descriptions = []
225
+ for tool in self.tools.values():
226
+ tool_descriptions.append(f"- {tool.name}: {tool.description}")
227
+
228
+ return f"""You are an AI assistant with access to the following tools:
229
+
230
+ {chr(10).join(tool_descriptions)}
231
+
232
+ When you need to use a tool, respond with:
233
+ TOOL: tool_name(arg1="value1", arg2="value2")
234
+
235
+ Important guidelines:
236
+ - Always include absolute paths starting with / when working with files
237
+ - Be thorough in your searches and analysis
238
+ - Provide clear, actionable results
239
+ - Edit files when requested to make changes
240
+ """
241
+
79
242
 
243
+ @final
244
+ class AgentTool(AgentClarificationMixin, BaseTool):
245
+ """Tool for delegating tasks to sub-agents using hanzo-agents SDK."""
246
+
80
247
  @property
81
248
  @override
82
249
  def name(self) -> str:
83
- """Get the tool name.
84
-
85
- Returns:
86
- Tool name
87
- """
250
+ """Get the tool name."""
88
251
  return "agent"
89
-
252
+
90
253
  @property
91
254
  @override
92
255
  def description(self) -> str:
93
- """Get the tool description.
94
-
95
- Returns:
96
- Tool description
97
- """
98
- # TODO: Add glob when it is implemented
256
+ """Get the tool description."""
257
+ if not HANZO_AGENTS_AVAILABLE:
258
+ return "Agent tool (hanzo-agents SDK not available - using fallback)"
259
+
99
260
  at = [t.name for t in self.available_tools]
100
-
101
- 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.
261
+ return f"""Launch a new agent that has access to the following tools: {at}.
102
262
 
103
263
  When to use the Agent tool:
104
- - 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
105
- - When you need to perform edits across multiple files based on search results
264
+ - If you are searching for a keyword like "config" or "logger"
265
+ - When you need to perform edits across multiple files
106
266
  - When you need to delegate complex file modification tasks
107
267
 
108
268
  When NOT to use the Agent tool:
109
- - 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
110
- - If you are searching for a specific class definition like \"class Foo\", use the glob tool instead, to find the match more quickly
111
- - 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
112
- - Writing code and running bash commands (use other tools for that)
113
- - Other tasks that are not related to searching for a keyword or file
269
+ - If you want to read a specific file path
270
+ - If you are searching for a specific class definition
271
+ - Writing code and running bash commands
272
+ - Other tasks that are not related to searching
114
273
 
115
274
  Usage notes:
116
- 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
117
- 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.
118
- 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.
275
+ 1. Launch multiple agents concurrently whenever possible
276
+ 2. Agent results are not visible to the user - summarize them
277
+ 3. Each agent invocation is stateless
119
278
  4. The agent's outputs should generally be trusted
120
- 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"""
121
-
279
+ 5. Clearly tell the agent whether you expect it to write code or just do research"""
280
+
122
281
  def __init__(
123
282
  self,
124
283
  permission_manager: PermissionManager,
@@ -129,26 +288,16 @@ Usage notes:
129
288
  max_iterations: int = 10,
130
289
  max_tool_uses: int = 30,
131
290
  ) -> None:
132
- """Initialize the agent tool.
133
-
134
- Args:
135
-
136
- permission_manager: Permission manager for access control
137
- model: Optional model name override in LiteLLM format (e.g., "openai/gpt-4o")
138
- api_key: Optional API key for the model provider
139
- base_url: Optional base URL for the model provider API endpoint
140
- max_tokens: Optional maximum tokens for model responses
141
- max_iterations: Maximum number of iterations for agent (default: 10)
142
- max_tool_uses: Maximum number of total tool uses for agent (default: 30)
143
- """
144
-
291
+ """Initialize the agent tool."""
145
292
  self.permission_manager = permission_manager
146
293
  self.model_override = model
147
294
  self.api_key_override = api_key
148
- self.base_url_override = base_url
295
+ self.base_url_override = base_url
149
296
  self.max_tokens_override = max_tokens
150
297
  self.max_iterations = max_iterations
151
298
  self.max_tool_uses = max_tool_uses
299
+
300
+ # Set up available tools
152
301
  self.available_tools: list[BaseTool] = []
153
302
  self.available_tools.extend(
154
303
  get_read_only_filesystem_tools(self.permission_manager)
@@ -157,485 +306,176 @@ Usage notes:
157
306
  get_read_only_jupyter_tools(self.permission_manager)
158
307
  )
159
308
 
160
- # Always add edit tools - agents should have edit access
309
+ # Add edit tools
161
310
  self.available_tools.append(Edit(self.permission_manager))
162
311
  self.available_tools.append(MultiEdit(self.permission_manager))
163
312
 
164
- # Add clarification tool for agents
313
+ # Add special tools
165
314
  self.available_tools.append(ClarificationTool())
166
-
167
- # Add critic tool for agents (devil's advocate)
168
315
  self.available_tools.append(CriticTool())
169
-
170
- # Add review tool for agents (balanced review)
171
316
  self.available_tools.append(ReviewTool())
172
-
173
- # Add I Ching tool for creative guidance
174
317
  self.available_tools.append(IChingTool())
175
318
 
176
319
  self.available_tools.append(
177
320
  BatchTool({t.name: t for t in self.available_tools})
178
321
  )
179
-
180
- # Initialize protocols
181
- self.critic_protocol = CriticProtocol()
182
- self.review_protocol = ReviewProtocol()
183
-
322
+
184
323
  @override
185
324
  async def call(
186
325
  self,
187
326
  ctx: MCPContext,
188
327
  **params: Unpack[AgentToolParams],
189
328
  ) -> 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
- """
329
+ """Execute the tool with the given parameters."""
199
330
  start_time = time.time()
200
-
331
+
201
332
  # Create tool context
202
333
  tool_ctx = create_tool_context(ctx)
203
334
  await tool_ctx.set_tool_info(self.name)
204
-
205
- # Extract and validate parameters
206
- prompts = params.get("prompts")
207
335
 
336
+ # Extract parameters
337
+ prompts = params.get("prompts")
208
338
  if prompts is None:
209
339
  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
-
340
+ return "Error: At least one prompt must be provided."
341
+
222
342
  # Handle both string and list inputs
223
343
  if isinstance(prompts, str):
224
344
  prompt_list = [prompts]
225
345
  elif isinstance(prompts, list):
226
346
  if not prompts:
227
347
  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."
348
+ return "Error: At least one prompt must be provided."
232
349
  prompt_list = prompts
233
350
  else:
234
351
  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
352
+ return "Error: Parameter 'prompts' must be a string or list of strings."
353
+
354
+ # Validate absolute paths
238
355
  absolute_path_pattern = r"/(?:[^/\s]+/)*[^/\s]+"
239
356
  for prompt in prompt_list:
240
357
  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)
358
+ await tool_ctx.error(f"Prompt missing absolute path: {prompt[:50]}...")
359
+ return "Error: All prompts must contain at least one absolute path."
360
+
361
+ # Require hanzo-agents SDK
362
+ if not HANZO_AGENTS_AVAILABLE:
363
+ await tool_ctx.error("hanzo-agents SDK is required but not available")
364
+ return "Error: hanzo-agents SDK is required for agent tool functionality. Please install it with: pip install hanzo-agents"
365
+
366
+ # Use hanzo-agents SDK
367
+ await tool_ctx.info(f"Launching {len(prompt_list)} agent(s) using hanzo-agents SDK")
368
+
369
+ # Determine model and agent type
370
+ model = params.get("model", self.model_override)
371
+ use_memory = params.get("use_memory", False)
372
+ memory_backend = params.get("memory_backend", "sqlite")
373
+
374
+ # Get appropriate agent class
375
+ agent_class = self._get_agent_class(model)
376
+
377
+ # Create state
378
+ state = MCPAgentState(
379
+ prompts=prompt_list,
380
+ context={
381
+ "permission_manager": self.permission_manager,
382
+ "api_key": self.api_key_override,
383
+ "base_url": self.base_url_override,
384
+ "max_tokens": self.max_tokens_override,
385
+ }
386
+ )
387
+
388
+ # Create memory if requested
389
+ memory_kv = None
390
+ memory_vector = None
391
+ if use_memory:
392
+ memory_kv = create_memory_kv(memory_backend)
393
+ memory_vector = create_memory_vector("simple")
394
+
395
+ # Create network
396
+ network = Network(
397
+ state=state,
398
+ agents=[agent_class],
399
+ router=sequential_router([agent_class] * len(prompt_list)),
400
+ memory_kv=memory_kv,
401
+ memory_vector=memory_vector,
402
+ max_steps=self.max_iterations * len(prompt_list),
403
+ )
404
+
405
+ # Execute
406
+ try:
407
+ final_state = await network.run()
256
408
  execution_time = time.time() - start_time
257
- formatted_result = f"""Agent execution completed in {execution_time:.2f} seconds.
409
+
410
+ # Format results
411
+ results = final_state.results
412
+ if len(results) == 1:
413
+ formatted_result = f"""Agent execution completed in {execution_time:.2f} seconds.
258
414
 
259
415
  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).
416
+ {results[0]}"""
417
+ else:
418
+ formatted_results = []
419
+ for i, result in enumerate(results):
420
+ formatted_results.append(f"Agent {i+1} Result:\n{result}")
421
+
422
+ formatted_result = f"""Multi-agent execution completed in {execution_time:.2f} seconds ({len(results)} agents).
268
423
 
269
424
  AGENT RESPONSES:
270
- {result}"""
271
- await tool_ctx.info(f"Multi-agent execution completed in {execution_time:.2f}s")
425
+ {chr(10).join(formatted_results)}"""
426
+
427
+ await tool_ctx.info(f"Execution completed in {execution_time:.2f}s")
272
428
  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(
309
- system_prompt, prompt, agent_tools, openai_tools, tool_ctx
310
- )
429
+
311
430
  except Exception as e:
312
- # Log and return error result
313
- error_message = f"Error executing agent: {str(e)}"
314
- await tool_ctx.error(error_message)
315
- return f"Error: {error_message}"
316
-
317
- return result if result else "No results returned from agent"
318
-
319
- async def _execute_multiple_agents(self, prompts: list[str], tool_ctx: ToolContext) -> str:
320
- """Execute multiple agents concurrently.
321
-
322
- Args:
323
- prompts: List of prompts for the agents
324
- tool_ctx: Tool context for logging
325
-
326
- Returns:
327
- Combined results from all agents
328
- """
329
- # Get available tools for the agents
330
- agent_tools = get_allowed_agent_tools(
331
- self.available_tools,
332
- self.permission_manager,
333
- )
334
-
335
- # Convert tools to OpenAI format
336
- openai_tools = convert_tools_to_openai_functions(agent_tools)
337
-
338
- # Create system prompt for the agents
339
- system_prompt = get_system_prompt(
340
- agent_tools,
341
- self.permission_manager,
342
- )
343
-
344
- # Create tasks for parallel execution
345
- tasks = []
346
- for i, prompt in enumerate(prompts):
347
- await tool_ctx.info(f"Creating agent task {i+1}: {prompt[:50]}...")
348
- task = self._execute_agent_with_tools(
349
- system_prompt, prompt, agent_tools, openai_tools, tool_ctx
350
- )
351
- tasks.append(task)
352
-
353
- # Execute all agents concurrently
354
- await tool_ctx.info(f"Executing {len(tasks)} agents in parallel")
355
- results = await asyncio.gather(*tasks, return_exceptions=True)
356
-
357
- # Handle single agent case
358
- if len(results) == 1:
359
- if isinstance(results[0], Exception):
360
- await tool_ctx.error(f"Agent execution failed: {str(results[0])}")
361
- return f"Error: {str(results[0])}"
362
- return results[0]
363
-
364
- # Format results for multiple agents
365
- formatted_results = []
366
- for i, result in enumerate(results):
367
- if isinstance(result, Exception):
368
- formatted_results.append(f"Agent {i+1} Error:\n{str(result)}")
369
- await tool_ctx.error(f"Agent {i+1} failed: {str(result)}")
370
- else:
371
- formatted_results.append(f"Agent {i+1} Result:\n{result}")
372
-
373
- return "\n\n---\n\n".join(formatted_results)
374
-
375
- async def _execute_agent_with_tools(
376
- self,
377
- system_prompt: str,
378
- user_prompt: str,
379
- available_tools: list[BaseTool],
380
- openai_tools: list[ChatCompletionToolParam],
381
- tool_ctx: ToolContext,
382
- ) -> str:
383
- """Execute agent with tool handling.
384
-
385
- Args:
386
- system_prompt: System prompt for the agent
387
- user_prompt: User prompt for the agent
388
- available_tools: List of available tools
389
- openai_tools: List of tools in OpenAI format
390
- tool_ctx: Tool context for logging
391
-
392
- Returns:
393
- Agent execution result
394
- """
395
- # Get model parameters and name
396
- model = get_default_model(self.model_override)
397
- params = get_model_parameters(max_tokens=self.max_tokens_override)
398
-
399
- # Initialize messages
400
- messages: Iterable[ChatCompletionMessageParam] = []
401
- messages.append({"role": "system", "content": system_prompt})
402
- messages.append({"role": "user", "content": user_prompt})
403
-
404
- # Track tool usage for metrics
405
- tool_usage = {}
406
- total_tool_use_count = 0
407
- iteration_count = 0
408
- max_tool_uses = self.max_tool_uses # Safety limit to prevent infinite loops
409
- max_iterations = (
410
- self.max_iterations
411
- ) # Add a maximum number of iterations for safety
412
-
413
- # Execute until the agent completes or reaches the limit
414
- while total_tool_use_count < max_tool_uses and iteration_count < max_iterations:
415
- iteration_count += 1
416
- await tool_ctx.info(f"Calling model (iteration {iteration_count})...")
417
-
418
- try:
419
- # Configure model parameters based on capabilities
420
- completion_params = {
421
- "model": model,
422
- "messages": messages,
423
- "tools": openai_tools,
424
- "tool_choice": "auto",
425
- "temperature": params["temperature"],
426
- "timeout": params["timeout"],
427
- }
428
-
429
- if self.api_key_override:
430
- completion_params["api_key"] = self.api_key_override
431
-
432
- # Add max_tokens if provided
433
- if params.get("max_tokens"):
434
- completion_params["max_tokens"] = params.get("max_tokens")
435
-
436
- # Add base_url if provided
437
- if self.base_url_override:
438
- completion_params["base_url"] = self.base_url_override
439
-
440
- # Make the model call
441
- response = litellm.completion(
442
- **completion_params # pyright: ignore
443
- )
444
-
445
- if len(response.choices) == 0: # pyright: ignore
446
- raise ValueError("No response choices returned")
447
-
448
- message = response.choices[0].message # pyright: ignore
449
-
450
- # Add message to conversation history
451
- messages.append(message) # pyright: ignore
452
-
453
- # If no tool calls, we're done
454
- if not message.tool_calls:
455
- return message.content or "Agent completed with no response."
456
-
457
- # Process tool calls
458
- tool_call_count = len(message.tool_calls)
459
- await tool_ctx.info(f"Processing {tool_call_count} tool calls")
460
-
461
- for tool_call in message.tool_calls:
462
- total_tool_use_count += 1
463
- function_name = tool_call.function.name
464
-
465
- # Track usage
466
- tool_usage[function_name] = tool_usage.get(function_name, 0) + 1
467
-
468
- # Log tool usage
469
- await tool_ctx.info(f"Agent using tool: {function_name}")
470
-
471
- # Parse the arguments
472
- try:
473
- function_args = json.loads(tool_call.function.arguments)
474
- except json.JSONDecodeError:
475
- function_args = {}
476
-
477
- # Find the matching tool
478
- tool = next(
479
- (t for t in available_tools if t.name == function_name), None
480
- )
481
- if not tool:
482
- tool_result = f"Error: Tool '{function_name}' not found"
483
- # Special handling for clarification requests
484
- elif function_name == "request_clarification":
485
- try:
486
- # Extract clarification parameters
487
- request_type = function_args.get("type", "ADDITIONAL_INFO")
488
- question = function_args.get("question", "")
489
- context = function_args.get("context", {})
490
- options = function_args.get("options", None)
491
-
492
- # Convert string type to enum
493
- clarification_type = ClarificationType[request_type]
494
-
495
- # Request clarification
496
- answer = await self.request_clarification(
497
- request_type=clarification_type,
498
- question=question,
499
- context=context,
500
- options=options
501
- )
502
-
503
- tool_result = self.format_clarification_in_output(question, answer)
504
- except Exception as e:
505
- tool_result = f"Error processing clarification: {str(e)}"
506
- # Special handling for critic requests
507
- elif function_name == "critic":
508
- try:
509
- # Extract critic parameters
510
- review_type = function_args.get("review_type", "GENERAL")
511
- work_description = function_args.get("work_description", "")
512
- code_snippets = function_args.get("code_snippets", None)
513
- file_paths = function_args.get("file_paths", None)
514
- specific_concerns = function_args.get("specific_concerns", None)
515
-
516
- # Request critical review
517
- tool_result = self.critic_protocol.request_review(
518
- review_type=review_type,
519
- work_description=work_description,
520
- code_snippets=code_snippets,
521
- file_paths=file_paths,
522
- specific_concerns=specific_concerns
523
- )
524
- except Exception as e:
525
- tool_result = f"Error processing critic review: {str(e)}"
526
- # Special handling for review requests
527
- elif function_name == "review":
528
- try:
529
- # Extract review parameters
530
- focus = function_args.get("focus", "GENERAL")
531
- work_description = function_args.get("work_description", "")
532
- code_snippets = function_args.get("code_snippets", None)
533
- file_paths = function_args.get("file_paths", None)
534
- context = function_args.get("context", None)
535
-
536
- # Request balanced review
537
- tool_result = self.review_protocol.request_review(
538
- focus=focus,
539
- work_description=work_description,
540
- code_snippets=code_snippets,
541
- file_paths=file_paths,
542
- context=context
543
- )
544
- except Exception as e:
545
- tool_result = f"Error processing review: {str(e)}"
546
- else:
547
- try:
548
- tool_result = await tool.call(
549
- ctx=tool_ctx.mcp_context, **function_args
550
- )
551
- except Exception as e:
552
- tool_result = f"Error executing {function_name}: {str(e)}"
553
-
554
- await tool_ctx.info(
555
- f"tool {function_name} run with args {function_args} and return {tool_result[: min(100, len(tool_result))]}"
556
- )
557
- # Add the tool result to messages
558
- messages.append(
559
- {
560
- "role": "tool",
561
- "tool_call_id": tool_call.id,
562
- "name": function_name,
563
- "content": tool_result,
564
- }
565
- )
566
-
567
- # Log progress
568
- await tool_ctx.info(
569
- f"Processed {len(message.tool_calls)} tool calls. Total: {total_tool_use_count}"
570
- )
571
-
572
- except Exception as e:
573
- await tool_ctx.error(f"Error in model call: {str(e)}")
574
- # Avoid trying to JSON serialize message objects
575
- await tool_ctx.error(f"Message count: {len(messages)}")
576
- return f"Error in agent execution: {str(e)}"
577
-
578
- # If we've reached the limit, add a warning and get final response
579
- if total_tool_use_count >= max_tool_uses or iteration_count >= max_iterations:
580
- messages.append(
581
- {
582
- "role": "system",
583
- "content": "You have reached the maximum iteration. Please provide your final response.",
584
- }
431
+ await tool_ctx.error(f"Agent execution failed: {str(e)}")
432
+ return f"Error: {str(e)}"
433
+
434
+ def _get_agent_class(self, model: Optional[str]) -> type[Agent]:
435
+ """Get appropriate agent class based on model."""
436
+ if not model:
437
+ model = "model://anthropic/claude-3-5-sonnet-20241022"
438
+
439
+ # Check for CLI agents
440
+ cli_agents = {
441
+ "claude_cli": ClaudeCodeAgent,
442
+ "codex_cli": OpenAICodexAgent,
443
+ "gemini_cli": GeminiAgent,
444
+ "grok_cli": GrokAgent,
445
+ }
446
+
447
+ if model in cli_agents:
448
+ return cli_agents[model]
449
+
450
+ # Return generic MCP agent
451
+ return type("DynamicMCPAgent", (MCPAgent,), {
452
+ "model": model,
453
+ "__init__": lambda self: MCPAgent.__init__(
454
+ self,
455
+ available_tools=self.available_tools,
456
+ permission_manager=self.permission_manager,
457
+ ctx=self.ctx,
458
+ model=model
585
459
  )
586
-
587
- try:
588
- # Make a final call to get the result
589
- final_response = litellm.completion(
590
- model=model,
591
- messages=messages,
592
- temperature=params["temperature"],
593
- timeout=params["timeout"],
594
- max_tokens=params.get("max_tokens"),
595
- )
596
-
597
- return (
598
- final_response.choices[0].message.content
599
- or "Agent reached max iteration limit without a response."
600
- ) # pyright: ignore
601
- except Exception as e:
602
- await tool_ctx.error(f"Error in final model call: {str(e)}")
603
- return f"Error in final response: {str(e)}"
604
-
605
- # Should not reach here but just in case
606
- return "Agent execution completed after maximum iterations."
607
-
608
- def _format_result(self, result: str, execution_time: float) -> str:
609
- """Format agent result with metrics.
610
-
611
- Args:
612
- result: Raw result from agent
613
- execution_time: Execution time in seconds
614
-
615
- Returns:
616
- Formatted result with metrics
617
- """
618
- return f"""Agent execution completed in {execution_time:.2f} seconds.
619
-
620
- AGENT RESPONSE:
621
- {result}
622
- """
623
-
460
+ })
461
+
624
462
  @override
625
463
  def register(self, mcp_server: FastMCP) -> None:
626
- """Register this agent tool with the MCP server.
627
-
628
- Creates a wrapper function with explicitly defined parameters that match
629
- the tool's parameter schema and registers it with the MCP server.
630
-
631
- Args:
632
- mcp_server: The FastMCP server instance
633
- """
634
- tool_self = self # Create a reference to self for use in the closure
635
-
464
+ """Register this agent tool with the MCP server."""
465
+ tool_self = self
466
+
636
467
  @mcp_server.tool(name=self.name, description=self.description)
637
468
  async def dispatch_agent(
638
469
  prompts: str | list[str],
639
- ctx: MCPContext
470
+ ctx: MCPContext,
471
+ model: Optional[str] = None,
472
+ use_memory: bool = False,
473
+ memory_backend: str = "sqlite"
640
474
  ) -> str:
641
- return await tool_self.call(ctx, prompts=prompts)
475
+ return await tool_self.call(
476
+ ctx,
477
+ prompts=prompts,
478
+ model=model,
479
+ use_memory=use_memory,
480
+ memory_backend=memory_backend
481
+ )