UNPKG

tlnt

Version:

TLNT - HMS-Powered Multi-Agent Platform with Government Agency Analysis, Deep Research, and Enterprise-Ready Deployment. Self-optimizing multi-domain AI agent with continuous learning and enterprise-grade performance monitoring.

341 lines 11.2 kB
import { EventEmitter } from 'events'; import { Redis } from 'ioredis'; export var MessagePriority; (function (MessagePriority) { MessagePriority[MessagePriority["LOW"] = 0] = "LOW"; MessagePriority[MessagePriority["NORMAL"] = 1] = "NORMAL"; MessagePriority[MessagePriority["HIGH"] = 2] = "HIGH"; MessagePriority[MessagePriority["URGENT"] = 3] = "URGENT"; })(MessagePriority || (MessagePriority = {})); /** * Redis-backed message bus for agent communication * Supports pub/sub patterns, control channels, and message persistence */ export class MessageBus extends EventEmitter { redis; subscriber; channels = new Map(); config; connected = false; retryCount = 0; constructor(config = {}) { super(); this.config = { redisUrl: config.redisUrl || 'redis://localhost:6379', redisOptions: config.redisOptions || {}, messageRetention: config.messageRetention || 3600, // 1 hour maxRetries: config.maxRetries || 5, retryDelay: config.retryDelay || 1000 }; this.redis = new Redis(this.config.redisUrl, { ...this.config.redisOptions, maxRetriesPerRequest: this.config.maxRetries, retryDelayOnFailover: this.config.retryDelay, lazyConnect: true }); this.subscriber = new Redis(this.config.redisUrl, { ...this.config.redisOptions, maxRetriesPerRequest: this.config.maxRetries, retryDelayOnFailover: this.config.retryDelay, lazyConnect: true }); this.setupEventHandlers(); } setupEventHandlers() { this.redis.on('connect', () => { this.connected = true; this.retryCount = 0; this.emit('connected'); }); this.redis.on('error', (error) => { this.connected = false; this.emit('error', error); }); this.redis.on('close', () => { this.connected = false; this.emit('disconnected'); this.attemptReconnect(); }); this.subscriber.on('message', (channel, data) => { this.handleIncomingMessage(channel, data); }); this.subscriber.on('pmessage', (pattern, channel, data) => { this.handleIncomingMessage(channel, data, pattern); }); } async attemptReconnect() { if (this.retryCount >= this.config.maxRetries) { this.emit('maxRetriesExceeded'); return; } this.retryCount++; const delay = this.config.retryDelay * Math.pow(2, this.retryCount - 1); setTimeout(async () => { try { await this.connect(); } catch (error) { this.emit('reconnectFailed', error); } }, delay); } async connect() { try { await Promise.all([ this.redis.connect(), this.subscriber.connect() ]); this.connected = true; this.emit('connected'); } catch (error) { this.connected = false; throw error; } } async disconnect() { this.connected = false; await Promise.all([ this.redis.disconnect(), this.subscriber.disconnect() ]); this.emit('disconnected'); } isConnected() { return this.connected; } /** * Publish a message to a specific channel */ async publish(channel, message) { if (!this.connected) { throw new Error('MessageBus not connected'); } const fullMessage = { ...message, id: this.generateMessageId(), timestamp: Date.now() }; const serialized = JSON.stringify(fullMessage); // Publish to channel await this.redis.publish(channel, serialized); // Store for message retention await this.storeMessage(channel, fullMessage); this.emit('messageSent', { channel, message: fullMessage }); return fullMessage.id; } /** * Subscribe to messages on a specific channel */ async subscribe(channel, handler) { if (!this.channels.has(channel)) { this.channels.set(channel, new Set()); await this.subscriber.subscribe(channel); } this.channels.get(channel).add(handler); this.emit('subscribed', { channel, handlerCount: this.channels.get(channel).size }); } /** * Subscribe to messages matching a pattern */ async subscribePattern(pattern, handler) { if (!this.channels.has(pattern)) { this.channels.set(pattern, new Set()); await this.subscriber.psubscribe(pattern); } this.channels.get(pattern).add(handler); this.emit('patternSubscribed', { pattern, handlerCount: this.channels.get(pattern).size }); } /** * Unsubscribe from a channel */ async unsubscribe(channel, handler) { const handlers = this.channels.get(channel); if (!handlers) return; if (handler) { handlers.delete(handler); if (handlers.size === 0) { this.channels.delete(channel); await this.subscriber.unsubscribe(channel); } } else { this.channels.delete(channel); await this.subscriber.unsubscribe(channel); } this.emit('unsubscribed', { channel }); } /** * Send a control command to an agent */ async sendControl(agentId, command) { const controlChannel = `agent.${agentId}.ctl`; return this.publish(controlChannel, { type: 'control', source: 'system', target: agentId, data: command, priority: MessagePriority.HIGH }); } /** * Send a delegation message */ async sendDelegation(fromAgent, toAgent, task) { const delegationChannel = `delegation.${toAgent}`; return this.publish(delegationChannel, { type: 'delegation', source: fromAgent, target: toAgent, data: { task }, priority: MessagePriority.NORMAL }); } /** * Broadcast a message to all agents */ async broadcast(message) { return this.publish('broadcast', { ...message, target: '*' }); } /** * Get message history for a channel */ async getMessageHistory(channel, limit = 100) { const key = `messages:${channel}`; const messages = await this.redis.lrange(key, -limit, -1); return messages.map((msg) => JSON.parse(msg)).reverse(); } /** * Get agent status */ async getAgentStatus(agentId) { const statusKey = `agent:${agentId}:status`; const status = await this.redis.get(statusKey); return status ? JSON.parse(status) : null; } /** * Update agent status */ async updateAgentStatus(agentId, status) { const statusKey = `agent:${agentId}:status`; const heartbeatKey = `agent:${agentId}:heartbeat`; await this.redis.multi() .set(statusKey, JSON.stringify(status)) .set(heartbeatKey, Date.now()) .expire(statusKey, 300) // 5 minutes .expire(heartbeatKey, 300) .exec(); // Notify status change await this.publish(`agent.${agentId}.status`, { type: 'status_update', source: agentId, data: status, priority: MessagePriority.NORMAL }); } /** * Check if agent is alive based on heartbeat */ async isAgentAlive(agentId, maxAge = 60000) { const heartbeatKey = `agent:${agentId}:heartbeat`; const lastHeartbeat = await this.redis.get(heartbeatKey); if (!lastHeartbeat) return false; const age = Date.now() - parseInt(lastHeartbeat); return age <= maxAge; } /** * Get all active agents */ async getActiveAgents() { const pattern = 'agent:*:heartbeat'; const keys = await this.redis.keys(pattern); const agents = []; for (const key of keys) { const agentId = key.split(':')[1]; if (await this.isAgentAlive(agentId)) { agents.push(agentId); } } return agents; } async handleIncomingMessage(channel, data, pattern) { try { const message = JSON.parse(data); // Find handlers for this channel or pattern const channelHandlers = this.channels.get(channel) || new Set(); const patternHandlers = pattern ? this.channels.get(pattern) || new Set() : new Set(); const allHandlers = new Set([...channelHandlers, ...patternHandlers]); // Execute all handlers const promises = Array.from(allHandlers).map((handler) => { try { return Promise.resolve(handler(message)); } catch (error) { this.emit('handlerError', { error, message, handler }); return Promise.resolve(); } }); await Promise.allSettled(promises); this.emit('messageReceived', { channel, message, pattern }); } catch (error) { this.emit('parseError', { error, channel, data }); } } async storeMessage(channel, message) { const key = `messages:${channel}`; await this.redis.multi() .lpush(key, JSON.stringify(message)) .ltrim(key, 0, 999) // Keep last 1000 messages .expire(key, this.config.messageRetention) .exec(); } generateMessageId() { return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; } /** * Get connection statistics */ getStats() { return { connected: this.connected, channelCount: this.channels.size, retryCount: this.retryCount, uptime: process.uptime() }; } /** * Health check */ async healthCheck() { try { const ping = await this.redis.ping(); const stats = this.getStats(); return { status: ping === 'PONG' && this.connected ? 'healthy' : 'unhealthy', details: { ...stats, redis: { ping, status: this.redis.status } } }; } catch (error) { return { status: 'unhealthy', details: { error: error instanceof Error ? error.message : String(error), ...this.getStats() } }; } } } //# sourceMappingURL=messageBus.js.map