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