@hanzo/dev 1.2.0 โ 2.0.0
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.
- package/.eslintrc.js +25 -0
- package/dist/cli/dev.js +8202 -553
- package/jest.config.js +30 -0
- package/package.json +13 -1
- package/src/cli/dev.ts +456 -106
- package/src/lib/agent-loop.ts +552 -0
- package/src/lib/code-act-agent.ts +378 -0
- package/src/lib/config.ts +163 -0
- package/src/lib/editor.ts +368 -0
- package/src/lib/function-calling.ts +318 -0
- package/src/lib/mcp-client.ts +259 -0
- package/src/lib/peer-agent-network.ts +584 -0
- package/src/lib/unified-workspace.ts +435 -0
- package/tests/browser-integration.test.ts +242 -0
- package/tests/code-act-agent.test.ts +305 -0
- package/tests/editor.test.ts +223 -0
- package/tests/mcp-client.test.ts +238 -0
- package/tests/peer-agent-network.test.ts +340 -0
- package/tests/setup.ts +25 -0
- package/tests/swe-bench.test.ts +357 -0
- package/tsconfig.json +13 -15
|
@@ -0,0 +1,584 @@
|
|
|
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
|
+
}
|