366 lines
13 KiB
JavaScript
366 lines
13 KiB
JavaScript
/**
|
|
* E2B Swarm Orchestrator - Multi-agent swarm with E2B sandbox isolation
|
|
*
|
|
* Spawns specialized agents in isolated E2B sandboxes for:
|
|
* - Parallel code execution
|
|
* - Secure multi-agent coordination
|
|
* - Resource-isolated task processing
|
|
*/
|
|
import { logger } from "../utils/logger.js";
|
|
import { E2BSandboxManager } from "./e2b-sandbox.js";
|
|
/**
|
|
* E2B Swarm Orchestrator
|
|
*/
|
|
export class E2BSwarmOrchestrator {
|
|
agents = new Map();
|
|
taskQueue = [];
|
|
taskResults = new Map();
|
|
isRunning = false;
|
|
config;
|
|
constructor(config) {
|
|
this.config = {
|
|
maxAgents: config?.maxAgents || 10,
|
|
defaultTimeout: config?.defaultTimeout || 300000,
|
|
retryAttempts: config?.retryAttempts || 3,
|
|
loadBalancing: config?.loadBalancing || 'capability-match'
|
|
};
|
|
}
|
|
/**
|
|
* Initialize the swarm
|
|
*/
|
|
async initialize() {
|
|
logger.info('E2B Swarm initializing', { maxAgents: this.config.maxAgents });
|
|
this.isRunning = true;
|
|
return true;
|
|
}
|
|
/**
|
|
* Spawn a new E2B agent with specific capability
|
|
*/
|
|
async spawnAgent(config) {
|
|
if (this.agents.size >= this.config.maxAgents) {
|
|
logger.warn('Max agents reached', { max: this.config.maxAgents });
|
|
return null;
|
|
}
|
|
const sandbox = new E2BSandboxManager({
|
|
apiKey: process.env.E2B_API_KEY,
|
|
template: config.template || this.getTemplateForCapability(config.capability),
|
|
timeout: config.timeout || this.config.defaultTimeout,
|
|
envVars: config.envVars
|
|
});
|
|
const agent = {
|
|
id: config.id,
|
|
name: config.name,
|
|
capability: config.capability,
|
|
sandbox,
|
|
status: 'initializing',
|
|
tasksCompleted: 0,
|
|
totalExecutionTime: 0,
|
|
errors: 0,
|
|
createdAt: Date.now()
|
|
};
|
|
this.agents.set(agent.id, agent);
|
|
try {
|
|
const useCodeInterpreter = ['python-executor', 'data-analyst', 'ml-trainer'].includes(config.capability);
|
|
const created = await sandbox.create(useCodeInterpreter);
|
|
if (!created) {
|
|
agent.status = 'error';
|
|
logger.error('Failed to create E2B sandbox for agent', { agentId: agent.id });
|
|
return null;
|
|
}
|
|
// Install required packages for capability
|
|
if (config.packages && config.packages.length > 0) {
|
|
const pkgManager = ['python-executor', 'data-analyst', 'ml-trainer'].includes(config.capability) ? 'pip' : 'npm';
|
|
await sandbox.installPackages(config.packages, pkgManager);
|
|
}
|
|
agent.status = 'ready';
|
|
logger.info('E2B agent spawned', { id: agent.id, name: agent.name, capability: config.capability });
|
|
return agent;
|
|
}
|
|
catch (error) {
|
|
agent.status = 'error';
|
|
agent.errors++;
|
|
logger.error('Error spawning E2B agent', { agentId: agent.id, error: error.message });
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Spawn multiple agents in parallel
|
|
*/
|
|
async spawnAgents(configs) {
|
|
const results = await Promise.allSettled(configs.map(config => this.spawnAgent(config)));
|
|
return results
|
|
.filter((r) => r.status === 'fulfilled')
|
|
.map(r => r.value)
|
|
.filter((a) => a !== null);
|
|
}
|
|
/**
|
|
* Execute a task on the swarm
|
|
*/
|
|
async executeTask(task) {
|
|
const startTime = Date.now();
|
|
// Find best agent for task
|
|
const agent = this.selectAgent(task);
|
|
if (!agent) {
|
|
return {
|
|
taskId: task.id,
|
|
agentId: 'none',
|
|
success: false,
|
|
output: '',
|
|
error: 'No suitable agent available',
|
|
executionTime: 0
|
|
};
|
|
}
|
|
agent.status = 'busy';
|
|
try {
|
|
let result;
|
|
switch (task.type) {
|
|
case 'python':
|
|
result = await agent.sandbox.runPython(task.code);
|
|
break;
|
|
case 'javascript':
|
|
result = await agent.sandbox.runJavaScript(task.code);
|
|
break;
|
|
case 'shell':
|
|
result = await agent.sandbox.runCommand('sh', ['-c', task.code]);
|
|
break;
|
|
case 'file-write':
|
|
const writeResult = await agent.sandbox.writeFile(task.metadata?.path || '/tmp/output.txt', task.code);
|
|
result = { success: writeResult.success, output: writeResult.path, error: writeResult.error, logs: [] };
|
|
break;
|
|
case 'file-read':
|
|
const readResult = await agent.sandbox.readFile(task.code);
|
|
result = { success: readResult.success, output: readResult.content || '', error: readResult.error, logs: [] };
|
|
break;
|
|
default:
|
|
result = { success: false, output: '', error: `Unknown task type: ${task.type}`, logs: [] };
|
|
}
|
|
const executionTime = Date.now() - startTime;
|
|
agent.tasksCompleted++;
|
|
agent.totalExecutionTime += executionTime;
|
|
agent.status = 'ready';
|
|
const taskResult = {
|
|
taskId: task.id,
|
|
agentId: agent.id,
|
|
success: result.success,
|
|
output: result.output,
|
|
error: result.error,
|
|
executionTime,
|
|
metadata: task.metadata
|
|
};
|
|
this.taskResults.set(task.id, taskResult);
|
|
return taskResult;
|
|
}
|
|
catch (error) {
|
|
agent.errors++;
|
|
agent.status = 'ready';
|
|
const taskResult = {
|
|
taskId: task.id,
|
|
agentId: agent.id,
|
|
success: false,
|
|
output: '',
|
|
error: error.message,
|
|
executionTime: Date.now() - startTime
|
|
};
|
|
this.taskResults.set(task.id, taskResult);
|
|
return taskResult;
|
|
}
|
|
}
|
|
/**
|
|
* Execute multiple tasks in parallel
|
|
*/
|
|
async executeTasks(tasks) {
|
|
// Group by priority
|
|
const critical = tasks.filter(t => t.priority === 'critical');
|
|
const high = tasks.filter(t => t.priority === 'high');
|
|
const medium = tasks.filter(t => t.priority === 'medium' || !t.priority);
|
|
const low = tasks.filter(t => t.priority === 'low');
|
|
const orderedTasks = [...critical, ...high, ...medium, ...low];
|
|
// Execute with concurrency based on available agents
|
|
const concurrency = Math.min(this.getReadyAgents().length, orderedTasks.length);
|
|
const results = [];
|
|
for (let i = 0; i < orderedTasks.length; i += concurrency) {
|
|
const batch = orderedTasks.slice(i, i + concurrency);
|
|
const batchResults = await Promise.all(batch.map(task => this.executeTask(task)));
|
|
results.push(...batchResults);
|
|
}
|
|
return results;
|
|
}
|
|
/**
|
|
* Select best agent for a task
|
|
*/
|
|
selectAgent(task) {
|
|
const readyAgents = this.getReadyAgents();
|
|
if (readyAgents.length === 0)
|
|
return null;
|
|
// If specific agent requested
|
|
if (task.targetAgent) {
|
|
const agent = this.agents.get(task.targetAgent);
|
|
if (agent && agent.status === 'ready')
|
|
return agent;
|
|
}
|
|
switch (this.config.loadBalancing) {
|
|
case 'round-robin':
|
|
return readyAgents[0];
|
|
case 'least-busy':
|
|
return readyAgents.reduce((min, agent) => agent.tasksCompleted < min.tasksCompleted ? agent : min);
|
|
case 'capability-match':
|
|
default:
|
|
// Match task type to capability
|
|
const capabilityMap = {
|
|
'python': ['python-executor', 'data-analyst', 'ml-trainer'],
|
|
'javascript': ['javascript-executor', 'api-tester'],
|
|
'shell': ['shell-executor', 'security-scanner', 'test-runner'],
|
|
'file-write': ['shell-executor', 'python-executor', 'javascript-executor'],
|
|
'file-read': ['shell-executor', 'python-executor', 'javascript-executor']
|
|
};
|
|
const preferredCapabilities = capabilityMap[task.type] || [];
|
|
const matchingAgents = readyAgents.filter(a => task.capability ? a.capability === task.capability : preferredCapabilities.includes(a.capability));
|
|
return matchingAgents.length > 0 ? matchingAgents[0] : readyAgents[0];
|
|
}
|
|
}
|
|
/**
|
|
* Get ready agents
|
|
*/
|
|
getReadyAgents() {
|
|
return Array.from(this.agents.values()).filter(a => a.status === 'ready');
|
|
}
|
|
/**
|
|
* Get template for capability
|
|
*/
|
|
getTemplateForCapability(capability) {
|
|
const templates = {
|
|
'python-executor': 'base',
|
|
'javascript-executor': 'base',
|
|
'shell-executor': 'base',
|
|
'data-analyst': 'base',
|
|
'code-reviewer': 'base',
|
|
'test-runner': 'base',
|
|
'security-scanner': 'base',
|
|
'performance-profiler': 'base',
|
|
'ml-trainer': 'base',
|
|
'api-tester': 'base'
|
|
};
|
|
return templates[capability] || 'base';
|
|
}
|
|
/**
|
|
* Get swarm metrics
|
|
*/
|
|
getMetrics() {
|
|
const agents = Array.from(this.agents.values());
|
|
const totalTasks = agents.reduce((sum, a) => sum + a.tasksCompleted, 0);
|
|
const totalErrors = agents.reduce((sum, a) => sum + a.errors, 0);
|
|
const totalTime = agents.reduce((sum, a) => sum + a.totalExecutionTime, 0);
|
|
const utilization = {};
|
|
for (const agent of agents) {
|
|
utilization[agent.id] = agent.tasksCompleted > 0
|
|
? agent.totalExecutionTime / (Date.now() - agent.createdAt)
|
|
: 0;
|
|
}
|
|
return {
|
|
totalAgents: agents.length,
|
|
activeAgents: agents.filter(a => a.status === 'busy').length,
|
|
tasksCompleted: totalTasks,
|
|
tasksInProgress: agents.filter(a => a.status === 'busy').length,
|
|
totalExecutionTime: totalTime,
|
|
averageExecutionTime: totalTasks > 0 ? totalTime / totalTasks : 0,
|
|
errorRate: totalTasks > 0 ? totalErrors / (totalTasks + totalErrors) : 0,
|
|
agentUtilization: utilization
|
|
};
|
|
}
|
|
/**
|
|
* Get all agents
|
|
*/
|
|
getAgents() {
|
|
return Array.from(this.agents.values());
|
|
}
|
|
/**
|
|
* Get agent by ID
|
|
*/
|
|
getAgent(id) {
|
|
return this.agents.get(id);
|
|
}
|
|
/**
|
|
* Terminate an agent
|
|
*/
|
|
async terminateAgent(id) {
|
|
const agent = this.agents.get(id);
|
|
if (!agent)
|
|
return false;
|
|
try {
|
|
await agent.sandbox.close();
|
|
agent.status = 'terminated';
|
|
this.agents.delete(id);
|
|
logger.info('E2B agent terminated', { id });
|
|
return true;
|
|
}
|
|
catch (error) {
|
|
logger.error('Error terminating agent', { id, error: error.message });
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Shutdown the swarm
|
|
*/
|
|
async shutdown() {
|
|
this.isRunning = false;
|
|
const terminatePromises = Array.from(this.agents.keys()).map(id => this.terminateAgent(id));
|
|
await Promise.allSettled(terminatePromises);
|
|
this.agents.clear();
|
|
this.taskResults.clear();
|
|
logger.info('E2B Swarm shutdown complete');
|
|
}
|
|
/**
|
|
* Health check
|
|
*/
|
|
async healthCheck() {
|
|
const agentHealth = await Promise.all(Array.from(this.agents.values()).map(async (agent) => {
|
|
let healthy = agent.status === 'ready' || agent.status === 'busy';
|
|
// Quick ping test
|
|
if (healthy && agent.status === 'ready') {
|
|
try {
|
|
const result = await agent.sandbox.runCommand('echo', ['ping']);
|
|
healthy = result.success;
|
|
}
|
|
catch {
|
|
healthy = false;
|
|
}
|
|
}
|
|
return { id: agent.id, status: agent.status, healthy };
|
|
}));
|
|
return {
|
|
healthy: agentHealth.every(a => a.healthy),
|
|
agents: agentHealth
|
|
};
|
|
}
|
|
}
|
|
/**
|
|
* Create default swarm with standard agent types
|
|
*/
|
|
export async function createDefaultE2BSwarm() {
|
|
const swarm = new E2BSwarmOrchestrator({
|
|
maxAgents: 8,
|
|
loadBalancing: 'capability-match'
|
|
});
|
|
await swarm.initialize();
|
|
const defaultAgents = [
|
|
{ id: 'python-1', name: 'Python Executor', capability: 'python-executor', packages: ['numpy', 'pandas'] },
|
|
{ id: 'js-1', name: 'JavaScript Executor', capability: 'javascript-executor' },
|
|
{ id: 'shell-1', name: 'Shell Executor', capability: 'shell-executor' },
|
|
{ id: 'data-1', name: 'Data Analyst', capability: 'data-analyst', packages: ['numpy', 'pandas', 'matplotlib'] },
|
|
{ id: 'test-1', name: 'Test Runner', capability: 'test-runner' },
|
|
{ id: 'security-1', name: 'Security Scanner', capability: 'security-scanner' }
|
|
];
|
|
await swarm.spawnAgents(defaultAgents);
|
|
return swarm;
|
|
}
|
|
/**
|
|
* Quick helper to run code in E2B swarm
|
|
*/
|
|
export async function runInSwarm(swarm, code, type = 'python') {
|
|
return swarm.executeTask({
|
|
id: `task-${Date.now()}`,
|
|
type,
|
|
code
|
|
});
|
|
}
|
|
//# sourceMappingURL=e2b-swarm.js.map
|