diff options
Diffstat (limited to '.bkp-rca-20261018_181308/module-agent-registry.js')
| -rw-r--r-- | .bkp-rca-20261018_181308/module-agent-registry.js | 473 |
1 files changed, 473 insertions, 0 deletions
diff --git a/.bkp-rca-20261018_181308/module-agent-registry.js b/.bkp-rca-20261018_181308/module-agent-registry.js new file mode 100644 index 0000000..68dca66 --- /dev/null +++ b/.bkp-rca-20261018_181308/module-agent-registry.js @@ -0,0 +1,473 @@ +// 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(); + this.commands['agent-status'] = (args) => this.showAgentStatus(args[0]); + this.commands['agent-register'] = (args) => this.registerAgent(args); + 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'); + if (data) this._contracts = JSON.parse(data); + } catch (e) {} + // Try loading individual contract files + const dir = this.terminal.fs.readDir('/.contracts/'); + if (dir) { + dir.forEach(e => { + if (e.type === 'file' && e.name.endsWith('.contract.json')) { + try { + const c = JSON.parse(this.terminal.fs.readFile('/.contracts/' + e.name)); + this._contracts[c.role] = c; + } catch (e) { console.warn('Bad contract:', e.name, e.message); } + } + }); + } + } + _saveContracts() { + try { + this.terminal.fs.fsWriteJSON('/.contracts/index.json', this._contracts); + } catch (e) { console.error('Contract save error:', e); } + } + registerAgent(args) { + 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) { + const role = args[0]; + if (!role) return this.terminal.print('Usage: contract-register <role> [input_fields...] [--output output_fields...]', 'warning'); + + const inputIdx = args.indexOf('--output'); + const hasOutputFlag = inputIdx !== -1; + + let inputFields = [], outputFields = []; + if (hasOutputFlag) { + inputFields = args.slice(1, inputIdx).filter(f => !f.startsWith('--')); + outputFields = args.slice(inputIdx + 1).filter(f => !f.startsWith('--')); + } else { + inputFields = args.slice(1).filter(f => !f.startsWith('--')); + outputFields = ['result']; // default output + } + + const contract = { + role, + capabilities: inputFields, + input_schema: inputFields, + output_schema: outputFields, + created: new Date().toISOString(), + updated: new Date().toISOString() + }; + this._contracts[role] = contract; + // Save as individual contract file + this.terminal.fs.fsWriteJSON(`/.contracts/${role}.contract.json`, contract); + this._saveContracts(); + this.terminal.print(`✅ Contract registered for role '${role}'`, 'success'); + this.terminal.print(` Input fields: ${inputFields.join(', ') || '(free-form)'}`, 'info'); + this.terminal.print(` Output fields: ${outputFields.join(', ') || '(free-form)'}`, 'info'); + } + listContracts() { + const roles = Object.keys(this._contracts); + if (roles.length === 0) return this.terminal.print('No contracts registered', 'info'); + this.terminal.print(`📜 A2A Contracts (${roles.length}):`, 'header'); + roles.forEach(r => { + const c = this._contracts[r]; + this.terminal.print(` ${r} — input:[${c.input_schema.join(',')}] → output:[${c.output_schema.join(',')}]`, 'info'); + }); + } + validateTask(role, task) { + const contract = this._contracts[role]; + if (!contract) return { valid: true, reason: 'no contract for role' }; // permissive fallback + if (contract.input_schema.length === 0) return { valid: true, reason: 'free-form input' }; + // Simple validation: check required input fields are present in task + const missing = contract.input_schema.filter(f => !task.toLowerCase().includes(f.toLowerCase())); + if (missing.length > 0) { + return { valid: false, reason: `missing required fields: ${missing.join(', ')}` }; + } + return { valid: true, reason: 'matched' }; + } + getContract(role) { + return this._contracts[role] || null; + } + listAgents() { + const agents = this.terminal.fs.ls('/agents/'); + if (!agents || agents.length === 0) { + return this.terminal.print('No agents registered', 'info'); + } + this.terminal.print(`📋 Registered Agents (${agents.length}):`, 'header'); + agents.forEach(a => { + if (a.type === 'dir' && a.name !== 'configured') { + const data = this.terminal.fs.readFile(`/agents/${a.name}/status.json`); + const s = data ? JSON.parse(data) : { status: 'unknown', capabilities: [] }; + this.terminal.print(` ${a.name} — ${s.status} (${(s.capabilities || []).length} capabilities)${s.contract ? ` → contract:${s.contract}` : ''}`, 'info'); + } + }); + } + 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'); + } + showAgentStatus(id) { + if (!id) return this.terminal.print('Usage: agent-status <agent-id>', 'error'); + const data = this.terminal.fs.readFile(`/agents/${id}/status.json`); + if (!data) return this.terminal.print(`❌ Agent '${id}' not found`, 'error'); + const s = JSON.parse(data); + this.terminal.print(`Agent: ${s.id}`, 'header'); + this.terminal.print(` Status: ${s.status}`, 'info'); + this.terminal.print(` Capabilities: ${(s.capabilities || []).join(', ')}`, 'info'); + this.terminal.print(` Contract: ${s.contract || 'none'}`, 'info'); + this.terminal.print(` Registered: ${s.registered}`, 'info'); + this.terminal.print(` Last Heartbeat: ${s.lastHeartbeat}`, 'info'); + } + getAgent(id) { + const data = this.terminal.fs.readFile(`/agents/${id}/status.json`); + return data ? JSON.parse(data) : null; + } + updateHeartbeat(id) { + const agent = this.getAgent(id); + if (!agent) return false; + agent.lastHeartbeat = new Date().toISOString(); + this.terminal.fs.writeFile(`agents/${id}/status.json`, JSON.stringify(agent)); + return true; + } +} |