hanzo 0.3.10__py3-none-any.whl → 0.3.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo might be problematic. Click here for more details.

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