jay-code
Version:
Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability
282 lines (237 loc) • 7.51 kB
text/typescript
/**
* Inter-agent messaging system
*/
import { Message, CoordinationConfig, SystemEvents } from '../utils/types.js';
import type { IEventBus } from '../core/event-bus.js';
import type { ILogger } from '../core/logger.js';
import type { CoordinationError } from '../utils/errors.js';
import { generateId, timeout as timeoutHelper } from '../utils/helpers.js';
interface MessageQueue {
messages: Message[];
handlers: Map<string, (message: Message) => void>;
}
interface PendingResponse {
resolve: (response: unknown) => void;
reject: (error: Error) => void;
timeout: number;
}
/**
* Message router for inter-agent communication
*/
export class MessageRouter {
private queues = new Map<string, MessageQueue>(); // agentId -> queue
private pendingResponses = new Map<string, PendingResponse>();
private messageCount = 0;
constructor(
private config: CoordinationConfig,
private eventBus: IEventBus,
private logger: ILogger,
) {}
async initialize(): Promise<void> {
this.logger.info('Initializing message router');
// Set up periodic cleanup
setInterval(() => this.cleanup(), 60000); // Every minute
}
async shutdown(): Promise<void> {
this.logger.info('Shutting down message router');
// Reject all pending responses
for (const [id, pending] of this.pendingResponses) {
pending.reject(new Error('Message router shutdown'));
clearTimeout(pending.timeout);
}
this.queues.clear();
this.pendingResponses.clear();
}
async send(from: string, to: string, payload: unknown): Promise<void> {
const message: Message = {
id: generateId('msg'),
type: 'agent-message',
payload,
timestamp: new Date(),
priority: 0,
};
await this.sendMessage(from, to, message);
}
async sendWithResponse<T = unknown>(
from: string,
to: string,
payload: unknown,
timeoutMs?: number,
): Promise<T> {
const message: Message = {
id: generateId('msg'),
type: 'agent-request',
payload,
timestamp: new Date(),
priority: 1,
};
// Create response promise
const responsePromise = new Promise<T>((resolve, reject) => {
const timeout = setTimeout(() => {
this.pendingResponses.delete(message.id);
reject(new Error(`Message response timeout: ${message.id}`));
}, timeoutMs || this.config.messageTimeout);
this.pendingResponses.set(message.id, {
resolve: resolve as (response: unknown) => void,
reject,
timeout: timeout as unknown as number,
});
});
// Send message
await this.sendMessage(from, to, message);
// Wait for response
return await responsePromise;
}
async broadcast(from: string, payload: unknown): Promise<void> {
const message: Message = {
id: generateId('broadcast'),
type: 'broadcast',
payload,
timestamp: new Date(),
priority: 0,
};
// Send to all agents
const agents = Array.from(this.queues.keys()).filter((id) => id !== from);
await Promise.all(agents.map((to) => this.sendMessage(from, to, message)));
}
subscribe(agentId: string, handler: (message: Message) => void): void {
const queue = this.ensureQueue(agentId);
queue.handlers.set(generateId('handler'), handler);
}
unsubscribe(agentId: string, handlerId: string): void {
const queue = this.queues.get(agentId);
if (queue) {
queue.handlers.delete(handlerId);
}
}
async sendResponse(originalMessageId: string, response: unknown): Promise<void> {
const pending = this.pendingResponses.get(originalMessageId);
if (!pending) {
this.logger.warn('No pending response found', { messageId: originalMessageId });
return;
}
clearTimeout(pending.timeout);
this.pendingResponses.delete(originalMessageId);
pending.resolve(response);
}
async getHealthStatus(): Promise<{
healthy: boolean;
error?: string;
metrics?: Record<string, number>;
}> {
const totalQueues = this.queues.size;
let totalMessages = 0;
let totalHandlers = 0;
for (const queue of this.queues.values()) {
totalMessages += queue.messages.length;
totalHandlers += queue.handlers.size;
}
return {
healthy: true,
metrics: {
activeQueues: totalQueues,
pendingMessages: totalMessages,
registeredHandlers: totalHandlers,
pendingResponses: this.pendingResponses.size,
totalMessagesSent: this.messageCount,
},
};
}
private async sendMessage(from: string, to: string, message: Message): Promise<void> {
this.logger.debug('Sending message', {
from,
to,
messageId: message.id,
type: message.type,
});
// Ensure destination queue exists
const queue = this.ensureQueue(to);
// Add to queue
queue.messages.push(message);
this.messageCount++;
// Emit event
this.eventBus.emit(SystemEvents.MESSAGE_SENT, { from, to, message });
// Process message immediately if handlers exist
if (queue.handlers.size > 0) {
await this.processMessage(to, message);
}
}
private async processMessage(agentId: string, message: Message): Promise<void> {
const queue = this.queues.get(agentId);
if (!queue) {
return;
}
// Remove message from queue
const index = queue.messages.indexOf(message);
if (index !== -1) {
queue.messages.splice(index, 1);
}
// Call all handlers
const handlers = Array.from(queue.handlers.values());
await Promise.all(
handlers.map((handler) => {
try {
handler(message);
} catch (error) {
this.logger.error('Message handler error', {
agentId,
messageId: message.id,
error,
});
}
}),
);
// Emit received event
this.eventBus.emit(SystemEvents.MESSAGE_RECEIVED, {
from: '', // Would need to track this
to: agentId,
message,
});
}
private ensureQueue(agentId: string): MessageQueue {
if (!this.queues.has(agentId)) {
this.queues.set(agentId, {
messages: [],
handlers: new Map(),
});
}
return this.queues.get(agentId)!;
}
async performMaintenance(): Promise<void> {
this.logger.debug('Performing message router maintenance');
this.cleanup();
}
private cleanup(): void {
const now = Date.now();
// Clean up old messages
for (const [agentId, queue] of this.queues) {
const filtered = queue.messages.filter((msg) => {
const age = now - msg.timestamp.getTime();
const maxAge = msg.expiry
? msg.expiry.getTime() - msg.timestamp.getTime()
: this.config.messageTimeout;
if (age > maxAge) {
this.logger.warn('Dropping expired message', {
agentId,
messageId: msg.id,
age,
});
return false;
}
return true;
});
queue.messages = filtered;
// Remove empty queues
if (queue.messages.length === 0 && queue.handlers.size === 0) {
this.queues.delete(agentId);
}
}
// Clean up timed out responses
for (const [id, pending] of this.pendingResponses) {
// This is handled by the timeout, but double-check
clearTimeout(pending.timeout);
pending.reject(new Error('Response timeout during cleanup'));
}
this.pendingResponses.clear();
}
}