hanzo-mcp 0.7.3__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.

Files changed (35) hide show
  1. hanzo_mcp/cli.py +10 -0
  2. hanzo_mcp/prompts/__init__.py +43 -0
  3. hanzo_mcp/prompts/example_custom_prompt.py +40 -0
  4. hanzo_mcp/prompts/tool_explorer.py +603 -0
  5. hanzo_mcp/tools/__init__.py +52 -51
  6. hanzo_mcp/tools/agent/__init__.py +3 -16
  7. hanzo_mcp/tools/agent/agent_tool.py +365 -525
  8. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +641 -0
  9. hanzo_mcp/tools/agent/network_tool.py +3 -5
  10. hanzo_mcp/tools/agent/swarm_tool.py +447 -349
  11. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +535 -0
  12. hanzo_mcp/tools/agent/tool_adapter.py +21 -2
  13. hanzo_mcp/tools/common/forgiving_edit.py +24 -14
  14. hanzo_mcp/tools/common/permissions.py +8 -0
  15. hanzo_mcp/tools/filesystem/__init__.py +5 -5
  16. hanzo_mcp/tools/filesystem/{symbols.py → ast_tool.py} +8 -8
  17. hanzo_mcp/tools/filesystem/batch_search.py +2 -2
  18. hanzo_mcp/tools/filesystem/directory_tree.py +8 -1
  19. hanzo_mcp/tools/filesystem/find.py +1 -0
  20. hanzo_mcp/tools/filesystem/grep.py +11 -2
  21. hanzo_mcp/tools/filesystem/read.py +8 -1
  22. hanzo_mcp/tools/filesystem/search_tool.py +1 -1
  23. hanzo_mcp/tools/jupyter/__init__.py +5 -1
  24. hanzo_mcp/tools/jupyter/base.py +2 -2
  25. hanzo_mcp/tools/jupyter/jupyter.py +89 -18
  26. hanzo_mcp/tools/search/find_tool.py +49 -8
  27. hanzo_mcp/tools/shell/base_process.py +7 -1
  28. hanzo_mcp/tools/shell/streaming_command.py +34 -1
  29. {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.6.dist-info}/METADATA +7 -1
  30. {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.6.dist-info}/RECORD +33 -31
  31. hanzo_mcp/tools/agent/agent_tool_v2.py +0 -492
  32. hanzo_mcp/tools/agent/swarm_tool_v2.py +0 -654
  33. {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.6.dist-info}/WHEEL +0 -0
  34. {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.6.dist-info}/entry_points.txt +0 -0
  35. {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.6.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
- """Swarm tool implementation for parallel and hierarchical agent execution.
1
+ """Swarm tool implementation using hanzo-agents SDK.
2
2
 
3
- This module implements the SwarmTool that enables both parallel execution of multiple
4
- agent instances and hierarchical workflows with specialized roles.
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
- from hanzo_mcp.tools.agent.agent_tool import AgentTool
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 multiple agent tasks in parallel.
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
- await tool_ctx.info(f"Starting swarm execution with {len(agents_config)} agents")
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
- # Build agent network
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 agent instances
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
- # Support CLI tools
245
- cli_tools = {
246
- "claude_cli": self._get_cli_tool("claude_cli"),
247
- "codex_cli": self._get_cli_tool("codex_cli"),
248
- "gemini_cli": self._get_cli_tool("gemini_cli"),
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 cli_tools:
253
- agent = cli_tools[model]
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
- # Regular agent with model
256
- agent = AgentTool(
257
- permission_manager=self.permission_manager,
258
- model=self._normalize_model(model),
259
- api_key=self.api_key,
260
- base_url=self.base_url,
261
- max_tokens=self.max_tokens,
262
- max_iterations=self.agent_max_iterations,
263
- max_tool_uses=self.agent_max_tool_uses,
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
- agent_instances[agent_id] = agent
518
+ agent_classes.append(agent_class)
267
519
 
268
- # Find entry points (agents with no incoming connections)
269
- if entry_point:
270
- await execution_queue.put((entry_point, initial_query, {}))
271
- else:
272
- # Find root agents (no receives_from)
273
- roots = []
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
- # Wait for all tasks to complete
392
- if running_tasks:
393
- await asyncio.gather(*running_tasks, return_exceptions=True)
527
+ # Create router
528
+ router = SwarmRouter(config)
394
529
 
395
- # Format results
396
- return self._format_network_results(agents_config, agent_results, entry_point)
397
-
398
- def _normalize_model(self, model: str) -> str:
399
- """Normalize model names to full format."""
400
- model_map = {
401
- "claude-3-5-sonnet": "anthropic/claude-3-5-sonnet-20241022",
402
- "claude-3-opus": "anthropic/claude-3-opus-20240229",
403
- "gpt-4o": "openai/gpt-4o",
404
- "gpt-4": "openai/gpt-4",
405
- "gemini-1.5-pro": "google/gemini-1.5-pro",
406
- "gemini-1.5-flash": "google/gemini-1.5-flash",
407
- }
408
- return model_map.get(model, model)
409
-
410
- def _get_cli_tool(self, tool_name: str):
411
- """Get CLI tool instance."""
412
- # Import here to avoid circular imports
413
- if tool_name == "claude_cli":
414
- from hanzo_mcp.tools.agent.claude_cli_tool import ClaudeCLITool
415
- return ClaudeCLITool(self.permission_manager)
416
- elif tool_name == "codex_cli":
417
- from hanzo_mcp.tools.agent.codex_cli_tool import CodexCLITool
418
- return CodexCLITool(self.permission_manager)
419
- elif tool_name == "gemini_cli":
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 Flow:")
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, result in results.items():
490
- config = agents_config.get(agent_id, {})
491
- role = config.get("role", "Agent")
492
-
493
- output.append(f"\n### {agent_id} ({role})")
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
- if len(lines) > 10:
505
- output.append(f"... ({len(lines) - 10} more lines)")
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
  )