@hanzo/dev 2.1.1 โ†’ 3.0.2

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.
@@ -1,584 +0,0 @@
1
- import { EventEmitter } from 'events';
2
- import { spawn, ChildProcess } from 'child_process';
3
- import { MCPClient, MCPSession, MCPServerConfig } from './mcp-client';
4
- import { FunctionCallingSystem } from './function-calling';
5
- import chalk from 'chalk';
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
-
9
- export interface AgentConfig {
10
- id: string;
11
- name: string;
12
- type: 'claude-code' | 'aider' | 'openhands' | 'custom';
13
- command?: string;
14
- args?: string[];
15
- env?: Record<string, string>;
16
- capabilities: string[];
17
- assignedFiles?: string[];
18
- mcpEndpoint?: string;
19
- }
20
-
21
- export interface AgentInstance {
22
- config: AgentConfig;
23
- process?: ChildProcess;
24
- mcpSession?: MCPSession;
25
- status: 'idle' | 'busy' | 'error';
26
- currentTask?: string;
27
- metrics: {
28
- tasksCompleted: number;
29
- errors: number;
30
- averageTime: number;
31
- };
32
- }
33
-
34
- export class PeerAgentNetwork extends EventEmitter {
35
- private agents: Map<string, AgentInstance> = new Map();
36
- private mcpClient: MCPClient;
37
- private functionCalling: FunctionCallingSystem;
38
- private networkTopology: Map<string, string[]> = new Map(); // agent -> connected agents
39
-
40
- constructor() {
41
- super();
42
- this.mcpClient = new MCPClient();
43
- this.functionCalling = new FunctionCallingSystem();
44
- }
45
-
46
- // Spawn multiple agents for a codebase
47
- async spawnAgentsForCodebase(
48
- basePath: string,
49
- agentType: AgentConfig['type'] = 'claude-code',
50
- strategy: 'one-per-file' | 'one-per-directory' | 'by-complexity' = 'one-per-file'
51
- ): Promise<void> {
52
- console.log(chalk.cyan(`\n๐ŸŒ Spawning agent network for ${basePath}...\n`));
53
-
54
- const files = await this.discoverFiles(basePath);
55
- const assignments = this.assignFilesToAgents(files, strategy);
56
-
57
- console.log(chalk.yellow(`Creating ${assignments.length} agents for ${files.length} files`));
58
-
59
- // Spawn agents
60
- for (const assignment of assignments) {
61
- const agentId = `${agentType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
62
- const config: AgentConfig = {
63
- id: agentId,
64
- name: `${agentType} (${assignment.files.length} files)`,
65
- type: agentType,
66
- capabilities: this.getAgentCapabilities(agentType),
67
- assignedFiles: assignment.files
68
- };
69
-
70
- await this.spawnAgent(config);
71
- }
72
-
73
- // Connect all agents to each other
74
- await this.establishPeerConnections();
75
-
76
- console.log(chalk.green(`\nโœ… Agent network ready with ${this.agents.size} agents\n`));
77
- }
78
-
79
- // Spawn a single agent
80
- async spawnAgent(config: AgentConfig): Promise<AgentInstance> {
81
- const instance: AgentInstance = {
82
- config,
83
- status: 'idle',
84
- metrics: {
85
- tasksCompleted: 0,
86
- errors: 0,
87
- averageTime: 0
88
- }
89
- };
90
-
91
- // Set up command based on agent type
92
- switch (config.type) {
93
- case 'claude-code':
94
- config.command = 'npx';
95
- config.args = ['claude-code', '--mcp-server', '--port', String(this.getNextPort())];
96
- break;
97
- case 'aider':
98
- config.command = 'aider';
99
- config.args = ['--no-interactive', '--mcp-mode'];
100
- break;
101
- case 'openhands':
102
- config.command = hasUvx() ? 'uvx' : 'python';
103
- config.args = hasUvx() ? ['hanzo-dev', '--mcp'] : ['-m', 'hanzo_dev', '--mcp'];
104
- break;
105
- }
106
-
107
- // Start MCP server for this agent
108
- if (config.command) {
109
- try {
110
- const mcpConfig: MCPServerConfig = {
111
- name: config.id,
112
- command: config.command,
113
- args: config.args,
114
- env: {
115
- ...process.env,
116
- ...config.env,
117
- AGENT_ID: config.id,
118
- ASSIGNED_FILES: JSON.stringify(config.assignedFiles || [])
119
- }
120
- };
121
-
122
- instance.mcpSession = await this.mcpClient.connect(mcpConfig);
123
- instance.mcpEndpoint = `mcp://${config.id}`;
124
-
125
- // Register this agent's tools with the function calling system
126
- await this.functionCalling.registerMCPServer(config.id, instance.mcpSession);
127
-
128
- console.log(chalk.green(` โœ“ Spawned ${config.name}`));
129
- } catch (error) {
130
- console.error(chalk.red(` โœ— Failed to spawn ${config.name}: ${error}`));
131
- instance.status = 'error';
132
- }
133
- }
134
-
135
- this.agents.set(config.id, instance);
136
- return instance;
137
- }
138
-
139
- // Establish peer connections between all agents
140
- async establishPeerConnections(): Promise<void> {
141
- console.log(chalk.yellow('\n๐Ÿ”— Establishing peer connections...\n'));
142
-
143
- const agentIds = Array.from(this.agents.keys());
144
-
145
- for (const agentId of agentIds) {
146
- const connections: string[] = [];
147
-
148
- // Connect to all other agents
149
- for (const peerId of agentIds) {
150
- if (agentId !== peerId) {
151
- await this.connectAgents(agentId, peerId);
152
- connections.push(peerId);
153
- }
154
- }
155
-
156
- this.networkTopology.set(agentId, connections);
157
- }
158
-
159
- console.log(chalk.green(` โœ“ Established ${this.calculateTotalConnections()} peer connections`));
160
- }
161
-
162
- // Connect two agents via MCP
163
- private async connectAgents(agentId: string, peerId: string): Promise<void> {
164
- const agent = this.agents.get(agentId);
165
- const peer = this.agents.get(peerId);
166
-
167
- if (!agent || !peer || !agent.mcpSession || !peer.mcpSession) return;
168
-
169
- // Register peer's tools with agent
170
- const peerTools = peer.mcpSession.tools.map(tool => ({
171
- name: `peer_${peerId}_${tool.name}`,
172
- description: `[Via ${peer.config.name}] ${tool.description}`,
173
- inputSchema: tool.inputSchema,
174
- handler: async (args: any) => {
175
- return this.mcpClient.callTool(peer.mcpSession!.id, tool.name, args);
176
- }
177
- }));
178
-
179
- // Add to agent's available tools
180
- for (const tool of peerTools) {
181
- this.functionCalling.registerTool(tool);
182
- }
183
- }
184
-
185
- // Delegate task to best agent
186
- async delegateTask(task: string, context?: any): Promise<any> {
187
- console.log(chalk.cyan(`\n๐Ÿ“‹ Delegating task: ${task}\n`));
188
-
189
- // Find best agent for task
190
- const agent = await this.selectBestAgent(task, context);
191
- if (!agent) {
192
- throw new Error('No suitable agent available');
193
- }
194
-
195
- console.log(chalk.gray(` โ†’ Assigned to ${agent.config.name}`));
196
-
197
- // Execute task
198
- agent.status = 'busy';
199
- agent.currentTask = task;
200
- const startTime = Date.now();
201
-
202
- try {
203
- const result = await this.executeAgentTask(agent, task, context);
204
-
205
- // Update metrics
206
- agent.metrics.tasksCompleted++;
207
- const duration = Date.now() - startTime;
208
- agent.metrics.averageTime =
209
- (agent.metrics.averageTime * (agent.metrics.tasksCompleted - 1) + duration) /
210
- agent.metrics.tasksCompleted;
211
-
212
- agent.status = 'idle';
213
- agent.currentTask = undefined;
214
-
215
- return result;
216
- } catch (error) {
217
- agent.metrics.errors++;
218
- agent.status = 'idle';
219
- agent.currentTask = undefined;
220
- throw error;
221
- }
222
- }
223
-
224
- // Select best agent for a task
225
- private async selectBestAgent(task: string, context?: any): Promise<AgentInstance | null> {
226
- // Score agents based on:
227
- // 1. Current status (idle preferred)
228
- // 2. Relevant files assigned
229
- // 3. Capabilities match
230
- // 4. Past performance metrics
231
-
232
- let bestAgent: AgentInstance | null = null;
233
- let bestScore = -1;
234
-
235
- for (const agent of this.agents.values()) {
236
- let score = 0;
237
-
238
- // Status score
239
- if (agent.status === 'idle') score += 10;
240
- else if (agent.status === 'busy') score -= 5;
241
- else continue; // Skip error agents
242
-
243
- // File relevance score
244
- if (context?.file && agent.config.assignedFiles?.includes(context.file)) {
245
- score += 20;
246
- }
247
-
248
- // Performance score
249
- if (agent.metrics.tasksCompleted > 0) {
250
- const successRate = 1 - (agent.metrics.errors / agent.metrics.tasksCompleted);
251
- score += successRate * 10;
252
- }
253
-
254
- // Capability match (simplified)
255
- if (task.includes('refactor') && agent.config.capabilities.includes('refactoring')) {
256
- score += 15;
257
- }
258
- if (task.includes('test') && agent.config.capabilities.includes('testing')) {
259
- score += 15;
260
- }
261
-
262
- if (score > bestScore) {
263
- bestScore = score;
264
- bestAgent = agent;
265
- }
266
- }
267
-
268
- return bestAgent;
269
- }
270
-
271
- // Execute task on specific agent
272
- private async executeAgentTask(
273
- agent: AgentInstance,
274
- task: string,
275
- context?: any
276
- ): Promise<any> {
277
- if (!agent.mcpSession) {
278
- throw new Error('Agent has no MCP session');
279
- }
280
-
281
- // Call the agent's main task execution tool
282
- return this.mcpClient.callTool(
283
- agent.mcpSession.id,
284
- 'execute_task',
285
- {
286
- task,
287
- context,
288
- assigned_files: agent.config.assignedFiles
289
- }
290
- );
291
- }
292
-
293
- // Parallel task execution across multiple agents
294
- async executeParallelTasks(tasks: Array<{task: string, context?: any}>): Promise<any[]> {
295
- console.log(chalk.cyan(`\nโšก Executing ${tasks.length} tasks in parallel...\n`));
296
-
297
- const promises = tasks.map(({task, context}) =>
298
- this.delegateTask(task, context).catch(error => ({
299
- error: error.message,
300
- task
301
- }))
302
- );
303
-
304
- const results = await Promise.all(promises);
305
-
306
- const successful = results.filter(r => !r.error).length;
307
- console.log(chalk.green(`\nโœ… Completed ${successful}/${tasks.length} tasks successfully\n`));
308
-
309
- return results;
310
- }
311
-
312
- // Swarm coordination for complex tasks
313
- async coordinateSwarm(
314
- masterTask: string,
315
- decompositionStrategy: 'auto' | 'by-file' | 'by-feature' = 'auto'
316
- ): Promise<void> {
317
- console.log(chalk.bold.cyan(`\n๐Ÿ Coordinating agent swarm for: ${masterTask}\n`));
318
-
319
- // Decompose master task into subtasks
320
- const subtasks = await this.decomposeTask(masterTask, decompositionStrategy);
321
- console.log(chalk.yellow(`Decomposed into ${subtasks.length} subtasks`));
322
-
323
- // Create execution plan
324
- const plan = this.createSwarmExecutionPlan(subtasks);
325
-
326
- // Execute plan
327
- for (const phase of plan.phases) {
328
- console.log(chalk.blue(`\nโ–ถ Phase ${phase.id}: ${phase.description}`));
329
-
330
- if (phase.parallel) {
331
- await this.executeParallelTasks(phase.tasks);
332
- } else {
333
- for (const task of phase.tasks) {
334
- await this.delegateTask(task.task, task.context);
335
- }
336
- }
337
- }
338
-
339
- console.log(chalk.bold.green(`\nโœ… Swarm task completed!\n`));
340
- }
341
-
342
- // Task decomposition
343
- private async decomposeTask(
344
- task: string,
345
- strategy: string
346
- ): Promise<Array<{task: string, context?: any}>> {
347
- // In real implementation, would use LLM for intelligent decomposition
348
- const subtasks: Array<{task: string, context?: any}> = [];
349
-
350
- if (strategy === 'by-file' || task.includes('refactor all')) {
351
- // Create subtask for each file
352
- for (const agent of this.agents.values()) {
353
- if (agent.config.assignedFiles) {
354
- for (const file of agent.config.assignedFiles) {
355
- subtasks.push({
356
- task: `${task} in ${file}`,
357
- context: { file }
358
- });
359
- }
360
- }
361
- }
362
- } else {
363
- // Simple decomposition
364
- subtasks.push(
365
- { task: `Analyze requirements for: ${task}` },
366
- { task: `Implement: ${task}` },
367
- { task: `Test: ${task}` },
368
- { task: `Document: ${task}` }
369
- );
370
- }
371
-
372
- return subtasks;
373
- }
374
-
375
- // Create execution plan for swarm
376
- private createSwarmExecutionPlan(subtasks: Array<{task: string, context?: any}>): {
377
- phases: Array<{
378
- id: number;
379
- description: string;
380
- parallel: boolean;
381
- tasks: Array<{task: string, context?: any}>;
382
- }>;
383
- } {
384
- // Group tasks into phases based on dependencies
385
- const phases = [];
386
-
387
- // Phase 1: Analysis (sequential)
388
- const analysisTasks = subtasks.filter(t => t.task.includes('Analyze'));
389
- if (analysisTasks.length > 0) {
390
- phases.push({
391
- id: 1,
392
- description: 'Analysis',
393
- parallel: false,
394
- tasks: analysisTasks
395
- });
396
- }
397
-
398
- // Phase 2: Implementation (parallel)
399
- const implementTasks = subtasks.filter(t =>
400
- t.task.includes('Implement') || t.task.includes('refactor')
401
- );
402
- if (implementTasks.length > 0) {
403
- phases.push({
404
- id: 2,
405
- description: 'Implementation',
406
- parallel: true,
407
- tasks: implementTasks
408
- });
409
- }
410
-
411
- // Phase 3: Testing (parallel)
412
- const testTasks = subtasks.filter(t => t.task.includes('Test'));
413
- if (testTasks.length > 0) {
414
- phases.push({
415
- id: 3,
416
- description: 'Testing',
417
- parallel: true,
418
- tasks: testTasks
419
- });
420
- }
421
-
422
- // Phase 4: Documentation (parallel)
423
- const docTasks = subtasks.filter(t => t.task.includes('Document'));
424
- if (docTasks.length > 0) {
425
- phases.push({
426
- id: 4,
427
- description: 'Documentation',
428
- parallel: true,
429
- tasks: docTasks
430
- });
431
- }
432
-
433
- return { phases };
434
- }
435
-
436
- // Helper methods
437
- private async discoverFiles(basePath: string): Promise<string[]> {
438
- const files: string[] = [];
439
-
440
- const walkDir = (dir: string) => {
441
- const entries = fs.readdirSync(dir);
442
- for (const entry of entries) {
443
- const fullPath = path.join(dir, entry);
444
- const stats = fs.statSync(fullPath);
445
-
446
- if (stats.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
447
- walkDir(fullPath);
448
- } else if (stats.isFile() && this.isCodeFile(entry)) {
449
- files.push(fullPath);
450
- }
451
- }
452
- };
453
-
454
- walkDir(basePath);
455
- return files;
456
- }
457
-
458
- private isCodeFile(filename: string): boolean {
459
- const extensions = ['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.go', '.rs'];
460
- return extensions.some(ext => filename.endsWith(ext));
461
- }
462
-
463
- private assignFilesToAgents(
464
- files: string[],
465
- strategy: string
466
- ): Array<{files: string[]}> {
467
- const assignments: Array<{files: string[]}> = [];
468
-
469
- if (strategy === 'one-per-file') {
470
- // One agent per file
471
- for (const file of files) {
472
- assignments.push({ files: [file] });
473
- }
474
- } else if (strategy === 'one-per-directory') {
475
- // Group by directory
476
- const byDir = new Map<string, string[]>();
477
- for (const file of files) {
478
- const dir = path.dirname(file);
479
- if (!byDir.has(dir)) byDir.set(dir, []);
480
- byDir.get(dir)!.push(file);
481
- }
482
- for (const files of byDir.values()) {
483
- assignments.push({ files });
484
- }
485
- } else {
486
- // By complexity - simplified: just split evenly
487
- const agentCount = Math.min(files.length, 10); // Max 10 agents
488
- const filesPerAgent = Math.ceil(files.length / agentCount);
489
-
490
- for (let i = 0; i < files.length; i += filesPerAgent) {
491
- assignments.push({
492
- files: files.slice(i, i + filesPerAgent)
493
- });
494
- }
495
- }
496
-
497
- return assignments;
498
- }
499
-
500
- private getAgentCapabilities(type: AgentConfig['type']): string[] {
501
- switch (type) {
502
- case 'claude-code':
503
- return ['editing', 'refactoring', 'analysis', 'testing', 'documentation'];
504
- case 'aider':
505
- return ['editing', 'git', 'refactoring', 'testing'];
506
- case 'openhands':
507
- return ['editing', 'execution', 'browsing', 'complex-tasks'];
508
- default:
509
- return ['editing'];
510
- }
511
- }
512
-
513
- private calculateTotalConnections(): number {
514
- const n = this.agents.size;
515
- return (n * (n - 1)) / 2; // Complete graph connections
516
- }
517
-
518
- private getNextPort(): number {
519
- // Simple port allocation starting from 9000
520
- return 9000 + this.agents.size;
521
- }
522
-
523
- // Get network status
524
- getNetworkStatus(): {
525
- totalAgents: number;
526
- activeAgents: number;
527
- totalConnections: number;
528
- taskMetrics: {
529
- total: number;
530
- successful: number;
531
- failed: number;
532
- averageTime: number;
533
- };
534
- } {
535
- let totalTasks = 0;
536
- let totalTime = 0;
537
- let totalErrors = 0;
538
-
539
- for (const agent of this.agents.values()) {
540
- totalTasks += agent.metrics.tasksCompleted;
541
- totalErrors += agent.metrics.errors;
542
- totalTime += agent.metrics.averageTime * agent.metrics.tasksCompleted;
543
- }
544
-
545
- return {
546
- totalAgents: this.agents.size,
547
- activeAgents: Array.from(this.agents.values()).filter(a => a.status !== 'error').length,
548
- totalConnections: this.calculateTotalConnections(),
549
- taskMetrics: {
550
- total: totalTasks,
551
- successful: totalTasks - totalErrors,
552
- failed: totalErrors,
553
- averageTime: totalTasks > 0 ? totalTime / totalTasks : 0
554
- }
555
- };
556
- }
557
-
558
- // Cleanup
559
- async cleanup(): Promise<void> {
560
- console.log(chalk.yellow('\n๐Ÿงน Cleaning up agent network...\n'));
561
-
562
- for (const agent of this.agents.values()) {
563
- if (agent.mcpSession) {
564
- await this.mcpClient.disconnect(agent.mcpSession.id);
565
- }
566
- if (agent.process) {
567
- agent.process.kill();
568
- }
569
- }
570
-
571
- this.agents.clear();
572
- this.networkTopology.clear();
573
- }
574
- }
575
-
576
- // Helper to check if uvx is available
577
- function hasUvx(): boolean {
578
- try {
579
- require('child_process').execSync('which uvx', { stdio: 'ignore' });
580
- return true;
581
- } catch {
582
- return false;
583
- }
584
- }