hanzo-mcp 0.7.2__py3-none-any.whl → 0.7.6__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/cli.py +10 -0
- hanzo_mcp/prompts/__init__.py +43 -0
- hanzo_mcp/prompts/example_custom_prompt.py +40 -0
- hanzo_mcp/prompts/tool_explorer.py +603 -0
- hanzo_mcp/tools/__init__.py +52 -51
- hanzo_mcp/tools/agent/__init__.py +3 -16
- hanzo_mcp/tools/agent/agent_tool.py +365 -525
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +641 -0
- hanzo_mcp/tools/agent/network_tool.py +3 -5
- hanzo_mcp/tools/agent/swarm_tool.py +447 -349
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +535 -0
- hanzo_mcp/tools/agent/tool_adapter.py +21 -2
- hanzo_mcp/tools/common/forgiving_edit.py +24 -14
- hanzo_mcp/tools/common/permissions.py +8 -0
- hanzo_mcp/tools/filesystem/__init__.py +5 -5
- hanzo_mcp/tools/filesystem/{symbols.py → ast_tool.py} +8 -8
- hanzo_mcp/tools/filesystem/batch_search.py +2 -2
- hanzo_mcp/tools/filesystem/directory_tree.py +8 -1
- hanzo_mcp/tools/filesystem/find.py +1 -0
- hanzo_mcp/tools/filesystem/grep.py +11 -2
- hanzo_mcp/tools/filesystem/read.py +8 -1
- hanzo_mcp/tools/filesystem/search_tool.py +1 -1
- hanzo_mcp/tools/jupyter/__init__.py +5 -1
- hanzo_mcp/tools/jupyter/base.py +2 -2
- hanzo_mcp/tools/jupyter/jupyter.py +89 -18
- hanzo_mcp/tools/search/find_tool.py +49 -8
- hanzo_mcp/tools/shell/base_process.py +7 -1
- hanzo_mcp/tools/shell/streaming_command.py +34 -1
- {hanzo_mcp-0.7.2.dist-info → hanzo_mcp-0.7.6.dist-info}/METADATA +8 -1
- {hanzo_mcp-0.7.2.dist-info → hanzo_mcp-0.7.6.dist-info}/RECORD +33 -31
- hanzo_mcp/tools/agent/agent_tool_v2.py +0 -492
- hanzo_mcp/tools/agent/swarm_tool_v2.py +0 -654
- {hanzo_mcp-0.7.2.dist-info → hanzo_mcp-0.7.6.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.2.dist-info → hanzo_mcp-0.7.6.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.2.dist-info → hanzo_mcp-0.7.6.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""Swarm tool implementation
|
|
1
|
+
"""Swarm tool implementation using hanzo-agents SDK.
|
|
2
2
|
|
|
3
|
-
This module implements the SwarmTool that
|
|
4
|
-
|
|
3
|
+
This module implements the SwarmTool that leverages the hanzo-agents SDK
|
|
4
|
+
for sophisticated multi-agent orchestration with flexible network topologies.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
@@ -14,24 +14,90 @@ from mcp.server import FastMCP
|
|
|
14
14
|
from mcp.server.fastmcp import Context as MCPContext
|
|
15
15
|
from pydantic import Field
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# Import hanzo-agents SDK with fallback
|
|
18
|
+
try:
|
|
19
|
+
from hanzo_agents import (
|
|
20
|
+
Agent, State, Network, Tool, History,
|
|
21
|
+
ModelRegistry, InferenceResult, ToolCall,
|
|
22
|
+
Router,
|
|
23
|
+
)
|
|
24
|
+
HANZO_AGENTS_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
# Define minimal stubs if hanzo-agents is not available
|
|
27
|
+
HANZO_AGENTS_AVAILABLE = False
|
|
28
|
+
class Agent: pass
|
|
29
|
+
class State: pass
|
|
30
|
+
class Network: pass
|
|
31
|
+
class Tool: pass
|
|
32
|
+
class History: pass
|
|
33
|
+
class ModelRegistry: pass
|
|
34
|
+
class InferenceResult: pass
|
|
35
|
+
class ToolCall: pass
|
|
36
|
+
class Router: pass
|
|
37
|
+
|
|
38
|
+
# Import optional components with fallbacks
|
|
39
|
+
try:
|
|
40
|
+
from hanzo_agents import DeterministicRouter, LLMRouter, HybridRouter
|
|
41
|
+
except ImportError:
|
|
42
|
+
try:
|
|
43
|
+
# Try core module import
|
|
44
|
+
from hanzo_agents.core.router import DeterministicRouter, LLMRouter, HybridRouter
|
|
45
|
+
except ImportError:
|
|
46
|
+
# Define stubs if not available
|
|
47
|
+
class DeterministicRouter: pass
|
|
48
|
+
class LLMRouter: pass
|
|
49
|
+
class HybridRouter: pass
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
from hanzo_agents import create_memory_kv, create_memory_vector
|
|
53
|
+
except ImportError:
|
|
54
|
+
try:
|
|
55
|
+
# Try core module import
|
|
56
|
+
from hanzo_agents.core.memory import create_memory_kv, create_memory_vector
|
|
57
|
+
except ImportError:
|
|
58
|
+
# Define stubs if not available
|
|
59
|
+
def create_memory_kv(*args, **kwargs): pass
|
|
60
|
+
def create_memory_vector(*args, **kwargs): pass
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
from hanzo_agents import sequential_router, conditional_router, state_based_router
|
|
64
|
+
except ImportError:
|
|
65
|
+
try:
|
|
66
|
+
# Try core module import
|
|
67
|
+
from hanzo_agents.core.router import sequential_router, conditional_router, state_based_router
|
|
68
|
+
except ImportError:
|
|
69
|
+
# Define stubs if not available
|
|
70
|
+
def sequential_router(*args, **kwargs): pass
|
|
71
|
+
def conditional_router(*args, **kwargs): pass
|
|
72
|
+
def state_based_router(*args, **kwargs): pass
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
from hanzo_agents.core.cli_agent import (
|
|
76
|
+
ClaudeCodeAgent, OpenAICodexAgent,
|
|
77
|
+
GeminiAgent, GrokAgent
|
|
78
|
+
)
|
|
79
|
+
except ImportError:
|
|
80
|
+
# Define stub classes if not available
|
|
81
|
+
class ClaudeCodeAgent(Agent):
|
|
82
|
+
pass
|
|
83
|
+
class OpenAICodexAgent(Agent):
|
|
84
|
+
pass
|
|
85
|
+
class GeminiAgent(Agent):
|
|
86
|
+
pass
|
|
87
|
+
class GrokAgent(Agent):
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
from hanzo_mcp.tools.agent.agent_tool import MCPAgent, MCPToolAdapter
|
|
18
91
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
19
92
|
from hanzo_mcp.tools.common.context import create_tool_context
|
|
20
93
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
94
|
+
from hanzo_mcp.tools.filesystem import get_read_only_filesystem_tools, Edit, MultiEdit
|
|
95
|
+
from hanzo_mcp.tools.jupyter import get_read_only_jupyter_tools
|
|
96
|
+
from hanzo_mcp.tools.common.batch_tool import BatchTool
|
|
21
97
|
|
|
22
98
|
|
|
23
99
|
class AgentNode(TypedDict):
|
|
24
|
-
"""Node in the agent network.
|
|
25
|
-
|
|
26
|
-
Attributes:
|
|
27
|
-
id: Unique identifier for this agent
|
|
28
|
-
query: The specific query/task for this agent
|
|
29
|
-
model: Optional model override (e.g., 'claude-3-5-sonnet', 'gpt-4o')
|
|
30
|
-
role: Optional role description (e.g., 'architect', 'frontend', 'reviewer')
|
|
31
|
-
connections: List of agent IDs this agent connects to (sends results to)
|
|
32
|
-
receives_from: Optional list of agent IDs this agent receives input from
|
|
33
|
-
file_path: Optional specific file for the agent to work on
|
|
34
|
-
"""
|
|
100
|
+
"""Node in the agent network."""
|
|
35
101
|
id: str
|
|
36
102
|
query: str
|
|
37
103
|
model: Optional[str]
|
|
@@ -42,40 +108,260 @@ class AgentNode(TypedDict):
|
|
|
42
108
|
|
|
43
109
|
|
|
44
110
|
class SwarmConfig(TypedDict):
|
|
45
|
-
"""Configuration for an agent network.
|
|
46
|
-
|
|
47
|
-
Attributes:
|
|
48
|
-
agents: Dictionary of agent configurations keyed by ID
|
|
49
|
-
entry_point: ID of the first agent to execute (optional, defaults to finding roots)
|
|
50
|
-
topology: Optional topology type (tree, dag, pipeline, star, mesh)
|
|
51
|
-
"""
|
|
111
|
+
"""Configuration for an agent network."""
|
|
52
112
|
agents: Dict[str, AgentNode]
|
|
53
113
|
entry_point: Optional[str]
|
|
54
114
|
topology: Optional[str]
|
|
55
115
|
|
|
56
116
|
|
|
57
117
|
class SwarmToolParams(TypedDict):
|
|
58
|
-
"""Parameters for the SwarmTool.
|
|
59
|
-
|
|
60
|
-
Attributes:
|
|
61
|
-
config: Agent network configuration
|
|
62
|
-
query: Initial query to send to entry point agent(s)
|
|
63
|
-
context: Optional context shared by all agents
|
|
64
|
-
max_concurrent: Maximum number of concurrent agents (default: 10)
|
|
65
|
-
"""
|
|
118
|
+
"""Parameters for the SwarmTool."""
|
|
66
119
|
config: SwarmConfig
|
|
67
120
|
query: str
|
|
68
121
|
context: Optional[str]
|
|
69
122
|
max_concurrent: Optional[int]
|
|
123
|
+
use_memory: Optional[bool]
|
|
124
|
+
memory_backend: Optional[str]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class SwarmState(State):
|
|
128
|
+
"""State for swarm execution."""
|
|
129
|
+
|
|
130
|
+
def __init__(self,
|
|
131
|
+
config: SwarmConfig,
|
|
132
|
+
initial_query: str,
|
|
133
|
+
context: Optional[str] = None):
|
|
134
|
+
"""Initialize swarm state."""
|
|
135
|
+
super().__init__()
|
|
136
|
+
self.config = config
|
|
137
|
+
self.initial_query = initial_query
|
|
138
|
+
self.context = context
|
|
139
|
+
self.agent_results = {}
|
|
140
|
+
self.completed_agents = set()
|
|
141
|
+
self.current_agent = None
|
|
142
|
+
self.execution_order = []
|
|
143
|
+
|
|
144
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
145
|
+
"""Convert to dictionary."""
|
|
146
|
+
base_dict = super().to_dict()
|
|
147
|
+
base_dict.update({
|
|
148
|
+
"config": self.config,
|
|
149
|
+
"initial_query": self.initial_query,
|
|
150
|
+
"context": self.context,
|
|
151
|
+
"agent_results": self.agent_results,
|
|
152
|
+
"completed_agents": list(self.completed_agents),
|
|
153
|
+
"current_agent": self.current_agent,
|
|
154
|
+
"execution_order": self.execution_order
|
|
155
|
+
})
|
|
156
|
+
return base_dict
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SwarmState":
|
|
160
|
+
"""Create from dictionary."""
|
|
161
|
+
state = cls(
|
|
162
|
+
config=data.get("config", {}),
|
|
163
|
+
initial_query=data.get("initial_query", ""),
|
|
164
|
+
context=data.get("context")
|
|
165
|
+
)
|
|
166
|
+
state.agent_results = data.get("agent_results", {})
|
|
167
|
+
state.completed_agents = set(data.get("completed_agents", []))
|
|
168
|
+
state.current_agent = data.get("current_agent")
|
|
169
|
+
state.execution_order = data.get("execution_order", [])
|
|
170
|
+
return state
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class SwarmAgent(MCPAgent):
|
|
174
|
+
"""Agent that executes within a swarm network."""
|
|
175
|
+
|
|
176
|
+
def __init__(self,
|
|
177
|
+
agent_id: str,
|
|
178
|
+
agent_config: AgentNode,
|
|
179
|
+
available_tools: List[BaseTool],
|
|
180
|
+
permission_manager: PermissionManager,
|
|
181
|
+
ctx: MCPContext,
|
|
182
|
+
**kwargs):
|
|
183
|
+
"""Initialize swarm agent."""
|
|
184
|
+
# Set name and description from config
|
|
185
|
+
self.name = agent_id
|
|
186
|
+
self.description = agent_config.get("role", f"Agent {agent_id}")
|
|
187
|
+
self.agent_config = agent_config
|
|
188
|
+
|
|
189
|
+
# Initialize with specified model
|
|
190
|
+
model = agent_config.get("model")
|
|
191
|
+
if model:
|
|
192
|
+
model = self._normalize_model(model)
|
|
193
|
+
else:
|
|
194
|
+
model = "model://anthropic/claude-3-5-sonnet-20241022"
|
|
195
|
+
|
|
196
|
+
super().__init__(
|
|
197
|
+
available_tools=available_tools,
|
|
198
|
+
permission_manager=permission_manager,
|
|
199
|
+
ctx=ctx,
|
|
200
|
+
model=model,
|
|
201
|
+
**kwargs
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def _normalize_model(self, model: str) -> str:
|
|
205
|
+
"""Normalize model names to full format."""
|
|
206
|
+
model_map = {
|
|
207
|
+
"claude-3-5-sonnet": "model://anthropic/claude-3-5-sonnet-20241022",
|
|
208
|
+
"claude-3-opus": "model://anthropic/claude-3-opus-20240229",
|
|
209
|
+
"gpt-4o": "model://openai/gpt-4o",
|
|
210
|
+
"gpt-4": "model://openai/gpt-4",
|
|
211
|
+
"gemini-1.5-pro": "model://google/gemini-1.5-pro",
|
|
212
|
+
"gemini-1.5-flash": "model://google/gemini-1.5-flash",
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Check if it's already a model:// URI
|
|
216
|
+
if model.startswith("model://"):
|
|
217
|
+
return model
|
|
218
|
+
|
|
219
|
+
# Check mapping
|
|
220
|
+
if model in model_map:
|
|
221
|
+
return model_map[model]
|
|
222
|
+
|
|
223
|
+
# Assume it's a provider/model format
|
|
224
|
+
if "/" in model:
|
|
225
|
+
return f"model://{model}"
|
|
226
|
+
|
|
227
|
+
# Default to anthropic
|
|
228
|
+
return f"model://anthropic/{model}"
|
|
229
|
+
|
|
230
|
+
async def run(self, state: SwarmState, history: History, network: Network) -> InferenceResult:
|
|
231
|
+
"""Execute the swarm agent."""
|
|
232
|
+
# Build prompt with context
|
|
233
|
+
prompt_parts = []
|
|
234
|
+
|
|
235
|
+
# Add role context
|
|
236
|
+
if self.agent_config.get("role"):
|
|
237
|
+
prompt_parts.append(f"Your role: {self.agent_config['role']}")
|
|
238
|
+
|
|
239
|
+
# Add shared context
|
|
240
|
+
if state.context:
|
|
241
|
+
prompt_parts.append(f"Context:\n{state.context}")
|
|
242
|
+
|
|
243
|
+
# Add inputs from connected agents
|
|
244
|
+
receives_from = self.agent_config.get("receives_from", [])
|
|
245
|
+
if receives_from:
|
|
246
|
+
inputs = {}
|
|
247
|
+
for agent_id in receives_from:
|
|
248
|
+
if agent_id in state.agent_results:
|
|
249
|
+
inputs[agent_id] = state.agent_results[agent_id]
|
|
250
|
+
|
|
251
|
+
if inputs:
|
|
252
|
+
prompt_parts.append("Input from previous agents:")
|
|
253
|
+
for input_agent, input_result in inputs.items():
|
|
254
|
+
prompt_parts.append(f"\n--- From {input_agent} ---\n{input_result}")
|
|
255
|
+
|
|
256
|
+
# Add file context if specified
|
|
257
|
+
if self.agent_config.get("file_path"):
|
|
258
|
+
prompt_parts.append(f"\nFile to work on: {self.agent_config['file_path']}")
|
|
259
|
+
|
|
260
|
+
# Add the main query
|
|
261
|
+
prompt_parts.append(f"\nTask: {self.agent_config['query']}")
|
|
262
|
+
|
|
263
|
+
# Add initial query if this is entry point
|
|
264
|
+
if state.current_agent == state.config.get("entry_point"):
|
|
265
|
+
prompt_parts.append(f"\nMain objective: {state.initial_query}")
|
|
266
|
+
|
|
267
|
+
full_prompt = "\n\n".join(prompt_parts)
|
|
268
|
+
|
|
269
|
+
# Execute using base class
|
|
270
|
+
messages = [
|
|
271
|
+
{"role": "system", "content": self._get_system_prompt()},
|
|
272
|
+
{"role": "user", "content": full_prompt}
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
# Call model
|
|
276
|
+
from hanzo_agents import ModelRegistry
|
|
277
|
+
adapter = ModelRegistry.get_adapter(self.model)
|
|
278
|
+
response = await adapter.chat(messages)
|
|
279
|
+
|
|
280
|
+
# Store result in state
|
|
281
|
+
state.agent_results[self.name] = response
|
|
282
|
+
state.completed_agents.add(self.name)
|
|
283
|
+
state.execution_order.append(self.name)
|
|
284
|
+
|
|
285
|
+
# Return result
|
|
286
|
+
return InferenceResult(
|
|
287
|
+
agent=self.name,
|
|
288
|
+
content=response,
|
|
289
|
+
metadata={
|
|
290
|
+
"agent_id": self.name,
|
|
291
|
+
"role": self.agent_config.get("role"),
|
|
292
|
+
"connections": self.agent_config.get("connections", [])
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class SwarmRouter(DeterministicRouter):
|
|
298
|
+
"""Router for swarm agent orchestration."""
|
|
299
|
+
|
|
300
|
+
def __init__(self, swarm_config: SwarmConfig):
|
|
301
|
+
"""Initialize swarm router."""
|
|
302
|
+
self.swarm_config = swarm_config
|
|
303
|
+
self.agents_config = swarm_config["agents"]
|
|
304
|
+
self.entry_point = swarm_config.get("entry_point")
|
|
305
|
+
|
|
306
|
+
# Build dependency graph
|
|
307
|
+
self.dependencies = {}
|
|
308
|
+
self.dependents = {}
|
|
309
|
+
|
|
310
|
+
for agent_id, config in self.agents_config.items():
|
|
311
|
+
# Dependencies (agents this one waits for)
|
|
312
|
+
self.dependencies[agent_id] = set(config.get("receives_from", []))
|
|
313
|
+
|
|
314
|
+
# Dependents (agents that wait for this one)
|
|
315
|
+
connections = config.get("connections", [])
|
|
316
|
+
for conn in connections:
|
|
317
|
+
if conn not in self.dependents:
|
|
318
|
+
self.dependents[conn] = set()
|
|
319
|
+
self.dependents[conn].add(agent_id)
|
|
320
|
+
|
|
321
|
+
def route(self, network, call_count, last_result, agent_stack):
|
|
322
|
+
"""Determine next agent to execute."""
|
|
323
|
+
state = network.state
|
|
324
|
+
|
|
325
|
+
# First call - start with entry point or roots
|
|
326
|
+
if call_count == 0:
|
|
327
|
+
if self.entry_point:
|
|
328
|
+
state.current_agent = self.entry_point
|
|
329
|
+
return self._get_agent_class(self.entry_point, agent_stack)
|
|
330
|
+
else:
|
|
331
|
+
# Find roots (no dependencies)
|
|
332
|
+
roots = [aid for aid, deps in self.dependencies.items() if not deps]
|
|
333
|
+
if roots:
|
|
334
|
+
state.current_agent = roots[0]
|
|
335
|
+
return self._get_agent_class(roots[0], agent_stack)
|
|
336
|
+
|
|
337
|
+
# Find next agent to execute
|
|
338
|
+
for agent_id in self.agents_config:
|
|
339
|
+
if agent_id in state.completed_agents:
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
# Check if all dependencies are met
|
|
343
|
+
deps = self.dependencies.get(agent_id, set())
|
|
344
|
+
if deps.issubset(state.completed_agents):
|
|
345
|
+
state.current_agent = agent_id
|
|
346
|
+
return self._get_agent_class(agent_id, agent_stack)
|
|
347
|
+
|
|
348
|
+
# No more agents to execute
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
def _get_agent_class(self, agent_id: str, agent_stack: List[type[Agent]]) -> type[Agent]:
|
|
352
|
+
"""Get agent class for given agent ID."""
|
|
353
|
+
# Find matching agent by name
|
|
354
|
+
for agent_class in agent_stack:
|
|
355
|
+
if hasattr(agent_class, "name") and agent_class.name == agent_id:
|
|
356
|
+
return agent_class
|
|
357
|
+
|
|
358
|
+
# Not found - this shouldn't happen
|
|
359
|
+
return None
|
|
70
360
|
|
|
71
361
|
|
|
72
362
|
@final
|
|
73
363
|
class SwarmTool(BaseTool):
|
|
74
|
-
"""Tool for executing
|
|
75
|
-
|
|
76
|
-
The SwarmTool enables efficient parallel processing of multiple files or tasks
|
|
77
|
-
by spawning independent agent instances for each task.
|
|
78
|
-
"""
|
|
364
|
+
"""Tool for executing agent networks using hanzo-agents SDK."""
|
|
79
365
|
|
|
80
366
|
@property
|
|
81
367
|
@override
|
|
@@ -99,9 +385,9 @@ Features:
|
|
|
99
385
|
- Agents automatically pass results to connected agents
|
|
100
386
|
- Parallel execution with dependency management
|
|
101
387
|
- Full editing capabilities for each agent
|
|
388
|
+
- Memory and state management via hanzo-agents SDK
|
|
102
389
|
|
|
103
390
|
Common Topologies:
|
|
104
|
-
|
|
105
391
|
1. Tree (Architect pattern):
|
|
106
392
|
architect → [frontend, backend, database] → reviewer
|
|
107
393
|
|
|
@@ -114,51 +400,11 @@ Common Topologies:
|
|
|
114
400
|
4. DAG (Complex dependencies):
|
|
115
401
|
Multiple agents with custom connections
|
|
116
402
|
|
|
117
|
-
Usage Example:
|
|
118
|
-
|
|
119
|
-
swarm(
|
|
120
|
-
config={
|
|
121
|
-
"agents": {
|
|
122
|
-
"architect": {
|
|
123
|
-
"id": "architect",
|
|
124
|
-
"query": "Analyze codebase and create refactoring plan",
|
|
125
|
-
"model": "claude-3-5-sonnet",
|
|
126
|
-
"connections": ["frontend", "backend", "database"]
|
|
127
|
-
},
|
|
128
|
-
"frontend": {
|
|
129
|
-
"id": "frontend",
|
|
130
|
-
"query": "Refactor UI components based on architect's plan",
|
|
131
|
-
"role": "Frontend Developer",
|
|
132
|
-
"connections": ["reviewer"]
|
|
133
|
-
},
|
|
134
|
-
"backend": {
|
|
135
|
-
"id": "backend",
|
|
136
|
-
"query": "Refactor API endpoints based on architect's plan",
|
|
137
|
-
"role": "Backend Developer",
|
|
138
|
-
"connections": ["reviewer"]
|
|
139
|
-
},
|
|
140
|
-
"database": {
|
|
141
|
-
"id": "database",
|
|
142
|
-
"query": "Optimize database schema based on architect's plan",
|
|
143
|
-
"role": "Database Expert",
|
|
144
|
-
"connections": ["reviewer"]
|
|
145
|
-
},
|
|
146
|
-
"reviewer": {
|
|
147
|
-
"id": "reviewer",
|
|
148
|
-
"query": "Review all changes and ensure consistency",
|
|
149
|
-
"model": "gpt-4o",
|
|
150
|
-
"receives_from": ["frontend", "backend", "database"]
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
"entry_point": "architect"
|
|
154
|
-
},
|
|
155
|
-
query="Refactor the authentication system for better security and performance"
|
|
156
|
-
)
|
|
157
|
-
|
|
158
403
|
Models can be specified as:
|
|
159
404
|
- Full: 'anthropic/claude-3-5-sonnet-20241022'
|
|
160
405
|
- Short: 'claude-3-5-sonnet', 'gpt-4o', 'gemini-1.5-pro'
|
|
161
406
|
- CLI tools: 'claude_cli', 'codex_cli', 'gemini_cli', 'grok_cli'
|
|
407
|
+
- Model URIs: 'model://anthropic/claude-3-opus'
|
|
162
408
|
"""
|
|
163
409
|
|
|
164
410
|
def __init__(
|
|
@@ -171,17 +417,7 @@ Models can be specified as:
|
|
|
171
417
|
agent_max_iterations: int = 10,
|
|
172
418
|
agent_max_tool_uses: int = 30,
|
|
173
419
|
):
|
|
174
|
-
"""Initialize the swarm tool.
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
permission_manager: Permission manager for access control
|
|
178
|
-
model: Optional model name override (defaults to Claude Sonnet)
|
|
179
|
-
api_key: Optional API key for the model provider
|
|
180
|
-
base_url: Optional base URL for the model provider
|
|
181
|
-
max_tokens: Optional maximum tokens for model responses
|
|
182
|
-
agent_max_iterations: Max iterations per agent (default: 10)
|
|
183
|
-
agent_max_tool_uses: Max tool uses per agent (default: 30)
|
|
184
|
-
"""
|
|
420
|
+
"""Initialize the swarm tool."""
|
|
185
421
|
self.permission_manager = permission_manager
|
|
186
422
|
# Default to latest Claude Sonnet if no model specified
|
|
187
423
|
from hanzo_mcp.tools.agent.code_auth import get_latest_claude_model
|
|
@@ -192,246 +428,140 @@ Models can be specified as:
|
|
|
192
428
|
self.agent_max_iterations = agent_max_iterations
|
|
193
429
|
self.agent_max_tool_uses = agent_max_tool_uses
|
|
194
430
|
|
|
431
|
+
# Set up available tools for agents
|
|
432
|
+
self.available_tools: list[BaseTool] = []
|
|
433
|
+
self.available_tools.extend(
|
|
434
|
+
get_read_only_filesystem_tools(self.permission_manager)
|
|
435
|
+
)
|
|
436
|
+
self.available_tools.extend(
|
|
437
|
+
get_read_only_jupyter_tools(self.permission_manager)
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Add edit tools
|
|
441
|
+
self.available_tools.append(Edit(self.permission_manager))
|
|
442
|
+
self.available_tools.append(MultiEdit(self.permission_manager))
|
|
443
|
+
|
|
444
|
+
# Add batch tool
|
|
445
|
+
self.available_tools.append(
|
|
446
|
+
BatchTool({t.name: t for t in self.available_tools})
|
|
447
|
+
)
|
|
448
|
+
|
|
195
449
|
@override
|
|
196
450
|
async def call(
|
|
197
451
|
self,
|
|
198
452
|
ctx: MCPContext,
|
|
199
453
|
**params: Unpack[SwarmToolParams],
|
|
200
454
|
) -> str:
|
|
201
|
-
"""Execute the swarm tool.
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
ctx: MCP context
|
|
205
|
-
**params: Tool parameters
|
|
206
|
-
|
|
207
|
-
Returns:
|
|
208
|
-
Combined results from all agents
|
|
209
|
-
"""
|
|
455
|
+
"""Execute the swarm tool."""
|
|
210
456
|
tool_ctx = create_tool_context(ctx)
|
|
211
457
|
await tool_ctx.set_tool_info(self.name)
|
|
212
458
|
|
|
213
|
-
# Extract parameters
|
|
214
|
-
agents = params.get("agents", [])
|
|
215
|
-
manager_query = params.get("manager_query")
|
|
216
|
-
reviewer_query = params.get("reviewer_query")
|
|
217
|
-
common_context = params.get("common_context", "")
|
|
218
|
-
max_concurrent = params.get("max_concurrent", 10)
|
|
219
|
-
|
|
220
|
-
if not agents:
|
|
221
|
-
await tool_ctx.error("No agents provided")
|
|
222
|
-
return "Error: At least one agent must be provided."
|
|
223
|
-
|
|
224
459
|
# Extract parameters
|
|
225
460
|
config = params.get("config", {})
|
|
226
461
|
initial_query = params.get("query", "")
|
|
227
462
|
context = params.get("context", "")
|
|
463
|
+
max_concurrent = params.get("max_concurrent", 10)
|
|
464
|
+
use_memory = params.get("use_memory", False)
|
|
465
|
+
memory_backend = params.get("memory_backend", "sqlite")
|
|
228
466
|
|
|
229
467
|
agents_config = config.get("agents", {})
|
|
230
|
-
entry_point = config.get("entry_point")
|
|
231
468
|
|
|
232
|
-
|
|
469
|
+
if not agents_config:
|
|
470
|
+
await tool_ctx.error("No agents provided")
|
|
471
|
+
return "Error: At least one agent must be provided."
|
|
472
|
+
|
|
473
|
+
# hanzo-agents SDK is required (already imported above)
|
|
233
474
|
|
|
234
|
-
|
|
235
|
-
agent_instances = {}
|
|
236
|
-
agent_results = {}
|
|
237
|
-
execution_queue = asyncio.Queue()
|
|
238
|
-
completed_agents = set()
|
|
475
|
+
await tool_ctx.info(f"Starting swarm execution with {len(agents_config)} agents using hanzo-agents SDK")
|
|
239
476
|
|
|
240
|
-
# Create
|
|
477
|
+
# Create state
|
|
478
|
+
state = SwarmState(
|
|
479
|
+
config=config,
|
|
480
|
+
initial_query=initial_query,
|
|
481
|
+
context=context
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Create agent classes dynamically
|
|
485
|
+
agent_classes = []
|
|
241
486
|
for agent_id, agent_config in agents_config.items():
|
|
487
|
+
# Check for CLI agents
|
|
242
488
|
model = agent_config.get("model", self.model)
|
|
243
489
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
249
|
-
"grok_cli": self._get_cli_tool("grok_cli"),
|
|
490
|
+
cli_agents = {
|
|
491
|
+
"claude_cli": ClaudeCodeAgent,
|
|
492
|
+
"codex_cli": OpenAICodexAgent,
|
|
493
|
+
"gemini_cli": GeminiAgent,
|
|
494
|
+
"grok_cli": GrokAgent,
|
|
250
495
|
}
|
|
251
496
|
|
|
252
|
-
if model in
|
|
253
|
-
|
|
497
|
+
if model in cli_agents:
|
|
498
|
+
# Use CLI agent
|
|
499
|
+
agent_class = type(f"Swarm{agent_id}", (cli_agents[model],), {
|
|
500
|
+
"name": agent_id,
|
|
501
|
+
"description": agent_config.get("role", f"Agent {agent_id}"),
|
|
502
|
+
"agent_config": agent_config
|
|
503
|
+
})
|
|
254
504
|
else:
|
|
255
|
-
#
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
505
|
+
# Create dynamic SwarmAgent class
|
|
506
|
+
agent_class = type(f"Swarm{agent_id}", (SwarmAgent,), {
|
|
507
|
+
"name": agent_id,
|
|
508
|
+
"__init__": lambda self, aid=agent_id, acfg=agent_config: SwarmAgent.__init__(
|
|
509
|
+
self,
|
|
510
|
+
agent_id=aid,
|
|
511
|
+
agent_config=acfg,
|
|
512
|
+
available_tools=self.available_tools,
|
|
513
|
+
permission_manager=self.permission_manager,
|
|
514
|
+
ctx=ctx
|
|
515
|
+
)
|
|
516
|
+
})
|
|
265
517
|
|
|
266
|
-
|
|
518
|
+
agent_classes.append(agent_class)
|
|
267
519
|
|
|
268
|
-
#
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
for agent_id, agent_config in agents_config.items():
|
|
275
|
-
if not agent_config.get("receives_from"):
|
|
276
|
-
# Check if any other agent connects to this one
|
|
277
|
-
has_incoming = False
|
|
278
|
-
for other_config in agents_config.values():
|
|
279
|
-
if other_config.get("connections") and agent_id in other_config["connections"]:
|
|
280
|
-
has_incoming = True
|
|
281
|
-
break
|
|
282
|
-
if not has_incoming:
|
|
283
|
-
roots.append(agent_id)
|
|
284
|
-
|
|
285
|
-
if not roots:
|
|
286
|
-
await tool_ctx.error("No entry point found in agent network")
|
|
287
|
-
return "Error: Could not determine entry point for agent network"
|
|
288
|
-
|
|
289
|
-
for root in roots:
|
|
290
|
-
await execution_queue.put((root, initial_query, {}))
|
|
291
|
-
|
|
292
|
-
# Execute agents in network order
|
|
293
|
-
async def execute_agent(agent_id: str, query: str, inputs: Dict[str, str]) -> str:
|
|
294
|
-
"""Execute a single agent in the network."""
|
|
295
|
-
async with semaphore:
|
|
296
|
-
try:
|
|
297
|
-
agent_config = agents_config[agent_id]
|
|
298
|
-
agent = agent_instances[agent_id]
|
|
299
|
-
|
|
300
|
-
await tool_ctx.info(f"Executing agent: {agent_id} ({agent_config.get('role', 'Agent')})")
|
|
301
|
-
|
|
302
|
-
# Build prompt with context and inputs
|
|
303
|
-
prompt_parts = []
|
|
304
|
-
|
|
305
|
-
# Add role context
|
|
306
|
-
if agent_config.get("role"):
|
|
307
|
-
prompt_parts.append(f"Your role: {agent_config['role']}")
|
|
308
|
-
|
|
309
|
-
# Add shared context
|
|
310
|
-
if context:
|
|
311
|
-
prompt_parts.append(f"Context:\n{context}")
|
|
312
|
-
|
|
313
|
-
# Add inputs from connected agents
|
|
314
|
-
if inputs:
|
|
315
|
-
prompt_parts.append("Input from previous agents:")
|
|
316
|
-
for input_agent, input_result in inputs.items():
|
|
317
|
-
prompt_parts.append(f"\n--- From {input_agent} ---\n{input_result}")
|
|
318
|
-
|
|
319
|
-
# Add file context if specified
|
|
320
|
-
if agent_config.get("file_path"):
|
|
321
|
-
prompt_parts.append(f"\nFile to work on: {agent_config['file_path']}")
|
|
322
|
-
|
|
323
|
-
# Add the main query
|
|
324
|
-
prompt_parts.append(f"\nTask: {agent_config['query']}")
|
|
325
|
-
|
|
326
|
-
# Combine query with initial query if this is entry point
|
|
327
|
-
if query and query != agent_config['query']:
|
|
328
|
-
prompt_parts.append(f"\nMain objective: {query}")
|
|
329
|
-
|
|
330
|
-
full_prompt = "\n\n".join(prompt_parts)
|
|
331
|
-
|
|
332
|
-
# Execute the agent
|
|
333
|
-
result = await agent.call(ctx, prompts=full_prompt)
|
|
334
|
-
|
|
335
|
-
await tool_ctx.info(f"Agent {agent_id} completed")
|
|
336
|
-
return result
|
|
337
|
-
|
|
338
|
-
except Exception as e:
|
|
339
|
-
error_msg = f"Agent {agent_id} failed: {str(e)}"
|
|
340
|
-
await tool_ctx.error(error_msg)
|
|
341
|
-
return f"Error: {error_msg}"
|
|
342
|
-
|
|
343
|
-
# Process agent network
|
|
344
|
-
running_tasks = set()
|
|
345
|
-
|
|
346
|
-
while not execution_queue.empty() or running_tasks:
|
|
347
|
-
# Start new tasks up to concurrency limit
|
|
348
|
-
while not execution_queue.empty() and len(running_tasks) < max_concurrent:
|
|
349
|
-
agent_id, query, inputs = await execution_queue.get()
|
|
350
|
-
|
|
351
|
-
if agent_id not in completed_agents:
|
|
352
|
-
# Check if all dependencies are met
|
|
353
|
-
agent_config = agents_config[agent_id]
|
|
354
|
-
receives_from = agent_config.get("receives_from", [])
|
|
355
|
-
|
|
356
|
-
# Collect inputs from dependencies
|
|
357
|
-
ready = True
|
|
358
|
-
for dep in receives_from:
|
|
359
|
-
if dep not in agent_results:
|
|
360
|
-
ready = False
|
|
361
|
-
# Re-queue for later
|
|
362
|
-
await execution_queue.put((agent_id, query, inputs))
|
|
363
|
-
break
|
|
364
|
-
else:
|
|
365
|
-
inputs[dep] = agent_results[dep]
|
|
366
|
-
|
|
367
|
-
if ready:
|
|
368
|
-
# Execute agent
|
|
369
|
-
task = asyncio.create_task(execute_agent(agent_id, query, inputs))
|
|
370
|
-
running_tasks.add(task)
|
|
371
|
-
|
|
372
|
-
async def handle_completion(task, agent_id=agent_id):
|
|
373
|
-
result = await task
|
|
374
|
-
agent_results[agent_id] = result
|
|
375
|
-
completed_agents.add(agent_id)
|
|
376
|
-
running_tasks.discard(task)
|
|
377
|
-
|
|
378
|
-
# Queue connected agents
|
|
379
|
-
agent_config = agents_config[agent_id]
|
|
380
|
-
connections = agent_config.get("connections", [])
|
|
381
|
-
for next_agent in connections:
|
|
382
|
-
if next_agent in agents_config:
|
|
383
|
-
await execution_queue.put((next_agent, "", {agent_id: result}))
|
|
384
|
-
|
|
385
|
-
asyncio.create_task(handle_completion(task))
|
|
386
|
-
|
|
387
|
-
# Wait a bit if we're at capacity
|
|
388
|
-
if running_tasks:
|
|
389
|
-
await asyncio.sleep(0.1)
|
|
520
|
+
# Create memory if requested
|
|
521
|
+
memory_kv = None
|
|
522
|
+
memory_vector = None
|
|
523
|
+
if use_memory:
|
|
524
|
+
memory_kv = create_memory_kv(memory_backend)
|
|
525
|
+
memory_vector = create_memory_vector("simple")
|
|
390
526
|
|
|
391
|
-
#
|
|
392
|
-
|
|
393
|
-
await asyncio.gather(*running_tasks, return_exceptions=True)
|
|
527
|
+
# Create router
|
|
528
|
+
router = SwarmRouter(config)
|
|
394
529
|
|
|
395
|
-
#
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
from hanzo_mcp.tools.agent.gemini_cli_tool import GeminiCLITool
|
|
421
|
-
return GeminiCLITool(self.permission_manager)
|
|
422
|
-
elif tool_name == "grok_cli":
|
|
423
|
-
from hanzo_mcp.tools.agent.grok_cli_tool import GrokCLITool
|
|
424
|
-
return GrokCLITool(self.permission_manager)
|
|
425
|
-
return None
|
|
530
|
+
# Create network
|
|
531
|
+
network = Network(
|
|
532
|
+
state=state,
|
|
533
|
+
agents=agent_classes,
|
|
534
|
+
router=router,
|
|
535
|
+
memory_kv=memory_kv,
|
|
536
|
+
memory_vector=memory_vector,
|
|
537
|
+
max_steps=self.agent_max_iterations * len(agents_config),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Execute
|
|
541
|
+
try:
|
|
542
|
+
final_state = await network.run()
|
|
543
|
+
|
|
544
|
+
# Format results
|
|
545
|
+
return self._format_network_results(
|
|
546
|
+
agents_config,
|
|
547
|
+
final_state.agent_results,
|
|
548
|
+
final_state.execution_order,
|
|
549
|
+
config.get("entry_point")
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
except Exception as e:
|
|
553
|
+
await tool_ctx.error(f"Swarm execution failed: {str(e)}")
|
|
554
|
+
return f"Error: {str(e)}"
|
|
426
555
|
|
|
427
556
|
def _format_network_results(
|
|
428
557
|
self,
|
|
429
558
|
agents_config: Dict[str, Any],
|
|
430
559
|
results: Dict[str, str],
|
|
560
|
+
execution_order: List[str],
|
|
431
561
|
entry_point: Optional[str]
|
|
432
562
|
) -> str:
|
|
433
563
|
"""Format results from agent network execution."""
|
|
434
|
-
output = ["Agent Network Execution Results"]
|
|
564
|
+
output = ["Agent Network Execution Results (hanzo-agents SDK)"]
|
|
435
565
|
output.append("=" * 80)
|
|
436
566
|
output.append(f"Total agents: {len(agents_config)}")
|
|
437
567
|
output.append(f"Completed: {len(results)}")
|
|
@@ -440,69 +570,33 @@ Models can be specified as:
|
|
|
440
570
|
if entry_point:
|
|
441
571
|
output.append(f"Entry point: {entry_point}")
|
|
442
572
|
|
|
443
|
-
output.append("\nExecution
|
|
573
|
+
output.append(f"\nExecution Order: {' → '.join(execution_order)}")
|
|
444
574
|
output.append("-" * 40)
|
|
445
575
|
|
|
446
|
-
# Show results in execution order
|
|
447
|
-
def format_agent_tree(agent_id: str, level: int = 0) -> List[str]:
|
|
448
|
-
lines = []
|
|
449
|
-
indent = " " * level
|
|
450
|
-
|
|
451
|
-
if agent_id in agents_config:
|
|
452
|
-
config = agents_config[agent_id]
|
|
453
|
-
role = config.get("role", "Agent")
|
|
454
|
-
model = config.get("model", "default")
|
|
455
|
-
|
|
456
|
-
status = "✅" if agent_id in results and not results[agent_id].startswith("Error:") else "❌"
|
|
457
|
-
lines.append(f"{indent}{status} {agent_id} ({role}) [{model}]")
|
|
458
|
-
|
|
459
|
-
# Show connections
|
|
460
|
-
connections = config.get("connections", [])
|
|
461
|
-
for conn in connections:
|
|
462
|
-
if conn in agents_config:
|
|
463
|
-
lines.extend(format_agent_tree(conn, level + 1))
|
|
464
|
-
|
|
465
|
-
return lines
|
|
466
|
-
|
|
467
|
-
# Start from entry point or roots
|
|
468
|
-
if entry_point:
|
|
469
|
-
output.extend(format_agent_tree(entry_point))
|
|
470
|
-
else:
|
|
471
|
-
# Find roots
|
|
472
|
-
roots = []
|
|
473
|
-
for agent_id in agents_config:
|
|
474
|
-
has_incoming = False
|
|
475
|
-
for config in agents_config.values():
|
|
476
|
-
if config.get("connections") and agent_id in config["connections"]:
|
|
477
|
-
has_incoming = True
|
|
478
|
-
break
|
|
479
|
-
if not has_incoming:
|
|
480
|
-
roots.append(agent_id)
|
|
481
|
-
|
|
482
|
-
for root in roots:
|
|
483
|
-
output.extend(format_agent_tree(root))
|
|
484
|
-
|
|
485
576
|
# Detailed results
|
|
486
577
|
output.append("\n\nDetailed Results:")
|
|
487
578
|
output.append("=" * 80)
|
|
488
579
|
|
|
489
|
-
for agent_id
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
output.append("-" * 40)
|
|
495
|
-
|
|
496
|
-
if result.startswith("Error:"):
|
|
497
|
-
output.append(result)
|
|
498
|
-
else:
|
|
499
|
-
# Show first part of result
|
|
500
|
-
lines = result.split('\n')
|
|
501
|
-
preview_lines = lines[:10]
|
|
502
|
-
output.extend(preview_lines)
|
|
580
|
+
for agent_id in execution_order:
|
|
581
|
+
if agent_id in results:
|
|
582
|
+
config = agents_config.get(agent_id, {})
|
|
583
|
+
role = config.get("role", "Agent")
|
|
584
|
+
model = config.get("model", "default")
|
|
503
585
|
|
|
504
|
-
|
|
505
|
-
|
|
586
|
+
output.append(f"\n### {agent_id} ({role}) [{model}]")
|
|
587
|
+
output.append("-" * 40)
|
|
588
|
+
|
|
589
|
+
result = results[agent_id]
|
|
590
|
+
if result.startswith("Error:"):
|
|
591
|
+
output.append(result)
|
|
592
|
+
else:
|
|
593
|
+
# Show first part of result
|
|
594
|
+
lines = result.split('\n')
|
|
595
|
+
preview_lines = lines[:10]
|
|
596
|
+
output.extend(preview_lines)
|
|
597
|
+
|
|
598
|
+
if len(lines) > 10:
|
|
599
|
+
output.append(f"... ({len(lines) - 10} more lines)")
|
|
506
600
|
|
|
507
601
|
return "\n".join(output)
|
|
508
602
|
|
|
@@ -518,6 +612,8 @@ Models can be specified as:
|
|
|
518
612
|
query: str,
|
|
519
613
|
context: Optional[str] = None,
|
|
520
614
|
max_concurrent: int = 10,
|
|
615
|
+
use_memory: bool = False,
|
|
616
|
+
memory_backend: str = "sqlite"
|
|
521
617
|
) -> str:
|
|
522
618
|
# Convert to typed format
|
|
523
619
|
typed_config = SwarmConfig(
|
|
@@ -531,5 +627,7 @@ Models can be specified as:
|
|
|
531
627
|
config=typed_config,
|
|
532
628
|
query=query,
|
|
533
629
|
context=context,
|
|
534
|
-
max_concurrent=max_concurrent
|
|
630
|
+
max_concurrent=max_concurrent,
|
|
631
|
+
use_memory=use_memory,
|
|
632
|
+
memory_backend=memory_backend
|
|
535
633
|
)
|