// module-orchestrator.js โ€” Multi-agent orchestration system // Commands: orchestrate, orchestrator-status, orchestrator-spawn, orchestrator-kill, orchestrator-purge class OrchestratorModule { constructor() { this.commands = {}; this.terminal = null; this._agents = {}; this._SPAWN_LIMIT = 3; this._heartbeatInterval = null; } registerCommands() { this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' ')); this.commands['orchestrator-status'] = () => this.showStatus(); this.commands['orchestrator-spawn'] = (args) => this.spawnAgent(args[0], args.slice(1)); this.commands['orchestrator-kill'] = (args) => this.killAgent(args[0]); this.commands['orchestrator-purge'] = () => this.purgeDeadAgents(); } init(terminal) { this.terminal = terminal; // Register orchestrator in the agent registry const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; if (registry) { registry.registerAgent(['register', 'orchestrator-0', 'delegate', 'spawn', 'monitor', 'purge']); } // Heartbeat watchdog every 3 seconds this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), 3000); this.terminal.print('๐Ÿง  Orchestrator loaded โ€” ready to delegate', 'success'); } spawnAgent(role, capabilities) { const id = `agent-${Object.keys(this._agents).length + 1}-${role || 'worker'}`; if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { this.terminal.print(`โŒ Agent spawn limit (${this._SPAWN_LIMIT}) reached`, 'error'); return null; } const agent = { id, role: role || 'worker', capabilities: capabilities.length ? capabilities : ['list_files', 'read_file', 'web_search'], createdAt: new Date().toISOString(), lastHeartbeat: Date.now(), status: 'idle', currentTask: null }; this._agents[id] = agent; // Register in agent registry const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; if (registry) registry.registerAgent(['register', id, ...agent.capabilities]); this.terminal.print(`๐Ÿค– Agent '${id}' spawned (role: ${role || 'worker'}, ${agent.capabilities.length} caps)`, 'success'); return agent; } killAgent(id) { if (!this._agents[id]) return this.terminal.print(`โŒ Agent '${id}' not found`, 'error'); // Revoke any active lease const leaseModule = this.terminal.moduleManager.modules['LeaseModule']; if (leaseModule) { Object.values(leaseModule._leases).forEach(l => { if (l.workerId === id && l.status === 'active') leaseModule.revokeLease(l.id); }); } // Unregister from registry const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; if (registry) registry.unregisterAgent(id); delete this._agents[id]; this.terminal.print(`๐Ÿ’€ Agent '${id}' killed`, 'warning'); } async delegateTask(task) { if (!task) return this.terminal.print('Usage: orchestrate ', 'error'); // Find an idle agent or auto-spawn let agent = Object.values(this._agents).find(a => a.status === 'idle'); if (!agent) { if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { return this.terminal.print('โŒ All agents busy and spawn limit reached', 'error'); } this.spawnAgent('worker', ['list_files', 'read_file', 'write_file', 'web_search']); agent = Object.values(this._agents).find(a => a.status === 'idle'); } if (!agent) return this.terminal.print('โŒ No available agents', 'error'); // Create a lease const leaseModule = this.terminal.moduleManager.modules['LeaseModule']; const lease = leaseModule ? leaseModule.createLease(['lease-create', agent.id, '60', task]) : null; // Mark agent as busy agent.status = 'busy'; agent.currentTask = task; agent.leaseId = lease?.id; // Write task to agent's inbox if (!this.terminal.fs.readFile(`/agents/${agent.id}/`)) this.terminal.fs.mkdir(`agents/${agent.id}`); this.terminal.fs.writeFile(`agents/${agent.id}/inbox.json`, JSON.stringify({ task, from: 'orchestrator-0', leaseId: lease?.id, timestamp: new Date().toISOString(), ttl_seconds: 60 })); this.terminal.print(`๐ŸŽฏ Delegated "${task.substring(0, 60)}" to ${agent.id} (lease: ${lease?.id || 'none'})`, 'info'); // Execute via AI assistant try { const aiModule = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; if (aiModule && typeof aiModule.chat === 'function') { const result = await aiModule.chat([`[DELEGATED] ${task}`]); agent.status = 'completed'; this.terminal.fs.writeFile(`agents/${agent.id}/outbox.json`, JSON.stringify({ result: result || 'completed', status: 'completed', timestamp: new Date().toISOString() })); this.terminal.print(`โœ… ${agent.id} completed task`, 'success'); } else { this.terminal.print(`โ„น๏ธ AI module not available โ€” task queued for ${agent.id}`, 'info'); } } catch (e) { agent.status = 'failed'; this.terminal.print(`โŒ ${agent.id} failed: ${e.message}`, 'error'); } // Return to idle pool agent.status = 'idle'; agent.currentTask = null; agent.lastHeartbeat = Date.now(); } _checkHeartbeats() { const now = Date.now(); Object.entries(this._agents).forEach(([id, agent]) => { if (agent.status === 'busy' && now - agent.lastHeartbeat > 15000) { this.terminal.print(`โš ๏ธ Agent '${id}' unresponsive (${Math.round((now - agent.lastHeartbeat) / 1000)}s since heartbeat)`, 'warning'); } }); } purgeDeadAgents() { const now = Date.now(); let count = 0; Object.entries(this._agents).forEach(([id, agent]) => { if (now - agent.lastHeartbeat > 60000) { this.killAgent(id); count++; } }); this.terminal.print(`๐Ÿงน Purged ${count} dead agents`, count > 0 ? 'warning' : 'info'); } showStatus() { const total = Object.keys(this._agents).length; const busy = Object.values(this._agents).filter(a => a.status === 'busy').length; const idle = Object.values(this._agents).filter(a => a.status === 'idle').length; const failed = Object.values(this._agents).filter(a => a.status === 'failed').length; this.terminal.print('๐Ÿง  Orchestrator Status', 'header'); this.terminal.print(` Agents: ${total} (${busy} busy, ${idle} idle, ${failed} failed)`, 'info'); this.terminal.print(` Spawn limit: ${this._SPAWN_LIMIT}`, 'info'); Object.values(this._agents).forEach(a => { const task = a.currentTask ? ` (task: ${a.currentTask.substring(0, 40)})` : ''; this.terminal.print(` ${a.id} โ€” ${a.status}${task}`, 'info'); }); } }