aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-agent-registry.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-17 06:01:20 +0200
committerAgando <roduccomm@gmail.com>2026-06-17 06:01:20 +0200
commitcebacc27a48168e666679018f8e27156d12d4527 (patch)
treeb306d33b67b9d8c84bd9b9c16b1e91ee217628a6 /modules/core/module-agent-registry.js
parent6aa24b976952a698c673d350977721b27193183d (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.js320
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
+}