diff options
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/module-aiassist.js | 48 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 6 | ||||
| -rw-r--r-- | modules/core/module-memory-graph.js | 400 | ||||
| -rw-r--r-- | modules/core/module-orchestrator.js | 474 | ||||
| -rw-r--r-- | modules/core/module-predictor.js | 279 | ||||
| -rw-r--r-- | modules/core/module-sandbox.js | 332 | ||||
| -rw-r--r-- | modules/core/module-world-model.js | 381 | ||||
| -rw-r--r-- | modules/core/sandbox-worker.js | 205 | ||||
| -rw-r--r-- | modules/core/terminal.js | 4 |
9 files changed, 2092 insertions, 37 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index cb1482f..b93b12d 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -572,6 +572,28 @@ RULES: } catch (e) {} } + // Inject memory graph context for cross-session awareness + try { + const memgraph = this.terminal.moduleManager.modules['MemoryGraphModule']; + if (memgraph) { + const mctx = memgraph.getContextForPrompt(); + if (mctx) { + messages.push({ role: 'system', content: mctx }); + } + } + } catch (e) { /* memory graph not available */ } + + // Inject world model context for system self-awareness + try { + const wm = this.terminal.moduleManager.modules['WorldModelModule']; + if (wm) { + const wctx = wm.getContextForPrompt(); + if (wctx) { + messages.push({ role: 'system', content: wctx }); + } + } + } catch (e) { /* world model not available */ } + loopResult = await this.agenticLoop(messages); this.context.push({ role: 'user', content: userMessage }); if (loopResult.finalAnswer) { @@ -1497,7 +1519,7 @@ RULES: if (resp.ok) { const src = await resp.text(); this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src); - return src.substring(0, 8000); + return src; } } catch (e) { /* fall through */ } } @@ -1519,7 +1541,7 @@ RULES: if (resp.ok) { const src = await resp.text(); this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, src); - return src.substring(0, 8000); + return src; } } } @@ -1542,6 +1564,28 @@ RULES: } } catch (e) {} } + // Fallback: use modules-index.json for core modules (not in registry) + if (!code) { + try { + const idxResp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); + if (idxResp.ok) { + const idx = await idxResp.json(); + let match = Object.values(idx.modules).find(m => m.class === modName); + if (!match) match = idx.modules[modName]; + if (match) { + const filePath = match.file; + const fetchUrl = `/spaces/newos4/${filePath}`; + const resp = await fetch(fetchUrl); + if (resp.ok) { + code = await resp.text(); + this.terminal.fs.writeFile(`_mods/${modName}.orig.js`, code); + this.terminal.fs.writeFile(`_mods/${modName}.js`, code); + this.terminal.fs.writeFile(`_mods/${modName}.meta.json`, JSON.stringify({ originalUrl: fetchUrl, modifiedAt: new Date().toISOString(), editCount: 0 })); + } + } + } + } catch (e) {} + } if (code) this.terminal.openCodeEditor(modName, url || '', code); return code ? `Editor opened for ${modName} (${code.length} chars)` : JSON.stringify({ error: `Could not load source for ${modName}` }); } diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index e7fc64a..b32577d 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -33,6 +33,10 @@ 'module-agent-registry.js', 'module-leases.js', 'module-orchestrator.js', + 'module-sandbox.js', + 'module-memory-graph.js', + 'module-predictor.js', + 'module-world-model.js', 'module-neural-vfs.js' ]; @@ -102,7 +106,7 @@ window.__moduleLoaderReload = reloadWithSelection; -const LATEST_VER = '2026-06-03-neuralvfs'; +const LATEST_VER = '2026-06-05-v4.4.0'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-memory-graph.js b/modules/core/module-memory-graph.js new file mode 100644 index 0000000..2382a4a --- /dev/null +++ b/modules/core/module-memory-graph.js @@ -0,0 +1,400 @@ +// module-memory-graph.js — Cross-session persistent memory graph +// Nodes: files, concepts, tasks, patterns, users +// Edges: relationships with weight, last_used +// Commands: memgraph-status, memgraph-search, memgraph-stats, memgraph-export, memgraph-prune +class MemoryGraphModule { + constructor() { + this.commands = {}; + this.terminal = null; + this._nodes = {}; + this._edges = []; + this._indexPath = '/.memory_graph/'; + this._debounceTimer = null; + this._initialized = false; + this._totalNodeAccesses = 0; + } + + getHelpInfo() { + return { + 'memgraph-status': 'memgraph-status — Show node/edge counts, last access', + 'memgraph-search': 'memgraph-search <query> — Find nodes by label or type', + 'memgraph-stats': 'memgraph-stats — Most connected nodes, strongest edges, cluster info', + 'memgraph-export': 'memgraph-export — Export graph as JSON', + 'memgraph-prune': 'memgraph-prune — Remove stale low-weight edges (>30d, <3 accesses)', + }; + } + + registerCommands() { + this.commands['memgraph-status'] = () => this.showStatus(); + this.commands['memgraph-search'] = (args) => this.search(args.join(' ')); + this.commands['memgraph-stats'] = () => this.showStats(); + this.commands['memgraph-export'] = () => this.exportGraph(); + this.commands['memgraph-prune'] = () => this.prune(); + } + + init(terminal) { + this.terminal = terminal; + this._loadFromVFS(); + this._initialized = true; + this.terminal.print('🧠 Memory Graph loaded — cross-session context active', 'info'); + // Log startup for graph + this.addNode('session', `session-${Date.now()}`, `Session ${new Date().toLocaleDateString()}`); + } + + // ─── Node API ─── + + addNode(type, id, label, metadata = {}) { + const key = `${type}:${id}`; + if (!this._nodes[key]) { + this._nodes[key] = { + id: key, + type, + label: label || id, + metadata: metadata || {}, + created: new Date().toISOString(), + updated: new Date().toISOString(), + access_count: 1 + }; + } else { + this._nodes[key].updated = new Date().toISOString(); + this._nodes[key].access_count++; + if (metadata && Object.keys(metadata).length > 0) { + Object.assign(this._nodes[key].metadata, metadata); + } + } + this._totalNodeAccesses++; + this._debounceSave(); + return key; + } + + getNode(type, id) { + return this._nodes[`${type}:${id}`] || null; + } + + findNodes(query) { + const q = query.toLowerCase(); + return Object.values(this._nodes).filter(n => + n.label.toLowerCase().includes(q) || + n.type.toLowerCase().includes(q) || + (n.id && n.id.toLowerCase().includes(q)) + ); + } + + // ─── Edge API ─── + + addEdge(sourceKey, targetKey, type, weight = 1) { + // Check if edge already exists + let edge = this._edges.find(e => + e.source === sourceKey && e.target === targetKey && e.type === type + ); + if (edge) { + edge.weight = (edge.weight + weight) / 2; + edge.last_used = new Date().toISOString(); + edge.use_count++; + } else { + this._edges.push({ + source: sourceKey, + target: targetKey, + type, + weight, + created: new Date().toISOString(), + last_used: new Date().toISOString(), + use_count: 1 + }); + } + this._debounceSave(); + } + + // ─── Event-based Learning ─── + + recordFileAccess(path) { + const name = path.split('/').pop() || path; + const nodeKey = this.addNode('file', path, name); + const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`; + this.addEdge(sessionKey, nodeKey, 'accessed', 0.5); + + // Extract file extension edge + const ext = path.split('.').pop(); + if (ext && ext.length < 10) { + this.addNode('type', ext, `.${ext} files`); + this.addEdge(nodeKey, `type:${ext}`, 'has_type', 0.3); + } + } + + recordToolCall(toolName, success) { + const nodeKey = this.addNode('tool', toolName, toolName); + const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`; + this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8); + } + + recordConcept(concept, filePath) { + const conceptKey = this.addNode('concept', concept, concept); + const fileKey = this.addNode('file', filePath, filePath.split('/').pop()); + this.addEdge(conceptKey, fileKey, 'maps_to', 0.7); + this.addEdge(fileKey, conceptKey, 'tagged_as', 0.7); + } + + recordTask(task, result, agentId) { + const taskId = `task-${Date.now()}`; + const taskKey = this.addNode('task', taskId, task.substring(0, 60), { + full: task, + result: result?.substring(0, 100), + agent: agentId + }); + const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`; + this.addEdge(sessionKey, taskKey, 'executed', 0.6); + + // Extract keywords as concepts + const words = task.toLowerCase().split(/\s+/).filter(w => w.length > 3); + for (const word of [...new Set(words)].slice(0, 5)) { + this.addNode('concept', word, word); + this.addEdge(taskKey, `concept:${word}`, 'involves', 0.4); + } + + // Link to tools used (if any found in task) + const knownTools = ['list_files', 'read_file', 'write_file', 'web_search', 'web_fetch', + 'edit_module', 'view_module', 'run_command', 'orchestrate']; + for (const tool of knownTools) { + if (task.toLowerCase().includes(tool.replace('_', ' '))) { + this.recordToolCall(tool, true); + } + } + } + + // ─── Context Injection ─── + + getContextSummary() { + // Get top active nodes from recent session + const now = Date.now(); + const today = `session:session-${Math.floor(now / 86400000)}`; + const yesterday = `session:session-${Math.floor(now / 86400000) - 1}`; + + const lines = []; + const totalNodes = Object.keys(this._nodes).length; + const totalEdges = this._edges.length; + + lines.push(`Memory Graph: ${totalNodes} nodes, ${totalEdges} edges`); + + // Recent task context + const recentTasks = Object.values(this._nodes) + .filter(n => n.type === 'task') + .sort((a, b) => new Date(b.updated) - new Date(a.updated)) + .slice(0, 3); + + if (recentTasks.length > 0) { + lines.push('Recent tasks:'); + for (const t of recentTasks) { + lines.push(` - ${t.label.substring(0, 50)}`); + } + } + + // Most accessed files + const topFiles = Object.values(this._nodes) + .filter(n => n.type === 'file') + .sort((a, b) => b.access_count - a.access_count) + .slice(0, 3); + + if (topFiles.length > 0) { + lines.push('Most accessed files:'); + for (const f of topFiles) { + lines.push(` - ${f.label} (${f.access_count}x)`); + } + } + + return lines.join('\\n'); + } + + getContextForPrompt() { + const summary = this.getContextSummary(); + if (!summary) return ''; + return `[Memory Graph Context]\\n${summary}\\n`; + } + + // ─── Commands ─── + + showStatus() { + const types = {}; + for (const n of Object.values(this._nodes)) { + types[n.type] = (types[n.type] || 0) + 1; + } + const typeStr = Object.entries(types) + .sort((a, b) => b[1] - a[1]) + .map(([t, c]) => `${t}: ${c}`) + .join(', '); + + this.terminal.print('🧠 Memory Graph', 'header'); + this.terminal.print(` Nodes: ${Object.keys(this._nodes).length} (${typeStr})`, 'info'); + this.terminal.print(` Edges: ${this._edges.length}`, 'info'); + this.terminal.print(` Total accesses: ${this._totalNodeAccesses}`, 'info'); + this.terminal.print(` Path: ${this._indexPath}`, 'info'); + } + + search(query) { + if (!query) return this.terminal.print('Usage: memgraph-search <query>', 'error'); + const results = this.findNodes(query); + if (results.length === 0) return this.terminal.print(`No nodes matching "${query}"`, 'info'); + + this.terminal.print(`🔍 Found ${results.length} nodes:`, 'header'); + for (const n of results.slice(0, 20)) { + const edges = this._edges.filter(e => e.source === n.id || e.target === n.id); + this.terminal.print(` ${n.type}:${n.label} (${edges.length} edges, ${n.access_count}x)`, 'info'); + for (const e of edges.slice(0, 3)) { + const other = e.source === n.id ? e.target : e.source; + const otherLabel = (this._nodes[other] || {}).label || other; + this.terminal.print(` └─ ${e.type} → ${otherLabel} (w:${e.weight.toFixed(1)})`, 'text-muted'); + } + } + if (results.length > 20) this.terminal.print(` ... and ${results.length - 20} more`, 'info'); + } + + showStats() { + if (Object.keys(this._nodes).length === 0) { + return this.terminal.print('No graph data yet. Use commands normally to build it.', 'info'); + } + + // Most connected nodes + const connected = {}; + for (const e of this._edges) { + connected[e.source] = (connected[e.source] || 0) + 1; + connected[e.target] = (connected[e.target] || 0) + 1; + } + const top = Object.entries(connected) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5); + + this.terminal.print('📊 Memory Graph Stats', 'header'); + this.terminal.print(' Most connected:', 'info'); + for (const [key, count] of top) { + const label = (this._nodes[key] || {}).label || key; + this.terminal.print(` ${label} — ${count} connections`, 'info'); + } + + // Strongest edges + const strong = [...this._edges].sort((a, b) => b.weight - a.weight).slice(0, 3); + if (strong.length > 0) { + this.terminal.print(' Strongest relationships:', 'info'); + for (const e of strong) { + const sLabel = (this._nodes[e.source] || {}).label || e.source; + const tLabel = (this._nodes[e.target] || {}).label || e.target; + this.terminal.print(` ${sLabel} → ${tLabel} (${e.type}, w:${e.weight.toFixed(2)}, used ${e.use_count}x)`, 'info'); + } + } + + // Cluster density + const density = this._edges.length / Math.max(Object.keys(this._nodes).length, 1); + this.terminal.print(` Density: ${density.toFixed(2)} edges/node`, 'info'); + } + + exportGraph() { + const exportData = { + version: 1, + exported: new Date().toISOString(), + nodes: Object.values(this._nodes), + edges: this._edges, + stats: { + totalNodes: Object.keys(this._nodes).length, + totalEdges: this._edges.length, + totalAccesses: this._totalNodeAccesses + } + }; + const json = JSON.stringify(exportData, null, 2); + // Save to VFS + try { + if (!this.terminal.fs.readDir(this._indexPath)) { + this.terminal.fs.mkdirRecursive(this._indexPath); + } + this.terminal.fs.writeFile(`${this._indexPath}export.json`, json); + this.terminal.print(`✅ Graph exported (${json.length} bytes) to ${this._indexPath}export.json`, 'success'); + } catch (e) { + this.terminal.print(`❌ Export failed: ${e.message}`, 'error'); + } + } + + prune() { + const now = Date.now(); + const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; + + const before = this._edges.length; + this._edges = this._edges.filter(e => { + const age = now - new Date(e.last_used).getTime(); + // Keep if: used recently (<30d) OR used 3+ times regardless of age + if (age < THIRTY_DAYS) return true; + if (e.use_count >= 3) return true; + // Or if weight is high + if (e.weight > 0.8) return true; + return false; + }); + const pruned = before - this._edges.length; + + // Also prune orphan nodes (no edges) + const connectedNodes = new Set(); + for (const e of this._edges) { + connectedNodes.add(e.source); + connectedNodes.add(e.target); + } + const nodeBefore = Object.keys(this._nodes).length; + for (const key of Object.keys(this._nodes)) { + if (!connectedNodes.has(key)) { + // Keep session and high-access nodes + if (this._nodes[key].access_count >= 5) continue; + delete this._nodes[key]; + } + } + const nodesPruned = nodeBefore - Object.keys(this._nodes).length; + + this._saveNow(); + this.terminal.print(`🧹 Pruned ${pruned} edges and ${nodesPruned} orphan nodes`, nodesPruned > 0 ? 'warning' : 'info'); + } + + // ─── Persistence ─── + + _loadFromVFS() { + try { + const fs = this.terminal.fs; + if (!fs) return; + + // Try loading from export first + const exportData = fs.fsReadJSON(`${this._indexPath}export.json`); + if (exportData && exportData.nodes) { + for (const n of exportData.nodes) { + this._nodes[n.id] = n; + } + this._edges = exportData.edges || []; + this._totalNodeAccesses = (exportData.stats?.totalAccesses) || 0; + return; + } + + // Fallback: load nodes and edges separately + const nodes = fs.fsReadJSON(`${this._indexPath}nodes.json`); + if (nodes) this._nodes = nodes; + const edges = fs.fsReadJSON(`${this._indexPath}edges.json`); + if (edges) this._edges = edges; + } catch (e) { + console.warn('[MemoryGraph] Load error (fresh start):', e.message); + } + } + + _saveNow() { + try { + const fs = this.terminal.fs; + if (!fs) return; + if (!fs.readDir(this._indexPath)) { + fs.mkdirRecursive(this._indexPath); + } + fs.fsWriteJSON(`${this._indexPath}nodes.json`, this._nodes); + fs.fsWriteJSON(`${this._indexPath}edges.json`, this._edges); + } catch (e) { + console.error('[MemoryGraph] Save error:', e.message); + } + } + + _debounceSave() { + if (this._debounceTimer) clearTimeout(this._debounceTimer); + this._debounceTimer = setTimeout(() => this._saveNow(), 2000); + } +} + +// Required for module system +if (typeof window !== 'undefined') { + window.MemoryGraphModule = MemoryGraphModule; +}
\ No newline at end of file diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js index de88424..ec385bb 100644 --- a/modules/core/module-orchestrator.js +++ b/modules/core/module-orchestrator.js @@ -1,12 +1,22 @@ -// module-orchestrator.js — Multi-agent orchestration system -// Commands: orchestrate, orchestrator-status, orchestrator-spawn, orchestrator-kill, orchestrator-purge +// 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 = 3; + 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._a2aPollTimer = null; this._heartbeatInterval = null; + this._a2aMessages = {}; + this._restartBackoffs = {}; } registerCommands() { this.commands['orchestrate'] = (args) => this.delegateTask(args.join(' ')); @@ -14,6 +24,29 @@ class OrchestratorModule { 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 <task> — 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 <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-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', + 'orchestrator-restart-policy': 'orchestrator-restart-policy <agent-id> <on-failure|always|never> — Set per-agent restart policy', + 'a2a-send': 'a2a-send <agent-id> <json-message> — 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; @@ -22,10 +55,129 @@ class OrchestratorModule { 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(), 500); + } // Heartbeat watchdog every 3 seconds this._heartbeatInterval = setInterval(() => this._checkHeartbeats(), 3000); - this.terminal.print('🧠 Orchestrator loaded — ready to delegate', 'success'); + // Idle timeout sweep every 30s + setInterval(() => this._sweepIdle(), 30000); + this.terminal.print('🧠 Orchestrator loaded — A2A mailbox transport active', 'success'); + } + + // ─── A2A Mailbox Transport ─── + + sendA2AMessage(args) { + if (args.length < 2) return this.terminal.print('Usage: a2a-send <agent-id> <json-message>', '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'); + } + + 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 ─── + spawnAgent(role, capabilities) { const id = `agent-${Object.keys(this._agents).length + 1}-${role || 'worker'}`; if (Object.keys(this._agents).length >= this._SPAWN_LIMIT) { @@ -39,15 +191,24 @@ class OrchestratorModule { createdAt: new Date().toISOString(), lastHeartbeat: Date.now(), status: 'idle', - currentTask: null + 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 @@ -60,31 +221,212 @@ class OrchestratorModule { // 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 <agent-id> <on-failure|always|never>', '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(); + 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'); + 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 <key> <value>', '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(), 500); + } else if (!this._A2A_POLLING && this._a2aPollTimer) { + clearInterval(this._a2aPollTimer); + this._a2aPollTimer = null; + } + return `a2a-polling ${this._A2A_POLLING ? 'on' : 'off'}`; + } + }; + + 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'); + } + + // ─── Task Delegation ─── + async delegateTask(task) { if (!task) return this.terminal.print('Usage: orchestrate <task description>', 'error'); - // Find an idle agent or auto-spawn + + // 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) { - return this.terminal.print('❌ All agents busy and spawn limit reached', 'error'); + // 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'); } - 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'); - // A2A Contract handshake — validate task against agent's contract + // 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'); - this.terminal.print(` Contract for '${agent.role}' requires: ${(registry.getContract(agent.role) || {}).input_schema || 'free-form'}`, 'warning'); - // Offer to proceed anyway (permissive) — user can create a proper contract contractValid = false; } else if (result.reason === 'matched') { this.terminal.print(`✅ A2A contract matched for role '${agent.role}'`, 'success'); @@ -94,52 +436,88 @@ class OrchestratorModule { // 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'); + 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: result || 'completed', + 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) { - agent.status = 'failed'; - this.terminal.print(`❌ ${agent.id} failed: ${e.message}`, 'error'); + 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(); } - // Return to idle pool - 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 > 15000) { + 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; @@ -151,17 +529,45 @@ class OrchestratorModule { }); 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 failed = Object.values(this._agents).filter(a => a.status === 'failed').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, ${failed} failed)`, 'info'); - this.terminal.print(` Spawn limit: ${this._SPAWN_LIMIT}`, 'info'); + 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)})` : ''; - this.terminal.print(` ${a.id} — ${a.status}${task}`, 'info'); + 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; }
\ No newline at end of file diff --git a/modules/core/module-predictor.js b/modules/core/module-predictor.js new file mode 100644 index 0000000..312325b --- /dev/null +++ b/modules/core/module-predictor.js @@ -0,0 +1,279 @@ +// module-predictor.js — Predictive resource allocation engine +// Analyzes historical telemetry + memory graph data to predict task duration/turns +// Commands: predict, predictor-status, predictor-config +class PredictorModule { + constructor() { + this.commands = {}; + this.terminal = null; + this._dataPoints = []; + this._predictions = 0; + this._accuracy = { correct: 0, total: 0 }; + this._CONFIDENCE_THRESHOLD = 0.5; + this._MIN_DATA_POINTS = 5; + this._loaded = false; + } + + getHelpInfo() { + return { + 'predict': 'predict <task description> — Predict task duration, tool turns, and confidence', + 'predictor-status': 'predictor-status — Model accuracy, data points, prediction count', + 'predictor-config': 'predictor-config <key> <value> — Tune: confidence-threshold, min-data-points', + }; + } + + registerCommands() { + this.commands['predict'] = (args) => this.predict(args.join(' ')); + this.commands['predictor-status'] = () => this.showStatus(); + this.commands['predictor-config'] = (args) => this.setConfig(args); + } + + init(terminal) { + this.terminal = terminal; + this._loadFromVFS(); + this._loaded = true; + this.terminal.print('📊 Predictor loaded — analyzing task patterns', 'info'); + } + + // ─── Prediction Engine ─── + + async predict(task) { + if (!task) return this.terminal.print('Usage: predict <task description>', 'error'); + + const startTime = performance.now(); + + // Extract features + const features = this._extractFeatures(task); + + // Find similar historical tasks + const similar = this._findSimilar(features); + + // Generate prediction + const prediction = this._computePrediction(similar, features); + + // Record this prediction for accuracy tracking + const duration = ((performance.now() - startTime) / 1000).toFixed(2); + + this._predictions++; + this._dataPoints.push({ + features, + predictedDuration: prediction.duration, + predictedTurns: prediction.toolTurns, + confidence: prediction.confidence, + similarCount: similar.length, + timestamp: new Date().toISOString() + }); + + this._debounceSave(); + + // Display result + this.terminal.print('📈 Task Prediction', 'header'); + this.terminal.print(` Task: ${task.substring(0, 50)}${task.length > 50 ? '...' : ''}`, 'info'); + + if (prediction.confidence >= this._CONFIDENCE_THRESHOLD) { + this.terminal.print(` ⏱ Estimated duration: ${prediction.duration}s`, 'info'); + this.terminal.print(` 🔧 Estimated tool turns: ${prediction.toolTurns}`, 'info'); + this.terminal.print(` 📊 Confidence: ${(prediction.confidence * 100).toFixed(0)}% (based on ${similar.length} similar tasks)`, 'success'); + } else if (similar.length > 0) { + this.terminal.print(` ⏱ Estimated duration: ${prediction.duration}s (low confidence)`, 'warning'); + this.terminal.print(` 🔧 Tool turns: ~${prediction.toolTurns}`, 'warning'); + this.terminal.print(` 📊 Confidence: ${(prediction.confidence * 100).toFixed(0)}% — only ${similar.length} similar data point(s)`, 'warning'); + this.terminal.print(' Tip: More task data will improve predictions.', 'info'); + } else { + this.terminal.print(' ❓ No historical data to base prediction on.', 'warning'); + this.terminal.print(' Tip: Run `orchestrate` with various tasks to build a baseline.', 'info'); + // Provide hardcoded defaults + this.terminal.print(' Default estimate: ~30s duration, ~5 tool turns', 'info'); + } + + // If got memory graph, offer recommendations + if (prediction.confidence >= this._CONFIDENCE_THRESHOLD && prediction.duration > 30) { + this.terminal.print(` 💡 Recommendation: Set TTL to ${Math.ceil(prediction.duration * 1.5)}s for predictable completion`, 'info'); + } + + return prediction; + } + + _extractFeatures(task) { + const lower = task.toLowerCase(); + return { + length: task.length, + wordCount: task.split(/\s+/).length, + hasSearch: /\b(search|find|look)\b/.test(lower), + hasWrite: /\b(write|create|save|edit)\b/.test(lower), + hasDelete: /\b(delete|remove|rm)\b/.test(lower), + hasInstall: /\b(install|wpm|package)\b/.test(lower), + hasRead: /\b(read|cat|view|list)\b/.test(lower), + hasWeb: /\b(fetch|url|http|web)\b/.test(lower), + hasOrchestrate: /\b(orchestrat|delegate|agent)\b/.test(lower), + hasModule: /\b(module|edit_module|view_module)\b/.test(lower), + complexity: Math.min(task.length / 50, 10), // 0-10 scale + keywords: [...new Set(lower.split(/\s+/).filter(w => w.length > 3))].slice(0, 10) + }; + } + + _findSimilar(features) { + return this._dataPoints.filter(dp => { + // Skip predictions (only use actual executions with observed results) + if (!dp.observedDuration) return false; + const f = dp.features; + if (!f) return false; + // Feature similarity scoring + let score = 0; + const matches = [ + features.hasSearch === f.hasSearch, + features.hasWrite === f.hasWrite, + features.hasDelete === f.hasDelete, + features.hasInstall === f.hasInstall, + features.hasRead === f.hasRead, + features.hasWeb === f.hasWeb, + features.hasOrchestrate === f.hasOrchestrate, + features.hasModule === f.hasModule, + ]; + score = matches.filter(Boolean).length / matches.length; + // Keyword overlap bonus + const kOverlap = features.keywords.filter(k => f.keywords?.includes(k)).length; + const kScore = kOverlap / Math.max(features.keywords.length, 1); + score = score * 0.6 + kScore * 0.4; + return score > 0.3; + }); + } + + _computePrediction(similar, features) { + if (similar.length === 0) { + // Hardcoded defaults + let baseDuration = 15; + let baseTurns = 3; + if (features.hasSearch || features.hasWeb) { baseDuration = 25; baseTurns = 5; } + if (features.hasWrite) { baseDuration = 20; baseTurns = 4; } + if (features.hasOrchestrate) { baseDuration = 40; baseTurns = 8; } + if (features.complexity > 5) baseDuration *= 1.5; + return { + duration: baseDuration, + toolTurns: baseTurns, + confidence: 0.1, + similarCount: 0 + }; + } + + const avgDuration = similar.reduce((s, dp) => s + (dp.observedDuration || 30), 0) / similar.length; + const avgTurns = similar.reduce((s, dp) => s + (dp.observedTurns || 3), 0) / similar.length; + const confidence = Math.min(similar.length / 20, 1) * 0.8 + 0.1; + + // Apply task complexity multiplier + const complexityMultiplier = 1 + (features.complexity - 3) * 0.1; + const adjustedDuration = Math.round(avgDuration * Math.max(complexityMultiplier, 0.5)); + + return { + duration: adjustedDuration, + toolTurns: Math.round(avgTurns), + confidence: Math.min(confidence, 0.95), + similarCount: similar.length + }; + } + + // ─── Learning from Results ─── + + recordResult(features, duration, toolTurns, success) { + this._dataPoints.push({ + features, + observedDuration: duration, + observedTurns: toolTurns, + success, + timestamp: new Date().toISOString() + }); + this._accuracy.total++; + // Compare with last prediction for this feature set + const lastPrediction = [...this._dataPoints] + .reverse() + .find(dp => dp.predictedDuration && dp.features?.hasSearch === features.hasSearch); + if (lastPrediction) { + const error = Math.abs(lastPrediction.predictedDuration - duration) / duration; + if (error < 0.3) this._accuracy.correct++; + } + this._debounceSave(); + } + + // ─── Commands ─── + + showStatus() { + const actualExecutions = this._dataPoints.filter(dp => dp.observedDuration).length; + const accPct = this._accuracy.total > 0 + ? ((this._accuracy.correct / this._accuracy.total) * 100).toFixed(0) + : 'N/A'; + + this.terminal.print('📊 Predictor Status', 'header'); + this.terminal.print(` Total data points: ${this._dataPoints.length}`, 'info'); + this.terminal.print(` Actual executions: ${actualExecutions}`, 'info'); + this.terminal.print(` Predictions made: ${this._predictions}`, 'info'); + this.terminal.print(` Accuracy: ${accPct}% (${this._accuracy.correct}/${this._accuracy.total})`, 'info'); + this.terminal.print(` Confidence threshold: ${this._CONFIDENCE_THRESHOLD}`, 'info'); + this.terminal.print(` Min data points: ${this._MIN_DATA_POINTS}`, 'info'); + } + + setConfig(args) { + if (args.length < 2) return this.terminal.print('Usage: predictor-config <key> <value>', 'error'); + const key = args[0]; + const val = args[1]; + + const configMap = { + 'confidence-threshold': () => { + const n = parseFloat(val); + if (isNaN(n) || n < 0 || n > 1) return 'Must be 0.0-1.0'; + this._CONFIDENCE_THRESHOLD = n; + return `confidence-threshold set to ${n}`; + }, + 'min-data-points': () => { + const n = parseInt(val); + if (isNaN(n) || n < 1) return 'Must be >= 1'; + this._MIN_DATA_POINTS = n; + return `min-data-points set to ${n}`; + }, + }; + + if (!configMap[key]) return this.terminal.print(`Unknown: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error'); + const msg = configMap[key](); + this.terminal.print(`✅ ${msg}`, 'success'); + } + + // ─── Persistence ─── + + _loadFromVFS() { + try { + const fs = this.terminal.fs; + if (!fs) return; + const data = fs.fsReadJSON('/system/metrics/predictor.json'); + if (data) { + this._dataPoints = data.dataPoints || []; + this._predictions = data.predictions || 0; + this._accuracy = data.accuracy || { correct: 0, total: 0 }; + } + } catch (e) { + console.warn('[Predictor] Load error (fresh start):', e.message); + } + } + + _saveNow() { + try { + const fs = this.terminal.fs; + if (!fs) return; + if (!fs.readDir('/system/metrics')) fs.mkdirRecursive('/system/metrics'); + fs.fsWriteJSON('/system/metrics/predictor.json', { + dataPoints: this._dataPoints.slice(-500), // keep last 500 + predictions: this._predictions, + accuracy: this._accuracy + }); + } catch (e) { + console.error('[Predictor] Save error:', e.message); + } + } + + _debounceSave() { + clearTimeout(this._saveTimer); + this._saveTimer = setTimeout(() => this._saveNow(), 3000); + } +} + +// Required for module system +if (typeof window !== 'undefined') { + window.PredictorModule = PredictorModule; +}
\ No newline at end of file diff --git a/modules/core/module-sandbox.js b/modules/core/module-sandbox.js new file mode 100644 index 0000000..dc8dc76 --- /dev/null +++ b/modules/core/module-sandbox.js @@ -0,0 +1,332 @@ +// module-sandbox.js — Sandboxed execution pool using WebWorkers +// Commands: sandbox-status, sandbox-config, sandbox-run +class SandboxModule { + constructor() { + this.commands = {}; + this.terminal = null; + this.workers = []; + this.MAX_WORKERS = 4; + this.MIN_WORKERS = 1; + this.queue = []; + this.activeTasks = {}; + this._nextTaskId = 0; + this._totalRuns = 0; + this._totalErrors = 0; + this._totalDuration = 0; + this._fallbackMode = false; // true if WebWorkers unavailable + this._workerReady = {}; + } + + getHelpInfo() { + return { + 'sandbox-status': 'sandbox-status — Show sandbox pool health and task metrics', + 'sandbox-config': 'sandbox-config <key> <value> — Tune: max-workers, min-workers, pool-timeout', + 'sandbox-run': 'sandbox-run <task> — Run a task in the sandbox (automatic routing)', + }; + } + + registerCommands() { + this.commands['sandbox-status'] = () => this.showStatus(); + this.commands['sandbox-config'] = (args) => this.setConfig(args); + this.commands['sandbox-run'] = (args) => this.runInSandbox(args.join(' ')); + } + + init(terminal) { + this.terminal = terminal; + this.workerUrl = '/spaces/newos4/modules/core/sandbox-worker.js'; + + // Detect Worker availability + if (typeof Worker === 'undefined') { + this._fallbackMode = true; + this.terminal.print('⚠️ WebWorkers not available — sandbox using inline fallback', 'warning'); + } else { + this._initPool(); + } + + this.terminal.print('🧪 Sandbox module loaded', 'info'); + } + + _initPool() { + // Start with MIN_WORKERS, grow on demand up to MAX_WORKERS + for (let i = 0; i < this.MIN_WORKERS; i++) { + this._spawnWorker(); + } + } + + _spawnWorker() { + if (this.workers.length >= this.MAX_WORKERS) return null; + try { + const worker = new Worker(this.workerUrl); + const id = `w${this.workers.length + 1}`; + worker._sandboxId = id; + worker._busy = false; + + worker.onmessage = (e) => this._handleResult(e.data, id); + worker.onerror = (e) => { + console.error(`[Sandbox] Worker ${id} error:`, e.message); + this._totalErrors++; + this._replaceWorker(id); + }; + + this.workers.push(worker); + this._workerReady[id] = true; + return worker; + } catch (e) { + console.error('[Sandbox] Failed to spawn worker:', e); + this._fallbackMode = true; + return null; + } + } + + _replaceWorker(id) { + const idx = this.workers.findIndex(w => w._sandboxId === id); + if (idx !== -1) { + this.workers[idx].terminate(); + this.workers.splice(idx, 1); + delete this._workerReady[id]; + } + // Re-spawn after a delay + setTimeout(() => this._spawnWorker(), 1000); + } + + _getIdleWorker() { + for (const w of this.workers) { + if (!w._busy) return w; + } + // Grow pool if under max + if (this.workers.length < this.MAX_WORKERS) { + const w = this._spawnWorker(); + if (w) return w; + } + return null; + } + + async runInSandbox(task, capabilities) { + if (!task) return this.terminal.print('Usage: sandbox-run <task>', 'error'); + + if (this._fallbackMode) { + return this._runInline(task, capabilities); + } + + const worker = this._getIdleWorker(); + if (!worker) { + // Queue it + return new Promise((resolve) => { + this.queue.push({ task, capabilities, resolve }); + this.terminal.print(`⏳ Task queued (${this.queue.length} waiting)`, 'info'); + this._drainQueue(); + }); + } + + return this._executeOnWorker(worker, task, capabilities); + } + + async _executeOnWorker(worker, task, capabilities) { + const taskId = `task-${++this._nextTaskId}`; + worker._busy = true; + + const startTime = performance.now(); + + return new Promise((resolve) => { + const handler = (e) => { + if (e.data && e.data.taskId === taskId) { + worker.removeEventListener('message', handler); + worker._busy = false; + + const duration = ((performance.now() - startTime) / 1000).toFixed(2); + this._totalRuns++; + this._totalDuration += parseFloat(duration); + + if (e.data.error) { + this._totalErrors++; + this.terminal.print(`❌ Sandbox task failed: ${e.data.error}`, 'error'); + resolve({ error: e.data.error, duration }); + } else { + this.terminal.print(`✅ Sandbox task complete (${duration}s, ${e.data.toolCalls || 0} tools)`, 'success'); + resolve({ result: e.data.result, duration, toolCalls: e.data.toolCalls || 0 }); + } + + this._drainQueue(); + } + }; + + worker.addEventListener('message', handler); + + // Get VFS snapshot from terminal + let vfsSnapshot = null; + try { + if (this.terminal && this.terminal.fs && this.terminal.fs.root) { + vfsSnapshot = JSON.stringify({ + root: this.terminal.fs.root, + currentPath: this.terminal.fs.currentPath || [] + }); + } + } catch (e) { + console.warn('[Sandbox] Could not serialize VFS:', e.message); + } + + const caps = capabilities || ['list_files', 'read_file']; + const allowedCommands = ['ls', 'cat', 'echo', 'find', 'grep', 'help', 'status', + 'touch', 'mkdir', 'rm', 'cp', 'mv']; + + worker.postMessage({ + taskId, + task, + capabilities: caps, + vfsSnapshot, + allowedCommands, + apiProxy: window.location.origin + '/api' + }); + + // Timeout after 30 seconds + setTimeout(() => { + if (worker._busy) { + worker._busy = false; + this._totalErrors++; + this.terminal.print(`⏰ Sandbox task ${taskId} timed out after 30s`, 'error'); + resolve({ error: 'Timeout (30s)', duration: 30 }); + this._drainQueue(); + } + }, 30000); + }); + } + + _handleResult(data, workerId) { + // Handled via promise in _executeOnWorker + // This is the fallback handler for tasks not tracked by promise + if (data.error) { + this.terminal.print(`[Sandbox ${workerId}] Error: ${data.error}`, 'error'); + } + } + + _drainQueue() { + if (this.queue.length === 0) return; + const next = this.queue.shift(); + this.runInSandbox(next.task, next.capabilities).then(result => { + next.resolve(result); + }); + } + + _runInline(task, capabilities) { + // Fallback when WebWorkers unavailable — runs in main thread + const startTime = performance.now(); + const parts = task.trim().split(/\s+/); + const cmd = parts[0].toLowerCase(); + let result = ''; + let toolCalls = 0; + + const fs = this.terminal ? this.terminal.fs : null; + + if (cmd === 'ls' && fs) { + const path = parts[1] || '/'; + const entries = fs.ls(path); + if (!entries) return this.terminal.print(`ls: ${path}: No such directory`, 'error'); + result = entries.map(e => `${e.type === 'dir' ? '[dir]' : '[file]'} ${e.name}`).join('\\n'); + toolCalls++; + } else if ((cmd === 'cat' || cmd === 'read') && fs) { + const path = parts[1] || ''; + const content = fs.readFile(path); + if (content === null) return this.terminal.print(`cat: ${path}: No such file`, 'error'); + result = typeof content === 'string' ? content.substring(0, 8000) : String(content); + toolCalls++; + } else if (cmd === 'find' && fs) { + const q = parts.slice(1).join(' ') || ''; + // Walk VFS + const results = []; + const walk = (p) => { + const entries = fs.ls(p); + if (!entries) return; + for (const e of entries) { + const full = p === '/' ? '/' + e.name : p + '/' + e.name; + if (e.type === 'file' && (!q || full.toLowerCase().includes(q.toLowerCase()))) { + results.push(full); + } + if (e.type === 'dir') walk(full); + } + }; + walk('/'); + result = results.join('\\n') || '(empty)'; + toolCalls++; + } else if (cmd === 'status') { + let f = 0, d = 0; + const walk = (p) => { + const entries = fs ? fs.ls(p) : []; + if (!entries) return; + for (const e of entries) { + if (e.type === 'file') f++; + else { d++; walk(p === '/' ? '/' + e.name : p + '/' + e.name); } + } + }; + walk('/'); + result = `Files: ${f}, Dirs: ${d}`; + toolCalls++; + } else { + result = `Task: "${task}" (inline fallback)`; + } + + const duration = ((performance.now() - startTime) / 1000).toFixed(2); + this._totalRuns++; + this._totalDuration += parseFloat(duration); + this.terminal.print(result, 'info'); + return { result, duration, toolCalls }; + } + + // ─── Config ─── + + setConfig(args) { + if (args.length < 2) return this.terminal.print('Usage: sandbox-config <key> <value>', 'error'); + const key = args[0]; + const val = args[1]; + + const configMap = { + 'max-workers': () => { + const n = parseInt(val); + if (isNaN(n) || n < 1 || n > 8) return 'Must be 1-8'; + this.MAX_WORKERS = n; + // Scale down if needed + while (this.workers.length > n) { + const w = this.workers.pop(); + w.terminate(); + } + // Scale up + for (let i = this.workers.length; i < Math.min(n, this.MIN_WORKERS); i++) { + this._spawnWorker(); + } + return `max-workers set to ${n}`; + }, + 'min-workers': () => { + const n = parseInt(val); + if (isNaN(n) || n < 0 || n > this.MAX_WORKERS) return `Must be 0-${this.MAX_WORKERS}`; + this.MIN_WORKERS = n; + while (this.workers.length < n) this._spawnWorker(); + return `min-workers set to ${n}`; + }, + }; + + if (!configMap[key]) { + return this.terminal.print(`Unknown: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error'); + } + const msg = configMap[key](); + this.terminal.print(`✅ ${msg}`, 'success'); + } + + // ─── Status ─── + + showStatus() { + const busy = this.workers.filter(w => w._busy).length; + const idle = this.workers.length - busy; + const avgDuration = this._totalRuns > 0 ? (this._totalDuration / this._totalRuns).toFixed(2) : 'N/A'; + + this.terminal.print('🧪 Sandbox Pool', 'header'); + this.terminal.print(` Workers: ${this.workers.length}/${this.MAX_WORKERS} (${busy} busy, ${idle} idle)`, 'info'); + this.terminal.print(` Mode: ${this._fallbackMode ? '⚠️ Inline fallback' : '✅ WebWorker'}`, 'info'); + this.terminal.print(` Queue: ${this.queue.length} pending`, 'info'); + this.terminal.print(` Total runs: ${this._totalRuns} (errors: ${this._totalErrors})`, 'info'); + this.terminal.print(` Avg duration: ${avgDuration}s`, 'info'); + } +} + +// Required for module system +if (typeof window !== 'undefined') { + window.SandboxModule = SandboxModule; +}
\ No newline at end of file diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js new file mode 100644 index 0000000..b7bb28f --- /dev/null +++ b/modules/core/module-world-model.js @@ -0,0 +1,381 @@ +// module-world-model.js — Global World Model: system self-awareness synthesis engine +// Aggregates from memory graph, neural VFS, skills index, telemetry +// Commands: world-model, world-model-summary, world-model-ask, world-model-refresh, world-model-config +class WorldModelModule { + constructor() { + this.commands = {}; + this.terminal = null; + this._model = null; + this._lastRefresh = null; + this._REFRESH_INTERVAL = 300000; // 5 minutes + this._MAX_PROJECTS = 10; + this._refreshTimer = null; + } + + getHelpInfo() { + return { + 'world-model': 'world-model — Show full world model as structured output', + 'world-model-summary': 'world-model-summary — One-line system self-awareness statement', + 'world-model-ask': 'world-model-ask <question> — Query the model (e.g. "what changed since yesterday")', + 'world-model-refresh': 'world-model-refresh — Force re-synthesis from all subsystems', + 'world-model-config': 'world-model-config <key> <value> — Tune: refresh-interval, max-projects', + }; + } + + registerCommands() { + this.commands['world-model'] = () => this.showModel(); + this.commands['world-model-summary'] = () => this.showSummary(); + this.commands['world-model-ask'] = (args) => this.ask(args.join(' ')); + this.commands['world-model-refresh'] = () => this.refresh(); + this.commands['world-model-config'] = (args) => this.setConfig(args); + } + + init(terminal) { + this.terminal = terminal; + this._loadFromVFS(); + // Auto-refresh on a timer + this._refreshTimer = setInterval(() => { + const age = Date.now() - (this._lastRefresh ? new Date(this._lastRefresh).getTime() : 0); + if (age > this._REFRESH_INTERVAL) this._synthesize(); + }, this._REFRESH_INTERVAL); + // Initial synthesis + this._synthesize(); + this.terminal.print('🌐 World Model loaded — system self-awareness active', 'info'); + } + + // ─── Synthesis Engine ─── + + _synthesize() { + try { + const fs = this.terminal.fs; + if (!fs) return; + + const model = { + version: 'v4.4', + synthesized: new Date().toISOString(), + system: this._gatherSystemInfo(fs), + modules: this._gatherModuleInfo(), + files: this._gatherFileInfo(fs), + skills: this._gatherSkillInfo(fs), + memory: this._gatherMemoryInfo(), + agents: this._gatherAgentInfo(), + telemetry: this._gatherTelemetry(fs), + projects: this._gatherProjects(fs), + recent: this._gatherRecent(fs) + }; + + this._model = model; + this._lastRefresh = model.synthesized; + this._saveToVFS(fs); + + // Log synthesis summary + const mCount = model.modules?.loaded || '?'; + const sCount = model.skills?.count || '?'; + const fCount = model.files?.total || '?'; + console.log(`[WorldModel] Synthesis complete: ${mCount} modules, ${sCount} skills, ${fCount} files`); + } catch (e) { + console.error('[WorldModel] Synthesis error:', e.message); + } + } + + _gatherSystemInfo(fs) { + const healthData = fs.fsReadJSON('/system/metrics/health.json'); + return { + version: 'newOS4 v4.4', + modules: Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0, + vfsSize: JSON.stringify(fs?.root || {}).length, + online: typeof navigator !== 'undefined' ? navigator.onLine : true, + memory: typeof performance !== 'undefined' && performance.memory + ? `${(performance.memory.usedJSHeapSize / 1048576).toFixed(1)}MB` + : 'N/A', + health: healthData?.status || 'OK', + lastHealthCheck: healthData?.timestamp || null + }; + } + + _gatherModuleInfo() { + const mm = this.terminal?.moduleManager; + if (!mm) return { loaded: 0, modified: [] }; + const modules = Object.keys(mm.modules || {}).map(name => { + const mod = mm.modules[name]; + const clsName = mod?.constructor?.name || name; + return { name: clsName, hasCommands: !!mod?.commands }; + }); + // Check for local modifications + const modsDir = this.terminal.fs?.readDir('/_mods/'); + const modified = modsDir + ? modsDir.filter(e => e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js')) + .map(e => e.name.replace('.js', '')) + : []; + return { + loaded: modules.length, + total: 49, + modules, + modified, + modifiedCount: modified.length + }; + } + + _gatherFileInfo(fs) { + if (!fs) return { total: 0, size: '0KB' }; + let fileCount = 0, dirCount = 0; + const walk = (path) => { + const entries = fs.ls(path); + if (!entries) return; + for (const e of entries) { + if (e.type === 'file') fileCount++; + else { dirCount++; walk(path === '/' ? '/' + e.name : path + '/' + e.name); } + } + }; + walk('/'); + return { + total: fileCount, + directories: dirCount, + size: `${(JSON.stringify(fs.root).length / 1024).toFixed(1)}KB` + }; + } + + _gatherSkillInfo(fs) { + try { + const idx = fs.fsReadJSON('/skills/index.json'); + const count = idx ? Object.keys(idx).length : 0; + return { count, skills: idx ? Object.keys(idx) : [] }; + } catch (e) { + return { count: 0, skills: [] }; + } + } + + _gatherMemoryInfo() { + const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; + if (mg && mg._nodes) { + const nodes = Object.keys(mg._nodes).length; + const edges = (mg._edges || []).length; + return { nodes, edges, active: true }; + } + // Fallback: count memory files + try { + const memIndex = this.terminal.fs?.fsReadJSON('/memory/index.json'); + return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false }; + } catch (e) { + return { nodes: 0, edges: 0, active: false }; + } + } + + _gatherAgentInfo() { + const orch = this.terminal?.moduleManager?.modules?.['OrchestratorModule']; + if (!orch || !orch._agents) return { agents: 0, active: 0 }; + const agents = Object.keys(orch._agents).length; + const active = Object.values(orch._agents).filter(a => a.status === 'busy').length; + return { agents, active, totalLeases: Object.keys(orch._a2aMessages || {}).length }; + } + + _gatherTelemetry(fs) { + try { + const latency = fs.fsReadJSON('/system/metrics/tool-latency.json'); + const errors = fs.fsReadJSON('/system/metrics/error-log.json'); + return { + toolLatencyDataPoints: latency ? Object.keys(latency).length : 0, + recentErrors: errors ? (Array.isArray(errors) ? errors.length : Object.keys(errors).length) : 0, + }; + } catch (e) { + return { toolLatencyDataPoints: 0, recentErrors: 0 }; + } + } + + _gatherProjects(fs) { + try { + const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule']; + if (mg && mg._nodes) { + // Find task clusters as "projects" + const tasks = Object.values(mg._nodes).filter(n => n.type === 'task'); + const recentFiles = Object.values(mg._nodes) + .filter(n => n.type === 'file') + .sort((a, b) => b.access_count - a.access_count) + .slice(0, this._MAX_PROJECTS); + return { + totalTasks: tasks.length, + activeProjects: recentFiles.slice(0, 5).map(f => ({ + name: f.label, + accesses: f.access_count, + lastModified: f.updated + })) + }; + } + return { totalTasks: 0, activeProjects: [] }; + } catch (e) { + return { totalTasks: 0, activeProjects: [] }; + } + } + + _gatherRecent(fs) { + const recent = []; + try { + // Check modified modules + const modsDir = fs.readDir('/_mods/'); + if (modsDir) { + for (const e of modsDir) { + if (e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js') && !e.name.endsWith('.meta.json')) { + recent.push(`Modified module: ${e.name.replace('.js', '')}`); + } + } + } + // Check skills activity + const skills = fs.ls('/skills/'); + if (skills && skills.length > 0) { + recent.push(`Skills directory: ${skills.filter(s => s.type === 'file').length} files`); + } + } catch (e) {} + return recent; + } + + // ─── Commands ─── + + showModel() { + if (!this._model) return this.terminal.print('No model yet — run world-model-refresh', 'warning'); + const m = this._model; + + this.terminal.print('🌐 World Model', 'header'); + this.terminal.print(` Version: ${m.version} (last synthsized: ${new Date(m.synthesized).toLocaleTimeString()})`, 'info'); + + this.terminal.print(` ⚙️ System — ${m.system.modules} modules loaded, ${m.system.vfsSize} VFS, ${m.system.memory}`, 'info'); + this.terminal.print(` 🧩 Modules — ${m.modules.loaded} loaded (${m.modules.modifiedCount} modified)`, 'info'); + this.terminal.print(` 📁 Files — ${m.files.total} files, ${m.files.directories} dirs, ${m.files.size}`, 'info'); + this.terminal.print(` 📚 Skills — ${m.skills.count} installed`, 'info'); + this.terminal.print(` 🧠 Memory Graph — ${m.memory.nodes} nodes, ${m.memory.edges} edges${m.memory.active ? ' ✅' : ''}`, 'info'); + this.terminal.print(` 🤖 Agents — ${m.agents.agents} registered (${m.agents.active} active)`, 'info'); + this.terminal.print(` 📊 Telemetry — ${m.telemetry.toolLatencyDataPoints} tool latency points, ${m.telemetry.recentErrors} recent errors`, 'info'); + + if (m.projects.activeProjects.length > 0) { + this.terminal.print(' Active context:', 'header'); + for (const p of m.projects.activeProjects.slice(0, 3)) { + this.terminal.print(` 📌 ${p.name} (${p.accesses}x accesses)`, 'info'); + } + } + + if (m.recent.length > 0) { + this.terminal.print(' Recent changes:', 'header'); + for (const r of m.recent.slice(0, 3)) { + this.terminal.print(` ${r}`, 'info'); + } + } + } + + showSummary() { + if (!this._model) return this.terminal.print('No model yet', 'warning'); + const m = this._model; + const summary = `newOS4 ${m.version} · ${m.modules.loaded}/${m.modules.total} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents · ${m.memory.nodes} graph nodes · ${m.telemetry.recentErrors > 0 ? `⚠️ ${m.telemetry.recentErrors} errors` : '✅ OK'}`; + this.terminal.print(summary, 'info'); + return summary; + } + + ask(question) { + if (!question) return this.terminal.print('Usage: world-model-ask <question>', 'error'); + if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning'); + + const q = question.toLowerCase(); + const m = this._model; + let answer = ''; + + if (q.includes('module') || q.includes('loaded')) { + answer = `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`; + } else if (q.includes('skill')) { + answer = `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`; + } else if (q.includes('file') || q.includes('vfs')) { + answer = `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`; + } else if (q.includes('agent') || q.includes('orchestrat')) { + answer = `${m.agents.agents} agents (${m.agents.active} active)`; + } else if (q.includes('health') || q.includes('status')) { + answer = `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`; + } else if (q.includes('memory') || q.includes('graph')) { + answer = `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`; + } else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) { + const recent = m.recent; + answer = recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected'; + } else if (q.includes('project') || q.includes('work')) { + const projects = m.projects.activeProjects; + answer = projects.length > 0 + ? `Active projects: ${projects.map(p => `${p.name} (${p.accesses}x)`).join(', ')}` + : 'No active projects detected'; + } else if (q.includes('telemetry') || q.includes('latency')) { + answer = `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked`; + } else if (q.includes('summary') || q.includes('overview') || q.includes('all')) { + answer = this.showSummary(); + return; + } else { + answer = `I'm not sure how to answer "${question}". Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`; + } + + this.terminal.print(`❓ ${question}`, 'info'); + this.terminal.print(`💡 ${answer}`, 'success'); + } + + refresh() { + this._synthesize(); + this.terminal.print(`✅ World Model refreshed at ${new Date().toLocaleTimeString()}`, 'success'); + } + + setConfig(args) { + if (args.length < 2) return this.terminal.print('Usage: world-model-config <key> <value>', 'error'); + const key = args[0]; + const val = args[1]; + + const configMap = { + 'refresh-interval': () => { + const n = parseInt(val) * 1000; + if (isNaN(n) || n < 10000) return 'Must be >= 10 seconds'; + this._REFRESH_INTERVAL = n; + if (this._refreshTimer) clearInterval(this._refreshTimer); + this._refreshTimer = setInterval(() => this._synthesize(), n); + return `refresh-interval set to ${val}s`; + }, + 'max-projects': () => { + const n = parseInt(val); + if (isNaN(n) || n < 1) return 'Must be >= 1'; + this._MAX_PROJECTS = n; + return `max-projects set to ${n}`; + }, + }; + + if (!configMap[key]) return this.terminal.print(`Unknown: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error'); + const msg = configMap[key](); + this.terminal.print(`✅ ${msg}`, 'success'); + } + + // ─── Context Injection ─── + + getContextForPrompt() { + if (!this._model) return ''; + const m = this._model; + return `[World Model Context]\\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`; + } + + // ─── Persistence ─── + + _loadFromVFS() { + try { + const fs = this.terminal.fs; + if (!fs) return; + const data = fs.fsReadJSON('/system/world-model.json'); + if (data) { + this._model = data; + this._lastRefresh = data.synthesized; + } + } catch (e) { + console.warn('[WorldModel] Load error (fresh start):', e.message); + } + } + + _saveToVFS(fs) { + try { + if (!fs.readDir('/system')) fs.mkdir('system'); + fs.fsWriteJSON('/system/world-model.json', this._model); + } catch (e) { + console.error('[WorldModel] Save error:', e.message); + } + } +} + +// Required for module system +if (typeof window !== 'undefined') { + window.WorldModelModule = WorldModelModule; +}
\ No newline at end of file diff --git a/modules/core/sandbox-worker.js b/modules/core/sandbox-worker.js new file mode 100644 index 0000000..5841582 --- /dev/null +++ b/modules/core/sandbox-worker.js @@ -0,0 +1,205 @@ +// sandbox-worker.js — WebWorker entry point for isolated agent execution +// Receives: { taskId, task, capabilities, vfsSnapshot, allowedCommands, apiProxy } +// Returns: { taskId, result, error, toolCalls, duration } + +let _vfs = null; +let _apiProxyOrigin = null; + +self.onmessage = async (e) => { + const { taskId, task, capabilities, vfsSnapshot, allowedCommands, apiProxy } = e.data; + _apiProxyOrigin = apiProxy ? new URL(apiProxy).origin : null; + + const startTime = performance.now(); + let toolCalls = 0; + let result = ''; + let error = null; + + try { + // Rebuild VFS from snapshot + _vfs = vfsSnapshot ? JSON.parse(vfsSnapshot) : { root: { type: 'dir', content: {} }, currentPath: [] }; + + // Log start + console.log(`[Sandbox ${taskId}] Starting task: "${task.substring(0, 60)}"`); + + // Parse the task for tool use + const toolResults = {}; + const commandWhitelist = allowedCommands || ['ls', 'cat', 'echo', 'find', 'grep', 'help', 'status']; + + // Execute basic commands + const parts = task.trim().split(/\s+/); + const cmd = parts[0].toLowerCase(); + + if (cmd === 'ls' || cmd === 'list') { + result = sandbox_ls(parts[1] || '/'); + toolCalls++; + } else if (cmd === 'cat' || cmd === 'read') { + result = sandbox_cat(parts[1] || ''); + toolCalls++; + } else if (cmd === 'find' || cmd === 'search') { + result = sandbox_find(parts.slice(1).join(' ') || ''); + toolCalls++; + } else if (cmd === 'grep') { + result = sandbox_grep(parts.slice(1).join(' ')); + toolCalls++; + } else if (cmd === 'echo') { + result = parts.slice(1).join(' '); + toolCalls++; + } else if (cmd === 'help') { + result = 'Available commands: ' + commandWhitelist.join(', '); + toolCalls++; + } else if (cmd === 'status') { + result = sandbox_status(); + toolCalls++; + } else if (capabilities.includes('run_command') && commandWhitelist.includes(cmd)) { + result = sandbox_run(cmd, parts.slice(1)); + toolCalls++; + } else { + // If the task is descriptive (not a command), run smart handling + if (capabilities.includes('list_files') || capabilities.includes('read_file')) { + // Smart mode: interpret task + const lower = task.toLowerCase(); + if (lower.includes('list') || lower.includes('file')) { + const dir = parts.find(p => p.startsWith('/') || p.startsWith('.')) || '/'; + result = sandbox_ls(dir); + } else if (lower.includes('find') || lower.includes('search')) { + const q = task.replace(/find|search/i, '').trim(); + result = sandbox_find(q || ''); + } else if (lower.includes('count') || lower.includes('how many')) { + result = sandbox_ls('/'); + const count = (result.match(/\[file\]|\[dir\]/g) || []).length; + result = `Found ${count} items in total.\\n${result}`; + } else if (lower.includes('status') || lower.includes('health')) { + result = sandbox_status(); + } else { + result = `Task interpreted: "${task}"\\nCapabilities: ${capabilities.join(', ')}\\nTip: Be more specific (e.g. "list files in /")`; + } + toolCalls++; + } else { + result = `Task: "${task}"\\nNo matching capability for command "${cmd}"`; + } + } + } catch (e) { + error = e.message; + console.error(`[Sandbox ${taskId}] Error:`, e); + } + + const duration = ((performance.now() - startTime) / 1000).toFixed(2); + + self.postMessage({ + taskId, + result, + error, + toolCalls, + duration: parseFloat(duration), + vfsSnapshot: JSON.stringify(_vfs) + }); +}; + +// ─── Sandboxed VFS Operations ─── + +function sandbox_ls(path) { + path = path || '/'; + const entries = _vfs_get_entries(path); + if (!entries) return `ls: ${path}: No such directory`; + const lines = entries.map(e => { + const icon = e.type === 'dir' ? '[dir]' : '[file]'; + const size = e.size !== undefined ? ` (${e.size}B)` : ''; + return `${icon} ${e.name}${size}`; + }); + return lines.join('\\n') || '(empty)'; +} + +function sandbox_cat(path) { + const content = _vfs_get_content(path); + if (content === null) return `cat: ${path}: No such file`; + if (typeof content === 'object') return `cat: ${path}: Is a directory`; + if (content.length > 8000) return content.substring(0, 8000) + '\\n... (truncated at 8KB)'; + return content; +} + +function sandbox_find(query) { + const results = []; + const q = query ? query.toLowerCase() : ''; + _vfs_walk('/', (path, node) => { + if (node.type === 'file') { + if (!q || path.toLowerCase().includes(q)) { + results.push(path); + } + } + }); + if (results.length === 0) return `No files found${query ? ` matching "${query}"` : ''}`; + return results.join('\\n'); +} + +function sandbox_grep(query) { + if (!query) return 'grep: missing pattern'; + const q = query.toLowerCase(); + const results = []; + _vfs_walk('/', (path, node) => { + if (node.type === 'file' && typeof node.content === 'string') { + const lines = node.content.split('\\n'); + for (let i = 0; i < lines.length; i++) { + if (lines[i].toLowerCase().includes(q)) { + results.push(`${path}:${i + 1}: ${lines[i].trim().substring(0, 100)}`); + } + } + } + }); + if (results.length === 0) return `grep: no matches for "${query}"`; + return results.slice(0, 50).join('\\n') + (results.length > 50 ? `\\n... (+${results.length - 50} more)` : ''); +} + +function sandbox_status() { + let fileCount = 0, dirCount = 0; + _vfs_walk('/', (path, node) => { + if (node.type === 'file') fileCount++; + else dirCount++; + }); + const vfsSize = JSON.stringify(_vfs).length; + return `Sandbox VFS Status:\\n Files: ${fileCount}\\n Directories: ${dirCount}\\n VFS Size: ${(vfsSize / 1024).toFixed(1)}KB`; +} + +function sandbox_run(cmd, args) { + return `[sandbox] ${cmd} ${args.join(' ')}\\n(Command execution limited in sandbox mode)`; +} + +// ─── VFS Helpers ─── + +function _vfs_get_entries(path) { + const dir = _vfs_resolve(path); + if (!dir || dir.type !== 'dir') return null; + return Object.entries(dir.content || {}).map(([name, node]) => ({ + name, + type: node.type || 'file', + size: typeof node.content === 'string' ? node.content.length : undefined + })).sort((a, b) => a.name.localeCompare(b.name)); +} + +function _vfs_get_content(path) { + const node = _vfs_resolve(path); + if (!node || node.type === 'dir') return null; + return node.content; +} + +function _vfs_resolve(path) { + if (!_vfs || !_vfs.root) return null; + if (path === '/' || path === '') return _vfs.root; + const parts = path.split('/').filter(p => p); + let current = _vfs.root; + for (const part of parts) { + if (!current.content || !current.content[part]) return null; + current = current.content[part]; + } + return current; +} + +function _vfs_walk(path, cb) { + const node = _vfs_resolve(path); + if (!node) return; + cb(path, node); + if (node.type === 'dir' && node.content) { + for (const [name, child] of Object.entries(node.content)) { + _vfs_walk(path === '/' ? '/' + name : path + '/' + name, cb); + } + } +}
\ No newline at end of file diff --git a/modules/core/terminal.js b/modules/core/terminal.js index e3f222c..d916ae6 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -1028,6 +1028,10 @@ this._safeRegister('LeaseModule', 'lease system'); this._safeRegister('OrchestratorModule', 'orchestrator'); this._safeRegister('NeuralVFSModule', 'neural VFS'); + this._safeRegister('SandboxModule', 'sandbox'); + this._safeRegister('MemoryGraphModule', 'memory graph'); + this._safeRegister('PredictorModule', 'predictor'); + this._safeRegister('WorldModelModule', 'world model'); this._safeRegister('ServerMonitorModule', 'server monitor'); this._safeRegister('SimpleGUIModule', 'simple GUI builder'); } |