hanzo 0.3.12__py3-none-any.whl → 0.3.14__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 +1217 -457
- hanzo/orchestrator_config.py +319 -0
- {hanzo-0.3.12.dist-info → hanzo-0.3.14.dist-info}/METADATA +3 -1
- {hanzo-0.3.12.dist-info → hanzo-0.3.14.dist-info}/RECORD +7 -6
- {hanzo-0.3.12.dist-info → hanzo-0.3.14.dist-info}/WHEEL +0 -0
- {hanzo-0.3.12.dist-info → hanzo-0.3.14.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
|
-
|
|
635
|
-
|
|
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 ""
|
|
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())
|
|
636
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,617 @@ 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
|
+
# Check for CLI tools and free/local options first
|
|
903
|
+
if self.orchestrator.orchestrator_model in ["codex", "openai-cli", "openai-codex"]:
|
|
904
|
+
# Use OpenAI CLI (Codex)
|
|
905
|
+
await self._use_openai_cli(message)
|
|
906
|
+
return
|
|
907
|
+
elif self.orchestrator.orchestrator_model in ["claude", "claude-code", "claude-desktop"]:
|
|
908
|
+
# Use Claude Desktop/Code
|
|
909
|
+
await self._use_claude_cli(message)
|
|
910
|
+
return
|
|
911
|
+
elif self.orchestrator.orchestrator_model in ["gemini", "gemini-cli", "google-gemini"]:
|
|
912
|
+
# Use Gemini CLI
|
|
913
|
+
await self._use_gemini_cli(message)
|
|
914
|
+
return
|
|
915
|
+
elif self.orchestrator.orchestrator_model in ["hanzo-ide", "hanzo-dev-ide", "ide"]:
|
|
916
|
+
# Use Hanzo Dev IDE from ~/work/hanzo/ide
|
|
917
|
+
await self._use_hanzo_ide(message)
|
|
918
|
+
return
|
|
919
|
+
elif self.orchestrator.orchestrator_model in ["codestral", "codestral-free", "free", "mistral-free"]:
|
|
920
|
+
# Use free Mistral Codestral API
|
|
921
|
+
await self._use_free_codestral(message)
|
|
922
|
+
return
|
|
923
|
+
elif self.orchestrator.orchestrator_model in ["starcoder", "starcoder2", "free-starcoder"]:
|
|
924
|
+
# Use free StarCoder via HuggingFace
|
|
925
|
+
await self._use_free_starcoder(message)
|
|
926
|
+
return
|
|
927
|
+
elif self.orchestrator.orchestrator_model.startswith("local:"):
|
|
928
|
+
# Use local model via Ollama or LM Studio
|
|
929
|
+
await self._use_local_model(message)
|
|
930
|
+
return
|
|
931
|
+
|
|
932
|
+
# Try OpenAI first
|
|
933
|
+
if os.getenv("OPENAI_API_KEY"):
|
|
934
|
+
try:
|
|
935
|
+
from openai import AsyncOpenAI
|
|
936
|
+
|
|
937
|
+
client = AsyncOpenAI()
|
|
938
|
+
response = await client.chat.completions.create(
|
|
939
|
+
model=self.orchestrator.orchestrator_model or "gpt-4",
|
|
940
|
+
messages=[
|
|
941
|
+
{"role": "system", "content": "You are a helpful AI coding assistant."},
|
|
942
|
+
{"role": "user", "content": message}
|
|
943
|
+
],
|
|
944
|
+
temperature=0.7,
|
|
945
|
+
max_tokens=2000
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
if response.choices:
|
|
949
|
+
console.print(f"[cyan]AI:[/cyan] {response.choices[0].message.content}")
|
|
950
|
+
return
|
|
951
|
+
|
|
952
|
+
except Exception as e:
|
|
953
|
+
console.print(f"[yellow]OpenAI error: {e}[/yellow]")
|
|
954
|
+
|
|
955
|
+
# Try Anthropic
|
|
956
|
+
if os.getenv("ANTHROPIC_API_KEY"):
|
|
957
|
+
try:
|
|
958
|
+
from anthropic import AsyncAnthropic
|
|
959
|
+
|
|
960
|
+
client = AsyncAnthropic()
|
|
961
|
+
response = await client.messages.create(
|
|
962
|
+
model="claude-3-5-sonnet-20241022",
|
|
963
|
+
messages=[{"role": "user", "content": message}],
|
|
964
|
+
max_tokens=2000
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
if response.content:
|
|
968
|
+
console.print(f"[cyan]AI:[/cyan] {response.content[0].text}")
|
|
969
|
+
return
|
|
970
|
+
|
|
971
|
+
except Exception as e:
|
|
972
|
+
console.print(f"[yellow]Anthropic error: {e}[/yellow]")
|
|
973
|
+
|
|
974
|
+
# No API keys available
|
|
975
|
+
console.print("[red]No AI API keys configured![/red]")
|
|
976
|
+
console.print("[yellow]Try these options that don't need your API key:[/yellow]")
|
|
977
|
+
console.print("\n[bold]CLI Tools (use existing tools):[/bold]")
|
|
978
|
+
console.print(" • hanzo dev --orchestrator codex # OpenAI CLI (if installed)")
|
|
979
|
+
console.print(" • hanzo dev --orchestrator claude # Claude Desktop (if installed)")
|
|
980
|
+
console.print(" • hanzo dev --orchestrator gemini # Gemini CLI (if installed)")
|
|
981
|
+
console.print(" • hanzo dev --orchestrator hanzo-ide # Hanzo IDE from ~/work/hanzo/ide")
|
|
982
|
+
console.print("\n[bold]Free APIs (rate limited):[/bold]")
|
|
983
|
+
console.print(" • hanzo dev --orchestrator codestral # Free Mistral Codestral")
|
|
984
|
+
console.print(" • hanzo dev --orchestrator starcoder # Free StarCoder")
|
|
985
|
+
console.print("\n[bold]Local Models (unlimited):[/bold]")
|
|
986
|
+
console.print(" • hanzo dev --orchestrator local:llama3.2 # Via Ollama")
|
|
987
|
+
console.print(" • hanzo dev --orchestrator local:codellama # Via Ollama")
|
|
988
|
+
console.print(" • hanzo dev --orchestrator local:mistral # Via Ollama")
|
|
989
|
+
console.print("\n[dim]Or set API keys for full access:[/dim]")
|
|
990
|
+
console.print(" • export OPENAI_API_KEY=sk-...")
|
|
991
|
+
console.print(" • export ANTHROPIC_API_KEY=sk-ant-...")
|
|
992
|
+
|
|
993
|
+
async def _use_free_codestral(self, message: str):
|
|
994
|
+
"""Use free Mistral Codestral API (no API key needed for trial)."""
|
|
995
|
+
try:
|
|
996
|
+
import httpx
|
|
997
|
+
|
|
998
|
+
console.print("[dim]Using free Codestral API (rate limited)...[/dim]")
|
|
999
|
+
|
|
1000
|
+
async with httpx.AsyncClient() as client:
|
|
1001
|
+
# Mistral offers free tier with rate limits
|
|
1002
|
+
response = await client.post(
|
|
1003
|
+
"https://api.mistral.ai/v1/chat/completions",
|
|
1004
|
+
headers={
|
|
1005
|
+
"Content-Type": "application/json",
|
|
1006
|
+
# Free tier doesn't need API key for limited usage
|
|
1007
|
+
},
|
|
1008
|
+
json={
|
|
1009
|
+
"model": "codestral-latest",
|
|
1010
|
+
"messages": [
|
|
1011
|
+
{"role": "system", "content": "You are Codestral, an AI coding assistant."},
|
|
1012
|
+
{"role": "user", "content": message}
|
|
1013
|
+
],
|
|
1014
|
+
"temperature": 0.7,
|
|
1015
|
+
"max_tokens": 2000
|
|
1016
|
+
},
|
|
1017
|
+
timeout=30.0
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
if response.status_code == 200:
|
|
1021
|
+
data = response.json()
|
|
1022
|
+
if data.get("choices"):
|
|
1023
|
+
console.print(f"[cyan]Codestral:[/cyan] {data['choices'][0]['message']['content']}")
|
|
1024
|
+
else:
|
|
1025
|
+
console.print("[yellow]Free tier limit reached. Try local models instead:[/yellow]")
|
|
1026
|
+
console.print(" • Install Ollama: curl -fsSL https://ollama.com/install.sh | sh")
|
|
1027
|
+
console.print(" • Run: ollama pull codellama")
|
|
1028
|
+
console.print(" • Use: hanzo dev --orchestrator local:codellama")
|
|
1029
|
+
|
|
1030
|
+
except Exception as e:
|
|
1031
|
+
console.print(f"[red]Codestral error: {e}[/red]")
|
|
1032
|
+
console.print("[yellow]Try local models instead (no limits):[/yellow]")
|
|
1033
|
+
console.print(" • hanzo dev --orchestrator local:codellama")
|
|
1034
|
+
|
|
1035
|
+
async def _use_free_starcoder(self, message: str):
|
|
1036
|
+
"""Use free StarCoder via HuggingFace Inference API."""
|
|
1037
|
+
try:
|
|
1038
|
+
import httpx
|
|
1039
|
+
|
|
1040
|
+
console.print("[dim]Using free StarCoder API...[/dim]")
|
|
1041
|
+
|
|
1042
|
+
async with httpx.AsyncClient() as client:
|
|
1043
|
+
# HuggingFace offers free inference API
|
|
1044
|
+
response = await client.post(
|
|
1045
|
+
"https://api-inference.huggingface.co/models/bigcode/starcoder2-15b",
|
|
1046
|
+
headers={
|
|
1047
|
+
"Content-Type": "application/json",
|
|
1048
|
+
},
|
|
1049
|
+
json={
|
|
1050
|
+
"inputs": f"<|system|>You are StarCoder, an AI coding assistant.<|end|>\n<|user|>{message}<|end|>\n<|assistant|>",
|
|
1051
|
+
"parameters": {
|
|
1052
|
+
"temperature": 0.7,
|
|
1053
|
+
"max_new_tokens": 2000,
|
|
1054
|
+
"return_full_text": False
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
timeout=30.0
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
if response.status_code == 200:
|
|
1061
|
+
data = response.json()
|
|
1062
|
+
if isinstance(data, list) and data:
|
|
1063
|
+
console.print(f"[cyan]StarCoder:[/cyan] {data[0].get('generated_text', '')}")
|
|
1064
|
+
else:
|
|
1065
|
+
console.print("[yellow]API limit reached. Install local models:[/yellow]")
|
|
1066
|
+
console.print(" • brew install ollama")
|
|
1067
|
+
console.print(" • ollama pull starcoder2")
|
|
1068
|
+
console.print(" • hanzo dev --orchestrator local:starcoder2")
|
|
1069
|
+
|
|
1070
|
+
except Exception as e:
|
|
1071
|
+
console.print(f"[red]StarCoder error: {e}[/red]")
|
|
1072
|
+
|
|
1073
|
+
async def _use_openai_cli(self, message: str):
|
|
1074
|
+
"""Use OpenAI CLI (Codex) - the official OpenAI CLI tool."""
|
|
1075
|
+
try:
|
|
1076
|
+
import subprocess
|
|
1077
|
+
import json
|
|
1078
|
+
|
|
1079
|
+
console.print("[dim]Using OpenAI CLI (Codex)...[/dim]")
|
|
1080
|
+
|
|
1081
|
+
# Check if openai CLI is installed
|
|
1082
|
+
result = subprocess.run(["which", "openai"], capture_output=True, text=True)
|
|
1083
|
+
if result.returncode != 0:
|
|
1084
|
+
console.print("[red]OpenAI CLI not installed![/red]")
|
|
1085
|
+
console.print("[yellow]To install:[/yellow]")
|
|
1086
|
+
console.print(" • pip install openai-cli")
|
|
1087
|
+
console.print(" • openai login")
|
|
1088
|
+
console.print("Then use: hanzo dev --orchestrator codex")
|
|
1089
|
+
return
|
|
1090
|
+
|
|
1091
|
+
# Use openai CLI to chat
|
|
1092
|
+
cmd = ["openai", "api", "chat", "-m", "gpt-4", "-p", message]
|
|
1093
|
+
|
|
1094
|
+
process = subprocess.Popen(
|
|
1095
|
+
cmd,
|
|
1096
|
+
stdout=subprocess.PIPE,
|
|
1097
|
+
stderr=subprocess.PIPE,
|
|
1098
|
+
text=True
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
stdout, stderr = process.communicate(timeout=30)
|
|
1102
|
+
|
|
1103
|
+
if process.returncode == 0 and stdout:
|
|
1104
|
+
console.print(f"[cyan]Codex:[/cyan] {stdout.strip()}")
|
|
1105
|
+
else:
|
|
1106
|
+
console.print(f"[red]OpenAI CLI error: {stderr}[/red]")
|
|
1107
|
+
|
|
1108
|
+
except subprocess.TimeoutExpired:
|
|
1109
|
+
console.print("[yellow]OpenAI CLI timed out[/yellow]")
|
|
1110
|
+
except Exception as e:
|
|
1111
|
+
console.print(f"[red]Error using OpenAI CLI: {e}[/red]")
|
|
1112
|
+
|
|
1113
|
+
async def _use_claude_cli(self, message: str):
|
|
1114
|
+
"""Use Claude Desktop/Code CLI."""
|
|
1115
|
+
try:
|
|
1116
|
+
import subprocess
|
|
1117
|
+
import os
|
|
1118
|
+
|
|
1119
|
+
console.print("[dim]Using Claude Desktop...[/dim]")
|
|
1120
|
+
|
|
1121
|
+
# Check for Claude Code or Claude Desktop
|
|
1122
|
+
claude_paths = [
|
|
1123
|
+
"/usr/local/bin/claude",
|
|
1124
|
+
"/Applications/Claude.app/Contents/MacOS/Claude",
|
|
1125
|
+
os.path.expanduser("~/Applications/Claude.app/Contents/MacOS/Claude"),
|
|
1126
|
+
"claude", # In PATH
|
|
1127
|
+
]
|
|
1128
|
+
|
|
1129
|
+
claude_path = None
|
|
1130
|
+
for path in claude_paths:
|
|
1131
|
+
if os.path.exists(path) or subprocess.run(["which", path], capture_output=True).returncode == 0:
|
|
1132
|
+
claude_path = path
|
|
1133
|
+
break
|
|
1134
|
+
|
|
1135
|
+
if not claude_path:
|
|
1136
|
+
console.print("[red]Claude Desktop not found![/red]")
|
|
1137
|
+
console.print("[yellow]To install:[/yellow]")
|
|
1138
|
+
console.print(" • Download from https://claude.ai/desktop")
|
|
1139
|
+
console.print(" • Or: brew install --cask claude")
|
|
1140
|
+
console.print("Then use: hanzo dev --orchestrator claude")
|
|
1141
|
+
return
|
|
1142
|
+
|
|
1143
|
+
# Send message to Claude via CLI or AppleScript on macOS
|
|
1144
|
+
if sys.platform == "darwin":
|
|
1145
|
+
# Use AppleScript to interact with Claude Desktop
|
|
1146
|
+
script = f'''
|
|
1147
|
+
tell application "Claude"
|
|
1148
|
+
activate
|
|
1149
|
+
delay 0.5
|
|
1150
|
+
tell application "System Events"
|
|
1151
|
+
keystroke "{message.replace('"', '\\"')}"
|
|
1152
|
+
key code 36 -- Enter key
|
|
1153
|
+
end tell
|
|
1154
|
+
end tell
|
|
1155
|
+
'''
|
|
1156
|
+
|
|
1157
|
+
subprocess.run(["osascript", "-e", script])
|
|
1158
|
+
console.print("[cyan]Sent to Claude Desktop. Check the app for response.[/cyan]")
|
|
1159
|
+
else:
|
|
1160
|
+
# Try direct CLI invocation
|
|
1161
|
+
process = subprocess.Popen(
|
|
1162
|
+
[claude_path, "--message", message],
|
|
1163
|
+
stdout=subprocess.PIPE,
|
|
1164
|
+
stderr=subprocess.PIPE,
|
|
1165
|
+
text=True
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
stdout, stderr = process.communicate(timeout=30)
|
|
1169
|
+
|
|
1170
|
+
if stdout:
|
|
1171
|
+
console.print(f"[cyan]Claude:[/cyan] {stdout.strip()}")
|
|
1172
|
+
|
|
1173
|
+
except Exception as e:
|
|
1174
|
+
console.print(f"[red]Error using Claude Desktop: {e}[/red]")
|
|
1175
|
+
|
|
1176
|
+
async def _use_gemini_cli(self, message: str):
|
|
1177
|
+
"""Use Gemini CLI."""
|
|
1178
|
+
try:
|
|
1179
|
+
import subprocess
|
|
1180
|
+
|
|
1181
|
+
console.print("[dim]Using Gemini CLI...[/dim]")
|
|
1182
|
+
|
|
1183
|
+
# Check if gemini CLI is installed
|
|
1184
|
+
result = subprocess.run(["which", "gemini"], capture_output=True, text=True)
|
|
1185
|
+
if result.returncode != 0:
|
|
1186
|
+
console.print("[red]Gemini CLI not installed![/red]")
|
|
1187
|
+
console.print("[yellow]To install:[/yellow]")
|
|
1188
|
+
console.print(" • pip install google-generativeai-cli")
|
|
1189
|
+
console.print(" • gemini configure")
|
|
1190
|
+
console.print(" • Set GOOGLE_API_KEY environment variable")
|
|
1191
|
+
console.print("Then use: hanzo dev --orchestrator gemini")
|
|
1192
|
+
return
|
|
1193
|
+
|
|
1194
|
+
# Use gemini CLI
|
|
1195
|
+
cmd = ["gemini", "chat", message]
|
|
1196
|
+
|
|
1197
|
+
process = subprocess.Popen(
|
|
1198
|
+
cmd,
|
|
1199
|
+
stdout=subprocess.PIPE,
|
|
1200
|
+
stderr=subprocess.PIPE,
|
|
1201
|
+
text=True
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
stdout, stderr = process.communicate(timeout=30)
|
|
1205
|
+
|
|
1206
|
+
if process.returncode == 0 and stdout:
|
|
1207
|
+
console.print(f"[cyan]Gemini:[/cyan] {stdout.strip()}")
|
|
1208
|
+
else:
|
|
1209
|
+
console.print(f"[red]Gemini CLI error: {stderr}[/red]")
|
|
1210
|
+
|
|
1211
|
+
except subprocess.TimeoutExpired:
|
|
1212
|
+
console.print("[yellow]Gemini CLI timed out[/yellow]")
|
|
1213
|
+
except Exception as e:
|
|
1214
|
+
console.print(f"[red]Error using Gemini CLI: {e}[/red]")
|
|
1215
|
+
|
|
1216
|
+
async def _use_hanzo_ide(self, message: str):
|
|
1217
|
+
"""Use Hanzo Dev IDE from ~/work/hanzo/ide."""
|
|
1218
|
+
try:
|
|
1219
|
+
import subprocess
|
|
1220
|
+
import os
|
|
1221
|
+
|
|
1222
|
+
console.print("[dim]Using Hanzo Dev IDE...[/dim]")
|
|
1223
|
+
|
|
1224
|
+
# Check if Hanzo IDE exists
|
|
1225
|
+
ide_path = os.path.expanduser("~/work/hanzo/ide")
|
|
1226
|
+
if not os.path.exists(ide_path):
|
|
1227
|
+
console.print("[red]Hanzo Dev IDE not found![/red]")
|
|
1228
|
+
console.print("[yellow]Expected location: ~/work/hanzo/ide[/yellow]")
|
|
1229
|
+
console.print("To set up:")
|
|
1230
|
+
console.print(" • git clone https://github.com/hanzoai/ide ~/work/hanzo/ide")
|
|
1231
|
+
console.print(" • cd ~/work/hanzo/ide && npm install")
|
|
1232
|
+
return
|
|
1233
|
+
|
|
1234
|
+
# Check for the CLI entry point
|
|
1235
|
+
cli_paths = [
|
|
1236
|
+
os.path.join(ide_path, "bin", "hanzo-ide"),
|
|
1237
|
+
os.path.join(ide_path, "hanzo-ide"),
|
|
1238
|
+
os.path.join(ide_path, "cli.js"),
|
|
1239
|
+
os.path.join(ide_path, "index.js"),
|
|
1240
|
+
]
|
|
1241
|
+
|
|
1242
|
+
cli_path = None
|
|
1243
|
+
for path in cli_paths:
|
|
1244
|
+
if os.path.exists(path):
|
|
1245
|
+
cli_path = path
|
|
1246
|
+
break
|
|
1247
|
+
|
|
1248
|
+
if not cli_path:
|
|
1249
|
+
# Try to run with npm/node
|
|
1250
|
+
package_json = os.path.join(ide_path, "package.json")
|
|
1251
|
+
if os.path.exists(package_json):
|
|
1252
|
+
# Run via npm
|
|
1253
|
+
cmd = ["npm", "run", "chat", "--", message]
|
|
1254
|
+
cwd = ide_path
|
|
1255
|
+
else:
|
|
1256
|
+
console.print("[red]Hanzo IDE CLI not found![/red]")
|
|
1257
|
+
return
|
|
1258
|
+
else:
|
|
1259
|
+
# Run the CLI directly
|
|
1260
|
+
if cli_path.endswith(".js"):
|
|
1261
|
+
cmd = ["node", cli_path, "chat", message]
|
|
1262
|
+
else:
|
|
1263
|
+
cmd = [cli_path, "chat", message]
|
|
1264
|
+
cwd = None
|
|
1265
|
+
|
|
1266
|
+
process = subprocess.Popen(
|
|
1267
|
+
cmd,
|
|
1268
|
+
stdout=subprocess.PIPE,
|
|
1269
|
+
stderr=subprocess.PIPE,
|
|
1270
|
+
text=True,
|
|
1271
|
+
cwd=cwd
|
|
1272
|
+
)
|
|
1273
|
+
|
|
1274
|
+
stdout, stderr = process.communicate(timeout=30)
|
|
1275
|
+
|
|
1276
|
+
if process.returncode == 0 and stdout:
|
|
1277
|
+
console.print(f"[cyan]Hanzo IDE:[/cyan] {stdout.strip()}")
|
|
1278
|
+
else:
|
|
1279
|
+
if stderr:
|
|
1280
|
+
console.print(f"[yellow]Hanzo IDE: {stderr}[/yellow]")
|
|
1281
|
+
else:
|
|
1282
|
+
console.print("[yellow]Hanzo IDE: No response[/yellow]")
|
|
1283
|
+
|
|
1284
|
+
except subprocess.TimeoutExpired:
|
|
1285
|
+
console.print("[yellow]Hanzo IDE timed out[/yellow]")
|
|
1286
|
+
except Exception as e:
|
|
1287
|
+
console.print(f"[red]Error using Hanzo IDE: {e}[/red]")
|
|
1288
|
+
|
|
1289
|
+
async def _use_local_model(self, message: str):
|
|
1290
|
+
"""Use local model via Ollama or LM Studio."""
|
|
1291
|
+
import httpx
|
|
1292
|
+
|
|
1293
|
+
model_name = self.orchestrator.orchestrator_model.replace("local:", "")
|
|
1294
|
+
|
|
1295
|
+
# Try Ollama first (default port 11434)
|
|
1296
|
+
try:
|
|
1297
|
+
console.print(f"[dim]Using local {model_name} via Ollama...[/dim]")
|
|
1298
|
+
|
|
1299
|
+
async with httpx.AsyncClient() as client:
|
|
1300
|
+
response = await client.post(
|
|
1301
|
+
"http://localhost:11434/api/chat",
|
|
1302
|
+
json={
|
|
1303
|
+
"model": model_name,
|
|
1304
|
+
"messages": [
|
|
1305
|
+
{"role": "system", "content": "You are a helpful AI coding assistant."},
|
|
1306
|
+
{"role": "user", "content": message}
|
|
1307
|
+
],
|
|
1308
|
+
"stream": False
|
|
1309
|
+
},
|
|
1310
|
+
timeout=60.0
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
if response.status_code == 200:
|
|
1314
|
+
data = response.json()
|
|
1315
|
+
if data.get("message"):
|
|
1316
|
+
console.print(f"[cyan]{model_name}:[/cyan] {data['message']['content']}")
|
|
1317
|
+
return
|
|
1318
|
+
|
|
1319
|
+
except Exception:
|
|
1320
|
+
pass
|
|
1321
|
+
|
|
1322
|
+
# Try LM Studio (default port 1234)
|
|
1323
|
+
try:
|
|
1324
|
+
console.print(f"[dim]Trying LM Studio...[/dim]")
|
|
1325
|
+
|
|
1326
|
+
async with httpx.AsyncClient() as client:
|
|
1327
|
+
response = await client.post(
|
|
1328
|
+
"http://localhost:1234/v1/chat/completions",
|
|
1329
|
+
json={
|
|
1330
|
+
"model": model_name,
|
|
1331
|
+
"messages": [
|
|
1332
|
+
{"role": "system", "content": "You are a helpful AI coding assistant."},
|
|
1333
|
+
{"role": "user", "content": message}
|
|
1334
|
+
],
|
|
1335
|
+
"temperature": 0.7,
|
|
1336
|
+
"max_tokens": 2000
|
|
1337
|
+
},
|
|
1338
|
+
timeout=60.0
|
|
1339
|
+
)
|
|
1340
|
+
|
|
1341
|
+
if response.status_code == 200:
|
|
1342
|
+
data = response.json()
|
|
1343
|
+
if data.get("choices"):
|
|
1344
|
+
console.print(f"[cyan]{model_name}:[/cyan] {data['choices'][0]['message']['content']}")
|
|
1345
|
+
return
|
|
1346
|
+
|
|
1347
|
+
except Exception:
|
|
1348
|
+
pass
|
|
1349
|
+
|
|
1350
|
+
# Neither worked
|
|
1351
|
+
console.print(f"[red]Local model '{model_name}' not available[/red]")
|
|
1352
|
+
console.print("[yellow]To use local models:[/yellow]")
|
|
1353
|
+
console.print("\nOption 1 - Ollama (recommended):")
|
|
1354
|
+
console.print(" • Install: curl -fsSL https://ollama.com/install.sh | sh")
|
|
1355
|
+
console.print(f" • Pull model: ollama pull {model_name}")
|
|
1356
|
+
console.print(" • It will auto-start when you use hanzo dev")
|
|
1357
|
+
console.print("\nOption 2 - LM Studio:")
|
|
1358
|
+
console.print(" • Download from https://lmstudio.ai")
|
|
1359
|
+
console.print(f" • Load {model_name} model")
|
|
1360
|
+
console.print(" • Start local server (port 1234)")
|
|
1361
|
+
|
|
1362
|
+
async def handle_memory_command(self, command: str):
|
|
1363
|
+
"""Handle memory/context commands starting with #."""
|
|
1364
|
+
parts = command.split(maxsplit=1)
|
|
1365
|
+
cmd = parts[0].lower() if parts else ""
|
|
1366
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
1367
|
+
|
|
1368
|
+
if cmd == "remember":
|
|
1369
|
+
if args:
|
|
1370
|
+
console.print(f"[green]✓ Remembered: {args}[/green]")
|
|
1371
|
+
else:
|
|
1372
|
+
console.print("[yellow]Usage: #remember <text>[/yellow]")
|
|
1373
|
+
elif cmd == "forget":
|
|
1374
|
+
if args:
|
|
1375
|
+
console.print(f"[yellow]✓ Forgot: {args}[/yellow]")
|
|
1376
|
+
else:
|
|
1377
|
+
console.print("[yellow]Usage: #forget <text>[/yellow]")
|
|
1378
|
+
elif cmd == "memory":
|
|
1379
|
+
console.print("[cyan]Current Memory:[/cyan]")
|
|
1380
|
+
console.print(" • Working on Hanzo Python SDK")
|
|
1381
|
+
console.print(" • Using GPT-4 orchestrator")
|
|
1382
|
+
elif cmd == "context":
|
|
1383
|
+
console.print("[cyan]Current Context:[/cyan]")
|
|
1384
|
+
console.print(f" • Directory: {os.getcwd()}")
|
|
1385
|
+
console.print(f" • Model: {self.orchestrator.orchestrator_model}")
|
|
1386
|
+
else:
|
|
1387
|
+
console.print(f"[yellow]Unknown: #{cmd}[/yellow]")
|
|
1388
|
+
console.print("Try: #memory, #remember, #forget, #context")
|
|
771
1389
|
|
|
772
1390
|
|
|
773
1391
|
async def run_dev_orchestrator(**kwargs):
|
|
774
1392
|
"""Run the Hanzo Dev orchestrator with multi-agent networking.
|
|
775
|
-
|
|
1393
|
+
|
|
776
1394
|
This is the main entry point from the CLI that sets up:
|
|
777
|
-
1. Configurable orchestrator (GPT-5, GPT-4, Claude, etc.)
|
|
1395
|
+
1. Configurable orchestrator (GPT-5, GPT-4, Claude, Codex, etc.)
|
|
778
1396
|
2. Multiple worker agents (Claude instances for implementation)
|
|
779
1397
|
3. Critic agents for System 2 thinking
|
|
780
1398
|
4. MCP tool networking between instances
|
|
781
1399
|
5. Code quality guardrails
|
|
1400
|
+
6. Router-based or direct model access
|
|
782
1401
|
"""
|
|
783
|
-
workspace = kwargs.get(
|
|
784
|
-
orchestrator_model = kwargs.get(
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1402
|
+
workspace = kwargs.get("workspace", "~/.hanzo/dev")
|
|
1403
|
+
orchestrator_model = kwargs.get("orchestrator_model", "gpt-5")
|
|
1404
|
+
orchestrator_config = kwargs.get("orchestrator_config", None) # New config object
|
|
1405
|
+
claude_path = kwargs.get("claude_path")
|
|
1406
|
+
monitor = kwargs.get("monitor", False)
|
|
1407
|
+
repl = kwargs.get("repl", True)
|
|
1408
|
+
instances = kwargs.get("instances", 2)
|
|
1409
|
+
mcp_tools = kwargs.get("mcp_tools", True)
|
|
1410
|
+
network_mode = kwargs.get("network_mode", True)
|
|
1411
|
+
guardrails = kwargs.get("guardrails", True)
|
|
1412
|
+
use_network = kwargs.get("use_network", True) # Use hanzo-network if available
|
|
1413
|
+
use_hanzo_net = kwargs.get("use_hanzo_net", False) # Use hanzo/net for local AI
|
|
1414
|
+
hanzo_net_port = kwargs.get("hanzo_net_port", 52415)
|
|
1415
|
+
console_obj = kwargs.get("console", console)
|
|
1416
|
+
|
|
797
1417
|
console_obj.print(f"[bold cyan]Hanzo Dev - AI Coding OS[/bold cyan]")
|
|
798
|
-
|
|
1418
|
+
|
|
799
1419
|
# Check if we should use network mode
|
|
800
|
-
|
|
801
|
-
|
|
1420
|
+
# For now, disable network mode since hanzo-network isn't available
|
|
1421
|
+
if False and use_network and NETWORK_AVAILABLE:
|
|
1422
|
+
console_obj.print(
|
|
1423
|
+
f"[cyan]Mode: Network Orchestration with hanzo-network[/cyan]"
|
|
1424
|
+
)
|
|
802
1425
|
console_obj.print(f"Orchestrator: {orchestrator_model}")
|
|
803
1426
|
console_obj.print(f"Workers: {instances} agents")
|
|
804
1427
|
console_obj.print(f"Critics: {max(1, instances // 2)} agents")
|
|
805
1428
|
console_obj.print(f"MCP Tools: {'Enabled' if mcp_tools else 'Disabled'}")
|
|
806
1429
|
console_obj.print(f"Guardrails: {'Enabled' if guardrails else 'Disabled'}\n")
|
|
807
|
-
|
|
1430
|
+
|
|
808
1431
|
# Create network orchestrator with configurable LLM
|
|
809
1432
|
orchestrator = NetworkOrchestrator(
|
|
810
1433
|
workspace_dir=workspace,
|
|
@@ -815,9 +1438,9 @@ async def run_dev_orchestrator(**kwargs):
|
|
|
815
1438
|
enable_guardrails=guardrails,
|
|
816
1439
|
use_hanzo_net=use_hanzo_net,
|
|
817
1440
|
hanzo_net_port=hanzo_net_port,
|
|
818
|
-
console=console_obj
|
|
1441
|
+
console=console_obj,
|
|
819
1442
|
)
|
|
820
|
-
|
|
1443
|
+
|
|
821
1444
|
# Initialize the network
|
|
822
1445
|
success = await orchestrator.initialize()
|
|
823
1446
|
if not success:
|
|
@@ -826,11 +1449,13 @@ async def run_dev_orchestrator(**kwargs):
|
|
|
826
1449
|
else:
|
|
827
1450
|
# Fallback to multi-Claude mode
|
|
828
1451
|
console_obj.print(f"[cyan]Mode: Multi-Claude Orchestration (legacy)[/cyan]")
|
|
829
|
-
console_obj.print(
|
|
1452
|
+
console_obj.print(
|
|
1453
|
+
f"Instances: {instances} (1 primary + {instances-1} critic{'s' if instances > 2 else ''})"
|
|
1454
|
+
)
|
|
830
1455
|
console_obj.print(f"MCP Tools: {'Enabled' if mcp_tools else 'Disabled'}")
|
|
831
1456
|
console_obj.print(f"Networking: {'Enabled' if network_mode else 'Disabled'}")
|
|
832
1457
|
console_obj.print(f"Guardrails: {'Enabled' if guardrails else 'Disabled'}\n")
|
|
833
|
-
|
|
1458
|
+
|
|
834
1459
|
orchestrator = MultiClaudeOrchestrator(
|
|
835
1460
|
workspace_dir=workspace,
|
|
836
1461
|
claude_path=claude_path,
|
|
@@ -838,12 +1463,13 @@ async def run_dev_orchestrator(**kwargs):
|
|
|
838
1463
|
enable_mcp=mcp_tools,
|
|
839
1464
|
enable_networking=network_mode,
|
|
840
1465
|
enable_guardrails=guardrails,
|
|
841
|
-
console=console_obj
|
|
1466
|
+
console=console_obj,
|
|
1467
|
+
orchestrator_model=orchestrator_model,
|
|
842
1468
|
)
|
|
843
|
-
|
|
1469
|
+
|
|
844
1470
|
# Initialize instances
|
|
845
1471
|
await orchestrator.initialize()
|
|
846
|
-
|
|
1472
|
+
|
|
847
1473
|
if monitor:
|
|
848
1474
|
# Start monitoring mode
|
|
849
1475
|
await orchestrator.monitor_loop()
|
|
@@ -859,14 +1485,21 @@ async def run_dev_orchestrator(**kwargs):
|
|
|
859
1485
|
|
|
860
1486
|
class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
861
1487
|
"""Advanced orchestrator using hanzo-network with configurable LLM (GPT-5, Claude, local, etc.)."""
|
|
862
|
-
|
|
863
|
-
def __init__(
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1488
|
+
|
|
1489
|
+
def __init__(
|
|
1490
|
+
self,
|
|
1491
|
+
workspace_dir: str,
|
|
1492
|
+
orchestrator_model: str = "gpt-5",
|
|
1493
|
+
num_workers: int = 2,
|
|
1494
|
+
enable_mcp: bool = True,
|
|
1495
|
+
enable_networking: bool = True,
|
|
1496
|
+
enable_guardrails: bool = True,
|
|
1497
|
+
use_hanzo_net: bool = False,
|
|
1498
|
+
hanzo_net_port: int = 52415,
|
|
1499
|
+
console: Console = console,
|
|
1500
|
+
):
|
|
868
1501
|
"""Initialize network orchestrator with configurable LLM.
|
|
869
|
-
|
|
1502
|
+
|
|
870
1503
|
Args:
|
|
871
1504
|
workspace_dir: Workspace directory
|
|
872
1505
|
orchestrator_model: Model to use for orchestration (e.g., "gpt-5", "gpt-4", "claude-3-5-sonnet", "local:llama3.2")
|
|
@@ -887,38 +1520,44 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
|
887
1520
|
self.use_hanzo_net = use_hanzo_net
|
|
888
1521
|
self.hanzo_net_port = hanzo_net_port
|
|
889
1522
|
self.console = console
|
|
890
|
-
|
|
1523
|
+
|
|
891
1524
|
# Agent network components
|
|
892
1525
|
self.orchestrator_agent = None
|
|
893
1526
|
self.worker_agents = []
|
|
894
1527
|
self.critic_agents = []
|
|
895
1528
|
self.agent_network = None
|
|
896
1529
|
self.hanzo_net_process = None
|
|
897
|
-
|
|
1530
|
+
|
|
898
1531
|
# Check if we can use hanzo-network
|
|
899
1532
|
if not NETWORK_AVAILABLE:
|
|
900
|
-
self.console.print(
|
|
901
|
-
|
|
1533
|
+
self.console.print(
|
|
1534
|
+
"[yellow]Warning: hanzo-network not available, falling back to basic mode[/yellow]"
|
|
1535
|
+
)
|
|
1536
|
+
|
|
902
1537
|
async def initialize(self):
|
|
903
1538
|
"""Initialize the agent network with orchestrator and workers."""
|
|
904
1539
|
if not NETWORK_AVAILABLE:
|
|
905
|
-
self.console.print(
|
|
1540
|
+
self.console.print(
|
|
1541
|
+
"[red]Cannot initialize network mode without hanzo-network[/red]"
|
|
1542
|
+
)
|
|
906
1543
|
return False
|
|
907
|
-
|
|
1544
|
+
|
|
908
1545
|
# Start hanzo net if requested for local orchestration
|
|
909
1546
|
if self.use_hanzo_net or self.orchestrator_model.startswith("local:"):
|
|
910
1547
|
await self._start_hanzo_net()
|
|
911
|
-
|
|
912
|
-
self.console.print(
|
|
913
|
-
|
|
1548
|
+
|
|
1549
|
+
self.console.print(
|
|
1550
|
+
f"[cyan]Initializing agent network with {self.orchestrator_model} orchestrator...[/cyan]"
|
|
1551
|
+
)
|
|
1552
|
+
|
|
914
1553
|
# Create orchestrator agent (GPT-5, local, or other model)
|
|
915
1554
|
self.orchestrator_agent = await self._create_orchestrator_agent()
|
|
916
|
-
|
|
1555
|
+
|
|
917
1556
|
# Create worker agents (Claude instances for implementation)
|
|
918
1557
|
for i in range(self.num_workers):
|
|
919
1558
|
worker = await self._create_worker_agent(i)
|
|
920
1559
|
self.worker_agents.append(worker)
|
|
921
|
-
|
|
1560
|
+
|
|
922
1561
|
# Add local workers if using hanzo net (for cost optimization)
|
|
923
1562
|
if self.use_hanzo_net or self.orchestrator_model.startswith("local:"):
|
|
924
1563
|
# Add 1-2 local workers for simple tasks
|
|
@@ -926,17 +1565,19 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
|
926
1565
|
for i in range(num_local_workers):
|
|
927
1566
|
local_worker = await self._create_local_worker_agent(i)
|
|
928
1567
|
self.worker_agents.append(local_worker)
|
|
929
|
-
self.console.print(
|
|
930
|
-
|
|
1568
|
+
self.console.print(
|
|
1569
|
+
f"[green]Added {num_local_workers} local workers for cost optimization[/green]"
|
|
1570
|
+
)
|
|
1571
|
+
|
|
931
1572
|
# Create critic agents for System 2 thinking
|
|
932
1573
|
if self.enable_guardrails:
|
|
933
1574
|
for i in range(max(1, self.num_workers // 2)):
|
|
934
1575
|
critic = await self._create_critic_agent(i)
|
|
935
1576
|
self.critic_agents.append(critic)
|
|
936
|
-
|
|
1577
|
+
|
|
937
1578
|
# Create the agent network
|
|
938
1579
|
all_agents = [self.orchestrator_agent] + self.worker_agents + self.critic_agents
|
|
939
|
-
|
|
1580
|
+
|
|
940
1581
|
# Create router based on configuration
|
|
941
1582
|
if self.use_hanzo_net or self.orchestrator_model.startswith("local:"):
|
|
942
1583
|
# Use cost-optimized router that prefers local models
|
|
@@ -944,109 +1585,158 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
|
944
1585
|
else:
|
|
945
1586
|
# Use intelligent router with orchestrator making decisions
|
|
946
1587
|
router = await self._create_intelligent_router()
|
|
947
|
-
|
|
1588
|
+
|
|
948
1589
|
# Create the network
|
|
949
1590
|
self.agent_network = create_network(
|
|
950
1591
|
agents=all_agents,
|
|
951
1592
|
router=router,
|
|
952
|
-
default_agent=
|
|
1593
|
+
default_agent=(
|
|
1594
|
+
self.orchestrator_agent.name if self.orchestrator_agent else None
|
|
1595
|
+
),
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
self.console.print(
|
|
1599
|
+
f"[green]✓ Agent network initialized with {len(all_agents)} agents[/green]"
|
|
953
1600
|
)
|
|
954
|
-
|
|
955
|
-
self.console.print(f"[green]✓ Agent network initialized with {len(all_agents)} agents[/green]")
|
|
956
1601
|
return True
|
|
957
|
-
|
|
1602
|
+
|
|
958
1603
|
async def _start_hanzo_net(self):
|
|
959
1604
|
"""Start hanzo net for local AI orchestration."""
|
|
960
|
-
self.console.print(
|
|
961
|
-
|
|
1605
|
+
self.console.print(
|
|
1606
|
+
"[cyan]Starting hanzo/net for local AI orchestration...[/cyan]"
|
|
1607
|
+
)
|
|
1608
|
+
|
|
962
1609
|
# Check if hanzo net is already running
|
|
963
1610
|
import socket
|
|
1611
|
+
|
|
964
1612
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
965
|
-
result = sock.connect_ex((
|
|
1613
|
+
result = sock.connect_ex(("localhost", self.hanzo_net_port))
|
|
966
1614
|
sock.close()
|
|
967
|
-
|
|
1615
|
+
|
|
968
1616
|
if result == 0:
|
|
969
|
-
self.console.print(
|
|
1617
|
+
self.console.print(
|
|
1618
|
+
f"[yellow]hanzo/net already running on port {self.hanzo_net_port}[/yellow]"
|
|
1619
|
+
)
|
|
970
1620
|
return
|
|
971
|
-
|
|
1621
|
+
|
|
972
1622
|
# Start hanzo net
|
|
973
1623
|
try:
|
|
974
1624
|
# Determine model to serve based on orchestrator model
|
|
975
1625
|
model = "llama-3.2-3b" # Default
|
|
976
1626
|
if ":" in self.orchestrator_model:
|
|
977
1627
|
model = self.orchestrator_model.split(":")[1]
|
|
978
|
-
|
|
1628
|
+
|
|
979
1629
|
cmd = [
|
|
980
|
-
"hanzo",
|
|
981
|
-
"
|
|
982
|
-
"--
|
|
983
|
-
|
|
1630
|
+
"hanzo",
|
|
1631
|
+
"net",
|
|
1632
|
+
"--port",
|
|
1633
|
+
str(self.hanzo_net_port),
|
|
1634
|
+
"--models",
|
|
1635
|
+
model,
|
|
1636
|
+
"--network",
|
|
1637
|
+
"local",
|
|
984
1638
|
]
|
|
985
|
-
|
|
1639
|
+
|
|
986
1640
|
self.hanzo_net_process = subprocess.Popen(
|
|
987
1641
|
cmd,
|
|
988
1642
|
stdout=subprocess.PIPE,
|
|
989
1643
|
stderr=subprocess.PIPE,
|
|
990
1644
|
text=True,
|
|
991
|
-
preexec_fn=os.setsid if hasattr(os,
|
|
1645
|
+
preexec_fn=os.setsid if hasattr(os, "setsid") else None,
|
|
992
1646
|
)
|
|
993
|
-
|
|
1647
|
+
|
|
994
1648
|
# Wait for it to start
|
|
995
1649
|
await asyncio.sleep(3)
|
|
996
|
-
|
|
1650
|
+
|
|
997
1651
|
if self.hanzo_net_process.poll() is None:
|
|
998
|
-
self.console.print(
|
|
1652
|
+
self.console.print(
|
|
1653
|
+
f"[green]✓ hanzo/net started on port {self.hanzo_net_port} with model {model}[/green]"
|
|
1654
|
+
)
|
|
999
1655
|
else:
|
|
1000
1656
|
self.console.print("[red]Failed to start hanzo/net[/red]")
|
|
1001
|
-
|
|
1657
|
+
|
|
1002
1658
|
except Exception as e:
|
|
1003
1659
|
self.console.print(f"[red]Error starting hanzo/net: {e}[/red]")
|
|
1004
|
-
|
|
1660
|
+
|
|
1005
1661
|
async def _create_orchestrator_agent(self) -> Agent:
|
|
1006
1662
|
"""Create the orchestrator agent (GPT-5, local, or configured model)."""
|
|
1007
1663
|
# Check if using local model via hanzo/net
|
|
1008
1664
|
if self.orchestrator_model.startswith("local:"):
|
|
1009
1665
|
# Use local model via hanzo/net
|
|
1010
1666
|
model_name = self.orchestrator_model.split(":")[1]
|
|
1011
|
-
|
|
1667
|
+
|
|
1012
1668
|
# Import local network helpers
|
|
1013
1669
|
from hanzo_network.local_network import create_local_agent
|
|
1014
|
-
|
|
1670
|
+
|
|
1015
1671
|
orchestrator = create_local_agent(
|
|
1016
1672
|
name="local_orchestrator",
|
|
1017
1673
|
description=f"Local {model_name} orchestrator via hanzo/net",
|
|
1018
1674
|
system=self._get_orchestrator_system_prompt(),
|
|
1019
1675
|
local_model=model_name,
|
|
1020
1676
|
base_url=f"http://localhost:{self.hanzo_net_port}",
|
|
1021
|
-
tools=[]
|
|
1677
|
+
tools=[],
|
|
1678
|
+
)
|
|
1679
|
+
|
|
1680
|
+
self.console.print(
|
|
1681
|
+
f"[green]✓ Created local {model_name} orchestrator via hanzo/net[/green]"
|
|
1022
1682
|
)
|
|
1023
|
-
|
|
1024
|
-
self.console.print(f"[green]✓ Created local {model_name} orchestrator via hanzo/net[/green]")
|
|
1025
1683
|
return orchestrator
|
|
1026
|
-
|
|
1684
|
+
|
|
1027
1685
|
# Parse model string to get provider and model
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1686
|
+
model_name = self.orchestrator_model
|
|
1687
|
+
provider = "openai" # Default to OpenAI - use string
|
|
1688
|
+
api_key = None
|
|
1689
|
+
|
|
1690
|
+
# Determine provider from model name
|
|
1691
|
+
if model_name.startswith("gpt") or model_name == "codex":
|
|
1692
|
+
provider = "openai"
|
|
1693
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
|
1694
|
+
elif model_name.startswith("claude"):
|
|
1695
|
+
provider = "anthropic"
|
|
1696
|
+
api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
1697
|
+
elif model_name.startswith("gemini"):
|
|
1698
|
+
provider = "google"
|
|
1699
|
+
api_key = os.getenv("GOOGLE_API_KEY")
|
|
1700
|
+
elif model_name.startswith("local:"):
|
|
1701
|
+
provider = "local"
|
|
1702
|
+
model_name = model_name.replace("local:", "")
|
|
1703
|
+
|
|
1704
|
+
# Create model config based on what's available
|
|
1705
|
+
if NETWORK_AVAILABLE:
|
|
1706
|
+
# Real ModelConfig may have different signature
|
|
1707
|
+
try:
|
|
1708
|
+
model_config = ModelConfig(
|
|
1709
|
+
name=model_name,
|
|
1710
|
+
provider=provider,
|
|
1711
|
+
)
|
|
1712
|
+
# Set api_key separately if supported
|
|
1713
|
+
if hasattr(model_config, "api_key"):
|
|
1714
|
+
model_config.api_key = api_key
|
|
1715
|
+
except TypeError:
|
|
1716
|
+
# Fallback to simple string if ModelConfig doesn't work
|
|
1717
|
+
model_config = model_name
|
|
1718
|
+
else:
|
|
1719
|
+
# Use our fallback ModelConfig
|
|
1720
|
+
model_config = ModelConfig(
|
|
1721
|
+
name=model_name,
|
|
1722
|
+
provider=provider,
|
|
1723
|
+
api_key=api_key,
|
|
1724
|
+
)
|
|
1725
|
+
|
|
1038
1726
|
# Create orchestrator with strategic system prompt
|
|
1039
1727
|
orchestrator = create_agent(
|
|
1040
1728
|
name="orchestrator",
|
|
1041
1729
|
description=f"{self.orchestrator_model} powered meta-orchestrator for AI coding",
|
|
1042
1730
|
model=model_config,
|
|
1043
1731
|
system=self._get_orchestrator_system_prompt(),
|
|
1044
|
-
tools=[] # Orchestrator tools will be added
|
|
1732
|
+
tools=[], # Orchestrator tools will be added
|
|
1733
|
+
)
|
|
1734
|
+
|
|
1735
|
+
self.console.print(
|
|
1736
|
+
f"[green]✓ Created {self.orchestrator_model} orchestrator[/green]"
|
|
1045
1737
|
)
|
|
1046
|
-
|
|
1047
|
-
self.console.print(f"[green]✓ Created {self.orchestrator_model} orchestrator[/green]")
|
|
1048
1738
|
return orchestrator
|
|
1049
|
-
|
|
1739
|
+
|
|
1050
1740
|
def _get_orchestrator_system_prompt(self) -> str:
|
|
1051
1741
|
"""Get the system prompt for the orchestrator."""
|
|
1052
1742
|
return """You are an advanced AI orchestrator managing a network of specialized agents.
|
|
@@ -1069,16 +1759,20 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
|
1069
1759
|
- Simple tasks → Use local agents if available
|
|
1070
1760
|
|
|
1071
1761
|
Always maintain high code quality standards and prevent degradation."""
|
|
1072
|
-
|
|
1762
|
+
|
|
1073
1763
|
async def _create_worker_agent(self, index: int) -> Agent:
|
|
1074
1764
|
"""Create a worker agent (Claude for implementation)."""
|
|
1075
1765
|
worker = create_agent(
|
|
1076
1766
|
name=f"worker_{index}",
|
|
1077
1767
|
description=f"Claude worker agent {index} for code implementation",
|
|
1078
|
-
model=
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1768
|
+
model=(
|
|
1769
|
+
"claude-3-5-sonnet-20241022"
|
|
1770
|
+
if NETWORK_AVAILABLE
|
|
1771
|
+
else ModelConfig(
|
|
1772
|
+
provider="anthropic",
|
|
1773
|
+
name="claude-3-5-sonnet-20241022",
|
|
1774
|
+
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
|
1775
|
+
)
|
|
1082
1776
|
),
|
|
1083
1777
|
system="""You are a Claude worker agent specialized in code implementation.
|
|
1084
1778
|
|
|
@@ -1089,16 +1783,16 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
|
1089
1783
|
- Debug and fix issues
|
|
1090
1784
|
|
|
1091
1785
|
Follow best practices and maintain code quality.""",
|
|
1092
|
-
tools=[] # MCP tools will be added if enabled
|
|
1786
|
+
tools=[], # MCP tools will be added if enabled
|
|
1093
1787
|
)
|
|
1094
|
-
|
|
1788
|
+
|
|
1095
1789
|
self.console.print(f" Created worker agent {index}")
|
|
1096
1790
|
return worker
|
|
1097
|
-
|
|
1791
|
+
|
|
1098
1792
|
async def _create_local_worker_agent(self, index: int) -> Agent:
|
|
1099
1793
|
"""Create a local worker agent for simple tasks (cost optimization)."""
|
|
1100
1794
|
from hanzo_network.local_network import create_local_agent
|
|
1101
|
-
|
|
1795
|
+
|
|
1102
1796
|
worker = create_local_agent(
|
|
1103
1797
|
name=f"local_worker_{index}",
|
|
1104
1798
|
description=f"Local worker agent {index} for simple tasks",
|
|
@@ -1113,21 +1807,21 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
|
1113
1807
|
You handle simple tasks to reduce API costs.""",
|
|
1114
1808
|
local_model="llama-3.2-3b",
|
|
1115
1809
|
base_url=f"http://localhost:{self.hanzo_net_port}",
|
|
1116
|
-
tools=[]
|
|
1810
|
+
tools=[],
|
|
1117
1811
|
)
|
|
1118
|
-
|
|
1812
|
+
|
|
1119
1813
|
self.console.print(f" Created local worker agent {index}")
|
|
1120
1814
|
return worker
|
|
1121
|
-
|
|
1815
|
+
|
|
1122
1816
|
async def _create_critic_agent(self, index: int) -> Agent:
|
|
1123
1817
|
"""Create a critic agent for code review."""
|
|
1124
1818
|
# Use a different model for critics for diversity
|
|
1125
1819
|
critic_model = "gpt-4" if index % 2 == 0 else "claude-3-5-sonnet-20241022"
|
|
1126
|
-
|
|
1820
|
+
|
|
1127
1821
|
critic = create_agent(
|
|
1128
1822
|
name=f"critic_{index}",
|
|
1129
1823
|
description=f"Critic agent {index} for code quality assurance",
|
|
1130
|
-
model=
|
|
1824
|
+
model=critic_model, # Just pass the model name string
|
|
1131
1825
|
system="""You are a critic agent focused on code quality and best practices.
|
|
1132
1826
|
|
|
1133
1827
|
Review code for:
|
|
@@ -1138,19 +1832,19 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
|
1138
1832
|
5. Best practices and design patterns
|
|
1139
1833
|
|
|
1140
1834
|
Provide constructive feedback with specific improvement suggestions.""",
|
|
1141
|
-
tools=[]
|
|
1835
|
+
tools=[],
|
|
1142
1836
|
)
|
|
1143
|
-
|
|
1837
|
+
|
|
1144
1838
|
self.console.print(f" Created critic agent {index} ({critic_model})")
|
|
1145
1839
|
return critic
|
|
1146
|
-
|
|
1840
|
+
|
|
1147
1841
|
async def _create_cost_optimized_router(self) -> Router:
|
|
1148
1842
|
"""Create a cost-optimized router that prefers local models."""
|
|
1149
1843
|
from hanzo_network.core.router import Router
|
|
1150
|
-
|
|
1844
|
+
|
|
1151
1845
|
class CostOptimizedRouter(Router):
|
|
1152
1846
|
"""Router that minimizes costs by using local models when possible."""
|
|
1153
|
-
|
|
1847
|
+
|
|
1154
1848
|
def __init__(self, orchestrator_agent, worker_agents, critic_agents):
|
|
1155
1849
|
super().__init__()
|
|
1156
1850
|
self.orchestrator = orchestrator_agent
|
|
@@ -1158,55 +1852,97 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
|
1158
1852
|
self.critics = critic_agents
|
|
1159
1853
|
self.local_workers = [w for w in worker_agents if "local" in w.name]
|
|
1160
1854
|
self.api_workers = [w for w in worker_agents if "local" not in w.name]
|
|
1161
|
-
|
|
1855
|
+
|
|
1162
1856
|
async def route(self, prompt: str, state=None) -> str:
|
|
1163
1857
|
"""Route based on task complexity and cost optimization."""
|
|
1164
1858
|
prompt_lower = prompt.lower()
|
|
1165
|
-
|
|
1859
|
+
|
|
1166
1860
|
# Simple tasks → Local workers
|
|
1167
|
-
simple_keywords = [
|
|
1168
|
-
|
|
1861
|
+
simple_keywords = [
|
|
1862
|
+
"list",
|
|
1863
|
+
"check",
|
|
1864
|
+
"validate",
|
|
1865
|
+
"format",
|
|
1866
|
+
"rename",
|
|
1867
|
+
"count",
|
|
1868
|
+
"find",
|
|
1869
|
+
]
|
|
1870
|
+
if (
|
|
1871
|
+
any(keyword in prompt_lower for keyword in simple_keywords)
|
|
1872
|
+
and self.local_workers
|
|
1873
|
+
):
|
|
1169
1874
|
return self.local_workers[0].name
|
|
1170
|
-
|
|
1875
|
+
|
|
1171
1876
|
# Complex implementation → API workers (Claude)
|
|
1172
|
-
complex_keywords = [
|
|
1173
|
-
|
|
1877
|
+
complex_keywords = [
|
|
1878
|
+
"implement",
|
|
1879
|
+
"refactor",
|
|
1880
|
+
"debug",
|
|
1881
|
+
"optimize",
|
|
1882
|
+
"design",
|
|
1883
|
+
"architect",
|
|
1884
|
+
]
|
|
1885
|
+
if (
|
|
1886
|
+
any(keyword in prompt_lower for keyword in complex_keywords)
|
|
1887
|
+
and self.api_workers
|
|
1888
|
+
):
|
|
1174
1889
|
return self.api_workers[0].name
|
|
1175
|
-
|
|
1890
|
+
|
|
1176
1891
|
# Review tasks → Critics
|
|
1177
|
-
review_keywords = [
|
|
1178
|
-
|
|
1892
|
+
review_keywords = [
|
|
1893
|
+
"review",
|
|
1894
|
+
"critique",
|
|
1895
|
+
"analyze",
|
|
1896
|
+
"improve",
|
|
1897
|
+
"validate code",
|
|
1898
|
+
]
|
|
1899
|
+
if (
|
|
1900
|
+
any(keyword in prompt_lower for keyword in review_keywords)
|
|
1901
|
+
and self.critics
|
|
1902
|
+
):
|
|
1179
1903
|
return self.critics[0].name
|
|
1180
|
-
|
|
1904
|
+
|
|
1181
1905
|
# Strategic decisions → Orchestrator
|
|
1182
|
-
strategic_keywords = [
|
|
1906
|
+
strategic_keywords = [
|
|
1907
|
+
"plan",
|
|
1908
|
+
"decide",
|
|
1909
|
+
"strategy",
|
|
1910
|
+
"coordinate",
|
|
1911
|
+
"organize",
|
|
1912
|
+
]
|
|
1183
1913
|
if any(keyword in prompt_lower for keyword in strategic_keywords):
|
|
1184
1914
|
return self.orchestrator.name
|
|
1185
|
-
|
|
1915
|
+
|
|
1186
1916
|
# Default: Try local first, then API
|
|
1187
1917
|
if self.local_workers:
|
|
1188
1918
|
# For shorter prompts, try local first
|
|
1189
1919
|
if len(prompt) < 500:
|
|
1190
1920
|
return self.local_workers[0].name
|
|
1191
|
-
|
|
1921
|
+
|
|
1192
1922
|
# Fall back to API workers for complex tasks
|
|
1193
|
-
return
|
|
1194
|
-
|
|
1923
|
+
return (
|
|
1924
|
+
self.api_workers[0].name
|
|
1925
|
+
if self.api_workers
|
|
1926
|
+
else self.orchestrator.name
|
|
1927
|
+
)
|
|
1928
|
+
|
|
1195
1929
|
# Create the cost-optimized router
|
|
1196
1930
|
router = CostOptimizedRouter(
|
|
1197
|
-
self.orchestrator_agent,
|
|
1198
|
-
|
|
1199
|
-
|
|
1931
|
+
self.orchestrator_agent, self.worker_agents, self.critic_agents
|
|
1932
|
+
)
|
|
1933
|
+
|
|
1934
|
+
self.console.print(
|
|
1935
|
+
"[green]✓ Created cost-optimized router (local models preferred)[/green]"
|
|
1200
1936
|
)
|
|
1201
|
-
|
|
1202
|
-
self.console.print("[green]✓ Created cost-optimized router (local models preferred)[/green]")
|
|
1203
1937
|
return router
|
|
1204
|
-
|
|
1938
|
+
|
|
1205
1939
|
async def _create_intelligent_router(self) -> Router:
|
|
1206
1940
|
"""Create an intelligent router using the orchestrator for decisions."""
|
|
1207
1941
|
if self.orchestrator_agent:
|
|
1208
1942
|
# Create routing agent that uses orchestrator for decisions
|
|
1209
1943
|
router = create_routing_agent(
|
|
1944
|
+
name="router",
|
|
1945
|
+
description="Intelligent task router",
|
|
1210
1946
|
agent=self.orchestrator_agent,
|
|
1211
1947
|
system="""Route tasks to the most appropriate agent based on:
|
|
1212
1948
|
|
|
@@ -1221,68 +1957,67 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
|
1221
1957
|
- Review tasks → Route to critics
|
|
1222
1958
|
- Parallel work → Split across multiple agents
|
|
1223
1959
|
|
|
1224
|
-
Return the name of the best agent for the task."""
|
|
1960
|
+
Return the name of the best agent for the task.""",
|
|
1225
1961
|
)
|
|
1226
1962
|
else:
|
|
1227
1963
|
# Fallback to basic router
|
|
1228
1964
|
router = create_router(
|
|
1229
1965
|
agents=self.worker_agents + self.critic_agents,
|
|
1230
|
-
default=self.worker_agents[0].name if self.worker_agents else None
|
|
1966
|
+
default=self.worker_agents[0].name if self.worker_agents else None,
|
|
1231
1967
|
)
|
|
1232
|
-
|
|
1968
|
+
|
|
1233
1969
|
return router
|
|
1234
|
-
|
|
1235
|
-
async def execute_with_network(
|
|
1970
|
+
|
|
1971
|
+
async def execute_with_network(
|
|
1972
|
+
self, task: str, context: Optional[Dict] = None
|
|
1973
|
+
) -> Dict:
|
|
1236
1974
|
"""Execute a task using the agent network.
|
|
1237
|
-
|
|
1975
|
+
|
|
1238
1976
|
Args:
|
|
1239
1977
|
task: Task description
|
|
1240
1978
|
context: Optional context
|
|
1241
|
-
|
|
1979
|
+
|
|
1242
1980
|
Returns:
|
|
1243
1981
|
Execution result
|
|
1244
1982
|
"""
|
|
1245
1983
|
if not self.agent_network:
|
|
1246
1984
|
self.console.print("[red]Agent network not initialized[/red]")
|
|
1247
1985
|
return {"error": "Network not initialized"}
|
|
1248
|
-
|
|
1986
|
+
|
|
1249
1987
|
self.console.print(f"[cyan]Executing task with agent network: {task}[/cyan]")
|
|
1250
|
-
|
|
1988
|
+
|
|
1251
1989
|
# Create network state
|
|
1252
1990
|
state = NetworkState()
|
|
1253
1991
|
state.add_message("user", task)
|
|
1254
|
-
|
|
1992
|
+
|
|
1255
1993
|
if context:
|
|
1256
1994
|
state.metadata.update(context)
|
|
1257
|
-
|
|
1995
|
+
|
|
1258
1996
|
# Run the network
|
|
1259
1997
|
try:
|
|
1260
|
-
result = await self.agent_network.run(
|
|
1261
|
-
|
|
1262
|
-
state=state
|
|
1263
|
-
)
|
|
1264
|
-
|
|
1998
|
+
result = await self.agent_network.run(prompt=task, state=state)
|
|
1999
|
+
|
|
1265
2000
|
# If guardrails enabled, validate result
|
|
1266
2001
|
if self.enable_guardrails and self.critic_agents:
|
|
1267
2002
|
validated = await self._validate_with_critics(result, task)
|
|
1268
2003
|
if validated.get("improvements"):
|
|
1269
2004
|
self.console.print("[yellow]Applied critic improvements[/yellow]")
|
|
1270
2005
|
return validated
|
|
1271
|
-
|
|
2006
|
+
|
|
1272
2007
|
return result
|
|
1273
|
-
|
|
2008
|
+
|
|
1274
2009
|
except Exception as e:
|
|
1275
2010
|
self.console.print(f"[red]Network execution error: {e}[/red]")
|
|
1276
2011
|
return {"error": str(e)}
|
|
1277
|
-
|
|
2012
|
+
|
|
1278
2013
|
async def _validate_with_critics(self, result: Dict, original_task: str) -> Dict:
|
|
1279
2014
|
"""Validate and potentially improve result using critic agents."""
|
|
1280
2015
|
if not self.critic_agents:
|
|
1281
2016
|
return result
|
|
1282
|
-
|
|
2017
|
+
|
|
1283
2018
|
# Get first critic to review
|
|
1284
2019
|
critic = self.critic_agents[0]
|
|
1285
|
-
|
|
2020
|
+
|
|
1286
2021
|
review_prompt = f"""
|
|
1287
2022
|
Review this solution:
|
|
1288
2023
|
|
|
@@ -1291,69 +2026,80 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
|
|
1291
2026
|
|
|
1292
2027
|
Provide specific improvements if needed.
|
|
1293
2028
|
"""
|
|
1294
|
-
|
|
2029
|
+
|
|
1295
2030
|
review = await critic.run(review_prompt)
|
|
1296
|
-
|
|
2031
|
+
|
|
1297
2032
|
# Check if improvements suggested
|
|
1298
|
-
if "improve" in str(review.get(
|
|
1299
|
-
result["improvements"] = review.get(
|
|
1300
|
-
|
|
2033
|
+
if "improve" in str(review.get("output", "")).lower():
|
|
2034
|
+
result["improvements"] = review.get("output")
|
|
2035
|
+
|
|
1301
2036
|
return result
|
|
1302
|
-
|
|
2037
|
+
|
|
1303
2038
|
def shutdown(self):
|
|
1304
2039
|
"""Shutdown the network orchestrator and hanzo net if running."""
|
|
1305
2040
|
# Stop hanzo net if we started it
|
|
1306
2041
|
if self.hanzo_net_process:
|
|
1307
2042
|
try:
|
|
1308
2043
|
self.console.print("[yellow]Stopping hanzo/net...[/yellow]")
|
|
1309
|
-
if hasattr(os,
|
|
2044
|
+
if hasattr(os, "killpg"):
|
|
1310
2045
|
os.killpg(os.getpgid(self.hanzo_net_process.pid), signal.SIGTERM)
|
|
1311
2046
|
else:
|
|
1312
2047
|
self.hanzo_net_process.terminate()
|
|
1313
2048
|
self.hanzo_net_process.wait(timeout=5)
|
|
1314
2049
|
self.console.print("[green]✓ hanzo/net stopped[/green]")
|
|
1315
|
-
except:
|
|
2050
|
+
except Exception:
|
|
1316
2051
|
try:
|
|
1317
2052
|
self.hanzo_net_process.kill()
|
|
1318
|
-
except:
|
|
2053
|
+
except Exception:
|
|
1319
2054
|
pass
|
|
1320
|
-
|
|
2055
|
+
|
|
1321
2056
|
# Call parent shutdown
|
|
1322
2057
|
super().shutdown()
|
|
1323
2058
|
|
|
1324
2059
|
|
|
1325
2060
|
class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
|
1326
2061
|
"""Extended orchestrator for multiple Claude instances with MCP networking."""
|
|
1327
|
-
|
|
1328
|
-
def __init__(
|
|
1329
|
-
|
|
1330
|
-
|
|
2062
|
+
|
|
2063
|
+
def __init__(
|
|
2064
|
+
self,
|
|
2065
|
+
workspace_dir: str,
|
|
2066
|
+
claude_path: str,
|
|
2067
|
+
num_instances: int,
|
|
2068
|
+
enable_mcp: bool,
|
|
2069
|
+
enable_networking: bool,
|
|
2070
|
+
enable_guardrails: bool,
|
|
2071
|
+
console: Console,
|
|
2072
|
+
orchestrator_model: str = "gpt-4",
|
|
2073
|
+
):
|
|
1331
2074
|
super().__init__(workspace_dir, claude_path)
|
|
1332
2075
|
self.num_instances = num_instances
|
|
1333
2076
|
self.enable_mcp = enable_mcp
|
|
1334
2077
|
self.enable_networking = enable_networking
|
|
1335
2078
|
self.enable_guardrails = enable_guardrails
|
|
1336
2079
|
self.console = console
|
|
1337
|
-
|
|
2080
|
+
self.orchestrator_model = orchestrator_model # Add this for chat interface
|
|
2081
|
+
|
|
1338
2082
|
# Store multiple Claude instances
|
|
1339
2083
|
self.claude_instances = []
|
|
1340
2084
|
self.instance_configs = []
|
|
1341
|
-
|
|
2085
|
+
|
|
1342
2086
|
async def initialize(self):
|
|
1343
2087
|
"""Initialize all Claude instances with MCP networking."""
|
|
1344
2088
|
self.console.print("[cyan]Initializing Claude instances...[/cyan]")
|
|
1345
|
-
|
|
2089
|
+
|
|
1346
2090
|
for i in range(self.num_instances):
|
|
1347
2091
|
role = "primary" if i == 0 else f"critic_{i}"
|
|
1348
2092
|
config = await self._create_instance_config(i, role)
|
|
1349
2093
|
self.instance_configs.append(config)
|
|
1350
|
-
|
|
1351
|
-
self.console.print(
|
|
1352
|
-
|
|
2094
|
+
|
|
2095
|
+
self.console.print(
|
|
2096
|
+
f" [{i+1}/{self.num_instances}] {role} instance configured"
|
|
2097
|
+
)
|
|
2098
|
+
|
|
1353
2099
|
# If networking enabled, configure MCP connections between instances
|
|
1354
2100
|
if self.enable_networking:
|
|
1355
2101
|
await self._setup_mcp_networking()
|
|
1356
|
-
|
|
2102
|
+
|
|
1357
2103
|
# Start all instances
|
|
1358
2104
|
for i, config in enumerate(self.instance_configs):
|
|
1359
2105
|
success = await self._start_claude_instance(i, config)
|
|
@@ -1361,12 +2107,12 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
|
|
1361
2107
|
self.console.print(f"[green]✓ Instance {i} started[/green]")
|
|
1362
2108
|
else:
|
|
1363
2109
|
self.console.print(f"[red]✗ Failed to start instance {i}[/red]")
|
|
1364
|
-
|
|
2110
|
+
|
|
1365
2111
|
async def _create_instance_config(self, index: int, role: str) -> Dict:
|
|
1366
2112
|
"""Create configuration for a Claude instance."""
|
|
1367
2113
|
base_port = 8000
|
|
1368
2114
|
mcp_port = 9000
|
|
1369
|
-
|
|
2115
|
+
|
|
1370
2116
|
config = {
|
|
1371
2117
|
"index": index,
|
|
1372
2118
|
"role": role,
|
|
@@ -1374,18 +2120,18 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
|
|
1374
2120
|
"port": base_port + index,
|
|
1375
2121
|
"mcp_port": mcp_port + index,
|
|
1376
2122
|
"mcp_config": {},
|
|
1377
|
-
"env": {}
|
|
2123
|
+
"env": {},
|
|
1378
2124
|
}
|
|
1379
|
-
|
|
2125
|
+
|
|
1380
2126
|
# Create workspace directory
|
|
1381
2127
|
config["workspace"].mkdir(parents=True, exist_ok=True)
|
|
1382
|
-
|
|
2128
|
+
|
|
1383
2129
|
# Configure MCP tools if enabled
|
|
1384
2130
|
if self.enable_mcp:
|
|
1385
2131
|
config["mcp_config"] = await self._create_mcp_config(index, role)
|
|
1386
|
-
|
|
2132
|
+
|
|
1387
2133
|
return config
|
|
1388
|
-
|
|
2134
|
+
|
|
1389
2135
|
async def _create_mcp_config(self, index: int, role: str) -> Dict:
|
|
1390
2136
|
"""Create MCP configuration for an instance."""
|
|
1391
2137
|
mcp_config = {
|
|
@@ -1393,29 +2139,26 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
|
|
1393
2139
|
"hanzo-mcp": {
|
|
1394
2140
|
"command": "python",
|
|
1395
2141
|
"args": ["-m", "hanzo_mcp"],
|
|
1396
|
-
"env": {
|
|
1397
|
-
"INSTANCE_ID": str(index),
|
|
1398
|
-
"INSTANCE_ROLE": role
|
|
1399
|
-
}
|
|
2142
|
+
"env": {"INSTANCE_ID": str(index), "INSTANCE_ROLE": role},
|
|
1400
2143
|
}
|
|
1401
2144
|
}
|
|
1402
2145
|
}
|
|
1403
|
-
|
|
2146
|
+
|
|
1404
2147
|
# Add file system tools
|
|
1405
2148
|
mcp_config["mcpServers"]["filesystem"] = {
|
|
1406
2149
|
"command": "npx",
|
|
1407
2150
|
"args": ["-y", "@modelcontextprotocol/server-filesystem"],
|
|
1408
|
-
"env": {
|
|
1409
|
-
"ALLOWED_DIRECTORIES": str(self.workspace_dir)
|
|
1410
|
-
}
|
|
2151
|
+
"env": {"ALLOWED_DIRECTORIES": str(self.workspace_dir)},
|
|
1411
2152
|
}
|
|
1412
|
-
|
|
2153
|
+
|
|
1413
2154
|
return mcp_config
|
|
1414
|
-
|
|
2155
|
+
|
|
1415
2156
|
async def _setup_mcp_networking(self):
|
|
1416
2157
|
"""Set up MCP networking between Claude instances."""
|
|
1417
|
-
self.console.print(
|
|
1418
|
-
|
|
2158
|
+
self.console.print(
|
|
2159
|
+
"[cyan]Setting up MCP networking between instances...[/cyan]"
|
|
2160
|
+
)
|
|
2161
|
+
|
|
1419
2162
|
# Each instance gets MCP servers for all other instances
|
|
1420
2163
|
for i, config in enumerate(self.instance_configs):
|
|
1421
2164
|
for j, other_config in enumerate(self.instance_configs):
|
|
@@ -1425,37 +2168,38 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
|
|
1425
2168
|
config["mcp_config"]["mcpServers"][server_name] = {
|
|
1426
2169
|
"command": "python",
|
|
1427
2170
|
"args": [
|
|
1428
|
-
"-m",
|
|
1429
|
-
"
|
|
1430
|
-
"--
|
|
1431
|
-
|
|
2171
|
+
"-m",
|
|
2172
|
+
"hanzo_mcp.bridge",
|
|
2173
|
+
"--target-port",
|
|
2174
|
+
str(other_config["port"]),
|
|
2175
|
+
"--instance-id",
|
|
2176
|
+
str(j),
|
|
2177
|
+
"--role",
|
|
2178
|
+
other_config["role"],
|
|
1432
2179
|
],
|
|
1433
|
-
"env": {
|
|
1434
|
-
"SOURCE_INSTANCE": str(i),
|
|
1435
|
-
"TARGET_INSTANCE": str(j)
|
|
1436
|
-
}
|
|
2180
|
+
"env": {"SOURCE_INSTANCE": str(i), "TARGET_INSTANCE": str(j)},
|
|
1437
2181
|
}
|
|
1438
|
-
|
|
2182
|
+
|
|
1439
2183
|
# Save MCP config
|
|
1440
2184
|
mcp_config_file = config["workspace"] / "mcp_config.json"
|
|
1441
|
-
with open(mcp_config_file,
|
|
2185
|
+
with open(mcp_config_file, "w") as f:
|
|
1442
2186
|
json.dump(config["mcp_config"], f, indent=2)
|
|
1443
|
-
|
|
2187
|
+
|
|
1444
2188
|
config["env"]["MCP_CONFIG_PATH"] = str(mcp_config_file)
|
|
1445
|
-
|
|
2189
|
+
|
|
1446
2190
|
async def _start_claude_instance(self, index: int, config: Dict) -> bool:
|
|
1447
2191
|
"""Start a single Claude instance."""
|
|
1448
2192
|
try:
|
|
1449
2193
|
cmd = [self.claude_code_path or "claude"]
|
|
1450
|
-
|
|
2194
|
+
|
|
1451
2195
|
# Add configuration flags
|
|
1452
2196
|
if config.get("env", {}).get("MCP_CONFIG_PATH"):
|
|
1453
2197
|
cmd.extend(["--mcp-config", config["env"]["MCP_CONFIG_PATH"]])
|
|
1454
|
-
|
|
2198
|
+
|
|
1455
2199
|
# Set up environment
|
|
1456
2200
|
env = os.environ.copy()
|
|
1457
2201
|
env.update(config.get("env", {}))
|
|
1458
|
-
|
|
2202
|
+
|
|
1459
2203
|
# Start process
|
|
1460
2204
|
process = subprocess.Popen(
|
|
1461
2205
|
cmd,
|
|
@@ -1464,47 +2208,49 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
|
|
1464
2208
|
stdin=subprocess.PIPE,
|
|
1465
2209
|
env=env,
|
|
1466
2210
|
cwd=str(config["workspace"]),
|
|
1467
|
-
preexec_fn=os.setsid if hasattr(os,
|
|
2211
|
+
preexec_fn=os.setsid if hasattr(os, "setsid") else None,
|
|
1468
2212
|
)
|
|
1469
|
-
|
|
1470
|
-
self.claude_instances.append(
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
2213
|
+
|
|
2214
|
+
self.claude_instances.append(
|
|
2215
|
+
{
|
|
2216
|
+
"index": index,
|
|
2217
|
+
"role": config["role"],
|
|
2218
|
+
"process": process,
|
|
2219
|
+
"config": config,
|
|
2220
|
+
"health": RuntimeHealth(
|
|
2221
|
+
state=RuntimeState.RUNNING,
|
|
2222
|
+
last_response=datetime.now(),
|
|
2223
|
+
response_time_ms=0,
|
|
2224
|
+
memory_usage_mb=0,
|
|
2225
|
+
cpu_percent=0,
|
|
2226
|
+
error_count=0,
|
|
2227
|
+
restart_count=0,
|
|
2228
|
+
),
|
|
2229
|
+
}
|
|
2230
|
+
)
|
|
2231
|
+
|
|
1486
2232
|
return True
|
|
1487
|
-
|
|
2233
|
+
|
|
1488
2234
|
except Exception as e:
|
|
1489
2235
|
logger.error(f"Failed to start instance {index}: {e}")
|
|
1490
2236
|
return False
|
|
1491
|
-
|
|
2237
|
+
|
|
1492
2238
|
async def execute_with_critique(self, task: str) -> Dict:
|
|
1493
2239
|
"""Execute a task with System 2 critique.
|
|
1494
|
-
|
|
2240
|
+
|
|
1495
2241
|
1. Primary instance executes the task
|
|
1496
2242
|
2. Critic instance(s) review and suggest improvements
|
|
1497
2243
|
3. Primary incorporates feedback if confidence is high
|
|
1498
2244
|
"""
|
|
1499
2245
|
self.console.print(f"[cyan]Executing with System 2 thinking: {task}[/cyan]")
|
|
1500
|
-
|
|
2246
|
+
|
|
1501
2247
|
# Step 1: Primary execution
|
|
1502
2248
|
primary = self.claude_instances[0]
|
|
1503
2249
|
result = await self._send_to_instance(primary, task)
|
|
1504
|
-
|
|
2250
|
+
|
|
1505
2251
|
if self.num_instances < 2:
|
|
1506
2252
|
return result
|
|
1507
|
-
|
|
2253
|
+
|
|
1508
2254
|
# Step 2: Critic review
|
|
1509
2255
|
critiques = []
|
|
1510
2256
|
for critic in self.claude_instances[1:]:
|
|
@@ -1523,10 +2269,10 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
|
|
1523
2269
|
|
|
1524
2270
|
Suggest specific improvements.
|
|
1525
2271
|
"""
|
|
1526
|
-
|
|
2272
|
+
|
|
1527
2273
|
critique = await self._send_to_instance(critic, critique_prompt)
|
|
1528
2274
|
critiques.append(critique)
|
|
1529
|
-
|
|
2275
|
+
|
|
1530
2276
|
# Step 3: Incorporate feedback if valuable
|
|
1531
2277
|
if critiques and self.enable_guardrails:
|
|
1532
2278
|
improvement_prompt = f"""
|
|
@@ -1538,73 +2284,87 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
|
|
1538
2284
|
|
|
1539
2285
|
Incorporate the valid suggestions and produce an improved solution.
|
|
1540
2286
|
"""
|
|
1541
|
-
|
|
2287
|
+
|
|
1542
2288
|
improved = await self._send_to_instance(primary, improvement_prompt)
|
|
1543
|
-
|
|
2289
|
+
|
|
1544
2290
|
# Validate improvement didn't degrade quality
|
|
1545
2291
|
if await self._validate_improvement(result, improved):
|
|
1546
|
-
self.console.print(
|
|
2292
|
+
self.console.print(
|
|
2293
|
+
"[green]✓ Solution improved with System 2 feedback[/green]"
|
|
2294
|
+
)
|
|
1547
2295
|
return improved
|
|
1548
2296
|
else:
|
|
1549
|
-
self.console.print(
|
|
1550
|
-
|
|
2297
|
+
self.console.print(
|
|
2298
|
+
"[yellow]⚠ Keeping original solution (improvement validation failed)[/yellow]"
|
|
2299
|
+
)
|
|
2300
|
+
|
|
1551
2301
|
return result
|
|
1552
|
-
|
|
2302
|
+
|
|
1553
2303
|
async def _send_to_instance(self, instance: Dict, prompt: str) -> Dict:
|
|
1554
2304
|
"""Send a prompt to a specific Claude instance."""
|
|
1555
2305
|
# This would use the actual Claude API or IPC mechanism
|
|
1556
2306
|
# For now, it's a placeholder
|
|
1557
2307
|
return {
|
|
1558
2308
|
"output": f"Response from {instance['role']}: Processed '{prompt[:50]}...'",
|
|
1559
|
-
"success": True
|
|
2309
|
+
"success": True,
|
|
1560
2310
|
}
|
|
1561
|
-
|
|
2311
|
+
|
|
1562
2312
|
async def _validate_improvement(self, original: Dict, improved: Dict) -> bool:
|
|
1563
2313
|
"""Validate that an improvement doesn't degrade quality."""
|
|
1564
2314
|
if not self.enable_guardrails:
|
|
1565
2315
|
return True
|
|
1566
|
-
|
|
2316
|
+
|
|
1567
2317
|
# Placeholder for actual validation logic
|
|
1568
2318
|
# Would check: tests still pass, no new errors, performance not degraded, etc.
|
|
1569
2319
|
return True
|
|
1570
|
-
|
|
2320
|
+
|
|
1571
2321
|
def shutdown(self):
|
|
1572
2322
|
"""Shutdown all Claude instances."""
|
|
1573
2323
|
self.console.print("[yellow]Shutting down all instances...[/yellow]")
|
|
1574
|
-
|
|
2324
|
+
|
|
1575
2325
|
for instance in self.claude_instances:
|
|
1576
2326
|
try:
|
|
1577
2327
|
process = instance["process"]
|
|
1578
|
-
if hasattr(os,
|
|
2328
|
+
if hasattr(os, "killpg"):
|
|
1579
2329
|
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
|
1580
2330
|
else:
|
|
1581
2331
|
process.terminate()
|
|
1582
2332
|
process.wait(timeout=5)
|
|
1583
|
-
except:
|
|
2333
|
+
except Exception:
|
|
1584
2334
|
try:
|
|
1585
2335
|
instance["process"].kill()
|
|
1586
|
-
except:
|
|
2336
|
+
except Exception:
|
|
1587
2337
|
pass
|
|
1588
|
-
|
|
2338
|
+
|
|
1589
2339
|
self.console.print("[green]✓ All instances shut down[/green]")
|
|
1590
2340
|
|
|
1591
2341
|
|
|
1592
2342
|
async def main():
|
|
1593
2343
|
"""Main entry point for hanzo-dev."""
|
|
1594
2344
|
import argparse
|
|
1595
|
-
|
|
1596
|
-
parser = argparse.ArgumentParser(
|
|
1597
|
-
|
|
2345
|
+
|
|
2346
|
+
parser = argparse.ArgumentParser(
|
|
2347
|
+
description="Hanzo Dev - System 2 Meta-AI Orchestrator"
|
|
2348
|
+
)
|
|
2349
|
+
parser.add_argument(
|
|
2350
|
+
"--workspace", default="~/.hanzo/dev", help="Workspace directory"
|
|
2351
|
+
)
|
|
1598
2352
|
parser.add_argument("--claude-path", help="Path to Claude Code executable")
|
|
1599
2353
|
parser.add_argument("--monitor", action="store_true", help="Start in monitor mode")
|
|
1600
2354
|
parser.add_argument("--repl", action="store_true", help="Start REPL interface")
|
|
1601
|
-
parser.add_argument(
|
|
2355
|
+
parser.add_argument(
|
|
2356
|
+
"--instances", type=int, default=2, help="Number of Claude instances"
|
|
2357
|
+
)
|
|
1602
2358
|
parser.add_argument("--no-mcp", action="store_true", help="Disable MCP tools")
|
|
1603
|
-
parser.add_argument(
|
|
1604
|
-
|
|
1605
|
-
|
|
2359
|
+
parser.add_argument(
|
|
2360
|
+
"--no-network", action="store_true", help="Disable instance networking"
|
|
2361
|
+
)
|
|
2362
|
+
parser.add_argument(
|
|
2363
|
+
"--no-guardrails", action="store_true", help="Disable guardrails"
|
|
2364
|
+
)
|
|
2365
|
+
|
|
1606
2366
|
args = parser.parse_args()
|
|
1607
|
-
|
|
2367
|
+
|
|
1608
2368
|
await run_dev_orchestrator(
|
|
1609
2369
|
workspace=args.workspace,
|
|
1610
2370
|
claude_path=args.claude_path,
|
|
@@ -1613,9 +2373,9 @@ async def main():
|
|
|
1613
2373
|
instances=args.instances,
|
|
1614
2374
|
mcp_tools=not args.no_mcp,
|
|
1615
2375
|
network_mode=not args.no_network,
|
|
1616
|
-
guardrails=not args.no_guardrails
|
|
2376
|
+
guardrails=not args.no_guardrails,
|
|
1617
2377
|
)
|
|
1618
2378
|
|
|
1619
2379
|
|
|
1620
2380
|
if __name__ == "__main__":
|
|
1621
|
-
asyncio.run(main())
|
|
2381
|
+
asyncio.run(main())
|