diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-06 06:00:27 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-06 06:00:27 +0200 |
| commit | ccc7d9956559d2195e20345e7a794662b52cd403 (patch) | |
| tree | 757354fbfec5c75b4577b2cb620d5ab1582e19b1 /modules/core/module-orchestrator.js | |
| parent | b538577cd0a4292e4cd4ca1c9f9829fc7e133caa (diff) | |
auto: 2026-06-06_06:00 changes
Diffstat (limited to 'modules/core/module-orchestrator.js')
| -rw-r--r-- | modules/core/module-orchestrator.js | 108 |
1 files changed, 91 insertions, 17 deletions
diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js index ec385bb..05e1598 100644 --- a/modules/core/module-orchestrator.js +++ b/modules/core/module-orchestrator.js @@ -13,10 +13,32 @@ class OrchestratorModule { 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(' ')); @@ -39,7 +61,7 @@ class OrchestratorModule { 'orchestrator-spawn': 'orchestrator-spawn [role] [capabilities...] — Manually spawn a new agent', 'orchestrator-kill': 'orchestrator-kill <agent-id> — Terminate an agent and revoke leases', 'orchestrator-purge': 'orchestrator-purge — Remove dead agents (>60s since heartbeat)', - 'orchestrator-config': 'orchestrator-config <key> <value> — Tune: spawn-limit, spawn-strategy, heartbeat-timeout, idle-timeout, restart-policy, a2a-polling', + 'orchestrator-config': 'orchestrator-config <key> <value> — 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 <agent-id> — Suspend agent to VFS', 'orchestrator-wake': 'orchestrator-wake <agent-id> — Restore agent from VFS sleep', @@ -59,13 +81,39 @@ class OrchestratorModule { this._reanimateFromVFS(); // A2A mailbox polling loop (500ms) if (this._A2A_POLLING) { - this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), 500); + this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), this._A2A_POLL_INTERVAL); } - // Heartbeat watchdog every 3 seconds - this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), 3000); - // Idle timeout sweep every 30s - setInterval(() => this._sweepIdle(), 30000); + // 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 ─── @@ -104,6 +152,10 @@ class OrchestratorModule { 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() { @@ -178,8 +230,12 @@ class OrchestratorModule { // ─── 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 id = `agent-${Object.keys(this._agents).length + 1}-${role || 'worker'}`; + 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; @@ -343,13 +399,26 @@ class OrchestratorModule { _deriveCapabilities(task) { const caps = ['list_files', 'read_file']; const lower = task.toLowerCase(); - if (lower.includes('search') || lower.includes('find') || lower.includes('look up') || lower.includes('web')) caps.push('web_search'); - if (lower.includes('write') || lower.includes('create') || lower.includes('save') || lower.includes('edit')) caps.push('write_file'); - if (lower.includes('delete') || lower.includes('remove') || lower.includes('rm ')) caps.push('delete_file'); - if (lower.includes('install') || lower.includes('package') || lower.includes('wpm')) caps.push('wpm_install'); - if (lower.includes('module') || lower.includes('view') || lower.includes('edit')) caps.push('view_module', 'edit_module'); - if (lower.includes('list') || lower.includes('status') || lower.includes('health')) caps.push('get_status'); - if (lower.includes('fetch') || lower.includes('url') || lower.includes('http')) caps.push('web_fetch'); + 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)]; } @@ -381,13 +450,17 @@ class OrchestratorModule { 'a2a-polling': () => { this._A2A_POLLING = val === 'on' || val === 'true'; if (this._A2A_POLLING && !this._a2aPollTimer) { - this._a2aPollTimer = setInterval(() => this._pollAgentMailboxes(), 500); + 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]) { @@ -395,6 +468,7 @@ class OrchestratorModule { } const result = configMap[key](); this.terminal.print(`✅ ${result}`, 'success'); + this._saveConfig(); } // ─── Task Delegation ─── @@ -522,7 +596,7 @@ class OrchestratorModule { const now = Date.now(); let count = 0; Object.entries(this._agents).forEach(([id, agent]) => { - if (now - agent.lastHeartbeat > 60000) { + if (now - agent.lastHeartbeat > this._DEAD_PURGE_TIMEOUT) { this.killAgent(id); count++; } |