// module-agent-registry.js — VFS-backed agent directory with A2A contracts // Commands: agent-list, agent-register, agent-unregister, agent-status, contract-register, contract-list class AgentRegistryModule { constructor() { this.commands = {}; this.terminal = null; this._contracts = {}; } 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(); } 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(); this.terminal.print('📋 Agent Registry loaded', 'info'); } _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[1] || `agent-${Date.now()}`; const capabilities = args.slice(2).length ? args.slice(2) : ['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', capabilities, registered: new Date().toISOString(), lastHeartbeat: new Date().toISOString(), contract: null })); 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 [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) { 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 ', '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; } }