// module-orchestrator.js — Multi-agent orchestration system with A2A mailbox transport // Commands: orchestrate, orchestrator-status, orchestrator-spawn, orchestrator-kill, // orchestrator-purge, orchestrator-config, orchestrator-list, orchestrator-sleep, // orchestrator-wake, orchestrator-restart-policy, a2a-send, a2a-list class OrchestratorModule { constructor() { this.commands = {}; this.terminal = null; this._agents = {}; this._SPAWN_LIMIT = 5; this._SPAWN_STRATEGY = 'eager'; this._HEARTBEAT_TIMEOUT = 15000; this._IDLE_TIMEOUT = 120000; this._RESTART_POLICY = 'never'; this._A2A_POLLING = true; this._A2A_POLL_INTERVAL = 500; this._HEARTBEAT_CHECK = 3000; this._IDLE_SWEEP_INTERVAL = 30000; this._DEAD_PURGE_TIMEOUT = 60000; this._a2aPollTimer = null; this._heartbeatInterval = null; this._a2aMessages = {}; this._restartBackoffs = {}; this._eventBus = {}; this._idleSweepInterval = null; this._loadConfig(); } _loadConfig() { try { const c = localStorage.getItem('webx11_orchestrator_config'); if (c) { const p = JSON.parse(c); if (p.spawnLimit) this._SPAWN_LIMIT = p.spawnLimit; if (p.heartbeatTimeout) this._HEARTBEAT_TIMEOUT = p.heartbeatTimeout; if (p.idleTimeout) this._IDLE_TIMEOUT = p.idleTimeout; if (p.restartPolicy) this._RESTART_POLICY = p.restartPolicy; if (p.a2aPollInterval) this._A2A_POLL_INTERVAL = p.a2aPollInterval; if (p.heartbeatCheck) this._HEARTBEAT_CHECK = p.heartbeatCheck; if (p.idleSweepInterval) this._IDLE_SWEEP_INTERVAL = p.idleSweepInterval; if (p.deadPurgeTimeout) this._DEAD_PURGE_TIMEOUT = p.deadPurgeTimeout; } } catch (e) {} } _saveConfig() { localStorage.setItem('webx11_orchestrator_config', JSON.stringify({ spawnLimit: this._SPAWN_LIMIT, heartbeatTimeout: this._HEARTBEAT_TIMEOUT, idleTimeout: this._IDLE_TIMEOUT, restartPolicy: this._RESTART_POLICY, a2aPollInterval: this._A2A_POLL_INTERVAL, heartbeatCheck: this._HEARTBEAT_CHECK, idleSweepInterval: this._IDLE_SWEEP_INTERVAL, deadPurgeTimeout: this._DEAD_PURGE_TIMEOUT })); } 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(); this.commands['orchestrator-config'] = (args) => this.setConfig(args); this.commands['orchestrator-list'] = () => this.listAgents(); this.commands['orchestrator-sleep'] = (args) => this.sleepAgent(args[0]); this.commands['orchestrator-wake'] = (args) => this.wakeAgent(args[0]); this.commands['orchestrator-restart-policy'] = (args) => this.setRestartPolicy(args); this.commands['a2a-send'] = (args) => this.sendA2AMessage(args); this.commands['a2a-list'] = () => this.listA2AMessages(); } getHelpInfo() { return { 'orchestrate': 'orchestrate — Delegate a task to an available agent (auto-spawns if needed)', 'orchestrator-status': 'orchestrator-status — Show agent pool status and config', 'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent', 'orchestrator-kill': 'orchestrator-kill — Terminate an agent and revoke leases', 'orchestrator-purge': 'orchestrator-purge — Remove dead agents (>60s since heartbeat)', 'orchestrator-config': 'orchestrator-config — Tune: spawn-limit, spawn-strategy, heartbeat-timeout, idle-timeout, restart-policy, a2a-polling, a2a-poll-interval, heartbeat-check, idle-sweep-interval, dead-purge-timeout', 'orchestrator-list': 'orchestrator-list — List all spawned agents with full details', 'orchestrator-sleep': 'orchestrator-sleep — Suspend agent to VFS', 'orchestrator-wake': 'orchestrator-wake — Restore agent from VFS sleep', 'orchestrator-restart-policy': 'orchestrator-restart-policy — Set per-agent restart policy', 'a2a-send': 'a2a-send — Send an A2A message to an agent\'s inbox', 'a2a-list': 'a2a-list — Show pending A2A messages across all agents', }; } 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']); } // Reanimate sleeping agents from VFS this._reanimateFromVFS(); // A2A mailbox polling loop (500ms) if (this._A2A_POLLING) { this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL); } // Heartbeat watchdog this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), this._HEARTBEAT_CHECK); // Idle timeout sweep this._idleSweepInterval = setInterval(() => this._sweepIdle(), this._IDLE_SWEEP_INTERVAL); this.terminal.print('🧠 Orchestrator loaded — A2A mailbox transport active', 'success'); // Subscribe to event bus for in-memory A2A delivery (instant, no VFS poll) this._on('a2a:message', (msg) => { if (msg.type === 'heartbeat' && msg.from && this._agents[msg.from]) { this._agents[msg.from].lastHeartbeat = Date.now(); } if (msg.type === 'response' && msg.from && this._agents[msg.from]) { this._agents[msg.from].status = 'idle'; this._agents[msg.from].currentTask = null; } }); } // ─── Event Bus (in-memory pub/sub for agent communication) ─── _on(event, callback) { if (!this._eventBus[event]) this._eventBus[event] = []; this._eventBus[event].push(callback); return () => { // Return unsubscribe function this._eventBus[event] = this._eventBus[event].filter(cb => cb !== callback); }; } _emit(event, data) { for (const cb of (this._eventBus[event] || [])) { try { cb(data); } catch (e) { /* subscriber error */ } } } // ─── A2A Mailbox Transport ─── sendA2AMessage(args) { if (args.length < 2) return this.terminal.print('Usage: a2a-send ', 'error'); const agentId = args[0]; let payload; try { payload = JSON.parse(args.slice(1).join(' ')); } catch (e) { // Treat as plain text message payload = { type: 'request', payload: { text: args.slice(1).join(' ') } }; } const agent = this._agents[agentId]; if (!agent) return this.terminal.print(`❌ Agent '${agentId}' not found`, 'error'); const msg = { a2a_version: '0.1', msg_id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`, type: payload.type || 'request', from: 'orchestrator-0', to: agentId, timestamp: new Date().toISOString(), ttl_seconds: 30, contract: agent.capabilities ? { capabilities: agent.capabilities } : {}, payload: payload.payload || payload }; // Write to agent's VFS inbox if (!this.terminal.fs.readFile(`/agents/${agentId}/`)) this.terminal.fs.mkdir(`agents/${agentId}`); this.terminal.fs.writeFile(`agents/${agentId}/inbox.json`, JSON.stringify(msg)); // Track in memory if (!this._a2aMessages[agentId]) this._a2aMessages[agentId] = []; this._a2aMessages[agentId].push({ ...msg, _processed: false }); this.terminal.print(`📨 A2A message sent to ${agentId} (${msg.type}, id: ${msg.msg_id})`, 'success'); // Emit via event bus for in-memory delivery (bypasses VFS polling) this._emit('a2a:message', msg); this._emit(`a2a:message:${agentId}`, msg); } listA2AMessages() { const ids = Object.keys(this._a2aMessages); if (ids.length === 0) return this.terminal.print('No A2A messages', 'info'); this.terminal.print('📨 A2A Messages:', 'header'); for (const agentId of ids) { const msgs = this._a2aMessages[agentId]; const pending = msgs.filter(m => !m._processed).length; this.terminal.print(` ${agentId}: ${msgs.length} total (${pending} pending)`, 'info'); for (const m of msgs.slice(-3)) { const status = m._processed ? '✅' : '⏳'; this.terminal.print(` ${status} ${m.msg_id} ${m.type} → ${m.to} (${(m.payload?.text || JSON.stringify(m.payload)).substring(0, 40)})`, 'text-muted'); } } } _pollAgentMailboxes() { const fs = this.terminal.fs; if (!fs) return; for (const [id, agent] of Object.entries(this._agents)) { try { const inbox = fs.fsReadJSON(`/agents/${id}/inbox.json`); if (!inbox) continue; // Check if this message is already processed const existing = this._a2aMessages[id] || []; if (existing.find(m => m.msg_id === inbox.msg_id && m._processed)) continue; // Route based on message type if (inbox.a2a_version && inbox.type) { inbox._processed = true; if (!this._a2aMessages[id]) this._a2aMessages[id] = []; this._a2aMessages[id].push(inbox); if (inbox.type === 'request') { this.terminal.print(`📨 A2A: ${inbox.from} → ${id} (${inbox.msg_id})`, 'info'); // Check outbox for response this._checkAgentOutbox(id); } else if (inbox.type === 'broadcast') { this.terminal.print(`📢 A2A broadcast from ${inbox.from}: ${(inbox.payload?.text || JSON.stringify(inbox.payload)).substring(0, 60)}`, 'info'); } else if (inbox.type === 'heartbeat') { agent.lastHeartbeat = Date.now(); } else if (inbox.type === 'response') { this.terminal.print(`📨 A2A response from ${id}: ${(inbox.payload?.result || JSON.stringify(inbox.payload)).substring(0, 60)}`, 'success'); agent.status = 'idle'; agent.currentTask = null; } } } catch (e) { /* skip agent with bad inbox */ } } } _checkAgentOutbox(agentId) { try { const outbox = this.terminal.fs.fsReadJSON(`/agents/${agentId}/outbox.json`); if (outbox && outbox.status === 'completed') { const msg = { a2a_version: '0.1', msg_id: `resp-${Date.now()}`, type: 'response', from: agentId, to: 'orchestrator-0', timestamp: new Date().toISOString(), payload: outbox }; if (!this._a2aMessages['orchestrator-0']) this._a2aMessages['orchestrator-0'] = []; this._a2aMessages['orchestrator-0'].push(msg); this.terminal.print(`📨 A2A: ${agentId} completed task (outbox)`, 'success'); } } catch (e) {} } // ─── Agent Lifecycle ─── _sanitizeId(name) { return (name || 'worker').replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 32) || 'worker'; } spawnAgent(role, capabilities) { const safeRole = this._sanitizeId(role); const id = `agent-${Object.keys(this._agents).length + 1}-${safeRole}`; 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, leaseId: null, restartPolicy: this._RESTART_POLICY, failureCount: 0, restartCount: 0, nextRestartAt: null }; this._agents[id] = agent; // Create VFS agent directory with status if (!this.terminal.fs.readFile(`/agents/${id}/`)) this.terminal.fs.mkdir(`agents/${id}`); this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(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); // Clean up VFS this.terminal.fs.deleteFile('agents/' + id); delete this._agents[id]; delete this._restartBackoffs[id]; this.terminal.print(`💀 Agent '${id}' killed`, 'warning'); } sleepAgent(id) { if (!this._agents[id]) return this.terminal.print(`❌ Agent '${id}' not found`, 'error'); const agent = this._agents[id]; // Serialize to VFS const state = { ...agent, _sleepTime: new Date().toISOString() }; delete state.leaseId; // leases don't survive sleep this.terminal.fs.writeFile(`agents/${id}/sleep.json`, JSON.stringify(state)); agent.status = 'sleeping'; this.terminal.print(`💤 Agent '${id}' sleeping (state saved to VFS)`, 'warning'); } wakeAgent(id) { const data = this.terminal.fs.fsReadJSON(`/agents/${id}/sleep.json`); if (!data) return this.terminal.print(`❌ No saved state for '${id}' — use orchestrator-spawn instead`, 'error'); // Restore agent from VFS const agent = { ...data, status: 'idle', currentTask: null, leaseId: null, lastHeartbeat: Date.now() }; delete agent._sleepTime; this._agents[id] = agent; this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify({ ...agent, status: 'idle' })); this.terminal.fs.deleteFile(`agents/${id}/sleep.json`); this.terminal.print(`🔄 Agent '${id}' reanimated from VFS sleep`, 'success'); } _reanimateFromVFS() { const fs = this.terminal.fs; const agentsDir = fs.readDir('/agents/'); if (!agentsDir) return; let reanimated = 0; for (const entry of agentsDir) { if (entry.type !== 'dir') continue; const sleepData = fs.fsReadJSON(`/agents/${entry.name}/sleep.json`); if (sleepData) { const agent = { ...sleepData, status: 'idle', currentTask: null, leaseId: null, lastHeartbeat: Date.now() }; delete agent._sleepTime; this._agents[entry.name] = agent; fs.writeFile(`agents/${entry.name}/status.json`, JSON.stringify({ ...agent, status: 'idle' })); reanimated++; } } if (reanimated > 0) { this.terminal.print(`🔄 Reanimated ${reanimated} agent(s) from VFS sleep`, 'info'); } } // ─── Restart Policies ─── setRestartPolicy(args) { if (args.length < 2) return this.terminal.print('Usage: orchestrator-restart-policy ', 'error'); const id = args[0]; const policy = args[1]; if (!['on-failure', 'always', 'never'].includes(policy)) { return this.terminal.print('Policy must be: on-failure, always, or never', 'error'); } if (id === 'default') { this._RESTART_POLICY = policy; return this.terminal.print(`✅ Default restart policy set to '${policy}'`, 'success'); } const agent = this._agents[id]; if (!agent) return this.terminal.print(`❌ Agent '${id}' not found`, 'error'); agent.restartPolicy = policy; this.terminal.print(`✅ Restart policy for '${id}' set to '${policy}'`, 'success'); } _handleAgentFailure(id, error) { const agent = this._agents[id]; if (!agent) return; agent.failureCount = (agent.failureCount || 0) + 1; agent.lastError = error; agent.lastErrorAt = new Date().toISOString(); let shouldRestart = false; if (agent.restartPolicy === 'always') { shouldRestart = true; } else if (agent.restartPolicy === 'on-failure' && agent.failureCount <= 1) { shouldRestart = true; } if (shouldRestart) { const backoff = Math.min(Math.pow(2, agent.failureCount), 60); // 2^N capped at 60s agent.nextRestartAt = Date.now() + (backoff * 1000); agent.restartCount = (agent.restartCount || 0) + 1; this._restartBackoffs[id] = setTimeout(() => { if (this._agents[id]) { this._agents[id].status = 'idle'; this._agents[id].currentTask = null; this.terminal.print(`🔄 Agent '${id}' auto-restarted (backoff: ${backoff}s, attempt: ${agent.restartCount})`, 'info'); } }, backoff * 1000); this.terminal.print(`⚠️ Agent '${id}' failed — restart in ${backoff}s (policy: ${agent.restartPolicy})`, 'warning'); } else { agent.status = 'failed'; this.terminal.print(`❌ Agent '${id}' failed — no restart (policy: ${agent.restartPolicy})`, 'error'); } } // ─── Autonomous Spawning ─── _deriveCapabilities(task) { const caps = ['list_files', 'read_file']; const lower = task.toLowerCase(); const CAP_MAP = [ { patterns: ['search', 'find', 'research', 'lookup', 'query', 'look up'], caps: ['web_search'] }, { patterns: ['write', 'create', 'generate', 'compose', 'draft', 'save'], caps: ['write_file'] }, { patterns: ['delete', 'remove', 'trash', 'rm '], caps: ['delete_file'] }, { patterns: ['read', 'view', 'cat', 'show', 'display', 'inspect', 'preview'], caps: [] }, // already has read_file { patterns: ['edit', 'modify', 'update', 'patch', 'change', 'fix'], caps: ['write_file'] }, { patterns: ['install', 'package', 'wpm', 'setup'], caps: ['wpm_install'] }, { patterns: ['module', 'plugin', 'ext'], caps: ['view_module', 'edit_module'] }, { patterns: ['list', 'status', 'health', 'audit', 'report', 'check'], caps: ['get_status'] }, { patterns: ['fetch', 'url', 'http', 'scrape'], caps: ['web_fetch'] }, { patterns: ['code', 'program', 'script', 'build', 'develop', 'compile'], caps: ['list_files', 'read_file', 'write_file'] }, { patterns: ['analyze', 'analyse', 'summarize', 'review', 'investigate'], caps: ['web_search', 'list_files', 'read_file'] }, { patterns: ['deploy', 'publish', 'upload'], caps: ['write_file', 'webdav_upload'] }, { patterns: ['monitor', 'watch', 'poll', 'track', 'observe'], caps: ['get_status'] }, ]; for (const entry of CAP_MAP) { if (entry.patterns.some(p => lower.includes(p))) { entry.caps.forEach(c => caps.push(c)); } } return [...new Set(caps)]; } _deriveRole(task) { const lower = task.toLowerCase(); if (lower.includes('search') || lower.includes('find')) return 'searcher'; if (lower.includes('write') || lower.includes('create') || lower.includes('save')) return 'writer'; if (lower.includes('delete') || lower.includes('remove') || lower.includes('clean')) return 'janitor'; if (lower.includes('install') || lower.includes('package')) return 'packager'; if (lower.includes('module') || lower.includes('code') || lower.includes('fix')) return 'coder'; if (lower.includes('health') || lower.includes('check') || lower.includes('audit')) return 'auditor'; if (lower.includes('fetch') || lower.includes('url') || lower.includes('http')) return 'fetcher'; return 'worker'; } // ─── Config ─── setConfig(args) { if (args.length < 2) return this.terminal.print('Usage: orchestrator-config ', 'error'); const key = args[0]; const val = args[1]; const configMap = { 'spawn-limit': () => { this._SPAWN_LIMIT = parseInt(val); return `spawn-limit set to ${val}`; }, 'spawn-strategy': () => { if (!['eager', 'lazy'].includes(val)) return 'Must be eager or lazy'; this._SPAWN_STRATEGY = val; return `spawn-strategy set to ${val}`; }, 'heartbeat-timeout': () => { this._HEARTBEAT_TIMEOUT = parseInt(val); return `heartbeat-timeout set to ${val}ms`; }, 'idle-timeout': () => { this._IDLE_TIMEOUT = parseInt(val); return `idle-timeout set to ${val}ms`; }, 'restart-policy': () => { if (!['on-failure', 'always', 'never'].includes(val)) return 'Must be on-failure, always, or never'; this._RESTART_POLICY = val; return `default restart-policy set to ${val}`; }, 'a2a-polling': () => { this._A2A_POLLING = val === 'on' || val === 'true'; if (this._A2A_POLLING && !this._a2aPollTimer) { this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL); } else if (!this._A2A_POLLING && this._a2aPollTimer) { clearInterval(this._a2aPollTimer); this._a2aPollTimer = null; } return `a2a-polling ${this._A2A_POLLING ? 'on' : 'off'}`; }, 'a2a-poll-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 50 || n > 30000) return 'Must be 50-30000ms'; clearInterval(this._a2aPollTimer); this._a2aPollTimer = null; this._A2A_POLL_INTERVAL = n; if (this._A2A_POLLING) { this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), n); } return `a2a-poll-interval set to ${n}ms`; }, 'heartbeat-check': () => { const n = parseInt(val); if (isNaN(n) || n < 500 || n > 60000) return 'Must be 500-60000ms'; clearInterval(this._heartbeatInterval); this._HEARTBEAT_CHECK = n; this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), n); return `heartbeat-check set to ${n}ms`; }, 'idle-sweep-interval': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; clearInterval(this._idleSweepInterval); this._IDLE_SWEEP_INTERVAL = n; this._idleSweepInterval = setInterval(() => this._sweepIdle(), n); return `idle-sweep-interval set to ${n}ms`; }, 'dead-purge-timeout': () => { const n = parseInt(val); if (isNaN(n) || n < 5000 || n > 600000) return 'Must be 5000-600000ms'; this._DEAD_PURGE_TIMEOUT = n; return `dead-purge-timeout set to ${n}ms`; } }; if (!configMap[key]) { return this.terminal.print(`Unknown config key: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error'); } const result = configMap[key](); this.terminal.print(`✅ ${result}`, 'success'); this._saveConfig(); } // ─── Task Delegation ─── async delegateTask(task) { if (!task) return this.terminal.print('Usage: orchestrate ', 'error'); // Find an idle agent let agent = Object.values(this._agents).find(a => a.status === 'idle'); // Auto-spawn with role-derived capabilities if (!agent) { if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { // Try finding any non-failed agent to reuse agent = Object.values(this._agents).find(a => a.status !== 'failed' && a.status !== 'sleeping'); } if (!agent) { const role = this._deriveRole(task); const caps = this._deriveCapabilities(task); this.spawnAgent(role, caps); agent = Object.values(this._agents).find(a => a.status === 'idle'); } } if (!agent) return this.terminal.print('❌ No available agents', 'error'); // A2A Contract handshake const registry = this.terminal.moduleManager.modules['AgentRegistryModule']; let contractValid = true; if (registry) { const result = registry.validateTask(agent.role, task); if (!result.valid) { this.terminal.print(`⚠️ A2A validation: ${result.reason}`, 'warning'); contractValid = false; } else if (result.reason === 'matched') { this.terminal.print(`✅ A2A contract matched for role '${agent.role}'`, 'success'); } } // 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; agent.lastHeartbeat = Date.now(); // Send A2A request via inbox this.sendA2AMessage([agent.id, JSON.stringify({ type: 'request', payload: { task, from: 'orchestrator-0', leaseId: lease?.id } })]); 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'; // Write result to outbox this.terminal.fs.writeFile(`agents/${agent.id}/outbox.json`, JSON.stringify({ result: typeof result === 'string' ? result : (result?.content || 'completed'), status: 'completed', timestamp: new Date().toISOString() })); // Record in memory graph const memgraph = this.terminal.moduleManager.modules['MemoryGraphModule']; if (memgraph) { memgraph.recordTask(task, typeof result === 'string' ? result : (result?.content || ''), agent.id); memgraph.recordFileAccess(`/agents/${agent.id}/outbox.json`); } this.terminal.print(`✅ ${agent.id} completed task`, 'success'); // Check restart policy — if always, set back to idle for next task if (agent.restartPolicy === 'always') { agent.status = 'idle'; agent.currentTask = null; agent.lastHeartbeat = Date.now(); } } else { this.terminal.print(`ℹ️ AI module not available — task queued for ${agent.id}`, 'info'); } } catch (e) { this._handleAgentFailure(agent.id, e.message); } // Return to idle pool (unless failed/restarting) if (agent.status === 'failed') { // handled by _handleAgentFailure } else if (agent.status !== 'busy') { agent.status = 'idle'; agent.currentTask = null; agent.lastHeartbeat = Date.now(); } } // ─── Monitoring ─── _checkHeartbeats() { const now = Date.now(); Object.entries(this._agents).forEach(([id, agent]) => { if (agent.status === 'busy' && now - agent.lastHeartbeat > this._HEARTBEAT_TIMEOUT) { this.terminal.print(`⚠️ Agent '${id}' unresponsive (${Math.round((now - agent.lastHeartbeat) / 1000)}s since heartbeat)`, 'warning'); } }); } _sweepIdle() { const now = Date.now(); Object.entries(this._agents).forEach(([id, agent]) => { if (agent.status === 'idle' && now - agent.lastHeartbeat > this._IDLE_TIMEOUT) { this.terminal.print(`💤 Agent '${id}' idle for >${this._IDLE_TIMEOUT/1000}s — auto-sleeping`, 'info'); this.sleepAgent(id); } }); } purgeDeadAgents() { const now = Date.now(); let count = 0; Object.entries(this._agents).forEach(([id, agent]) => { if (now - agent.lastHeartbeat > this._DEAD_PURGE_TIMEOUT) { this.killAgent(id); count++; } }); this.terminal.print(`🧹 Purged ${count} dead agents`, count > 0 ? 'warning' : 'info'); } // ─── Status & List ─── 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 sleeping = Object.values(this._agents).filter(a => a.status === 'sleeping').length; const failed = Object.values(this._agents).filter(a => a.status === 'failed' || a.status === 'restarting').length; const a2aTotal = Object.values(this._a2aMessages).reduce((s, msgs) => s + msgs.length, 0); const a2aPending = Object.values(this._a2aMessages).reduce((s, msgs) => s + msgs.filter(m => !m._processed).length, 0); this.terminal.print('🧠 Orchestrator Status', 'header'); this.terminal.print(` Agents: ${total} (${busy} busy, ${idle} idle, ${sleeping} sleeping, ${failed} failed)`, 'info'); this.terminal.print(` Spawn limit: ${this._SPAWN_LIMIT} (strategy: ${this._SPAWN_STRATEGY})`, 'info'); this.terminal.print(` Default restart: ${this._RESTART_POLICY}`, 'info'); this.terminal.print(` A2A Messages: ${a2aTotal} (${a2aPending} pending)`, 'info'); Object.values(this._agents).forEach(a => { const task = a.currentTask ? ` (task: ${a.currentTask.substring(0, 40)})` : ''; const backoff = a.nextRestartAt && a.nextRestartAt > Date.now() ? ` [restart in ${Math.round((a.nextRestartAt - Date.now()) / 1000)}s]` : ''; this.terminal.print(` ${a.id} [${a.role}] — ${a.status}${task}${backoff}`, 'info'); }); } listAgents() { const all = Object.values(this._agents); if (all.length === 0) return this.terminal.print('No agents spawned', 'info'); this.terminal.print(`📋 Agents (${all.length}):`, 'header'); all.forEach(a => { const caps = (a.capabilities || []).join(', '); this.terminal.print(` ${a.id} [${a.role}] — ${a.status}`, 'info'); this.terminal.print(` Caps: ${caps}`, 'text-muted'); this.terminal.print(` Policy: ${a.restartPolicy || this._RESTART_POLICY} | Failures: ${a.failureCount || 0} | Restarts: ${a.restartCount || 0}`, 'text-muted'); if (a.currentTask) this.terminal.print(` Task: ${a.currentTask.substring(0, 60)}`, 'text-muted'); }); } } // Required for module system if (typeof window !== 'undefined') { window.OrchestratorModule = OrchestratorModule; }