diff options
Diffstat (limited to 'modules/core/module-orchestrator.js')
| -rw-r--r-- | modules/core/module-orchestrator.js | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js new file mode 100644 index 0000000..4fab599 --- /dev/null +++ b/modules/core/module-orchestrator.js @@ -0,0 +1,151 @@ +// 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 <task description>', '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'); + }); + } +}
\ No newline at end of file |