hanzo 0.3.11__py3-none-any.whl → 0.3.12__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 might be problematic. Click here for more details.
- hanzo/cli.py +62 -0
- hanzo/dev.py +1621 -0
- {hanzo-0.3.11.dist-info → hanzo-0.3.12.dist-info}/METADATA +1 -1
- {hanzo-0.3.11.dist-info → hanzo-0.3.12.dist-info}/RECORD +6 -5
- {hanzo-0.3.11.dist-info → hanzo-0.3.12.dist-info}/WHEEL +0 -0
- {hanzo-0.3.11.dist-info → hanzo-0.3.12.dist-info}/entry_points.txt +0 -0
hanzo/dev.py
ADDED
|
@@ -0,0 +1,1621 @@
|
|
|
1
|
+
"""Hanzo Dev - System 2 Thinking Meta-AI for Managing Claude Code Runtime.
|
|
2
|
+
|
|
3
|
+
This module provides a sophisticated orchestration layer that:
|
|
4
|
+
1. Acts as a System 2 thinking agent (deliberative, analytical)
|
|
5
|
+
2. Manages Claude Code runtime lifecycle
|
|
6
|
+
3. Provides persistence and recovery mechanisms
|
|
7
|
+
4. Includes health checks and auto-restart capabilities
|
|
8
|
+
5. Integrates with REPL for interactive control
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import signal
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass, asdict
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Dict, List, Optional, Any, Callable, Union
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
from rich.live import Live
|
|
27
|
+
from rich.layout import Layout
|
|
28
|
+
from rich.panel import Panel
|
|
29
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
30
|
+
|
|
31
|
+
# Setup logging first
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
console = Console()
|
|
34
|
+
|
|
35
|
+
# Import hanzo-network for agent orchestration
|
|
36
|
+
try:
|
|
37
|
+
from hanzo_network import (
|
|
38
|
+
Agent, Network, Router, NetworkState,
|
|
39
|
+
create_agent, create_network, create_router, create_routing_agent,
|
|
40
|
+
ModelConfig, ModelProvider,
|
|
41
|
+
DistributedNetwork, create_distributed_network,
|
|
42
|
+
LOCAL_COMPUTE_AVAILABLE
|
|
43
|
+
)
|
|
44
|
+
NETWORK_AVAILABLE = True
|
|
45
|
+
except ImportError:
|
|
46
|
+
NETWORK_AVAILABLE = False
|
|
47
|
+
logger.warning("hanzo-network not available, using basic orchestration")
|
|
48
|
+
|
|
49
|
+
# Provide fallback implementations
|
|
50
|
+
class Agent:
|
|
51
|
+
"""Fallback Agent class when hanzo-network is not available."""
|
|
52
|
+
def __init__(self, name: str, model: str = "gpt-4", **kwargs):
|
|
53
|
+
self.name = name
|
|
54
|
+
self.model = model
|
|
55
|
+
self.config = kwargs
|
|
56
|
+
|
|
57
|
+
class Network:
|
|
58
|
+
"""Fallback Network class."""
|
|
59
|
+
def __init__(self):
|
|
60
|
+
self.agents = []
|
|
61
|
+
|
|
62
|
+
class Router:
|
|
63
|
+
"""Fallback Router class."""
|
|
64
|
+
def __init__(self):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
class NetworkState:
|
|
68
|
+
"""Fallback NetworkState class."""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
class ModelConfig:
|
|
72
|
+
"""Fallback ModelConfig class."""
|
|
73
|
+
def __init__(self, **kwargs):
|
|
74
|
+
self.__dict__.update(kwargs)
|
|
75
|
+
|
|
76
|
+
class ModelProvider:
|
|
77
|
+
"""Fallback ModelProvider class."""
|
|
78
|
+
OPENAI = "openai"
|
|
79
|
+
ANTHROPIC = "anthropic"
|
|
80
|
+
LOCAL = "local"
|
|
81
|
+
|
|
82
|
+
LOCAL_COMPUTE_AVAILABLE = False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AgentState(Enum):
|
|
86
|
+
"""State of an AI agent."""
|
|
87
|
+
IDLE = "idle"
|
|
88
|
+
THINKING = "thinking" # System 2 deliberation
|
|
89
|
+
EXECUTING = "executing"
|
|
90
|
+
STUCK = "stuck"
|
|
91
|
+
CRASHED = "crashed"
|
|
92
|
+
RECOVERING = "recovering"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class RuntimeState(Enum):
|
|
96
|
+
"""State of Claude Code runtime."""
|
|
97
|
+
NOT_STARTED = "not_started"
|
|
98
|
+
STARTING = "starting"
|
|
99
|
+
RUNNING = "running"
|
|
100
|
+
RESPONDING = "responding"
|
|
101
|
+
NOT_RESPONDING = "not_responding"
|
|
102
|
+
CRASHED = "crashed"
|
|
103
|
+
RESTARTING = "restarting"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class AgentContext:
|
|
108
|
+
"""Context for agent decision making."""
|
|
109
|
+
task: str
|
|
110
|
+
goal: str
|
|
111
|
+
constraints: List[str]
|
|
112
|
+
success_criteria: List[str]
|
|
113
|
+
max_attempts: int = 3
|
|
114
|
+
timeout_seconds: int = 300
|
|
115
|
+
checkpoint_interval: int = 60
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class RuntimeHealth:
|
|
120
|
+
"""Health status of Claude Code runtime."""
|
|
121
|
+
state: RuntimeState
|
|
122
|
+
last_response: datetime
|
|
123
|
+
response_time_ms: float
|
|
124
|
+
memory_usage_mb: float
|
|
125
|
+
cpu_percent: float
|
|
126
|
+
error_count: int
|
|
127
|
+
restart_count: int
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class ThinkingResult:
|
|
132
|
+
"""Result of System 2 thinking process."""
|
|
133
|
+
decision: str
|
|
134
|
+
reasoning: List[str]
|
|
135
|
+
confidence: float
|
|
136
|
+
alternatives: List[str]
|
|
137
|
+
risks: List[str]
|
|
138
|
+
next_steps: List[str]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class HanzoDevOrchestrator:
|
|
142
|
+
"""Main orchestrator for Hanzo Dev System 2 thinking."""
|
|
143
|
+
|
|
144
|
+
def __init__(self,
|
|
145
|
+
workspace_dir: str = "~/.hanzo/dev",
|
|
146
|
+
claude_code_path: Optional[str] = None):
|
|
147
|
+
"""Initialize the orchestrator.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
workspace_dir: Directory for persistence and checkpoints
|
|
151
|
+
claude_code_path: Path to Claude Code executable
|
|
152
|
+
"""
|
|
153
|
+
self.workspace_dir = Path(workspace_dir).expanduser()
|
|
154
|
+
self.workspace_dir.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
|
|
156
|
+
self.claude_code_path = claude_code_path or self._find_claude_code()
|
|
157
|
+
self.state_file = self.workspace_dir / "orchestrator_state.json"
|
|
158
|
+
self.checkpoint_dir = self.workspace_dir / "checkpoints"
|
|
159
|
+
self.checkpoint_dir.mkdir(exist_ok=True)
|
|
160
|
+
|
|
161
|
+
self.agent_state = AgentState.IDLE
|
|
162
|
+
self.runtime_health = RuntimeHealth(
|
|
163
|
+
state=RuntimeState.NOT_STARTED,
|
|
164
|
+
last_response=datetime.now(),
|
|
165
|
+
response_time_ms=0,
|
|
166
|
+
memory_usage_mb=0,
|
|
167
|
+
cpu_percent=0,
|
|
168
|
+
error_count=0,
|
|
169
|
+
restart_count=0
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
self.current_context: Optional[AgentContext] = None
|
|
173
|
+
self.claude_process: Optional[subprocess.Popen] = None
|
|
174
|
+
self.thinking_history: List[ThinkingResult] = []
|
|
175
|
+
self._shutdown = False
|
|
176
|
+
|
|
177
|
+
def _find_claude_code(self) -> str:
|
|
178
|
+
"""Find Claude Code executable."""
|
|
179
|
+
# Check common locations
|
|
180
|
+
possible_paths = [
|
|
181
|
+
"/usr/local/bin/claude",
|
|
182
|
+
"/opt/claude/claude",
|
|
183
|
+
"~/.local/bin/claude",
|
|
184
|
+
"claude" # Rely on PATH
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
for path in possible_paths:
|
|
188
|
+
expanded = Path(path).expanduser()
|
|
189
|
+
if expanded.exists() or (path == "claude" and os.system(f"which {path} >/dev/null 2>&1") == 0):
|
|
190
|
+
return str(expanded) if expanded.exists() else path
|
|
191
|
+
|
|
192
|
+
raise RuntimeError("Claude Code not found. Please specify path.")
|
|
193
|
+
|
|
194
|
+
async def think(self, problem: str, context: Dict[str, Any]) -> ThinkingResult:
|
|
195
|
+
"""System 2 thinking process - deliberative and analytical.
|
|
196
|
+
|
|
197
|
+
This implements slow, deliberate thinking:
|
|
198
|
+
1. Analyze the problem thoroughly
|
|
199
|
+
2. Consider multiple approaches
|
|
200
|
+
3. Evaluate risks and trade-offs
|
|
201
|
+
4. Make a reasoned decision
|
|
202
|
+
"""
|
|
203
|
+
self.agent_state = AgentState.THINKING
|
|
204
|
+
console.print("[yellow]🤔 Engaging System 2 thinking...[/yellow]")
|
|
205
|
+
|
|
206
|
+
# Simulate deep thinking process
|
|
207
|
+
reasoning = []
|
|
208
|
+
alternatives = []
|
|
209
|
+
risks = []
|
|
210
|
+
|
|
211
|
+
# Step 1: Problem decomposition
|
|
212
|
+
reasoning.append(f"Decomposing problem: {problem}")
|
|
213
|
+
sub_problems = self._decompose_problem(problem)
|
|
214
|
+
reasoning.append(f"Identified {len(sub_problems)} sub-problems")
|
|
215
|
+
|
|
216
|
+
# Step 2: Generate alternatives
|
|
217
|
+
for sub in sub_problems:
|
|
218
|
+
alt = f"Approach for '{sub}': {self._generate_approach(sub, context)}"
|
|
219
|
+
alternatives.append(alt)
|
|
220
|
+
|
|
221
|
+
# Step 3: Risk assessment
|
|
222
|
+
risks = self._assess_risks(problem, alternatives, context)
|
|
223
|
+
|
|
224
|
+
# Step 4: Decision synthesis
|
|
225
|
+
decision = self._synthesize_decision(problem, alternatives, risks, context)
|
|
226
|
+
confidence = self._calculate_confidence(decision, risks)
|
|
227
|
+
|
|
228
|
+
# Step 5: Plan next steps
|
|
229
|
+
next_steps = self._plan_next_steps(decision, context)
|
|
230
|
+
|
|
231
|
+
result = ThinkingResult(
|
|
232
|
+
decision=decision,
|
|
233
|
+
reasoning=reasoning,
|
|
234
|
+
confidence=confidence,
|
|
235
|
+
alternatives=alternatives,
|
|
236
|
+
risks=risks,
|
|
237
|
+
next_steps=next_steps
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
self.thinking_history.append(result)
|
|
241
|
+
self.agent_state = AgentState.IDLE
|
|
242
|
+
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
def _decompose_problem(self, problem: str) -> List[str]:
|
|
246
|
+
"""Decompose a problem into sub-problems."""
|
|
247
|
+
# Simple heuristic decomposition
|
|
248
|
+
sub_problems = []
|
|
249
|
+
|
|
250
|
+
# Check for common patterns
|
|
251
|
+
if "and" in problem.lower():
|
|
252
|
+
parts = problem.split(" and ")
|
|
253
|
+
sub_problems.extend(parts)
|
|
254
|
+
|
|
255
|
+
if "then" in problem.lower():
|
|
256
|
+
parts = problem.split(" then ")
|
|
257
|
+
sub_problems.extend(parts)
|
|
258
|
+
|
|
259
|
+
if not sub_problems:
|
|
260
|
+
sub_problems = [problem]
|
|
261
|
+
|
|
262
|
+
return sub_problems
|
|
263
|
+
|
|
264
|
+
def _generate_approach(self, sub_problem: str, context: Dict[str, Any]) -> str:
|
|
265
|
+
"""Generate an approach for a sub-problem."""
|
|
266
|
+
# Heuristic approach generation
|
|
267
|
+
if "stuck" in sub_problem.lower():
|
|
268
|
+
return "Analyze error logs, restart with verbose mode, try alternative approach"
|
|
269
|
+
elif "slow" in sub_problem.lower():
|
|
270
|
+
return "Profile performance, optimize bottlenecks, consider caching"
|
|
271
|
+
elif "error" in sub_problem.lower():
|
|
272
|
+
return "Examine stack trace, validate inputs, add error handling"
|
|
273
|
+
else:
|
|
274
|
+
return "Execute standard workflow with monitoring"
|
|
275
|
+
|
|
276
|
+
def _assess_risks(self, problem: str, alternatives: List[str], context: Dict[str, Any]) -> List[str]:
|
|
277
|
+
"""Assess risks of different approaches."""
|
|
278
|
+
risks = []
|
|
279
|
+
|
|
280
|
+
if "restart" in str(alternatives).lower():
|
|
281
|
+
risks.append("Restarting may lose current state")
|
|
282
|
+
|
|
283
|
+
if "force" in str(alternatives).lower():
|
|
284
|
+
risks.append("Forcing operations may cause data corruption")
|
|
285
|
+
|
|
286
|
+
if context.get("error_count", 0) > 5:
|
|
287
|
+
risks.append("High error rate indicates systemic issue")
|
|
288
|
+
|
|
289
|
+
return risks
|
|
290
|
+
|
|
291
|
+
def _synthesize_decision(self, problem: str, alternatives: List[str],
|
|
292
|
+
risks: List[str], context: Dict[str, Any]) -> str:
|
|
293
|
+
"""Synthesize a decision from analysis."""
|
|
294
|
+
if len(risks) > 2:
|
|
295
|
+
return "Proceed cautiously with incremental approach and rollback capability"
|
|
296
|
+
elif alternatives:
|
|
297
|
+
return f"Execute primary approach: {alternatives[0]}"
|
|
298
|
+
else:
|
|
299
|
+
return "Gather more information before proceeding"
|
|
300
|
+
|
|
301
|
+
def _calculate_confidence(self, decision: str, risks: List[str]) -> float:
|
|
302
|
+
"""Calculate confidence in decision."""
|
|
303
|
+
base_confidence = 0.8
|
|
304
|
+
risk_penalty = len(risks) * 0.1
|
|
305
|
+
return max(0.2, min(1.0, base_confidence - risk_penalty))
|
|
306
|
+
|
|
307
|
+
def _plan_next_steps(self, decision: str, context: Dict[str, Any]) -> List[str]:
|
|
308
|
+
"""Plan concrete next steps."""
|
|
309
|
+
steps = []
|
|
310
|
+
|
|
311
|
+
if "cautiously" in decision.lower():
|
|
312
|
+
steps.append("Create checkpoint before proceeding")
|
|
313
|
+
steps.append("Enable verbose logging")
|
|
314
|
+
|
|
315
|
+
steps.append("Execute decision with monitoring")
|
|
316
|
+
steps.append("Validate results against success criteria")
|
|
317
|
+
steps.append("Report outcome and update state")
|
|
318
|
+
|
|
319
|
+
return steps
|
|
320
|
+
|
|
321
|
+
async def start_claude_runtime(self, resume: bool = False) -> bool:
|
|
322
|
+
"""Start or resume Claude Code runtime.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
resume: Whether to resume from checkpoint
|
|
326
|
+
"""
|
|
327
|
+
if self.claude_process and self.claude_process.poll() is None:
|
|
328
|
+
console.print("[yellow]Claude Code already running[/yellow]")
|
|
329
|
+
return True
|
|
330
|
+
|
|
331
|
+
self.runtime_health.state = RuntimeState.STARTING
|
|
332
|
+
console.print("[cyan]Starting Claude Code runtime...[/cyan]")
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
# Load checkpoint if resuming
|
|
336
|
+
checkpoint_file = None
|
|
337
|
+
if resume:
|
|
338
|
+
checkpoint_file = self._get_latest_checkpoint()
|
|
339
|
+
if checkpoint_file:
|
|
340
|
+
console.print(f"[green]Resuming from checkpoint: {checkpoint_file.name}[/green]")
|
|
341
|
+
|
|
342
|
+
# Prepare command
|
|
343
|
+
cmd = [self.claude_code_path]
|
|
344
|
+
if checkpoint_file:
|
|
345
|
+
cmd.extend(["--resume", str(checkpoint_file)])
|
|
346
|
+
|
|
347
|
+
# Start process with proper signal handling
|
|
348
|
+
self.claude_process = subprocess.Popen(
|
|
349
|
+
cmd,
|
|
350
|
+
stdout=subprocess.PIPE,
|
|
351
|
+
stderr=subprocess.PIPE,
|
|
352
|
+
stdin=subprocess.PIPE,
|
|
353
|
+
text=True,
|
|
354
|
+
preexec_fn=os.setsid if hasattr(os, 'setsid') else None
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Wait for startup
|
|
358
|
+
await asyncio.sleep(2)
|
|
359
|
+
|
|
360
|
+
if self.claude_process.poll() is None:
|
|
361
|
+
self.runtime_health.state = RuntimeState.RUNNING
|
|
362
|
+
self.runtime_health.last_response = datetime.now()
|
|
363
|
+
console.print("[green]✓ Claude Code runtime started[/green]")
|
|
364
|
+
return True
|
|
365
|
+
else:
|
|
366
|
+
self.runtime_health.state = RuntimeState.CRASHED
|
|
367
|
+
console.print("[red]✗ Claude Code failed to start[/red]")
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
except Exception as e:
|
|
371
|
+
console.print(f"[red]Error starting Claude Code: {e}[/red]")
|
|
372
|
+
self.runtime_health.state = RuntimeState.CRASHED
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
def _get_latest_checkpoint(self) -> Optional[Path]:
|
|
376
|
+
"""Get the latest checkpoint file."""
|
|
377
|
+
checkpoints = list(self.checkpoint_dir.glob("checkpoint_*.json"))
|
|
378
|
+
if checkpoints:
|
|
379
|
+
return max(checkpoints, key=lambda p: p.stat().st_mtime)
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
async def health_check(self) -> bool:
|
|
383
|
+
"""Check health of Claude Code runtime."""
|
|
384
|
+
if not self.claude_process:
|
|
385
|
+
self.runtime_health.state = RuntimeState.NOT_STARTED
|
|
386
|
+
return False
|
|
387
|
+
|
|
388
|
+
# Check if process is alive
|
|
389
|
+
if self.claude_process.poll() is not None:
|
|
390
|
+
self.runtime_health.state = RuntimeState.CRASHED
|
|
391
|
+
self.runtime_health.error_count += 1
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
# Try to communicate
|
|
395
|
+
try:
|
|
396
|
+
# Send a health check command (this is a placeholder)
|
|
397
|
+
# In reality, you'd have a proper API or IPC mechanism
|
|
398
|
+
start_time = time.time()
|
|
399
|
+
|
|
400
|
+
# Simulate health check
|
|
401
|
+
await asyncio.sleep(0.1)
|
|
402
|
+
|
|
403
|
+
response_time = (time.time() - start_time) * 1000
|
|
404
|
+
self.runtime_health.response_time_ms = response_time
|
|
405
|
+
self.runtime_health.last_response = datetime.now()
|
|
406
|
+
|
|
407
|
+
if response_time > 5000:
|
|
408
|
+
self.runtime_health.state = RuntimeState.NOT_RESPONDING
|
|
409
|
+
return False
|
|
410
|
+
else:
|
|
411
|
+
self.runtime_health.state = RuntimeState.RUNNING
|
|
412
|
+
return True
|
|
413
|
+
|
|
414
|
+
except Exception as e:
|
|
415
|
+
logger.error(f"Health check failed: {e}")
|
|
416
|
+
self.runtime_health.state = RuntimeState.NOT_RESPONDING
|
|
417
|
+
self.runtime_health.error_count += 1
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
async def restart_if_needed(self) -> bool:
|
|
421
|
+
"""Restart Claude Code if it's stuck or crashed."""
|
|
422
|
+
if self.runtime_health.state in [RuntimeState.CRASHED, RuntimeState.NOT_RESPONDING]:
|
|
423
|
+
console.print("[yellow]Claude Code needs restart...[/yellow]")
|
|
424
|
+
|
|
425
|
+
# Kill existing process
|
|
426
|
+
if self.claude_process:
|
|
427
|
+
try:
|
|
428
|
+
if hasattr(os, 'killpg'):
|
|
429
|
+
os.killpg(os.getpgid(self.claude_process.pid), signal.SIGTERM)
|
|
430
|
+
else:
|
|
431
|
+
self.claude_process.terminate()
|
|
432
|
+
await asyncio.sleep(2)
|
|
433
|
+
if self.claude_process.poll() is None:
|
|
434
|
+
self.claude_process.kill()
|
|
435
|
+
except:
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
self.runtime_health.restart_count += 1
|
|
439
|
+
self.runtime_health.state = RuntimeState.RESTARTING
|
|
440
|
+
|
|
441
|
+
# Start with resume
|
|
442
|
+
return await self.start_claude_runtime(resume=True)
|
|
443
|
+
|
|
444
|
+
return True
|
|
445
|
+
|
|
446
|
+
async def create_checkpoint(self, name: Optional[str] = None) -> Path:
|
|
447
|
+
"""Create a checkpoint of current state."""
|
|
448
|
+
checkpoint_name = name or f"checkpoint_{int(time.time())}"
|
|
449
|
+
checkpoint_file = self.checkpoint_dir / f"{checkpoint_name}.json"
|
|
450
|
+
|
|
451
|
+
checkpoint_data = {
|
|
452
|
+
"timestamp": datetime.now().isoformat(),
|
|
453
|
+
"agent_state": self.agent_state.value,
|
|
454
|
+
"runtime_health": asdict(self.runtime_health),
|
|
455
|
+
"current_context": asdict(self.current_context) if self.current_context else None,
|
|
456
|
+
"thinking_history": [asdict(t) for t in self.thinking_history[-10:]], # Last 10
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
with open(checkpoint_file, 'w') as f:
|
|
460
|
+
json.dump(checkpoint_data, f, indent=2, default=str)
|
|
461
|
+
|
|
462
|
+
console.print(f"[green]✓ Checkpoint saved: {checkpoint_file.name}[/green]")
|
|
463
|
+
return checkpoint_file
|
|
464
|
+
|
|
465
|
+
async def restore_checkpoint(self, checkpoint_file: Path) -> bool:
|
|
466
|
+
"""Restore from a checkpoint."""
|
|
467
|
+
try:
|
|
468
|
+
with open(checkpoint_file, 'r') as f:
|
|
469
|
+
data = json.load(f)
|
|
470
|
+
|
|
471
|
+
self.agent_state = AgentState(data["agent_state"])
|
|
472
|
+
# Restore other state as needed
|
|
473
|
+
|
|
474
|
+
console.print(f"[green]✓ Restored from checkpoint: {checkpoint_file.name}[/green]")
|
|
475
|
+
return True
|
|
476
|
+
except Exception as e:
|
|
477
|
+
console.print(f"[red]Failed to restore checkpoint: {e}[/red]")
|
|
478
|
+
return False
|
|
479
|
+
|
|
480
|
+
async def monitor_loop(self):
|
|
481
|
+
"""Main monitoring loop."""
|
|
482
|
+
console.print("[cyan]Starting monitoring loop...[/cyan]")
|
|
483
|
+
|
|
484
|
+
while not self._shutdown:
|
|
485
|
+
try:
|
|
486
|
+
# Health check
|
|
487
|
+
healthy = await self.health_check()
|
|
488
|
+
|
|
489
|
+
if not healthy:
|
|
490
|
+
console.print(f"[yellow]Health check failed. State: {self.runtime_health.state.value}[/yellow]")
|
|
491
|
+
|
|
492
|
+
# Use System 2 thinking to decide what to do
|
|
493
|
+
thinking_result = await self.think(
|
|
494
|
+
f"Claude Code is {self.runtime_health.state.value}",
|
|
495
|
+
{"health": asdict(self.runtime_health)}
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
console.print(f"[cyan]Decision: {thinking_result.decision}[/cyan]")
|
|
499
|
+
console.print(f"[cyan]Confidence: {thinking_result.confidence:.2f}[/cyan]")
|
|
500
|
+
|
|
501
|
+
# Execute decision
|
|
502
|
+
if thinking_result.confidence > 0.6:
|
|
503
|
+
await self.restart_if_needed()
|
|
504
|
+
|
|
505
|
+
# Create periodic checkpoints
|
|
506
|
+
if int(time.time()) % 300 == 0: # Every 5 minutes
|
|
507
|
+
await self.create_checkpoint()
|
|
508
|
+
|
|
509
|
+
await asyncio.sleep(10) # Check every 10 seconds
|
|
510
|
+
|
|
511
|
+
except Exception as e:
|
|
512
|
+
logger.error(f"Monitor loop error: {e}")
|
|
513
|
+
await asyncio.sleep(10)
|
|
514
|
+
|
|
515
|
+
async def execute_task(self, context: AgentContext) -> bool:
|
|
516
|
+
"""Execute a task with System 2 oversight.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
context: The task context
|
|
520
|
+
"""
|
|
521
|
+
self.current_context = context
|
|
522
|
+
self.agent_state = AgentState.EXECUTING
|
|
523
|
+
|
|
524
|
+
console.print(f"[cyan]Executing task: {context.task}[/cyan]")
|
|
525
|
+
console.print(f"[cyan]Goal: {context.goal}[/cyan]")
|
|
526
|
+
|
|
527
|
+
attempts = 0
|
|
528
|
+
while attempts < context.max_attempts:
|
|
529
|
+
attempts += 1
|
|
530
|
+
console.print(f"[yellow]Attempt {attempts}/{context.max_attempts}[/yellow]")
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
# Start Claude if needed
|
|
534
|
+
if self.runtime_health.state != RuntimeState.RUNNING:
|
|
535
|
+
await self.start_claude_runtime(resume=attempts > 1)
|
|
536
|
+
|
|
537
|
+
# Execute the task (placeholder - would send to Claude)
|
|
538
|
+
# In reality, this would involve IPC with Claude Code
|
|
539
|
+
start_time = time.time()
|
|
540
|
+
|
|
541
|
+
# Simulate task execution
|
|
542
|
+
await asyncio.sleep(2)
|
|
543
|
+
|
|
544
|
+
# Check success criteria
|
|
545
|
+
success = self._evaluate_success(context)
|
|
546
|
+
|
|
547
|
+
if success:
|
|
548
|
+
console.print("[green]✓ Task completed successfully[/green]")
|
|
549
|
+
self.agent_state = AgentState.IDLE
|
|
550
|
+
return True
|
|
551
|
+
else:
|
|
552
|
+
console.print("[yellow]Task not yet complete[/yellow]")
|
|
553
|
+
|
|
554
|
+
# Use System 2 thinking to decide next action
|
|
555
|
+
thinking_result = await self.think(
|
|
556
|
+
f"Task '{context.task}' incomplete after attempt {attempts}",
|
|
557
|
+
{"context": asdict(context), "attempts": attempts}
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
if thinking_result.confidence < 0.4:
|
|
561
|
+
console.print("[red]Low confidence, aborting task[/red]")
|
|
562
|
+
break
|
|
563
|
+
|
|
564
|
+
except asyncio.TimeoutError:
|
|
565
|
+
console.print("[red]Task timed out[/red]")
|
|
566
|
+
self.agent_state = AgentState.STUCK
|
|
567
|
+
|
|
568
|
+
except Exception as e:
|
|
569
|
+
console.print(f"[red]Task error: {e}[/red]")
|
|
570
|
+
self.runtime_health.error_count += 1
|
|
571
|
+
|
|
572
|
+
self.agent_state = AgentState.IDLE
|
|
573
|
+
return False
|
|
574
|
+
|
|
575
|
+
def _evaluate_success(self, context: AgentContext) -> bool:
|
|
576
|
+
"""Evaluate if success criteria are met."""
|
|
577
|
+
# Placeholder - would check actual results
|
|
578
|
+
# In reality, this would analyze Claude's output
|
|
579
|
+
return False # For now, always require thinking
|
|
580
|
+
|
|
581
|
+
def shutdown(self):
|
|
582
|
+
"""Shutdown the orchestrator."""
|
|
583
|
+
self._shutdown = True
|
|
584
|
+
|
|
585
|
+
if self.claude_process:
|
|
586
|
+
try:
|
|
587
|
+
self.claude_process.terminate()
|
|
588
|
+
self.claude_process.wait(timeout=5)
|
|
589
|
+
except:
|
|
590
|
+
self.claude_process.kill()
|
|
591
|
+
|
|
592
|
+
console.print("[green]✓ Orchestrator shutdown complete[/green]")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class HanzoDevREPL:
|
|
596
|
+
"""REPL interface for driving Hanzo Dev orchestrator."""
|
|
597
|
+
|
|
598
|
+
def __init__(self, orchestrator: HanzoDevOrchestrator):
|
|
599
|
+
self.orchestrator = orchestrator
|
|
600
|
+
self.commands = {
|
|
601
|
+
"start": self.cmd_start,
|
|
602
|
+
"stop": self.cmd_stop,
|
|
603
|
+
"restart": self.cmd_restart,
|
|
604
|
+
"status": self.cmd_status,
|
|
605
|
+
"think": self.cmd_think,
|
|
606
|
+
"execute": self.cmd_execute,
|
|
607
|
+
"checkpoint": self.cmd_checkpoint,
|
|
608
|
+
"restore": self.cmd_restore,
|
|
609
|
+
"monitor": self.cmd_monitor,
|
|
610
|
+
"help": self.cmd_help,
|
|
611
|
+
"exit": self.cmd_exit,
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async def run(self):
|
|
615
|
+
"""Run the REPL."""
|
|
616
|
+
console.print("[bold cyan]Hanzo Dev REPL - System 2 Orchestrator[/bold cyan]")
|
|
617
|
+
console.print("Type 'help' for commands\n")
|
|
618
|
+
|
|
619
|
+
while True:
|
|
620
|
+
try:
|
|
621
|
+
command = await asyncio.get_event_loop().run_in_executor(
|
|
622
|
+
None, input, "hanzo-dev> "
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
if not command:
|
|
626
|
+
continue
|
|
627
|
+
|
|
628
|
+
parts = command.strip().split(maxsplit=1)
|
|
629
|
+
cmd = parts[0].lower()
|
|
630
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
631
|
+
|
|
632
|
+
if cmd in self.commands:
|
|
633
|
+
await self.commands[cmd](args)
|
|
634
|
+
else:
|
|
635
|
+
console.print(f"[red]Unknown command: {cmd}[/red]")
|
|
636
|
+
|
|
637
|
+
except KeyboardInterrupt:
|
|
638
|
+
console.print("\n[yellow]Use 'exit' to quit[/yellow]")
|
|
639
|
+
except Exception as e:
|
|
640
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
641
|
+
|
|
642
|
+
async def cmd_start(self, args: str):
|
|
643
|
+
"""Start Claude Code runtime."""
|
|
644
|
+
resume = "--resume" in args
|
|
645
|
+
success = await self.orchestrator.start_claude_runtime(resume=resume)
|
|
646
|
+
if success:
|
|
647
|
+
console.print("[green]Runtime started successfully[/green]")
|
|
648
|
+
else:
|
|
649
|
+
console.print("[red]Failed to start runtime[/red]")
|
|
650
|
+
|
|
651
|
+
async def cmd_stop(self, args: str):
|
|
652
|
+
"""Stop Claude Code runtime."""
|
|
653
|
+
if self.orchestrator.claude_process:
|
|
654
|
+
self.orchestrator.claude_process.terminate()
|
|
655
|
+
console.print("[yellow]Runtime stopped[/yellow]")
|
|
656
|
+
else:
|
|
657
|
+
console.print("[yellow]Runtime not running[/yellow]")
|
|
658
|
+
|
|
659
|
+
async def cmd_restart(self, args: str):
|
|
660
|
+
"""Restart Claude Code runtime."""
|
|
661
|
+
await self.cmd_stop("")
|
|
662
|
+
await asyncio.sleep(1)
|
|
663
|
+
await self.cmd_start("--resume")
|
|
664
|
+
|
|
665
|
+
async def cmd_status(self, args: str):
|
|
666
|
+
"""Show current status."""
|
|
667
|
+
table = Table(title="Hanzo Dev Status")
|
|
668
|
+
table.add_column("Property", style="cyan")
|
|
669
|
+
table.add_column("Value", style="white")
|
|
670
|
+
|
|
671
|
+
table.add_row("Agent State", self.orchestrator.agent_state.value)
|
|
672
|
+
table.add_row("Runtime State", self.orchestrator.runtime_health.state.value)
|
|
673
|
+
table.add_row("Last Response", str(self.orchestrator.runtime_health.last_response))
|
|
674
|
+
table.add_row("Response Time", f"{self.orchestrator.runtime_health.response_time_ms:.2f}ms")
|
|
675
|
+
table.add_row("Error Count", str(self.orchestrator.runtime_health.error_count))
|
|
676
|
+
table.add_row("Restart Count", str(self.orchestrator.runtime_health.restart_count))
|
|
677
|
+
|
|
678
|
+
console.print(table)
|
|
679
|
+
|
|
680
|
+
async def cmd_think(self, args: str):
|
|
681
|
+
"""Trigger System 2 thinking."""
|
|
682
|
+
if not args:
|
|
683
|
+
console.print("[red]Usage: think <problem>[/red]")
|
|
684
|
+
return
|
|
685
|
+
|
|
686
|
+
result = await self.orchestrator.think(args, {})
|
|
687
|
+
|
|
688
|
+
console.print(f"\n[bold cyan]Thinking Result:[/bold cyan]")
|
|
689
|
+
console.print(f"Decision: {result.decision}")
|
|
690
|
+
console.print(f"Confidence: {result.confidence:.2f}")
|
|
691
|
+
console.print(f"Reasoning: {', '.join(result.reasoning)}")
|
|
692
|
+
console.print(f"Risks: {', '.join(result.risks)}")
|
|
693
|
+
console.print(f"Next Steps: {', '.join(result.next_steps)}")
|
|
694
|
+
|
|
695
|
+
async def cmd_execute(self, args: str):
|
|
696
|
+
"""Execute a task."""
|
|
697
|
+
if not args:
|
|
698
|
+
console.print("[red]Usage: execute <task>[/red]")
|
|
699
|
+
return
|
|
700
|
+
|
|
701
|
+
context = AgentContext(
|
|
702
|
+
task=args,
|
|
703
|
+
goal="Complete the specified task",
|
|
704
|
+
constraints=["Stay within resource limits", "Maintain data integrity"],
|
|
705
|
+
success_criteria=["Task output is valid", "No errors occurred"],
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
success = await self.orchestrator.execute_task(context)
|
|
709
|
+
if success:
|
|
710
|
+
console.print("[green]Task executed successfully[/green]")
|
|
711
|
+
else:
|
|
712
|
+
console.print("[red]Task execution failed[/red]")
|
|
713
|
+
|
|
714
|
+
async def cmd_checkpoint(self, args: str):
|
|
715
|
+
"""Create a checkpoint."""
|
|
716
|
+
checkpoint = await self.orchestrator.create_checkpoint(args if args else None)
|
|
717
|
+
console.print(f"[green]Checkpoint created: {checkpoint.name}[/green]")
|
|
718
|
+
|
|
719
|
+
async def cmd_restore(self, args: str):
|
|
720
|
+
"""Restore from checkpoint."""
|
|
721
|
+
if not args:
|
|
722
|
+
# Show available checkpoints
|
|
723
|
+
checkpoints = list(self.orchestrator.checkpoint_dir.glob("checkpoint_*.json"))
|
|
724
|
+
if checkpoints:
|
|
725
|
+
console.print("[cyan]Available checkpoints:[/cyan]")
|
|
726
|
+
for cp in checkpoints:
|
|
727
|
+
console.print(f" - {cp.name}")
|
|
728
|
+
else:
|
|
729
|
+
console.print("[yellow]No checkpoints available[/yellow]")
|
|
730
|
+
return
|
|
731
|
+
|
|
732
|
+
checkpoint_file = self.orchestrator.checkpoint_dir / args
|
|
733
|
+
if checkpoint_file.exists():
|
|
734
|
+
success = await self.orchestrator.restore_checkpoint(checkpoint_file)
|
|
735
|
+
if success:
|
|
736
|
+
console.print("[green]Checkpoint restored[/green]")
|
|
737
|
+
else:
|
|
738
|
+
console.print(f"[red]Checkpoint not found: {args}[/red]")
|
|
739
|
+
|
|
740
|
+
async def cmd_monitor(self, args: str):
|
|
741
|
+
"""Start monitoring loop."""
|
|
742
|
+
console.print("[cyan]Starting monitor mode (Ctrl+C to stop)...[/cyan]")
|
|
743
|
+
try:
|
|
744
|
+
await self.orchestrator.monitor_loop()
|
|
745
|
+
except KeyboardInterrupt:
|
|
746
|
+
console.print("\n[yellow]Monitor stopped[/yellow]")
|
|
747
|
+
|
|
748
|
+
async def cmd_help(self, args: str):
|
|
749
|
+
"""Show help."""
|
|
750
|
+
help_text = """
|
|
751
|
+
Available commands:
|
|
752
|
+
start [--resume] - Start Claude Code runtime
|
|
753
|
+
stop - Stop Claude Code runtime
|
|
754
|
+
restart - Restart runtime with resume
|
|
755
|
+
status - Show current status
|
|
756
|
+
think <problem> - Trigger System 2 thinking
|
|
757
|
+
execute <task> - Execute a task with oversight
|
|
758
|
+
checkpoint [name] - Create a checkpoint
|
|
759
|
+
restore <name> - Restore from checkpoint
|
|
760
|
+
monitor - Start monitoring loop
|
|
761
|
+
help - Show this help
|
|
762
|
+
exit - Exit REPL
|
|
763
|
+
"""
|
|
764
|
+
console.print(help_text)
|
|
765
|
+
|
|
766
|
+
async def cmd_exit(self, args: str):
|
|
767
|
+
"""Exit the REPL."""
|
|
768
|
+
self.orchestrator.shutdown()
|
|
769
|
+
console.print("[green]Goodbye![/green]")
|
|
770
|
+
sys.exit(0)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
async def run_dev_orchestrator(**kwargs):
|
|
774
|
+
"""Run the Hanzo Dev orchestrator with multi-agent networking.
|
|
775
|
+
|
|
776
|
+
This is the main entry point from the CLI that sets up:
|
|
777
|
+
1. Configurable orchestrator (GPT-5, GPT-4, Claude, etc.)
|
|
778
|
+
2. Multiple worker agents (Claude instances for implementation)
|
|
779
|
+
3. Critic agents for System 2 thinking
|
|
780
|
+
4. MCP tool networking between instances
|
|
781
|
+
5. Code quality guardrails
|
|
782
|
+
"""
|
|
783
|
+
workspace = kwargs.get('workspace', '~/.hanzo/dev')
|
|
784
|
+
orchestrator_model = kwargs.get('orchestrator_model', 'gpt-5')
|
|
785
|
+
claude_path = kwargs.get('claude_path')
|
|
786
|
+
monitor = kwargs.get('monitor', False)
|
|
787
|
+
repl = kwargs.get('repl', True)
|
|
788
|
+
instances = kwargs.get('instances', 2)
|
|
789
|
+
mcp_tools = kwargs.get('mcp_tools', True)
|
|
790
|
+
network_mode = kwargs.get('network_mode', True)
|
|
791
|
+
guardrails = kwargs.get('guardrails', True)
|
|
792
|
+
use_network = kwargs.get('use_network', True) # Use hanzo-network if available
|
|
793
|
+
use_hanzo_net = kwargs.get('use_hanzo_net', False) # Use hanzo/net for local AI
|
|
794
|
+
hanzo_net_port = kwargs.get('hanzo_net_port', 52415)
|
|
795
|
+
console_obj = kwargs.get('console', console)
|
|
796
|
+
|
|
797
|
+
console_obj.print(f"[bold cyan]Hanzo Dev - AI Coding OS[/bold cyan]")
|
|
798
|
+
|
|
799
|
+
# Check if we should use network mode
|
|
800
|
+
if use_network and NETWORK_AVAILABLE:
|
|
801
|
+
console_obj.print(f"[cyan]Mode: Network Orchestration with hanzo-network[/cyan]")
|
|
802
|
+
console_obj.print(f"Orchestrator: {orchestrator_model}")
|
|
803
|
+
console_obj.print(f"Workers: {instances} agents")
|
|
804
|
+
console_obj.print(f"Critics: {max(1, instances // 2)} agents")
|
|
805
|
+
console_obj.print(f"MCP Tools: {'Enabled' if mcp_tools else 'Disabled'}")
|
|
806
|
+
console_obj.print(f"Guardrails: {'Enabled' if guardrails else 'Disabled'}\n")
|
|
807
|
+
|
|
808
|
+
# Create network orchestrator with configurable LLM
|
|
809
|
+
orchestrator = NetworkOrchestrator(
|
|
810
|
+
workspace_dir=workspace,
|
|
811
|
+
orchestrator_model=orchestrator_model,
|
|
812
|
+
num_workers=instances,
|
|
813
|
+
enable_mcp=mcp_tools,
|
|
814
|
+
enable_networking=network_mode,
|
|
815
|
+
enable_guardrails=guardrails,
|
|
816
|
+
use_hanzo_net=use_hanzo_net,
|
|
817
|
+
hanzo_net_port=hanzo_net_port,
|
|
818
|
+
console=console_obj
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
# Initialize the network
|
|
822
|
+
success = await orchestrator.initialize()
|
|
823
|
+
if not success:
|
|
824
|
+
console_obj.print("[red]Failed to initialize network[/red]")
|
|
825
|
+
return
|
|
826
|
+
else:
|
|
827
|
+
# Fallback to multi-Claude mode
|
|
828
|
+
console_obj.print(f"[cyan]Mode: Multi-Claude Orchestration (legacy)[/cyan]")
|
|
829
|
+
console_obj.print(f"Instances: {instances} (1 primary + {instances-1} critic{'s' if instances > 2 else ''})")
|
|
830
|
+
console_obj.print(f"MCP Tools: {'Enabled' if mcp_tools else 'Disabled'}")
|
|
831
|
+
console_obj.print(f"Networking: {'Enabled' if network_mode else 'Disabled'}")
|
|
832
|
+
console_obj.print(f"Guardrails: {'Enabled' if guardrails else 'Disabled'}\n")
|
|
833
|
+
|
|
834
|
+
orchestrator = MultiClaudeOrchestrator(
|
|
835
|
+
workspace_dir=workspace,
|
|
836
|
+
claude_path=claude_path,
|
|
837
|
+
num_instances=instances,
|
|
838
|
+
enable_mcp=mcp_tools,
|
|
839
|
+
enable_networking=network_mode,
|
|
840
|
+
enable_guardrails=guardrails,
|
|
841
|
+
console=console_obj
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
# Initialize instances
|
|
845
|
+
await orchestrator.initialize()
|
|
846
|
+
|
|
847
|
+
if monitor:
|
|
848
|
+
# Start monitoring mode
|
|
849
|
+
await orchestrator.monitor_loop()
|
|
850
|
+
elif repl:
|
|
851
|
+
# Start REPL interface
|
|
852
|
+
repl_interface = HanzoDevREPL(orchestrator)
|
|
853
|
+
await repl_interface.run()
|
|
854
|
+
else:
|
|
855
|
+
# Run once
|
|
856
|
+
await asyncio.sleep(10)
|
|
857
|
+
orchestrator.shutdown()
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
861
|
+
"""Advanced orchestrator using hanzo-network with configurable LLM (GPT-5, Claude, local, etc.)."""
|
|
862
|
+
|
|
863
|
+
def __init__(self, workspace_dir: str, orchestrator_model: str = "gpt-5",
|
|
864
|
+
num_workers: int = 2, enable_mcp: bool = True,
|
|
865
|
+
enable_networking: bool = True, enable_guardrails: bool = True,
|
|
866
|
+
use_hanzo_net: bool = False, hanzo_net_port: int = 52415,
|
|
867
|
+
console: Console = console):
|
|
868
|
+
"""Initialize network orchestrator with configurable LLM.
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
workspace_dir: Workspace directory
|
|
872
|
+
orchestrator_model: Model to use for orchestration (e.g., "gpt-5", "gpt-4", "claude-3-5-sonnet", "local:llama3.2")
|
|
873
|
+
num_workers: Number of worker agents (Claude instances)
|
|
874
|
+
enable_mcp: Enable MCP tools
|
|
875
|
+
enable_networking: Enable agent networking
|
|
876
|
+
enable_guardrails: Enable quality guardrails
|
|
877
|
+
use_hanzo_net: Use hanzo/net for local orchestration
|
|
878
|
+
hanzo_net_port: Port for hanzo/net (default 52415)
|
|
879
|
+
console: Console for output
|
|
880
|
+
"""
|
|
881
|
+
super().__init__(workspace_dir)
|
|
882
|
+
self.orchestrator_model = orchestrator_model
|
|
883
|
+
self.num_workers = num_workers
|
|
884
|
+
self.enable_mcp = enable_mcp
|
|
885
|
+
self.enable_networking = enable_networking
|
|
886
|
+
self.enable_guardrails = enable_guardrails
|
|
887
|
+
self.use_hanzo_net = use_hanzo_net
|
|
888
|
+
self.hanzo_net_port = hanzo_net_port
|
|
889
|
+
self.console = console
|
|
890
|
+
|
|
891
|
+
# Agent network components
|
|
892
|
+
self.orchestrator_agent = None
|
|
893
|
+
self.worker_agents = []
|
|
894
|
+
self.critic_agents = []
|
|
895
|
+
self.agent_network = None
|
|
896
|
+
self.hanzo_net_process = None
|
|
897
|
+
|
|
898
|
+
# Check if we can use hanzo-network
|
|
899
|
+
if not NETWORK_AVAILABLE:
|
|
900
|
+
self.console.print("[yellow]Warning: hanzo-network not available, falling back to basic mode[/yellow]")
|
|
901
|
+
|
|
902
|
+
async def initialize(self):
|
|
903
|
+
"""Initialize the agent network with orchestrator and workers."""
|
|
904
|
+
if not NETWORK_AVAILABLE:
|
|
905
|
+
self.console.print("[red]Cannot initialize network mode without hanzo-network[/red]")
|
|
906
|
+
return False
|
|
907
|
+
|
|
908
|
+
# Start hanzo net if requested for local orchestration
|
|
909
|
+
if self.use_hanzo_net or self.orchestrator_model.startswith("local:"):
|
|
910
|
+
await self._start_hanzo_net()
|
|
911
|
+
|
|
912
|
+
self.console.print(f"[cyan]Initializing agent network with {self.orchestrator_model} orchestrator...[/cyan]")
|
|
913
|
+
|
|
914
|
+
# Create orchestrator agent (GPT-5, local, or other model)
|
|
915
|
+
self.orchestrator_agent = await self._create_orchestrator_agent()
|
|
916
|
+
|
|
917
|
+
# Create worker agents (Claude instances for implementation)
|
|
918
|
+
for i in range(self.num_workers):
|
|
919
|
+
worker = await self._create_worker_agent(i)
|
|
920
|
+
self.worker_agents.append(worker)
|
|
921
|
+
|
|
922
|
+
# Add local workers if using hanzo net (for cost optimization)
|
|
923
|
+
if self.use_hanzo_net or self.orchestrator_model.startswith("local:"):
|
|
924
|
+
# Add 1-2 local workers for simple tasks
|
|
925
|
+
num_local_workers = min(2, self.num_workers)
|
|
926
|
+
for i in range(num_local_workers):
|
|
927
|
+
local_worker = await self._create_local_worker_agent(i)
|
|
928
|
+
self.worker_agents.append(local_worker)
|
|
929
|
+
self.console.print(f"[green]Added {num_local_workers} local workers for cost optimization[/green]")
|
|
930
|
+
|
|
931
|
+
# Create critic agents for System 2 thinking
|
|
932
|
+
if self.enable_guardrails:
|
|
933
|
+
for i in range(max(1, self.num_workers // 2)):
|
|
934
|
+
critic = await self._create_critic_agent(i)
|
|
935
|
+
self.critic_agents.append(critic)
|
|
936
|
+
|
|
937
|
+
# Create the agent network
|
|
938
|
+
all_agents = [self.orchestrator_agent] + self.worker_agents + self.critic_agents
|
|
939
|
+
|
|
940
|
+
# Create router based on configuration
|
|
941
|
+
if self.use_hanzo_net or self.orchestrator_model.startswith("local:"):
|
|
942
|
+
# Use cost-optimized router that prefers local models
|
|
943
|
+
router = await self._create_cost_optimized_router()
|
|
944
|
+
else:
|
|
945
|
+
# Use intelligent router with orchestrator making decisions
|
|
946
|
+
router = await self._create_intelligent_router()
|
|
947
|
+
|
|
948
|
+
# Create the network
|
|
949
|
+
self.agent_network = create_network(
|
|
950
|
+
agents=all_agents,
|
|
951
|
+
router=router,
|
|
952
|
+
default_agent=self.orchestrator_agent.name if self.orchestrator_agent else None
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
self.console.print(f"[green]✓ Agent network initialized with {len(all_agents)} agents[/green]")
|
|
956
|
+
return True
|
|
957
|
+
|
|
958
|
+
async def _start_hanzo_net(self):
|
|
959
|
+
"""Start hanzo net for local AI orchestration."""
|
|
960
|
+
self.console.print("[cyan]Starting hanzo/net for local AI orchestration...[/cyan]")
|
|
961
|
+
|
|
962
|
+
# Check if hanzo net is already running
|
|
963
|
+
import socket
|
|
964
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
965
|
+
result = sock.connect_ex(('localhost', self.hanzo_net_port))
|
|
966
|
+
sock.close()
|
|
967
|
+
|
|
968
|
+
if result == 0:
|
|
969
|
+
self.console.print(f"[yellow]hanzo/net already running on port {self.hanzo_net_port}[/yellow]")
|
|
970
|
+
return
|
|
971
|
+
|
|
972
|
+
# Start hanzo net
|
|
973
|
+
try:
|
|
974
|
+
# Determine model to serve based on orchestrator model
|
|
975
|
+
model = "llama-3.2-3b" # Default
|
|
976
|
+
if ":" in self.orchestrator_model:
|
|
977
|
+
model = self.orchestrator_model.split(":")[1]
|
|
978
|
+
|
|
979
|
+
cmd = [
|
|
980
|
+
"hanzo", "net",
|
|
981
|
+
"--port", str(self.hanzo_net_port),
|
|
982
|
+
"--models", model,
|
|
983
|
+
"--network", "local"
|
|
984
|
+
]
|
|
985
|
+
|
|
986
|
+
self.hanzo_net_process = subprocess.Popen(
|
|
987
|
+
cmd,
|
|
988
|
+
stdout=subprocess.PIPE,
|
|
989
|
+
stderr=subprocess.PIPE,
|
|
990
|
+
text=True,
|
|
991
|
+
preexec_fn=os.setsid if hasattr(os, 'setsid') else None
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
# Wait for it to start
|
|
995
|
+
await asyncio.sleep(3)
|
|
996
|
+
|
|
997
|
+
if self.hanzo_net_process.poll() is None:
|
|
998
|
+
self.console.print(f"[green]✓ hanzo/net started on port {self.hanzo_net_port} with model {model}[/green]")
|
|
999
|
+
else:
|
|
1000
|
+
self.console.print("[red]Failed to start hanzo/net[/red]")
|
|
1001
|
+
|
|
1002
|
+
except Exception as e:
|
|
1003
|
+
self.console.print(f"[red]Error starting hanzo/net: {e}[/red]")
|
|
1004
|
+
|
|
1005
|
+
async def _create_orchestrator_agent(self) -> Agent:
|
|
1006
|
+
"""Create the orchestrator agent (GPT-5, local, or configured model)."""
|
|
1007
|
+
# Check if using local model via hanzo/net
|
|
1008
|
+
if self.orchestrator_model.startswith("local:"):
|
|
1009
|
+
# Use local model via hanzo/net
|
|
1010
|
+
model_name = self.orchestrator_model.split(":")[1]
|
|
1011
|
+
|
|
1012
|
+
# Import local network helpers
|
|
1013
|
+
from hanzo_network.local_network import create_local_agent
|
|
1014
|
+
|
|
1015
|
+
orchestrator = create_local_agent(
|
|
1016
|
+
name="local_orchestrator",
|
|
1017
|
+
description=f"Local {model_name} orchestrator via hanzo/net",
|
|
1018
|
+
system=self._get_orchestrator_system_prompt(),
|
|
1019
|
+
local_model=model_name,
|
|
1020
|
+
base_url=f"http://localhost:{self.hanzo_net_port}",
|
|
1021
|
+
tools=[]
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
self.console.print(f"[green]✓ Created local {model_name} orchestrator via hanzo/net[/green]")
|
|
1025
|
+
return orchestrator
|
|
1026
|
+
|
|
1027
|
+
# Parse model string to get provider and model
|
|
1028
|
+
model_config = ModelConfig.from_string(self.orchestrator_model)
|
|
1029
|
+
|
|
1030
|
+
# Add API key from environment if needed
|
|
1031
|
+
if model_config.provider == ModelProvider.OPENAI:
|
|
1032
|
+
model_config.api_key = os.getenv("OPENAI_API_KEY")
|
|
1033
|
+
elif model_config.provider == ModelProvider.ANTHROPIC:
|
|
1034
|
+
model_config.api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
1035
|
+
elif model_config.provider == ModelProvider.GOOGLE:
|
|
1036
|
+
model_config.api_key = os.getenv("GOOGLE_API_KEY")
|
|
1037
|
+
|
|
1038
|
+
# Create orchestrator with strategic system prompt
|
|
1039
|
+
orchestrator = create_agent(
|
|
1040
|
+
name="orchestrator",
|
|
1041
|
+
description=f"{self.orchestrator_model} powered meta-orchestrator for AI coding",
|
|
1042
|
+
model=model_config,
|
|
1043
|
+
system=self._get_orchestrator_system_prompt(),
|
|
1044
|
+
tools=[] # Orchestrator tools will be added
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
self.console.print(f"[green]✓ Created {self.orchestrator_model} orchestrator[/green]")
|
|
1048
|
+
return orchestrator
|
|
1049
|
+
|
|
1050
|
+
def _get_orchestrator_system_prompt(self) -> str:
|
|
1051
|
+
"""Get the system prompt for the orchestrator."""
|
|
1052
|
+
return """You are an advanced AI orchestrator managing a network of specialized agents.
|
|
1053
|
+
Your responsibilities:
|
|
1054
|
+
1. Strategic Planning: Break down complex tasks into manageable subtasks
|
|
1055
|
+
2. Agent Coordination: Delegate work to appropriate specialist agents
|
|
1056
|
+
3. Quality Control: Ensure code quality through critic agents
|
|
1057
|
+
4. System 2 Thinking: Invoke deliberative reasoning for complex decisions
|
|
1058
|
+
5. Resource Management: Optimize agent usage for cost and performance
|
|
1059
|
+
|
|
1060
|
+
Available agents:
|
|
1061
|
+
- Worker agents: Claude instances for code implementation and MCP tool usage
|
|
1062
|
+
- Critic agents: Review and improve code quality
|
|
1063
|
+
- Local agents: Fast, cost-effective for simple tasks
|
|
1064
|
+
|
|
1065
|
+
Decision framework:
|
|
1066
|
+
- Complex reasoning → Use your advanced capabilities
|
|
1067
|
+
- Code implementation → Delegate to worker agents
|
|
1068
|
+
- Quality review → Invoke critic agents
|
|
1069
|
+
- Simple tasks → Use local agents if available
|
|
1070
|
+
|
|
1071
|
+
Always maintain high code quality standards and prevent degradation."""
|
|
1072
|
+
|
|
1073
|
+
async def _create_worker_agent(self, index: int) -> Agent:
|
|
1074
|
+
"""Create a worker agent (Claude for implementation)."""
|
|
1075
|
+
worker = create_agent(
|
|
1076
|
+
name=f"worker_{index}",
|
|
1077
|
+
description=f"Claude worker agent {index} for code implementation",
|
|
1078
|
+
model=ModelConfig(
|
|
1079
|
+
provider=ModelProvider.ANTHROPIC,
|
|
1080
|
+
model="claude-3-5-sonnet-20241022",
|
|
1081
|
+
api_key=os.getenv("ANTHROPIC_API_KEY")
|
|
1082
|
+
),
|
|
1083
|
+
system="""You are a Claude worker agent specialized in code implementation.
|
|
1084
|
+
|
|
1085
|
+
Your capabilities:
|
|
1086
|
+
- Write and modify code
|
|
1087
|
+
- Use MCP tools for file operations
|
|
1088
|
+
- Execute commands and tests
|
|
1089
|
+
- Debug and fix issues
|
|
1090
|
+
|
|
1091
|
+
Follow best practices and maintain code quality.""",
|
|
1092
|
+
tools=[] # MCP tools will be added if enabled
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
self.console.print(f" Created worker agent {index}")
|
|
1096
|
+
return worker
|
|
1097
|
+
|
|
1098
|
+
async def _create_local_worker_agent(self, index: int) -> Agent:
|
|
1099
|
+
"""Create a local worker agent for simple tasks (cost optimization)."""
|
|
1100
|
+
from hanzo_network.local_network import create_local_agent
|
|
1101
|
+
|
|
1102
|
+
worker = create_local_agent(
|
|
1103
|
+
name=f"local_worker_{index}",
|
|
1104
|
+
description=f"Local worker agent {index} for simple tasks",
|
|
1105
|
+
system="""You are a local worker agent optimized for simple tasks.
|
|
1106
|
+
|
|
1107
|
+
Your capabilities:
|
|
1108
|
+
- Simple code transformations
|
|
1109
|
+
- Basic file operations
|
|
1110
|
+
- Quick validation checks
|
|
1111
|
+
- Pattern matching
|
|
1112
|
+
|
|
1113
|
+
You handle simple tasks to reduce API costs.""",
|
|
1114
|
+
local_model="llama-3.2-3b",
|
|
1115
|
+
base_url=f"http://localhost:{self.hanzo_net_port}",
|
|
1116
|
+
tools=[]
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
self.console.print(f" Created local worker agent {index}")
|
|
1120
|
+
return worker
|
|
1121
|
+
|
|
1122
|
+
async def _create_critic_agent(self, index: int) -> Agent:
|
|
1123
|
+
"""Create a critic agent for code review."""
|
|
1124
|
+
# Use a different model for critics for diversity
|
|
1125
|
+
critic_model = "gpt-4" if index % 2 == 0 else "claude-3-5-sonnet-20241022"
|
|
1126
|
+
|
|
1127
|
+
critic = create_agent(
|
|
1128
|
+
name=f"critic_{index}",
|
|
1129
|
+
description=f"Critic agent {index} for code quality assurance",
|
|
1130
|
+
model=ModelConfig.from_string(critic_model),
|
|
1131
|
+
system="""You are a critic agent focused on code quality and best practices.
|
|
1132
|
+
|
|
1133
|
+
Review code for:
|
|
1134
|
+
1. Correctness and bug detection
|
|
1135
|
+
2. Performance optimization opportunities
|
|
1136
|
+
3. Security vulnerabilities
|
|
1137
|
+
4. Maintainability and readability
|
|
1138
|
+
5. Best practices and design patterns
|
|
1139
|
+
|
|
1140
|
+
Provide constructive feedback with specific improvement suggestions.""",
|
|
1141
|
+
tools=[]
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
self.console.print(f" Created critic agent {index} ({critic_model})")
|
|
1145
|
+
return critic
|
|
1146
|
+
|
|
1147
|
+
async def _create_cost_optimized_router(self) -> Router:
|
|
1148
|
+
"""Create a cost-optimized router that prefers local models."""
|
|
1149
|
+
from hanzo_network.core.router import Router
|
|
1150
|
+
|
|
1151
|
+
class CostOptimizedRouter(Router):
|
|
1152
|
+
"""Router that minimizes costs by using local models when possible."""
|
|
1153
|
+
|
|
1154
|
+
def __init__(self, orchestrator_agent, worker_agents, critic_agents):
|
|
1155
|
+
super().__init__()
|
|
1156
|
+
self.orchestrator = orchestrator_agent
|
|
1157
|
+
self.workers = worker_agents
|
|
1158
|
+
self.critics = critic_agents
|
|
1159
|
+
self.local_workers = [w for w in worker_agents if "local" in w.name]
|
|
1160
|
+
self.api_workers = [w for w in worker_agents if "local" not in w.name]
|
|
1161
|
+
|
|
1162
|
+
async def route(self, prompt: str, state=None) -> str:
|
|
1163
|
+
"""Route based on task complexity and cost optimization."""
|
|
1164
|
+
prompt_lower = prompt.lower()
|
|
1165
|
+
|
|
1166
|
+
# Simple tasks → Local workers
|
|
1167
|
+
simple_keywords = ["list", "check", "validate", "format", "rename", "count", "find"]
|
|
1168
|
+
if any(keyword in prompt_lower for keyword in simple_keywords) and self.local_workers:
|
|
1169
|
+
return self.local_workers[0].name
|
|
1170
|
+
|
|
1171
|
+
# Complex implementation → API workers (Claude)
|
|
1172
|
+
complex_keywords = ["implement", "refactor", "debug", "optimize", "design", "architect"]
|
|
1173
|
+
if any(keyword in prompt_lower for keyword in complex_keywords) and self.api_workers:
|
|
1174
|
+
return self.api_workers[0].name
|
|
1175
|
+
|
|
1176
|
+
# Review tasks → Critics
|
|
1177
|
+
review_keywords = ["review", "critique", "analyze", "improve", "validate code"]
|
|
1178
|
+
if any(keyword in prompt_lower for keyword in review_keywords) and self.critics:
|
|
1179
|
+
return self.critics[0].name
|
|
1180
|
+
|
|
1181
|
+
# Strategic decisions → Orchestrator
|
|
1182
|
+
strategic_keywords = ["plan", "decide", "strategy", "coordinate", "organize"]
|
|
1183
|
+
if any(keyword in prompt_lower for keyword in strategic_keywords):
|
|
1184
|
+
return self.orchestrator.name
|
|
1185
|
+
|
|
1186
|
+
# Default: Try local first, then API
|
|
1187
|
+
if self.local_workers:
|
|
1188
|
+
# For shorter prompts, try local first
|
|
1189
|
+
if len(prompt) < 500:
|
|
1190
|
+
return self.local_workers[0].name
|
|
1191
|
+
|
|
1192
|
+
# Fall back to API workers for complex tasks
|
|
1193
|
+
return self.api_workers[0].name if self.api_workers else self.orchestrator.name
|
|
1194
|
+
|
|
1195
|
+
# Create the cost-optimized router
|
|
1196
|
+
router = CostOptimizedRouter(
|
|
1197
|
+
self.orchestrator_agent,
|
|
1198
|
+
self.worker_agents,
|
|
1199
|
+
self.critic_agents
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
self.console.print("[green]✓ Created cost-optimized router (local models preferred)[/green]")
|
|
1203
|
+
return router
|
|
1204
|
+
|
|
1205
|
+
async def _create_intelligent_router(self) -> Router:
|
|
1206
|
+
"""Create an intelligent router using the orchestrator for decisions."""
|
|
1207
|
+
if self.orchestrator_agent:
|
|
1208
|
+
# Create routing agent that uses orchestrator for decisions
|
|
1209
|
+
router = create_routing_agent(
|
|
1210
|
+
agent=self.orchestrator_agent,
|
|
1211
|
+
system="""Route tasks to the most appropriate agent based on:
|
|
1212
|
+
|
|
1213
|
+
1. Task complexity and requirements
|
|
1214
|
+
2. Agent capabilities and specialization
|
|
1215
|
+
3. Current workload and availability
|
|
1216
|
+
4. Cost/performance optimization
|
|
1217
|
+
|
|
1218
|
+
Routing strategy:
|
|
1219
|
+
- Strategic decisions → Stay with orchestrator
|
|
1220
|
+
- Implementation tasks → Route to workers
|
|
1221
|
+
- Review tasks → Route to critics
|
|
1222
|
+
- Parallel work → Split across multiple agents
|
|
1223
|
+
|
|
1224
|
+
Return the name of the best agent for the task."""
|
|
1225
|
+
)
|
|
1226
|
+
else:
|
|
1227
|
+
# Fallback to basic router
|
|
1228
|
+
router = create_router(
|
|
1229
|
+
agents=self.worker_agents + self.critic_agents,
|
|
1230
|
+
default=self.worker_agents[0].name if self.worker_agents else None
|
|
1231
|
+
)
|
|
1232
|
+
|
|
1233
|
+
return router
|
|
1234
|
+
|
|
1235
|
+
async def execute_with_network(self, task: str, context: Optional[Dict] = None) -> Dict:
|
|
1236
|
+
"""Execute a task using the agent network.
|
|
1237
|
+
|
|
1238
|
+
Args:
|
|
1239
|
+
task: Task description
|
|
1240
|
+
context: Optional context
|
|
1241
|
+
|
|
1242
|
+
Returns:
|
|
1243
|
+
Execution result
|
|
1244
|
+
"""
|
|
1245
|
+
if not self.agent_network:
|
|
1246
|
+
self.console.print("[red]Agent network not initialized[/red]")
|
|
1247
|
+
return {"error": "Network not initialized"}
|
|
1248
|
+
|
|
1249
|
+
self.console.print(f"[cyan]Executing task with agent network: {task}[/cyan]")
|
|
1250
|
+
|
|
1251
|
+
# Create network state
|
|
1252
|
+
state = NetworkState()
|
|
1253
|
+
state.add_message("user", task)
|
|
1254
|
+
|
|
1255
|
+
if context:
|
|
1256
|
+
state.metadata.update(context)
|
|
1257
|
+
|
|
1258
|
+
# Run the network
|
|
1259
|
+
try:
|
|
1260
|
+
result = await self.agent_network.run(
|
|
1261
|
+
prompt=task,
|
|
1262
|
+
state=state
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
# If guardrails enabled, validate result
|
|
1266
|
+
if self.enable_guardrails and self.critic_agents:
|
|
1267
|
+
validated = await self._validate_with_critics(result, task)
|
|
1268
|
+
if validated.get("improvements"):
|
|
1269
|
+
self.console.print("[yellow]Applied critic improvements[/yellow]")
|
|
1270
|
+
return validated
|
|
1271
|
+
|
|
1272
|
+
return result
|
|
1273
|
+
|
|
1274
|
+
except Exception as e:
|
|
1275
|
+
self.console.print(f"[red]Network execution error: {e}[/red]")
|
|
1276
|
+
return {"error": str(e)}
|
|
1277
|
+
|
|
1278
|
+
async def _validate_with_critics(self, result: Dict, original_task: str) -> Dict:
|
|
1279
|
+
"""Validate and potentially improve result using critic agents."""
|
|
1280
|
+
if not self.critic_agents:
|
|
1281
|
+
return result
|
|
1282
|
+
|
|
1283
|
+
# Get first critic to review
|
|
1284
|
+
critic = self.critic_agents[0]
|
|
1285
|
+
|
|
1286
|
+
review_prompt = f"""
|
|
1287
|
+
Review this solution:
|
|
1288
|
+
|
|
1289
|
+
Task: {original_task}
|
|
1290
|
+
Solution: {result.get('output', '')}
|
|
1291
|
+
|
|
1292
|
+
Provide specific improvements if needed.
|
|
1293
|
+
"""
|
|
1294
|
+
|
|
1295
|
+
review = await critic.run(review_prompt)
|
|
1296
|
+
|
|
1297
|
+
# Check if improvements suggested
|
|
1298
|
+
if "improve" in str(review.get('output', '')).lower():
|
|
1299
|
+
result["improvements"] = review.get('output')
|
|
1300
|
+
|
|
1301
|
+
return result
|
|
1302
|
+
|
|
1303
|
+
def shutdown(self):
|
|
1304
|
+
"""Shutdown the network orchestrator and hanzo net if running."""
|
|
1305
|
+
# Stop hanzo net if we started it
|
|
1306
|
+
if self.hanzo_net_process:
|
|
1307
|
+
try:
|
|
1308
|
+
self.console.print("[yellow]Stopping hanzo/net...[/yellow]")
|
|
1309
|
+
if hasattr(os, 'killpg'):
|
|
1310
|
+
os.killpg(os.getpgid(self.hanzo_net_process.pid), signal.SIGTERM)
|
|
1311
|
+
else:
|
|
1312
|
+
self.hanzo_net_process.terminate()
|
|
1313
|
+
self.hanzo_net_process.wait(timeout=5)
|
|
1314
|
+
self.console.print("[green]✓ hanzo/net stopped[/green]")
|
|
1315
|
+
except:
|
|
1316
|
+
try:
|
|
1317
|
+
self.hanzo_net_process.kill()
|
|
1318
|
+
except:
|
|
1319
|
+
pass
|
|
1320
|
+
|
|
1321
|
+
# Call parent shutdown
|
|
1322
|
+
super().shutdown()
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
|
1326
|
+
"""Extended orchestrator for multiple Claude instances with MCP networking."""
|
|
1327
|
+
|
|
1328
|
+
def __init__(self, workspace_dir: str, claude_path: str, num_instances: int,
|
|
1329
|
+
enable_mcp: bool, enable_networking: bool, enable_guardrails: bool,
|
|
1330
|
+
console: Console):
|
|
1331
|
+
super().__init__(workspace_dir, claude_path)
|
|
1332
|
+
self.num_instances = num_instances
|
|
1333
|
+
self.enable_mcp = enable_mcp
|
|
1334
|
+
self.enable_networking = enable_networking
|
|
1335
|
+
self.enable_guardrails = enable_guardrails
|
|
1336
|
+
self.console = console
|
|
1337
|
+
|
|
1338
|
+
# Store multiple Claude instances
|
|
1339
|
+
self.claude_instances = []
|
|
1340
|
+
self.instance_configs = []
|
|
1341
|
+
|
|
1342
|
+
async def initialize(self):
|
|
1343
|
+
"""Initialize all Claude instances with MCP networking."""
|
|
1344
|
+
self.console.print("[cyan]Initializing Claude instances...[/cyan]")
|
|
1345
|
+
|
|
1346
|
+
for i in range(self.num_instances):
|
|
1347
|
+
role = "primary" if i == 0 else f"critic_{i}"
|
|
1348
|
+
config = await self._create_instance_config(i, role)
|
|
1349
|
+
self.instance_configs.append(config)
|
|
1350
|
+
|
|
1351
|
+
self.console.print(f" [{i+1}/{self.num_instances}] {role} instance configured")
|
|
1352
|
+
|
|
1353
|
+
# If networking enabled, configure MCP connections between instances
|
|
1354
|
+
if self.enable_networking:
|
|
1355
|
+
await self._setup_mcp_networking()
|
|
1356
|
+
|
|
1357
|
+
# Start all instances
|
|
1358
|
+
for i, config in enumerate(self.instance_configs):
|
|
1359
|
+
success = await self._start_claude_instance(i, config)
|
|
1360
|
+
if success:
|
|
1361
|
+
self.console.print(f"[green]✓ Instance {i} started[/green]")
|
|
1362
|
+
else:
|
|
1363
|
+
self.console.print(f"[red]✗ Failed to start instance {i}[/red]")
|
|
1364
|
+
|
|
1365
|
+
async def _create_instance_config(self, index: int, role: str) -> Dict:
|
|
1366
|
+
"""Create configuration for a Claude instance."""
|
|
1367
|
+
base_port = 8000
|
|
1368
|
+
mcp_port = 9000
|
|
1369
|
+
|
|
1370
|
+
config = {
|
|
1371
|
+
"index": index,
|
|
1372
|
+
"role": role,
|
|
1373
|
+
"workspace": self.workspace_dir / f"instance_{index}",
|
|
1374
|
+
"port": base_port + index,
|
|
1375
|
+
"mcp_port": mcp_port + index,
|
|
1376
|
+
"mcp_config": {},
|
|
1377
|
+
"env": {}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
# Create workspace directory
|
|
1381
|
+
config["workspace"].mkdir(parents=True, exist_ok=True)
|
|
1382
|
+
|
|
1383
|
+
# Configure MCP tools if enabled
|
|
1384
|
+
if self.enable_mcp:
|
|
1385
|
+
config["mcp_config"] = await self._create_mcp_config(index, role)
|
|
1386
|
+
|
|
1387
|
+
return config
|
|
1388
|
+
|
|
1389
|
+
async def _create_mcp_config(self, index: int, role: str) -> Dict:
|
|
1390
|
+
"""Create MCP configuration for an instance."""
|
|
1391
|
+
mcp_config = {
|
|
1392
|
+
"mcpServers": {
|
|
1393
|
+
"hanzo-mcp": {
|
|
1394
|
+
"command": "python",
|
|
1395
|
+
"args": ["-m", "hanzo_mcp"],
|
|
1396
|
+
"env": {
|
|
1397
|
+
"INSTANCE_ID": str(index),
|
|
1398
|
+
"INSTANCE_ROLE": role
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
# Add file system tools
|
|
1405
|
+
mcp_config["mcpServers"]["filesystem"] = {
|
|
1406
|
+
"command": "npx",
|
|
1407
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem"],
|
|
1408
|
+
"env": {
|
|
1409
|
+
"ALLOWED_DIRECTORIES": str(self.workspace_dir)
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
return mcp_config
|
|
1414
|
+
|
|
1415
|
+
async def _setup_mcp_networking(self):
|
|
1416
|
+
"""Set up MCP networking between Claude instances."""
|
|
1417
|
+
self.console.print("[cyan]Setting up MCP networking between instances...[/cyan]")
|
|
1418
|
+
|
|
1419
|
+
# Each instance gets MCP servers for all other instances
|
|
1420
|
+
for i, config in enumerate(self.instance_configs):
|
|
1421
|
+
for j, other_config in enumerate(self.instance_configs):
|
|
1422
|
+
if i != j:
|
|
1423
|
+
# Add other instance as MCP server
|
|
1424
|
+
server_name = f"claude_instance_{j}"
|
|
1425
|
+
config["mcp_config"]["mcpServers"][server_name] = {
|
|
1426
|
+
"command": "python",
|
|
1427
|
+
"args": [
|
|
1428
|
+
"-m", "hanzo_mcp.bridge",
|
|
1429
|
+
"--target-port", str(other_config["port"]),
|
|
1430
|
+
"--instance-id", str(j),
|
|
1431
|
+
"--role", other_config["role"]
|
|
1432
|
+
],
|
|
1433
|
+
"env": {
|
|
1434
|
+
"SOURCE_INSTANCE": str(i),
|
|
1435
|
+
"TARGET_INSTANCE": str(j)
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
# Save MCP config
|
|
1440
|
+
mcp_config_file = config["workspace"] / "mcp_config.json"
|
|
1441
|
+
with open(mcp_config_file, 'w') as f:
|
|
1442
|
+
json.dump(config["mcp_config"], f, indent=2)
|
|
1443
|
+
|
|
1444
|
+
config["env"]["MCP_CONFIG_PATH"] = str(mcp_config_file)
|
|
1445
|
+
|
|
1446
|
+
async def _start_claude_instance(self, index: int, config: Dict) -> bool:
|
|
1447
|
+
"""Start a single Claude instance."""
|
|
1448
|
+
try:
|
|
1449
|
+
cmd = [self.claude_code_path or "claude"]
|
|
1450
|
+
|
|
1451
|
+
# Add configuration flags
|
|
1452
|
+
if config.get("env", {}).get("MCP_CONFIG_PATH"):
|
|
1453
|
+
cmd.extend(["--mcp-config", config["env"]["MCP_CONFIG_PATH"]])
|
|
1454
|
+
|
|
1455
|
+
# Set up environment
|
|
1456
|
+
env = os.environ.copy()
|
|
1457
|
+
env.update(config.get("env", {}))
|
|
1458
|
+
|
|
1459
|
+
# Start process
|
|
1460
|
+
process = subprocess.Popen(
|
|
1461
|
+
cmd,
|
|
1462
|
+
stdout=subprocess.PIPE,
|
|
1463
|
+
stderr=subprocess.PIPE,
|
|
1464
|
+
stdin=subprocess.PIPE,
|
|
1465
|
+
env=env,
|
|
1466
|
+
cwd=str(config["workspace"]),
|
|
1467
|
+
preexec_fn=os.setsid if hasattr(os, 'setsid') else None
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
self.claude_instances.append({
|
|
1471
|
+
"index": index,
|
|
1472
|
+
"role": config["role"],
|
|
1473
|
+
"process": process,
|
|
1474
|
+
"config": config,
|
|
1475
|
+
"health": RuntimeHealth(
|
|
1476
|
+
state=RuntimeState.RUNNING,
|
|
1477
|
+
last_response=datetime.now(),
|
|
1478
|
+
response_time_ms=0,
|
|
1479
|
+
memory_usage_mb=0,
|
|
1480
|
+
cpu_percent=0,
|
|
1481
|
+
error_count=0,
|
|
1482
|
+
restart_count=0
|
|
1483
|
+
)
|
|
1484
|
+
})
|
|
1485
|
+
|
|
1486
|
+
return True
|
|
1487
|
+
|
|
1488
|
+
except Exception as e:
|
|
1489
|
+
logger.error(f"Failed to start instance {index}: {e}")
|
|
1490
|
+
return False
|
|
1491
|
+
|
|
1492
|
+
async def execute_with_critique(self, task: str) -> Dict:
|
|
1493
|
+
"""Execute a task with System 2 critique.
|
|
1494
|
+
|
|
1495
|
+
1. Primary instance executes the task
|
|
1496
|
+
2. Critic instance(s) review and suggest improvements
|
|
1497
|
+
3. Primary incorporates feedback if confidence is high
|
|
1498
|
+
"""
|
|
1499
|
+
self.console.print(f"[cyan]Executing with System 2 thinking: {task}[/cyan]")
|
|
1500
|
+
|
|
1501
|
+
# Step 1: Primary execution
|
|
1502
|
+
primary = self.claude_instances[0]
|
|
1503
|
+
result = await self._send_to_instance(primary, task)
|
|
1504
|
+
|
|
1505
|
+
if self.num_instances < 2:
|
|
1506
|
+
return result
|
|
1507
|
+
|
|
1508
|
+
# Step 2: Critic review
|
|
1509
|
+
critiques = []
|
|
1510
|
+
for critic in self.claude_instances[1:]:
|
|
1511
|
+
critique_prompt = f"""
|
|
1512
|
+
Review this code/solution and provide constructive criticism:
|
|
1513
|
+
|
|
1514
|
+
Task: {task}
|
|
1515
|
+
Solution: {result.get('output', '')}
|
|
1516
|
+
|
|
1517
|
+
Evaluate for:
|
|
1518
|
+
1. Correctness
|
|
1519
|
+
2. Performance
|
|
1520
|
+
3. Security
|
|
1521
|
+
4. Maintainability
|
|
1522
|
+
5. Best practices
|
|
1523
|
+
|
|
1524
|
+
Suggest specific improvements.
|
|
1525
|
+
"""
|
|
1526
|
+
|
|
1527
|
+
critique = await self._send_to_instance(critic, critique_prompt)
|
|
1528
|
+
critiques.append(critique)
|
|
1529
|
+
|
|
1530
|
+
# Step 3: Incorporate feedback if valuable
|
|
1531
|
+
if critiques and self.enable_guardrails:
|
|
1532
|
+
improvement_prompt = f"""
|
|
1533
|
+
Original task: {task}
|
|
1534
|
+
Original solution: {result.get('output', '')}
|
|
1535
|
+
|
|
1536
|
+
Critiques received:
|
|
1537
|
+
{json.dumps(critiques, indent=2)}
|
|
1538
|
+
|
|
1539
|
+
Incorporate the valid suggestions and produce an improved solution.
|
|
1540
|
+
"""
|
|
1541
|
+
|
|
1542
|
+
improved = await self._send_to_instance(primary, improvement_prompt)
|
|
1543
|
+
|
|
1544
|
+
# Validate improvement didn't degrade quality
|
|
1545
|
+
if await self._validate_improvement(result, improved):
|
|
1546
|
+
self.console.print("[green]✓ Solution improved with System 2 feedback[/green]")
|
|
1547
|
+
return improved
|
|
1548
|
+
else:
|
|
1549
|
+
self.console.print("[yellow]⚠ Keeping original solution (improvement validation failed)[/yellow]")
|
|
1550
|
+
|
|
1551
|
+
return result
|
|
1552
|
+
|
|
1553
|
+
async def _send_to_instance(self, instance: Dict, prompt: str) -> Dict:
|
|
1554
|
+
"""Send a prompt to a specific Claude instance."""
|
|
1555
|
+
# This would use the actual Claude API or IPC mechanism
|
|
1556
|
+
# For now, it's a placeholder
|
|
1557
|
+
return {
|
|
1558
|
+
"output": f"Response from {instance['role']}: Processed '{prompt[:50]}...'",
|
|
1559
|
+
"success": True
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async def _validate_improvement(self, original: Dict, improved: Dict) -> bool:
|
|
1563
|
+
"""Validate that an improvement doesn't degrade quality."""
|
|
1564
|
+
if not self.enable_guardrails:
|
|
1565
|
+
return True
|
|
1566
|
+
|
|
1567
|
+
# Placeholder for actual validation logic
|
|
1568
|
+
# Would check: tests still pass, no new errors, performance not degraded, etc.
|
|
1569
|
+
return True
|
|
1570
|
+
|
|
1571
|
+
def shutdown(self):
|
|
1572
|
+
"""Shutdown all Claude instances."""
|
|
1573
|
+
self.console.print("[yellow]Shutting down all instances...[/yellow]")
|
|
1574
|
+
|
|
1575
|
+
for instance in self.claude_instances:
|
|
1576
|
+
try:
|
|
1577
|
+
process = instance["process"]
|
|
1578
|
+
if hasattr(os, 'killpg'):
|
|
1579
|
+
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
|
1580
|
+
else:
|
|
1581
|
+
process.terminate()
|
|
1582
|
+
process.wait(timeout=5)
|
|
1583
|
+
except:
|
|
1584
|
+
try:
|
|
1585
|
+
instance["process"].kill()
|
|
1586
|
+
except:
|
|
1587
|
+
pass
|
|
1588
|
+
|
|
1589
|
+
self.console.print("[green]✓ All instances shut down[/green]")
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
async def main():
|
|
1593
|
+
"""Main entry point for hanzo-dev."""
|
|
1594
|
+
import argparse
|
|
1595
|
+
|
|
1596
|
+
parser = argparse.ArgumentParser(description="Hanzo Dev - System 2 Meta-AI Orchestrator")
|
|
1597
|
+
parser.add_argument("--workspace", default="~/.hanzo/dev", help="Workspace directory")
|
|
1598
|
+
parser.add_argument("--claude-path", help="Path to Claude Code executable")
|
|
1599
|
+
parser.add_argument("--monitor", action="store_true", help="Start in monitor mode")
|
|
1600
|
+
parser.add_argument("--repl", action="store_true", help="Start REPL interface")
|
|
1601
|
+
parser.add_argument("--instances", type=int, default=2, help="Number of Claude instances")
|
|
1602
|
+
parser.add_argument("--no-mcp", action="store_true", help="Disable MCP tools")
|
|
1603
|
+
parser.add_argument("--no-network", action="store_true", help="Disable instance networking")
|
|
1604
|
+
parser.add_argument("--no-guardrails", action="store_true", help="Disable guardrails")
|
|
1605
|
+
|
|
1606
|
+
args = parser.parse_args()
|
|
1607
|
+
|
|
1608
|
+
await run_dev_orchestrator(
|
|
1609
|
+
workspace=args.workspace,
|
|
1610
|
+
claude_path=args.claude_path,
|
|
1611
|
+
monitor=args.monitor,
|
|
1612
|
+
repl=args.repl or not args.monitor,
|
|
1613
|
+
instances=args.instances,
|
|
1614
|
+
mcp_tools=not args.no_mcp,
|
|
1615
|
+
network_mode=not args.no_network,
|
|
1616
|
+
guardrails=not args.no_guardrails
|
|
1617
|
+
)
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
if __name__ == "__main__":
|
|
1621
|
+
asyncio.run(main())
|