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
JavaScript
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