feat: integrate swarm with agent loop and add interactive mode
- Replace separate swarm mode with integrated swarm tools in agent loop - Add interactive chat mode when running 'dev' without arguments - Support programmatic mode with 'dev -p prompt' - Add session management (save/load conversations) - Fix glob timeout by using sync version - Remove shell deprecation warnings - Swarm functionality now available as tools within agent - Version bump to 2.2.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hanzo/dev",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.0",
|
||||
"description": "Hanzo Dev - Meta AI development CLI that manages and runs all LLMs and CLI tools",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
|
||||
+23
-48
@@ -16,6 +16,7 @@ import { PeerAgentNetwork } from '../lib/peer-agent-network';
|
||||
import { BenchmarkRunner, BenchmarkConfig } from '../lib/benchmark-runner';
|
||||
import { ConfigurableAgentLoop, LLMProvider } from '../lib/agent-loop';
|
||||
import { SwarmRunner, SwarmOptions } from '../lib/swarm-runner';
|
||||
import { InteractiveAgent } from '../lib/interactive-agent';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
@@ -888,59 +889,33 @@ async function runSwarmMode(options: any): Promise<void> {
|
||||
// Default action
|
||||
program
|
||||
.action(async (options) => {
|
||||
// Check if swarm mode is requested
|
||||
if (options.swarm) {
|
||||
await runSwarmMode(options);
|
||||
return;
|
||||
// Determine provider
|
||||
let providerType: 'claude' | 'openai' | 'gemini' | 'grok' | 'local' = 'claude';
|
||||
if (options.openai) providerType = 'openai';
|
||||
else if (options.gemini) providerType = 'gemini';
|
||||
else if (options.grok) providerType = 'grok';
|
||||
else if (options.local) providerType = 'local';
|
||||
|
||||
// Get provider config
|
||||
const providers = ConfigurableAgentLoop.getAvailableProviders();
|
||||
const provider = providers.find(p => p.type === providerType);
|
||||
|
||||
if (!provider) {
|
||||
console.error(chalk.red(`Provider '${providerType}' not configured`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if a specific provider is requested
|
||||
if (options.claude || options.openai || options.gemini || options.grok || options.local) {
|
||||
let provider = 'claude';
|
||||
if (options.claude) provider = 'claude';
|
||||
else if (options.openai) provider = 'openai';
|
||||
else if (options.gemini) provider = 'gemini';
|
||||
else if (options.grok) provider = 'grok';
|
||||
else if (options.local) provider = 'local';
|
||||
// Create interactive agent with swarm support if requested
|
||||
const agent = new InteractiveAgent(provider, options.swarm ? parseInt(options.swarm) : undefined);
|
||||
|
||||
// Map provider to tool name
|
||||
const toolMap: Record<string, string> = {
|
||||
claude: 'claude',
|
||||
openai: 'codex',
|
||||
gemini: 'gemini',
|
||||
grok: 'grok',
|
||||
local: 'hanzo-dev'
|
||||
};
|
||||
|
||||
const toolName = toolMap[provider];
|
||||
if (toolName && TOOLS[toolName as keyof typeof TOOLS]) {
|
||||
console.log(chalk.gray(`Launching ${TOOLS[toolName as keyof typeof TOOLS].name}...`));
|
||||
runTool(toolName, options.prompt ? [options.prompt] : ['.']);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultTool = await getDefaultTool();
|
||||
if (defaultTool && process.argv.length === 2) {
|
||||
console.log(chalk.gray(`Auto-launching ${TOOLS[defaultTool as keyof typeof TOOLS].name}...`));
|
||||
runTool(defaultTool, ['.']);
|
||||
// If prompt provided, run it
|
||||
if (options.prompt) {
|
||||
await agent.start(options.prompt);
|
||||
} else {
|
||||
interactiveMode();
|
||||
// No prompt - start interactive mode
|
||||
await agent.start();
|
||||
}
|
||||
});
|
||||
|
||||
// Parse arguments
|
||||
program.parse();
|
||||
|
||||
// If no arguments, run interactive mode
|
||||
if (process.argv.length === 2) {
|
||||
(async () => {
|
||||
const defaultTool = await getDefaultTool();
|
||||
if (defaultTool) {
|
||||
console.log(chalk.gray(`Auto-launching ${TOOLS[defaultTool as keyof typeof TOOLS].name}...`));
|
||||
runTool(defaultTool, ['.']);
|
||||
} else {
|
||||
interactiveMode();
|
||||
}
|
||||
})();
|
||||
}
|
||||
program.parse();
|
||||
@@ -31,11 +31,13 @@ export interface AgentMessage {
|
||||
}
|
||||
|
||||
export class ConfigurableAgentLoop extends EventEmitter {
|
||||
private config: AgentLoopConfig;
|
||||
public config: AgentLoopConfig;
|
||||
private functionCalling: FunctionCallingSystem;
|
||||
private mcpClient: MCPClient;
|
||||
private messages: AgentMessage[] = [];
|
||||
private iterations: number = 0;
|
||||
private context: any = {};
|
||||
private customTools: Map<string, any> = new Map();
|
||||
|
||||
constructor(config: AgentLoopConfig) {
|
||||
super();
|
||||
@@ -549,4 +551,93 @@ Always use tools to accomplish tasks. Think step by step.`;
|
||||
updateConfig(updates: Partial<AgentLoopConfig>): void {
|
||||
this.config = { ...this.config, ...updates };
|
||||
}
|
||||
|
||||
// Add custom tool
|
||||
addTool(tool: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: any;
|
||||
execute: (params: any) => Promise<any>;
|
||||
}): void {
|
||||
this.customTools.set(tool.name, tool);
|
||||
this.functionCalling.registerFunction(
|
||||
tool.name,
|
||||
tool.execute,
|
||||
tool.description,
|
||||
tool.parameters
|
||||
);
|
||||
}
|
||||
|
||||
// Get conversation messages
|
||||
getMessages(): AgentMessage[] {
|
||||
return [...this.messages];
|
||||
}
|
||||
|
||||
// Get current context
|
||||
getContext(): any {
|
||||
return { ...this.context };
|
||||
}
|
||||
|
||||
// Load session data
|
||||
loadSession(messages: AgentMessage[], context: any): void {
|
||||
this.messages = [...messages];
|
||||
this.context = { ...context };
|
||||
}
|
||||
|
||||
// Process a user message
|
||||
async processMessage(content: string): Promise<string> {
|
||||
// Add user message
|
||||
const userMessage: AgentMessage = {
|
||||
role: 'user',
|
||||
content
|
||||
};
|
||||
this.messages.push(userMessage);
|
||||
|
||||
// Process through agent loop
|
||||
try {
|
||||
const response = await this.executeIteration();
|
||||
return response.content;
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error processing message: ${error}`));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute one iteration of the agent loop
|
||||
private async executeIteration(): Promise<AgentMessage> {
|
||||
this.iterations++;
|
||||
|
||||
// Call appropriate LLM
|
||||
let response: AgentMessage;
|
||||
switch (this.config.provider.type) {
|
||||
case 'anthropic':
|
||||
response = await this.callAnthropic();
|
||||
break;
|
||||
case 'openai':
|
||||
response = await this.callOpenAI();
|
||||
break;
|
||||
case 'hanzo-app':
|
||||
response = await this.callHanzoApp();
|
||||
break;
|
||||
case 'local':
|
||||
default:
|
||||
response = await this.callLocalLLM();
|
||||
break;
|
||||
}
|
||||
|
||||
// Execute any tool calls
|
||||
if (response.toolCalls && response.toolCalls.length > 0) {
|
||||
const toolResults = await this.executeToolCalls(response.toolCalls);
|
||||
response.toolResults = toolResults;
|
||||
|
||||
// Add tool results as a message
|
||||
this.messages.push({
|
||||
role: 'tool',
|
||||
content: JSON.stringify(toolResults),
|
||||
toolResults
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,20 @@ export class FunctionCallingSystem {
|
||||
this.tools.set(tool.name, tool);
|
||||
}
|
||||
|
||||
registerFunction(
|
||||
name: string,
|
||||
handler: (args: any) => Promise<any>,
|
||||
description: string,
|
||||
parameters: any
|
||||
): void {
|
||||
this.registerTool({
|
||||
name,
|
||||
description,
|
||||
parameters,
|
||||
handler
|
||||
});
|
||||
}
|
||||
|
||||
async registerMCPServer(name: string, session: MCPSession): Promise<void> {
|
||||
this.mcpSessions.set(name, session);
|
||||
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import * as readline from 'readline';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { ConfigurableAgentLoop, AgentLoopConfig, LLMProvider } from './agent-loop';
|
||||
import { SwarmTool } from './swarm-tool';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
messages: any[];
|
||||
context: any;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export class InteractiveAgent {
|
||||
private rl: readline.Interface;
|
||||
private agent: ConfigurableAgentLoop;
|
||||
private sessionId: string;
|
||||
private sessionDir: string;
|
||||
private swarmTool: SwarmTool;
|
||||
private swarmEnabled: boolean = false;
|
||||
private swarmCount: number = 5;
|
||||
|
||||
constructor(provider?: LLMProvider, swarmCount?: number) {
|
||||
this.sessionId = uuidv4();
|
||||
this.sessionDir = path.join(process.cwd(), '.dev-sessions');
|
||||
this.swarmTool = new SwarmTool();
|
||||
|
||||
if (swarmCount) {
|
||||
this.swarmEnabled = true;
|
||||
this.swarmCount = swarmCount;
|
||||
}
|
||||
|
||||
// Ensure session directory exists
|
||||
if (!fs.existsSync(this.sessionDir)) {
|
||||
fs.mkdirSync(this.sessionDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create readline interface
|
||||
this.rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
prompt: chalk.cyan('dev> ')
|
||||
});
|
||||
|
||||
// Initialize agent with config
|
||||
const config: AgentLoopConfig = {
|
||||
provider: provider || this.getDefaultProvider(),
|
||||
maxIterations: 10,
|
||||
enableMCP: true,
|
||||
enableBrowser: false,
|
||||
enableSwarm: this.swarmEnabled,
|
||||
streamOutput: true,
|
||||
confirmActions: false
|
||||
};
|
||||
|
||||
this.agent = new ConfigurableAgentLoop(config);
|
||||
this.setupAgentTools();
|
||||
}
|
||||
|
||||
private getDefaultProvider(): LLMProvider {
|
||||
// Check for available providers
|
||||
const providers = ConfigurableAgentLoop.getAvailableProviders();
|
||||
return providers[0] || {
|
||||
name: 'Local',
|
||||
type: 'local',
|
||||
model: 'dev-agent',
|
||||
supportsTools: true,
|
||||
supportsStreaming: false
|
||||
};
|
||||
}
|
||||
|
||||
private setupAgentTools(): void {
|
||||
// Add swarm tool if enabled
|
||||
if (this.swarmEnabled) {
|
||||
this.agent.addTool({
|
||||
name: 'distribute_work',
|
||||
description: 'Distribute work across multiple parallel agents',
|
||||
parameters: {
|
||||
files: { type: 'array', description: 'Files to process' },
|
||||
task: { type: 'string', description: 'Task to perform on files' },
|
||||
batchSize: { type: 'number', description: 'Files per worker', default: 5 }
|
||||
},
|
||||
execute: async (params: any) => {
|
||||
const tasks = this.swarmTool.createTaskBatches(
|
||||
params.files,
|
||||
params.batchSize || 5,
|
||||
params.task,
|
||||
this.agent.config.provider.type
|
||||
);
|
||||
|
||||
const results = await this.swarmTool.executeParallelTasks(tasks);
|
||||
return {
|
||||
success: true,
|
||||
results: results,
|
||||
summary: `Processed ${params.files.length} files across ${tasks.length} workers`
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add session management tools
|
||||
this.agent.addTool({
|
||||
name: 'save_session',
|
||||
description: 'Save the current session',
|
||||
parameters: {
|
||||
name: { type: 'string', description: 'Session name', optional: true }
|
||||
},
|
||||
execute: async (params: any) => {
|
||||
const sessionName = params.name || `session-${Date.now()}`;
|
||||
await this.saveSession(sessionName);
|
||||
return { success: true, message: `Session saved as: ${sessionName}` };
|
||||
}
|
||||
});
|
||||
|
||||
this.agent.addTool({
|
||||
name: 'load_session',
|
||||
description: 'Load a previous session',
|
||||
parameters: {
|
||||
name: { type: 'string', description: 'Session name or ID' }
|
||||
},
|
||||
execute: async (params: any) => {
|
||||
const loaded = await this.loadSession(params.name);
|
||||
return {
|
||||
success: loaded,
|
||||
message: loaded ? `Session loaded: ${params.name}` : 'Session not found'
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async start(initialPrompt?: string): Promise<void> {
|
||||
console.log(chalk.bold.cyan('\n🤖 Hanzo Dev Interactive Mode\n'));
|
||||
console.log(chalk.gray('Type your commands or questions. Use /help for available commands.\n'));
|
||||
|
||||
if (this.swarmEnabled) {
|
||||
console.log(chalk.yellow(`🐝 Swarm mode enabled with ${this.swarmCount} workers\n`));
|
||||
}
|
||||
|
||||
// Handle initial prompt if provided
|
||||
if (initialPrompt) {
|
||||
await this.handleInput(initialPrompt);
|
||||
}
|
||||
|
||||
// Show prompt
|
||||
this.rl.prompt();
|
||||
|
||||
// Handle line input
|
||||
this.rl.on('line', async (line) => {
|
||||
const input = line.trim();
|
||||
|
||||
if (!input) {
|
||||
this.rl.prompt();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle special commands
|
||||
if (input.startsWith('/')) {
|
||||
await this.handleCommand(input);
|
||||
} else {
|
||||
await this.handleInput(input);
|
||||
}
|
||||
|
||||
this.rl.prompt();
|
||||
});
|
||||
|
||||
// Handle close
|
||||
this.rl.on('close', () => {
|
||||
console.log(chalk.gray('\nGoodbye! 👋'));
|
||||
this.cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleCommand(command: string): Promise<void> {
|
||||
const [cmd, ...args] = command.split(' ');
|
||||
|
||||
switch (cmd) {
|
||||
case '/help':
|
||||
this.showHelp();
|
||||
break;
|
||||
|
||||
case '/save':
|
||||
await this.saveSession(args[0] || `session-${Date.now()}`);
|
||||
console.log(chalk.green('Session saved!'));
|
||||
break;
|
||||
|
||||
case '/load':
|
||||
if (args[0]) {
|
||||
const loaded = await this.loadSession(args[0]);
|
||||
if (loaded) {
|
||||
console.log(chalk.green('Session loaded!'));
|
||||
} else {
|
||||
console.log(chalk.red('Session not found!'));
|
||||
}
|
||||
} else {
|
||||
await this.listSessions();
|
||||
}
|
||||
break;
|
||||
|
||||
case '/clear':
|
||||
console.clear();
|
||||
break;
|
||||
|
||||
case '/exit':
|
||||
case '/quit':
|
||||
this.rl.close();
|
||||
break;
|
||||
|
||||
case '/swarm':
|
||||
if (args[0] === 'on') {
|
||||
this.swarmEnabled = true;
|
||||
this.setupAgentTools();
|
||||
console.log(chalk.yellow('Swarm mode enabled'));
|
||||
} else if (args[0] === 'off') {
|
||||
this.swarmEnabled = false;
|
||||
console.log(chalk.gray('Swarm mode disabled'));
|
||||
} else {
|
||||
console.log(chalk.gray(`Swarm mode: ${this.swarmEnabled ? 'ON' : 'OFF'}`));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(chalk.red(`Unknown command: ${cmd}`));
|
||||
console.log(chalk.gray('Use /help for available commands'));
|
||||
}
|
||||
}
|
||||
|
||||
private async handleInput(input: string): Promise<void> {
|
||||
try {
|
||||
// Send to agent
|
||||
const response = await this.agent.processMessage(input);
|
||||
|
||||
// Response is streamed by the agent if streaming is enabled
|
||||
if (!this.agent.config.streamOutput) {
|
||||
console.log(response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error}`));
|
||||
}
|
||||
}
|
||||
|
||||
private showHelp(): void {
|
||||
console.log(chalk.bold('\nAvailable Commands:'));
|
||||
console.log(chalk.gray(' /help - Show this help message'));
|
||||
console.log(chalk.gray(' /save [name] - Save current session'));
|
||||
console.log(chalk.gray(' /load [name] - Load a previous session (or list if no name)'));
|
||||
console.log(chalk.gray(' /clear - Clear the screen'));
|
||||
console.log(chalk.gray(' /swarm on/off - Toggle swarm mode'));
|
||||
console.log(chalk.gray(' /exit - Exit interactive mode'));
|
||||
console.log(chalk.gray('\nJust type normally to chat with the agent!\n'));
|
||||
}
|
||||
|
||||
private async saveSession(name: string): Promise<void> {
|
||||
const session: Session = {
|
||||
id: this.sessionId,
|
||||
messages: this.agent.getMessages(),
|
||||
context: this.agent.getContext(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
const sessionPath = path.join(this.sessionDir, `${name}.json`);
|
||||
fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2));
|
||||
}
|
||||
|
||||
private async loadSession(name: string): Promise<boolean> {
|
||||
const sessionPath = path.join(this.sessionDir, `${name}.json`);
|
||||
|
||||
if (!fs.existsSync(sessionPath)) {
|
||||
// Try with .json extension if not provided
|
||||
const altPath = path.join(this.sessionDir, `${name}.json`);
|
||||
if (!fs.existsSync(altPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const sessionData = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
|
||||
this.sessionId = sessionData.id;
|
||||
this.agent.loadSession(sessionData.messages, sessionData.context);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error loading session: ${error}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async listSessions(): Promise<void> {
|
||||
const files = fs.readdirSync(this.sessionDir)
|
||||
.filter(f => f.endsWith('.json'))
|
||||
.map(f => f.replace('.json', ''));
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log(chalk.gray('No saved sessions found.'));
|
||||
} else {
|
||||
console.log(chalk.bold('\nSaved Sessions:'));
|
||||
files.forEach(f => {
|
||||
console.log(chalk.gray(` - ${f}`));
|
||||
});
|
||||
console.log(chalk.gray('\nUse /load <name> to load a session\n'));
|
||||
}
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
this.swarmTool.cleanup();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { glob } from 'glob';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
interface WorkerTask {
|
||||
id: string;
|
||||
type: 'analyze' | 'edit' | 'verify';
|
||||
files: string[];
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
interface WorkerResult {
|
||||
taskId: string;
|
||||
workerId: string;
|
||||
success: boolean;
|
||||
files: string[];
|
||||
summary: string;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
interface SwarmWorker {
|
||||
id: string;
|
||||
process?: ChildProcess;
|
||||
status: 'idle' | 'busy' | 'error';
|
||||
currentTask?: WorkerTask;
|
||||
}
|
||||
|
||||
export interface SwarmCoordinatorOptions {
|
||||
provider: 'claude' | 'openai' | 'gemini' | 'grok' | 'local';
|
||||
workerCount: number;
|
||||
prompt: string;
|
||||
cwd?: string;
|
||||
pattern?: string;
|
||||
maxIterations?: number;
|
||||
}
|
||||
|
||||
export class SwarmCoordinator {
|
||||
private options: SwarmCoordinatorOptions;
|
||||
private workers: Map<string, SwarmWorker> = new Map();
|
||||
private taskQueue: WorkerTask[] = [];
|
||||
private results: WorkerResult[] = [];
|
||||
private tempDir: string;
|
||||
private iteration: number = 0;
|
||||
|
||||
constructor(options: SwarmCoordinatorOptions) {
|
||||
this.options = {
|
||||
...options,
|
||||
cwd: options.cwd || process.cwd(),
|
||||
maxIterations: options.maxIterations || 5
|
||||
};
|
||||
|
||||
// Create temp directory for worker communication
|
||||
this.tempDir = path.join(process.cwd(), '.swarm-tmp', uuidv4());
|
||||
fs.mkdirSync(this.tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const spinner = ora('Starting swarm coordinator...').start();
|
||||
|
||||
try {
|
||||
// Phase 1: Initialize main coordinator
|
||||
spinner.text = 'Initializing coordinator agent...';
|
||||
const coordinator = await this.startCoordinator();
|
||||
|
||||
// Phase 2: Analyze the task and create work plan
|
||||
spinner.text = 'Analyzing task and creating work plan...';
|
||||
const workPlan = await this.analyzeTask(coordinator);
|
||||
|
||||
// Phase 3: Initialize worker pool
|
||||
spinner.text = `Spawning ${this.options.workerCount} worker agents...`;
|
||||
await this.initializeWorkers();
|
||||
spinner.succeed('Worker pool initialized');
|
||||
|
||||
// Phase 4: Execute work plan with iterations
|
||||
while (this.iteration < this.options.maxIterations && this.taskQueue.length > 0) {
|
||||
this.iteration++;
|
||||
console.log(chalk.cyan(`\n🔄 Iteration ${this.iteration}/${this.options.maxIterations}`));
|
||||
|
||||
// Distribute tasks to workers
|
||||
await this.distributeTasks();
|
||||
|
||||
// Collect results
|
||||
const iterationResults = await this.collectResults();
|
||||
|
||||
// Send results back to coordinator for analysis
|
||||
const nextSteps = await this.coordinatorAnalyze(coordinator, iterationResults);
|
||||
|
||||
if (nextSteps.complete) {
|
||||
console.log(chalk.green('\n✅ Task completed successfully!'));
|
||||
break;
|
||||
}
|
||||
|
||||
// Add new tasks to queue
|
||||
this.taskQueue.push(...nextSteps.tasks);
|
||||
}
|
||||
|
||||
// Phase 5: Final summary
|
||||
await this.generateFinalReport(coordinator);
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail(`Swarm error: ${error}`);
|
||||
throw error;
|
||||
} finally {
|
||||
// Cleanup
|
||||
await this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private async startCoordinator(): Promise<ChildProcess> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const coordinatorPrompt = `
|
||||
You are the coordinator of a swarm of AI agents. Your role is to:
|
||||
1. Analyze the main task: "${this.options.prompt}"
|
||||
2. Break it down into smaller tasks for worker agents
|
||||
3. Collect and analyze results from workers
|
||||
4. Decide on next steps based on results
|
||||
5. Determine when the overall task is complete
|
||||
|
||||
Working directory: ${this.options.cwd}
|
||||
Number of workers available: ${this.options.workerCount}
|
||||
|
||||
First, analyze the task and create a work plan.
|
||||
`;
|
||||
|
||||
const args = this.buildCoordinatorArgs(coordinatorPrompt);
|
||||
const coordinator = spawn(this.getCommand(), args, {
|
||||
cwd: this.options.cwd,
|
||||
env: this.getEnv()
|
||||
});
|
||||
|
||||
// Set up communication via temp files
|
||||
const outputPath = path.join(this.tempDir, 'coordinator-output.json');
|
||||
let output = '';
|
||||
|
||||
coordinator.stdout?.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
coordinator.stderr?.on('data', (data) => {
|
||||
console.error(chalk.red('Coordinator error:'), data.toString());
|
||||
});
|
||||
|
||||
coordinator.on('error', (error) => {
|
||||
reject(new Error(`Failed to start coordinator: ${error.message}`));
|
||||
});
|
||||
|
||||
// Give coordinator time to initialize
|
||||
setTimeout(() => resolve(coordinator), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
private async analyzeTask(coordinator: ChildProcess): Promise<WorkerTask[]> {
|
||||
// Send analysis request to coordinator
|
||||
const analysisPrompt = `
|
||||
Analyze the files in the current directory and create a work plan.
|
||||
Output your plan as a JSON array of tasks in the following format:
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"type": "analyze|edit|verify",
|
||||
"files": ["file1.js", "file2.js"],
|
||||
"instructions": "Specific instructions for this task"
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
// Write prompt to temp file for coordinator to read
|
||||
const promptPath = path.join(this.tempDir, 'analysis-prompt.txt');
|
||||
fs.writeFileSync(promptPath, analysisPrompt);
|
||||
|
||||
// Wait for coordinator response
|
||||
const responsePath = path.join(this.tempDir, 'analysis-response.json');
|
||||
|
||||
// In real implementation, we'd wait for the coordinator to write the response
|
||||
// For now, let's create a default work plan based on files found
|
||||
const files = await this.findFiles();
|
||||
const tasks: WorkerTask[] = [];
|
||||
|
||||
// Group files into batches for workers
|
||||
const batchSize = Math.ceil(files.length / this.options.workerCount);
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
tasks.push({
|
||||
id: uuidv4(),
|
||||
type: 'edit',
|
||||
files: batch,
|
||||
instructions: this.options.prompt
|
||||
});
|
||||
}
|
||||
|
||||
this.taskQueue.push(...tasks);
|
||||
return tasks;
|
||||
}
|
||||
|
||||
private async initializeWorkers(): Promise<void> {
|
||||
for (let i = 0; i < this.options.workerCount; i++) {
|
||||
const worker: SwarmWorker = {
|
||||
id: `worker-${i}`,
|
||||
status: 'idle'
|
||||
};
|
||||
this.workers.set(worker.id, worker);
|
||||
}
|
||||
}
|
||||
|
||||
private async distributeTasks(): Promise<void> {
|
||||
const availableWorkers = Array.from(this.workers.values()).filter(w => w.status === 'idle');
|
||||
|
||||
for (const worker of availableWorkers) {
|
||||
if (this.taskQueue.length === 0) break;
|
||||
|
||||
const task = this.taskQueue.shift()!;
|
||||
worker.currentTask = task;
|
||||
worker.status = 'busy';
|
||||
|
||||
// Start worker process
|
||||
await this.startWorker(worker, task);
|
||||
}
|
||||
}
|
||||
|
||||
private async startWorker(worker: SwarmWorker, task: WorkerTask): Promise<void> {
|
||||
const workerPrompt = `
|
||||
You are worker ${worker.id} in a swarm of AI agents.
|
||||
Your task: ${task.instructions}
|
||||
Files to process: ${task.files.join(', ')}
|
||||
|
||||
Complete the task and report your results.
|
||||
`;
|
||||
|
||||
const args = this.buildWorkerArgs(workerPrompt, task.files);
|
||||
const process = spawn(this.getCommand(), args, {
|
||||
cwd: this.options.cwd,
|
||||
env: this.getEnv()
|
||||
});
|
||||
|
||||
worker.process = process;
|
||||
|
||||
let output = '';
|
||||
process.stdout?.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
// Save result
|
||||
const result: WorkerResult = {
|
||||
taskId: task.id,
|
||||
workerId: worker.id,
|
||||
success: code === 0,
|
||||
files: task.files,
|
||||
summary: output,
|
||||
errors: code !== 0 ? [output] : undefined
|
||||
};
|
||||
|
||||
this.results.push(result);
|
||||
worker.status = 'idle';
|
||||
worker.currentTask = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private async collectResults(): Promise<WorkerResult[]> {
|
||||
// Wait for all busy workers to complete
|
||||
await new Promise<void>((resolve) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
const busyWorkers = Array.from(this.workers.values()).filter(w => w.status === 'busy');
|
||||
if (busyWorkers.length === 0) {
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Return results from this iteration
|
||||
const iterationResults = [...this.results];
|
||||
this.results = []; // Clear for next iteration
|
||||
return iterationResults;
|
||||
}
|
||||
|
||||
private async coordinatorAnalyze(coordinator: ChildProcess, results: WorkerResult[]): Promise<{
|
||||
complete: boolean;
|
||||
tasks: WorkerTask[];
|
||||
}> {
|
||||
// Send results to coordinator for analysis
|
||||
const analysisPrompt = `
|
||||
Worker results from iteration ${this.iteration}:
|
||||
${JSON.stringify(results, null, 2)}
|
||||
|
||||
Analyze these results and determine:
|
||||
1. Is the overall task complete?
|
||||
2. If not, what additional tasks need to be performed?
|
||||
|
||||
Respond with JSON:
|
||||
{
|
||||
"complete": true/false,
|
||||
"tasks": [...] // New tasks if not complete
|
||||
}
|
||||
`;
|
||||
|
||||
// In real implementation, communicate with coordinator
|
||||
// For now, assume task is complete after first iteration
|
||||
return {
|
||||
complete: true,
|
||||
tasks: []
|
||||
};
|
||||
}
|
||||
|
||||
private async generateFinalReport(coordinator: ChildProcess): Promise<void> {
|
||||
console.log(chalk.bold.cyan('\n📊 Swarm Execution Summary\n'));
|
||||
console.log(chalk.white('Total iterations:'), this.iteration);
|
||||
console.log(chalk.white('Workers used:'), this.options.workerCount);
|
||||
console.log(chalk.white('Tasks completed:'), this.results.length);
|
||||
|
||||
// Show results summary
|
||||
const successful = this.results.filter(r => r.success).length;
|
||||
const failed = this.results.filter(r => !r.success).length;
|
||||
|
||||
console.log(chalk.green('Successful:'), successful);
|
||||
if (failed > 0) {
|
||||
console.log(chalk.red('Failed:'), failed);
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanup(): Promise<void> {
|
||||
// Kill any remaining processes
|
||||
for (const worker of this.workers.values()) {
|
||||
if (worker.process) {
|
||||
worker.process.kill();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove temp directory
|
||||
if (fs.existsSync(this.tempDir)) {
|
||||
fs.rmSync(this.tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
private async findFiles(): Promise<string[]> {
|
||||
// Reuse logic from original swarm-runner
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
cwd: this.options.cwd,
|
||||
nodir: true,
|
||||
ignore: [
|
||||
'**/node_modules/**',
|
||||
'**/.git/**',
|
||||
'**/dist/**',
|
||||
'**/build/**'
|
||||
]
|
||||
};
|
||||
|
||||
glob(this.options.pattern || '**/*', options, (err, files) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const editableFiles = files.filter(file => {
|
||||
const ext = path.extname(file);
|
||||
return ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.rs', '.md', '.txt', '.json', '.yaml', '.yml'].includes(ext);
|
||||
});
|
||||
resolve(editableFiles);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getCommand(): string {
|
||||
switch (this.options.provider) {
|
||||
case 'claude': return 'claude';
|
||||
case 'openai': return 'openai';
|
||||
case 'gemini': return 'gemini';
|
||||
case 'grok': return 'grok';
|
||||
case 'local': return 'dev';
|
||||
default: return 'claude';
|
||||
}
|
||||
}
|
||||
|
||||
private buildCoordinatorArgs(prompt: string): string[] {
|
||||
// Build args specific to each provider for the coordinator
|
||||
switch (this.options.provider) {
|
||||
case 'claude':
|
||||
return ['-p', prompt, '--thinking'];
|
||||
case 'local':
|
||||
return ['agent', '-p', prompt];
|
||||
default:
|
||||
return [prompt];
|
||||
}
|
||||
}
|
||||
|
||||
private buildWorkerArgs(prompt: string, files: string[]): string[] {
|
||||
// Build args specific to each provider for workers
|
||||
switch (this.options.provider) {
|
||||
case 'claude':
|
||||
return ['-p', prompt, ...files];
|
||||
case 'local':
|
||||
return ['agent', '-p', prompt, ...files];
|
||||
default:
|
||||
return [prompt, ...files];
|
||||
}
|
||||
}
|
||||
|
||||
private getEnv(): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...process.env,
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
|
||||
GROK_API_KEY: process.env.GROK_API_KEY
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { glob } from 'glob';
|
||||
import { glob, globSync } from 'glob';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
@@ -88,9 +88,9 @@ export class SwarmRunner extends EventEmitter {
|
||||
}
|
||||
|
||||
private async findFiles(): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const options = {
|
||||
cwd: this.options.cwd,
|
||||
cwd: this.options.cwd || process.cwd(),
|
||||
nodir: true,
|
||||
ignore: [
|
||||
'**/node_modules/**',
|
||||
@@ -98,35 +98,31 @@ export class SwarmRunner extends EventEmitter {
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/*.min.js',
|
||||
'**/*.map'
|
||||
]
|
||||
'**/*.map',
|
||||
'**/.swarm-tmp/**'
|
||||
],
|
||||
absolute: false
|
||||
};
|
||||
|
||||
// Add timeout to prevent hanging
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('File search timed out'));
|
||||
}, 30000);
|
||||
|
||||
const pattern = this.options.pattern || '**/*';
|
||||
console.log(chalk.gray(`Searching with pattern: ${pattern} in ${options.cwd || process.cwd()}`));
|
||||
console.log(chalk.gray(`Searching with pattern: ${pattern} in ${options.cwd}`));
|
||||
|
||||
glob(pattern, options, (err, files) => {
|
||||
clearTimeout(timeout);
|
||||
if (err) {
|
||||
console.error(chalk.red('Glob error:'), err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log(chalk.gray(`Found ${files.length} total files`));
|
||||
// Filter to only editable files
|
||||
const editableFiles = files.filter(file => {
|
||||
const ext = path.extname(file);
|
||||
return ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.h', '.go', '.rs', '.rb', '.php', '.swift', '.kt', '.scala', '.r', '.m', '.mm', '.md', '.txt', '.json', '.xml', '.yaml', '.yml', '.toml', '.ini', '.conf', '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd'].includes(ext);
|
||||
});
|
||||
console.log(chalk.gray(`Filtered to ${editableFiles.length} editable files`));
|
||||
resolve(editableFiles);
|
||||
}
|
||||
// Use sync version for reliability
|
||||
const files = globSync(pattern, options);
|
||||
console.log(chalk.gray(`Found ${files.length} total files`));
|
||||
|
||||
// Filter to only editable files
|
||||
const editableFiles = files.filter(file => {
|
||||
const ext = path.extname(file);
|
||||
return ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.h', '.go', '.rs', '.rb', '.php', '.swift', '.kt', '.scala', '.r', '.m', '.mm', '.md', '.txt', '.json', '.xml', '.yaml', '.yml', '.toml', '.ini', '.conf', '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd'].includes(ext);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(chalk.gray(`Filtered to ${editableFiles.length} editable files`));
|
||||
return editableFiles;
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Error finding files:'), error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async processFiles(): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface SwarmTask {
|
||||
id: string;
|
||||
files: string[];
|
||||
instructions: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface SwarmResult {
|
||||
taskId: string;
|
||||
success: boolean;
|
||||
filesProcessed: string[];
|
||||
summary: string;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export class SwarmTool {
|
||||
private activeWorkers: Map<string, ChildProcess> = new Map();
|
||||
|
||||
/**
|
||||
* Execute tasks in parallel using worker agents
|
||||
* This tool is called by the main agent loop to distribute work
|
||||
*/
|
||||
async executeParallelTasks(tasks: SwarmTask[]): Promise<SwarmResult[]> {
|
||||
console.log(chalk.cyan(`\n🐝 Distributing ${tasks.length} tasks to swarm workers...\n`));
|
||||
|
||||
const results: SwarmResult[] = [];
|
||||
const promises = tasks.map(task => this.executeTask(task));
|
||||
|
||||
try {
|
||||
const taskResults = await Promise.all(promises);
|
||||
results.push(...taskResults);
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Swarm execution error:'), error);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single task with a worker agent
|
||||
*/
|
||||
private async executeTask(task: SwarmTask): Promise<SwarmResult> {
|
||||
return new Promise((resolve) => {
|
||||
const workerId = `worker-${uuidv4().slice(0, 8)}`;
|
||||
console.log(chalk.gray(`[${workerId}] Processing ${task.files.length} files...`));
|
||||
|
||||
// Build command based on provider
|
||||
const { cmd, args } = this.buildCommand(task);
|
||||
|
||||
// Spawn worker process
|
||||
const worker = spawn(cmd, args, {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
// Ensure worker runs in non-interactive mode
|
||||
CLAUDE_CODE_PERMISSION_MODE: 'acceptEdits'
|
||||
}
|
||||
});
|
||||
|
||||
this.activeWorkers.set(workerId, worker);
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
worker.stdout?.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
worker.stderr?.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
worker.on('close', (code) => {
|
||||
this.activeWorkers.delete(workerId);
|
||||
|
||||
const result: SwarmResult = {
|
||||
taskId: task.id,
|
||||
success: code === 0,
|
||||
filesProcessed: task.files,
|
||||
summary: output || 'No output captured',
|
||||
errors: code !== 0 ? [errorOutput || `Process exited with code ${code}`] : undefined
|
||||
};
|
||||
|
||||
if (code === 0) {
|
||||
console.log(chalk.green(`[${workerId}] ✓ Completed successfully`));
|
||||
} else {
|
||||
console.log(chalk.red(`[${workerId}] ✗ Failed with code ${code}`));
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
// Add timeout to prevent hanging
|
||||
setTimeout(() => {
|
||||
if (this.activeWorkers.has(workerId)) {
|
||||
console.log(chalk.yellow(`[${workerId}] Timeout - killing worker`));
|
||||
worker.kill();
|
||||
this.activeWorkers.delete(workerId);
|
||||
|
||||
resolve({
|
||||
taskId: task.id,
|
||||
success: false,
|
||||
filesProcessed: task.files,
|
||||
summary: 'Worker timed out',
|
||||
errors: ['Task execution timed out after 5 minutes']
|
||||
});
|
||||
}
|
||||
}, 5 * 60 * 1000); // 5 minute timeout per task
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build command and arguments based on provider
|
||||
*/
|
||||
private buildCommand(task: SwarmTask): { cmd: string; args: string[] } {
|
||||
const prompt = `${task.instructions}\n\nFiles to process:\n${task.files.join('\n')}`;
|
||||
|
||||
switch (task.provider) {
|
||||
case 'claude':
|
||||
return {
|
||||
cmd: 'claude',
|
||||
args: [
|
||||
'-p', prompt,
|
||||
'--max-turns', '5',
|
||||
'--thinking',
|
||||
...task.files
|
||||
]
|
||||
};
|
||||
|
||||
case 'openai':
|
||||
return {
|
||||
cmd: 'openai',
|
||||
args: ['chat', '--prompt', prompt, ...task.files]
|
||||
};
|
||||
|
||||
case 'gemini':
|
||||
return {
|
||||
cmd: 'gemini',
|
||||
args: ['edit', '--prompt', prompt, ...task.files]
|
||||
};
|
||||
|
||||
case 'local':
|
||||
return {
|
||||
cmd: 'dev',
|
||||
args: ['agent', '-p', prompt, ...task.files]
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
cmd: 'claude',
|
||||
args: ['-p', prompt, ...task.files]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create task batches for parallel execution
|
||||
* Called by the main agent to divide work
|
||||
*/
|
||||
createTaskBatches(files: string[], batchSize: number, instructions: string, provider: string): SwarmTask[] {
|
||||
const tasks: SwarmTask[] = [];
|
||||
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize);
|
||||
tasks.push({
|
||||
id: uuidv4(),
|
||||
files: batch,
|
||||
instructions,
|
||||
provider
|
||||
});
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup any remaining workers
|
||||
*/
|
||||
cleanup(): void {
|
||||
for (const [workerId, worker] of this.activeWorkers) {
|
||||
console.log(chalk.yellow(`Cleaning up worker ${workerId}`));
|
||||
worker.kill();
|
||||
}
|
||||
this.activeWorkers.clear();
|
||||
}
|
||||
}
|
||||
+1
Submodule test/fixtures/genjo added at 3bcfd01559
Reference in New Issue
Block a user