UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

200 lines (175 loc) 5.41 kB
/** * Typed internal event bus for AIWG messaging subsystem. * * Connects AIWG lifecycle event producers (Ralph orchestrator, HITL gates, * security auditor) to messaging consumers (platform adapters) without * requiring an external message broker. * * @implements @.aiwg/architecture/adrs/ADR-messaging-bot-mode.md */ import { EventEmitter } from 'events'; /** * @typedef {Object} AiwgEvent * @property {string} topic - Event topic (e.g., "ralph.completed") * @property {string} timestamp - ISO-8601 timestamp * @property {string} source - Event source (e.g., "ralph-orchestrator") * @property {string} [loopId] - Ralph loop ID if applicable * @property {string} [gateId] - Gate ID if applicable * @property {'info'|'warning'|'critical'} severity * @property {string} summary - One-line human-readable summary * @property {Record<string, unknown>} details - Event-specific payload * @property {string} project - Project root basename */ export class EventBus { /** @type {EventEmitter} */ #emitter; /** @type {Map<string, Function[]>} */ #subscribers; /** @type {Array<{event: AiwgEvent, error: Error, handler: string}>} */ #deadLetterQueue; /** @type {number} */ #maxRetries; /** @type {number} */ #maxDeadLetters; constructor({ maxRetries = 3, maxDeadLetters = 100 } = {}) { this.#emitter = new EventEmitter(); this.#emitter.setMaxListeners(50); this.#subscribers = new Map(); this.#deadLetterQueue = []; this.#maxRetries = maxRetries; this.#maxDeadLetters = maxDeadLetters; } /** * Subscribe to events matching a topic pattern. * Supports exact match ("ralph.completed") and wildcard ("ralph.*"). * * @param {string} pattern - Topic or wildcard pattern * @param {(event: AiwgEvent) => Promise<void>} handler * @param {string} [name] - Handler name for debugging */ subscribe(pattern, handler, name) { const handlerName = name || handler.name || 'anonymous'; const wrappedHandler = async (event) => { if (!this.#matchesPattern(event.topic, pattern)) return; let lastError; for (let attempt = 1; attempt <= this.#maxRetries; attempt++) { try { await handler(event); return; } catch (error) { lastError = error; if (attempt < this.#maxRetries) { // Brief delay before retry await new Promise(r => setTimeout(r, 100 * attempt)); } } } // All retries exhausted — send to dead letter queue this.#addToDeadLetter(event, lastError, handlerName); }; if (!this.#subscribers.has(pattern)) { this.#subscribers.set(pattern, []); } this.#subscribers.get(pattern).push(wrappedHandler); this.#emitter.on('event', wrappedHandler); } /** * Publish an event to all matching subscribers. * * @param {AiwgEvent} event */ publish(event) { if (!event.timestamp) { event.timestamp = new Date().toISOString(); } this.#emitter.emit('event', event); } /** * Remove all subscribers for a pattern. * * @param {string} pattern */ unsubscribe(pattern) { const handlers = this.#subscribers.get(pattern); if (handlers) { for (const handler of handlers) { this.#emitter.removeListener('event', handler); } this.#subscribers.delete(pattern); } } /** * Get the dead letter queue contents. * * @returns {Array<{event: AiwgEvent, error: Error, handler: string, timestamp: string}>} */ getDeadLetters() { return [...this.#deadLetterQueue]; } /** * Clear the dead letter queue. */ clearDeadLetters() { this.#deadLetterQueue.length = 0; } /** * Get subscriber count. * * @returns {number} */ get subscriberCount() { let count = 0; for (const handlers of this.#subscribers.values()) { count += handlers.length; } return count; } /** * Remove all subscribers and clear state. */ destroy() { this.#emitter.removeAllListeners(); this.#subscribers.clear(); this.#deadLetterQueue.length = 0; } /** * Check if a topic matches a subscription pattern. * Supports: exact match, single wildcard ("ralph.*"), double wildcard ("*") * * @param {string} topic * @param {string} pattern * @returns {boolean} */ #matchesPattern(topic, pattern) { if (pattern === '*') return true; if (pattern === topic) return true; if (pattern.endsWith('.*')) { const prefix = pattern.slice(0, -2); return topic.startsWith(prefix + '.'); } return false; } /** * Add a failed event to the dead letter queue. * * @param {AiwgEvent} event * @param {Error} error * @param {string} handlerName */ #addToDeadLetter(event, error, handlerName) { this.#deadLetterQueue.push({ event, error: { message: error.message, stack: error.stack }, handler: handlerName, timestamp: new Date().toISOString(), }); // Evict oldest entries if over capacity while (this.#deadLetterQueue.length > this.#maxDeadLetters) { this.#deadLetterQueue.shift(); } // Log but don't throw — messaging failures must not crash the daemon console.error( `[EventBus] Handler "${handlerName}" failed after ${this.#maxRetries} retries for topic "${event.topic}": ${error.message}` ); } }