diff options
| author | Agando <roduccomm@gmail.com> | 2026-06-17 06:01:20 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-06-17 06:01:20 +0200 |
| commit | cebacc27a48168e666679018f8e27156d12d4527 (patch) | |
| tree | b306d33b67b9d8c84bd9b9c16b1e91ee217628a6 /modules/core/module-agent-registry.js | |
| parent | 6aa24b976952a698c673d350977721b27193183d (diff) | |
auto: 2026-06-17_06:01 changes
Diffstat (limited to 'modules/core/module-agent-registry.js')
| -rw-r--r-- | modules/core/module-agent-registry.js | 320 |
1 files changed, 317 insertions, 3 deletions
diff --git a/modules/core/module-agent-registry.js b/modules/core/module-agent-registry.js index 2439ab8..68dca66 100644 --- a/modules/core/module-agent-registry.js +++ b/modules/core/module-agent-registry.js @@ -1,10 +1,12 @@ // module-agent-registry.js — VFS-backed agent directory with A2A contracts // Commands: agent-list, agent-register, agent-unregister, agent-status, contract-register, contract-list +// A2A Scratchpad commands: a2a-post, a2a-poll, a2a-ack, a2a-reject, a2a-status, a2a-prune class AgentRegistryModule { constructor() { this.commands = {}; this.terminal = null; this._contracts = {}; + this._a2aPruneTimer = null; } registerCommands() { this.commands['agent-list'] = () => this.listAgents(); @@ -13,14 +15,321 @@ class AgentRegistryModule { this.commands['agent-unregister'] = (args) => this.unregisterAgent(args[0]); this.commands['contract-register'] = (args) => this.registerContract(args); this.commands['contract-list'] = () => this.listContracts(); + // A2A Scratchpad commands + this.commands['a2a-post'] = (args) => this.a2aPostCommand(args); + this.commands['a2a-poll'] = (args) => this.a2aPollCommand(args); + this.commands['a2a-ack'] = (args) => this.a2aAckCommand(args); + this.commands['a2a-reject'] = (args) => this.a2aRejectCommand(args); + this.commands['a2a-status'] = () => this.a2aStatus(); + this.commands['a2a-prune'] = () => this.a2aPrune(); + } + getHelpInfo() { + return { + 'a2a-post': 'a2a-post <to> <type> <capability> <payload...> — Post an A2A message', + 'a2a-poll': 'a2a-poll [agentId] — Poll for messages addressed to an agent (default: current or "user")', + 'a2a-ack': 'a2a-ack <msgId> <result...> — Acknowledge a message with result', + 'a2a-reject': 'a2a-reject <msgId> <error...> — Reject a message with error', + 'a2a-status': 'a2a-status — Show scratchpad stats per agent', + 'a2a-prune': 'a2a-prune — Clean up expired and old completed messages' + }; } init(terminal) { this.terminal = terminal; if (!this.terminal.fs.readDir('/agents/')) this.terminal.fs.mkdir('agents'); if (!this.terminal.fs.readDir('/.contracts/')) this.terminal.fs.mkdir('.contracts'); this._loadContracts(); + // Initialize scratchpad + this._ensureScratchpad(); + // Auto-prune every 60 seconds + this._a2aPruneTimer = setInterval(() => this.a2aPrune(), 60000); + // Save the spec-vs-reality delta table + this._saveDeltaTable(); this.terminal.print('📋 Agent Registry loaded', 'info'); } + // ─── Task 1: Save spec-vs-reality delta table ─── + _saveDeltaTable() { + try { + const fs = this.terminal.fs; + if (!fs.readDir('/_docs/')) fs.mkdir('_docs'); + const delta = `# Spec-vs-Reality Delta — 2026-06-17\n\n` + +`| # | Whitepaper Concept | Spec'd Behavior | Current Reality | Δ | Priority |\n` + +`|---|---|---|---|---|---|\n` + +`| 1 | Hyper-Modular Kernel | Real-time self-rewriting drivers for task-specific optimization via WPM | WPM exists but is manual; no hot-patch daemon or runtime driver swap | Partial | Medium |\n` + +`| 2 | A2A Protocol | Standardized neural communication layer for agent negotiation; typed messages, no central broker | VFS mailbox transport works (a2a-send/a2a-list/500ms polling) but orchestrator acts as central broker | Partial | High |\n` + +`| 3 | Neural VFS | Semantic-based file storage (concept:// URIs) as primary access method; hierarchical paths are secondary | concept:// URIs work (nvfs-index/nvfs-resolve) but hierarchical paths remain primary access; concept resolution requires explicit index | Partial | High |\n` + +`| 4 | Recursive Self-Optimization | Monitor → Profile → Diagnose → Patch → Test → Promote feedback loop | system-health telemetry captures latency/VFS stats but no auto-patch generation or hot-promotion pipeline | Partial | Medium |\n` + +`| 5 | Agent Discovery | No central broker — agents discover each other via VFS registrations spontaneously | Orchestrator is central broker; all task delegation goes through it; agents don't autonomously discover each other | Critical Gap | Critical |\n` + +`| 6 | Contract Schema | Every A2A message carries a contract schema defining capabilities and constraints | Contracts exist (contract-register/contract-list) but schema validation is basic substring matching; not enforced on every message | Partial | Medium |\n` + +`| 7 | Semantic Addressing | Neural VFS as primary storage paradigm — users navigate by concept, not path | Hierarchical path access still primary; concept:// requires explicit nvfs-index; no transparent concept-to-path fallback | Critical Gap | Critical |\n` + +`| 8 | Warm-start Guarantee | Kernel caches last N stable driver configs; boot in under 200ms | No golden config cache; full localStorage restore on every page load | Spec Violation | High |\n` + +`| 9 | Self-rewrite Daemon | Monitors performance telemetry and generates optimized driver variants at runtime | No self-rewrite daemon exists; all module changes are manual | Missing | Low |\n` + +`| 10 | Agent Lifecycle | spawn, sleep, wake, kill, restart policies with exponential backoff | Fully implemented in orchestrator with heartbeat, idle sweep, restart policies, VFS sleep serialization | Fully Aligned | — |\n\n` + +`**Summary: 2 Critical gaps, 5 Partial, 1 Spec Violation, 1 Missing, 1 Fully Aligned**\n`; + fs.writeFile('_docs/spec-vs-reality-delta.md', delta); + this.terminal.print('📄 Spec-vs-Reality delta saved to /_docs/spec-vs-reality-delta.md', 'info'); + } catch (e) { + console.warn('Failed to save delta table:', e.message); + } + } + // ─── A2A Scratchpad Helpers ─── + _ensureScratchpad() { + try { + const data = this.terminal.fs.fsReadJSON('/_agents/scratchpad.json'); + if (!data) throw new Error('no scratchpad'); + } catch (e) { + this.terminal.fs.mkdir('_agents'); + this.terminal.fs.fsWriteJSON('/_agents/scratchpad.json', { + messages: [], + seq: 0, + agents: {} + }); + } + } + _getScratchpad() { + try { + const data = this.terminal.fs.fsReadJSON('/_agents/scratchpad.json'); + if (data && data.messages) return data; + } catch (e) {} + return { messages: [], seq: 0, agents: {} }; + } + _saveScratchpad(sp) { + try { + this.terminal.fs.mkdir('_agents'); + this.terminal.fs.fsWriteJSON('/_agents/scratchpad.json', sp); + } catch (e) { + console.warn('Scratchpad save error:', e.message); + } + } + // ─── A2A Scratchpad Functions ─── + a2aPost(from, to, type, capability, payload, ttl) { + if (!from || !to || !type) { + this.terminal.print('a2aPost: from, to, and type are required', 'warning'); + return null; + } + const sp = this._getScratchpad(); + sp.seq = (sp.seq || 0) + 1; + const msg = { + id: `msg_${String(sp.seq).padStart(3, '0')}`, + from: from, + to: to, + type: type, + capability: capability || null, + payload: payload || {}, + ttl: (typeof ttl === 'number' ? ttl : 300000), + status: 'pending', + ts: Date.now(), + reply_to: null + }; + sp.messages.push(msg); + this._saveScratchpad(sp); + return msg; + } + a2aPoll(agentId) { + if (!agentId) return []; + const sp = this._getScratchpad(); + const pending = []; + for (const msg of sp.messages) { + if (msg.to === agentId && msg.status === 'pending') { + msg.status = 'delivered'; + pending.push(msg); + } + } + if (pending.length > 0) this._saveScratchpad(sp); + return pending; + } + a2aAck(messageId, resultPayload) { + const sp = this._getScratchpad(); + const msg = sp.messages.find(m => m.id === messageId); + if (!msg) { + this.terminal.print(`a2aAck: message ${messageId} not found`, 'warning'); + return false; + } + if (msg.status === 'completed' || msg.status === 'failed') { + this.terminal.print(`a2aAck: message ${messageId} already ${msg.status}`, 'warning'); + return false; + } + msg.status = 'completed'; + msg.result = resultPayload || {}; + msg.completedAt = Date.now(); + this._saveScratchpad(sp); + return true; + } + a2aReject(messageId, error) { + const sp = this._getScratchpad(); + const msg = sp.messages.find(m => m.id === messageId); + if (!msg) { + this.terminal.print(`a2aReject: message ${messageId} not found`, 'warning'); + return false; + } + if (msg.status === 'completed' || msg.status === 'failed') { + this.terminal.print(`a2aReject: message ${messageId} already ${msg.status}`, 'warning'); + return false; + } + msg.status = 'failed'; + msg.error = error || 'unspecified error'; + msg.completedAt = Date.now(); + this._saveScratchpad(sp); + return true; + } + a2aRegister(agentId, capabilities) { + if (!agentId) return; + const sp = this._getScratchpad(); + sp.agents[agentId] = { + last_poll: Date.now(), + capabilities: capabilities || [] + }; + this._saveScratchpad(sp); + } + a2aUnregister(agentId) { + if (!agentId) return; + const sp = this._getScratchpad(); + delete sp.agents[agentId]; + this._saveScratchpad(sp); + } + a2aPrune() { + try { + const sp = this._getScratchpad(); + const now = Date.now(); + const before = sp.messages.length; + sp.messages = sp.messages.filter(m => { + // Remove expired messages (past TTL) + if (m.status === 'pending' && (now - m.ts > m.ttl)) return false; + // Remove completed/failed messages older than 1 hour + if ((m.status === 'completed' || m.status === 'failed') && m.completedAt && (now - m.completedAt > 3600000)) return false; + return true; + }); + const removed = before - sp.messages.length; + if (removed > 0) this._saveScratchpad(sp); + return removed; + } catch (e) { + console.warn('a2aPrune error:', e.message); + return 0; + } + } + a2aStatus() { + const sp = this._getScratchpad(); + const counts = {}; + for (const msg of sp.messages) { + if (!counts[msg.to]) counts[msg.to] = { pending: 0, delivered: 0, completed: 0, failed: 0 }; + if (msg.status === 'pending') counts[msg.to].pending++; + else if (msg.status === 'delivered') counts[msg.to].delivered++; + else if (msg.status === 'completed') counts[msg.to].completed++; + else if (msg.status === 'failed') counts[msg.to].failed++; + } + this.terminal.print('📊 A2A Scratchpad Status:', 'header'); + this.terminal.print(` Total messages: ${sp.messages.length}`, 'info'); + this.terminal.print(` Registered agents: ${Object.keys(sp.agents).length}`, 'info'); + this.terminal.print(` Sequence: ${sp.seq}`, 'info'); + const agentNames = Object.keys(counts); + if (agentNames.length === 0) { + this.terminal.print(' No messages in scratchpad', 'info'); + return; + } + for (const agentId of agentNames) { + const c = counts[agentId]; + const total = c.pending + c.delivered + c.completed + c.failed; + const caps = sp.agents[agentId] ? ` [${sp.agents[agentId].capabilities.join(', ')}]` : ''; + this.terminal.print(` ${agentId}${caps}: ${total} msgs (⏳${c.pending} 📬${c.delivered} ✅${c.completed} ❌${c.failed})`, 'info'); + } + } + _a2aCleanup() { + // Remove agent entries from scratchpad when agent directory is deleted from /agents/ + try { + const sp = this._getScratchpad(); + const agentDirs = {}; + const dir = this.terminal.fs.readDir('/agents/'); + if (dir) { + for (const entry of dir) { + if (entry.type === 'dir') agentDirs[entry.name] = true; + } + } + let changed = false; + for (const agentId of Object.keys(sp.agents)) { + if (!agentDirs[agentId]) { + delete sp.agents[agentId]; + changed = true; + } + } + if (changed) this._saveScratchpad(sp); + } catch (e) { + console.warn('a2aCleanup error:', e.message); + } + } + // ─── A2A Command Handlers ─── + a2aPostCommand(args) { + // a2a-post <to> <type> <capability> <payload...> + if (args.length < 3) { + return this.terminal.print('Usage: a2a-post <to> <type> <capability> <payload...>', 'warning'); + } + const to = args[0]; + const type = args[1]; + const capability = args[2]; + const payloadRaw = args.slice(3).join(' '); + let payload = {}; + try { + // Try parsing as JSON + payload = JSON.parse(payloadRaw || '{}'); + } catch (e) { + // Treat as plain text + payload = { text: payloadRaw }; + } + const from = 'user'; + const ttl = 300000; + const msg = this.a2aPost(from, to, type, capability, payload, ttl); + if (msg) { + this.terminal.print(`📨 A2A message ${msg.id} posted to ${to} (type: ${type})`, 'success'); + } + } + a2aPollCommand(args) { + // Use explicit agent ID if provided, otherwise try to determine current agent + let agentId = (args && args[0]) || 'user'; + if (!args || !args[0]) { + try { + const orchestrator = this.terminal.moduleManager.modules['OrchestratorModule']; + if (orchestrator && orchestrator._agents) { + const myAgents = Object.entries(orchestrator._agents).filter(([, a]) => a.status === 'busy'); + if (myAgents.length > 0) agentId = myAgents[0][0]; + } + } catch (e) {} + } + const msgs = this.a2aPoll(agentId); + if (msgs.length === 0) { + this.terminal.print(`📭 No pending messages for ${agentId}`, 'info'); + return; + } + this.terminal.print(`📬 ${msgs.length} message(s) for ${agentId}:`, 'header'); + for (const msg of msgs) { + const capInfo = msg.capability ? ` [${msg.capability}]` : ''; + this.terminal.print(` ${msg.id} from ${msg.from} (${msg.type}${capInfo})`, 'info'); + this.terminal.print(` Payload: ${JSON.stringify(msg.payload).substring(0, 120)}`, 'text-muted'); + } + } + a2aAckCommand(args) { + if (args.length < 1) { + return this.terminal.print('Usage: a2a-ack <msgId> [result...]', 'warning'); + } + const msgId = args[0]; + const resultRaw = args.slice(1).join(' '); + let result = {}; + try { + result = JSON.parse(resultRaw || '{}'); + } catch (e) { + result = { text: resultRaw }; + } + if (this.a2aAck(msgId, result)) { + this.terminal.print(`✅ A2A message ${msgId} completed`, 'success'); + } + } + a2aRejectCommand(args) { + if (args.length < 1) { + return this.terminal.print('Usage: a2a-reject <msgId> [error...]', 'warning'); + } + const msgId = args[0]; + const error = args.slice(1).join(' ') || 'rejected by user'; + if (this.a2aReject(msgId, error)) { + this.terminal.print(`❌ A2A message ${msgId} rejected: ${error}`, 'warning'); + } + } _loadContracts() { try { const data = this.terminal.fs.readFile('/.contracts/index.json'); @@ -45,18 +354,21 @@ class AgentRegistryModule { } catch (e) { console.error('Contract save error:', e); } } registerAgent(args) { - const id = args[1] || `agent-${Date.now()}`; - const capabilities = args.slice(2).length ? args.slice(2) : ['list_files', 'read_file']; + const id = args[0] || `agent-${Date.now()}`; + const capabilities = args[1] ? args[1].split(',').map(s => s.trim()).filter(Boolean) : ['list_files', 'read_file']; const agentDir = `agents/${id}`; if (!this.terminal.fs.readDir('/' + agentDir + '/')) this.terminal.fs.mkdir(agentDir); this.terminal.fs.writeFile(`${agentDir}/status.json`, JSON.stringify({ id, status: 'idle', + model: null, capabilities, registered: new Date().toISOString(), lastHeartbeat: new Date().toISOString(), contract: null })); + // Auto-register in A2A scratchpad + this.a2aRegister(id, capabilities); this.terminal.print(`✅ Agent '${id}' registered with ${capabilities.length} capabilities`, 'success'); } registerContract(args) { @@ -129,6 +441,8 @@ class AgentRegistryModule { }); } unregisterAgent(id) { + // Unregister from A2A scratchpad + this.a2aUnregister(id); const ok = this.terminal.fs.deleteFile('agents/' + id); if (ok) this.terminal.print(`✅ Agent '${id}' unregistered`, 'success'); else this.terminal.print(`❌ Agent '${id}' not found`, 'error'); @@ -156,4 +470,4 @@ class AgentRegistryModule { this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent)); return true; } -}
\ No newline at end of file +} |