agentic-flow 1.6.3 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,460 @@
1
+ // QUIC-enabled Swarm Coordinator
2
+ // Manages agent-to-agent communication over QUIC transport with fallback to HTTP/2
3
+ import { logger } from '../utils/logger.js';
4
+ /**
5
+ * QuicCoordinator - Manages multi-agent swarm coordination over QUIC
6
+ *
7
+ * Features:
8
+ * - Agent-to-agent communication via QUIC streams
9
+ * - Topology-aware message routing (mesh, hierarchical, ring, star)
10
+ * - Connection pooling for efficient resource usage
11
+ * - Real-time state synchronization
12
+ * - Per-agent statistics tracking
13
+ */
14
+ export class QuicCoordinator {
15
+ config;
16
+ state;
17
+ messageQueue;
18
+ heartbeatTimer;
19
+ syncTimer;
20
+ messageStats;
21
+ constructor(config) {
22
+ this.config = {
23
+ ...config,
24
+ heartbeatInterval: config.heartbeatInterval || 10000,
25
+ statesSyncInterval: config.statesSyncInterval || 5000,
26
+ enableCompression: config.enableCompression ?? true
27
+ };
28
+ this.state = {
29
+ swarmId: config.swarmId,
30
+ topology: config.topology,
31
+ agents: new Map(),
32
+ connections: new Map(),
33
+ stats: {
34
+ totalAgents: 0,
35
+ activeAgents: 0,
36
+ totalMessages: 0,
37
+ messagesPerSecond: 0,
38
+ averageLatency: 0,
39
+ quicStats: {
40
+ totalConnections: 0,
41
+ activeConnections: 0,
42
+ totalStreams: 0,
43
+ activeStreams: 0,
44
+ bytesReceived: 0,
45
+ bytesSent: 0,
46
+ packetsLost: 0,
47
+ rttMs: 0
48
+ }
49
+ }
50
+ };
51
+ this.messageQueue = [];
52
+ this.messageStats = new Map();
53
+ logger.info('QUIC Coordinator initialized', {
54
+ swarmId: config.swarmId,
55
+ topology: config.topology,
56
+ maxAgents: config.maxAgents
57
+ });
58
+ }
59
+ /**
60
+ * Start the coordinator (heartbeat and state sync)
61
+ */
62
+ async start() {
63
+ logger.info('Starting QUIC Coordinator', { swarmId: this.config.swarmId });
64
+ // Initialize QUIC client
65
+ await this.config.quicClient.initialize();
66
+ // Start heartbeat
67
+ this.startHeartbeat();
68
+ // Start state synchronization
69
+ this.startStateSync();
70
+ logger.info('QUIC Coordinator started successfully');
71
+ }
72
+ /**
73
+ * Stop the coordinator
74
+ */
75
+ async stop() {
76
+ logger.info('Stopping QUIC Coordinator', { swarmId: this.config.swarmId });
77
+ // Stop timers
78
+ if (this.heartbeatTimer) {
79
+ clearInterval(this.heartbeatTimer);
80
+ }
81
+ if (this.syncTimer) {
82
+ clearInterval(this.syncTimer);
83
+ }
84
+ // Close all connections
85
+ for (const [agentId, connection] of this.state.connections.entries()) {
86
+ await this.config.quicClient.closeConnection(connection.id);
87
+ logger.debug('Closed connection', { agentId, connectionId: connection.id });
88
+ }
89
+ // Shutdown QUIC client
90
+ await this.config.quicClient.shutdown();
91
+ logger.info('QUIC Coordinator stopped');
92
+ }
93
+ /**
94
+ * Register an agent in the swarm
95
+ */
96
+ async registerAgent(agent) {
97
+ if (this.state.agents.size >= this.config.maxAgents) {
98
+ throw new Error(`Maximum agents (${this.config.maxAgents}) reached`);
99
+ }
100
+ logger.info('Registering agent', {
101
+ agentId: agent.id,
102
+ role: agent.role,
103
+ host: agent.host,
104
+ port: agent.port
105
+ });
106
+ // Establish QUIC connection to agent
107
+ const connection = await this.config.connectionPool.getConnection(agent.host, agent.port);
108
+ // Store agent and connection
109
+ this.state.agents.set(agent.id, agent);
110
+ this.state.connections.set(agent.id, connection);
111
+ this.messageStats.set(agent.id, { sent: 0, received: 0, latency: [] });
112
+ // Update stats
113
+ this.state.stats.totalAgents = this.state.agents.size;
114
+ this.state.stats.activeAgents = this.state.agents.size;
115
+ // Establish topology-specific connections
116
+ await this.establishTopologyConnections(agent);
117
+ logger.info('Agent registered successfully', { agentId: agent.id });
118
+ }
119
+ /**
120
+ * Unregister an agent from the swarm
121
+ */
122
+ async unregisterAgent(agentId) {
123
+ const agent = this.state.agents.get(agentId);
124
+ if (!agent) {
125
+ logger.warn('Agent not found', { agentId });
126
+ return;
127
+ }
128
+ logger.info('Unregistering agent', { agentId });
129
+ // Close connection
130
+ const connection = this.state.connections.get(agentId);
131
+ if (connection) {
132
+ await this.config.quicClient.closeConnection(connection.id);
133
+ }
134
+ // Remove from state
135
+ this.state.agents.delete(agentId);
136
+ this.state.connections.delete(agentId);
137
+ this.messageStats.delete(agentId);
138
+ // Update stats
139
+ this.state.stats.totalAgents = this.state.agents.size;
140
+ this.state.stats.activeAgents = this.state.agents.size;
141
+ logger.info('Agent unregistered successfully', { agentId });
142
+ }
143
+ /**
144
+ * Send message to one or more agents
145
+ */
146
+ async sendMessage(message) {
147
+ const startTime = Date.now();
148
+ logger.debug('Sending message', {
149
+ messageId: message.id,
150
+ from: message.from,
151
+ to: message.to,
152
+ type: message.type
153
+ });
154
+ // Determine recipients based on topology
155
+ const recipients = this.resolveRecipients(message);
156
+ // Send message to each recipient
157
+ for (const recipientId of recipients) {
158
+ await this.sendToAgent(recipientId, message);
159
+ }
160
+ // Update stats
161
+ const latency = Date.now() - startTime;
162
+ this.updateMessageStats(message.from, 'sent', latency);
163
+ this.state.stats.totalMessages++;
164
+ logger.debug('Message sent successfully', {
165
+ messageId: message.id,
166
+ recipients: recipients.length,
167
+ latency
168
+ });
169
+ }
170
+ /**
171
+ * Broadcast message to all agents (except sender)
172
+ */
173
+ async broadcast(message) {
174
+ const broadcastMessage = {
175
+ ...message,
176
+ to: '*'
177
+ };
178
+ await this.sendMessage(broadcastMessage);
179
+ }
180
+ /**
181
+ * Get current swarm state
182
+ */
183
+ async getState() {
184
+ return {
185
+ ...this.state,
186
+ stats: await this.calculateStats()
187
+ };
188
+ }
189
+ /**
190
+ * Get agent statistics
191
+ */
192
+ getAgentStats(agentId) {
193
+ const stats = this.messageStats.get(agentId);
194
+ if (!stats) {
195
+ return null;
196
+ }
197
+ return {
198
+ sent: stats.sent,
199
+ received: stats.received,
200
+ avgLatency: stats.latency.length > 0
201
+ ? stats.latency.reduce((sum, l) => sum + l, 0) / stats.latency.length
202
+ : 0
203
+ };
204
+ }
205
+ /**
206
+ * Get all agent statistics
207
+ */
208
+ getAllAgentStats() {
209
+ const allStats = new Map();
210
+ for (const [agentId, stats] of this.messageStats.entries()) {
211
+ allStats.set(agentId, {
212
+ sent: stats.sent,
213
+ received: stats.received,
214
+ avgLatency: stats.latency.length > 0
215
+ ? stats.latency.reduce((sum, l) => sum + l, 0) / stats.latency.length
216
+ : 0
217
+ });
218
+ }
219
+ return allStats;
220
+ }
221
+ /**
222
+ * Synchronize state across all agents
223
+ */
224
+ async syncState() {
225
+ logger.debug('Synchronizing swarm state', { swarmId: this.config.swarmId });
226
+ const stateMessage = {
227
+ id: `sync-${Date.now()}`,
228
+ from: 'coordinator',
229
+ to: '*',
230
+ type: 'sync',
231
+ payload: {
232
+ swarmId: this.state.swarmId,
233
+ topology: this.state.topology,
234
+ agents: Array.from(this.state.agents.values()),
235
+ stats: this.calculateStats()
236
+ },
237
+ timestamp: Date.now()
238
+ };
239
+ await this.broadcast(stateMessage);
240
+ }
241
+ // ========== Private Methods ==========
242
+ /**
243
+ * Establish topology-specific connections
244
+ */
245
+ async establishTopologyConnections(agent) {
246
+ switch (this.config.topology) {
247
+ case 'mesh':
248
+ // In mesh, each agent connects to all others (handled by caller)
249
+ logger.debug('Mesh topology: agent connects to all', { agentId: agent.id });
250
+ break;
251
+ case 'hierarchical':
252
+ // In hierarchical, workers connect to coordinators
253
+ if (agent.role === 'worker') {
254
+ const coordinators = Array.from(this.state.agents.values())
255
+ .filter(a => a.role === 'coordinator');
256
+ logger.debug('Hierarchical topology: connecting worker to coordinators', {
257
+ agentId: agent.id,
258
+ coordinators: coordinators.length
259
+ });
260
+ }
261
+ break;
262
+ case 'ring':
263
+ // In ring, each agent connects to next agent in circular order
264
+ const agents = Array.from(this.state.agents.values());
265
+ if (agents.length > 1) {
266
+ logger.debug('Ring topology: establishing ring connections', {
267
+ agentId: agent.id,
268
+ totalAgents: agents.length
269
+ });
270
+ }
271
+ break;
272
+ case 'star':
273
+ // In star, all agents connect to central coordinator
274
+ if (agent.role === 'coordinator') {
275
+ logger.debug('Star topology: coordinator established', { agentId: agent.id });
276
+ }
277
+ else {
278
+ const coordinator = Array.from(this.state.agents.values())
279
+ .find(a => a.role === 'coordinator');
280
+ if (coordinator) {
281
+ logger.debug('Star topology: connecting to coordinator', {
282
+ agentId: agent.id,
283
+ coordinator: coordinator.id
284
+ });
285
+ }
286
+ }
287
+ break;
288
+ }
289
+ }
290
+ /**
291
+ * Resolve message recipients based on topology
292
+ */
293
+ resolveRecipients(message) {
294
+ const recipients = [];
295
+ const to = Array.isArray(message.to) ? message.to : [message.to];
296
+ for (const target of to) {
297
+ if (target === '*') {
298
+ // Broadcast to all (except sender)
299
+ recipients.push(...Array.from(this.state.agents.keys())
300
+ .filter(id => id !== message.from));
301
+ }
302
+ else {
303
+ recipients.push(target);
304
+ }
305
+ }
306
+ // Apply topology-specific routing
307
+ return this.applyTopologyRouting(message.from, recipients);
308
+ }
309
+ /**
310
+ * Apply topology-specific routing rules
311
+ */
312
+ applyTopologyRouting(senderId, recipients) {
313
+ switch (this.config.topology) {
314
+ case 'mesh':
315
+ // Direct routing in mesh topology
316
+ return recipients;
317
+ case 'hierarchical':
318
+ // Route through coordinator in hierarchical
319
+ const sender = this.state.agents.get(senderId);
320
+ if (sender?.role === 'worker') {
321
+ // Worker messages go through coordinator
322
+ const coordinators = Array.from(this.state.agents.values())
323
+ .filter(a => a.role === 'coordinator')
324
+ .map(a => a.id);
325
+ return coordinators.length > 0 ? coordinators : recipients;
326
+ }
327
+ return recipients;
328
+ case 'ring':
329
+ // Forward to next agent in ring
330
+ const agents = Array.from(this.state.agents.keys());
331
+ const currentIndex = agents.indexOf(senderId);
332
+ const nextIndex = (currentIndex + 1) % agents.length;
333
+ return [agents[nextIndex]];
334
+ case 'star':
335
+ // Route through central coordinator
336
+ const coordinator = Array.from(this.state.agents.values())
337
+ .find(a => a.role === 'coordinator');
338
+ if (coordinator && senderId !== coordinator.id) {
339
+ return [coordinator.id];
340
+ }
341
+ return recipients;
342
+ default:
343
+ return recipients;
344
+ }
345
+ }
346
+ /**
347
+ * Send message to specific agent
348
+ */
349
+ async sendToAgent(agentId, message) {
350
+ const connection = this.state.connections.get(agentId);
351
+ if (!connection) {
352
+ logger.warn('Connection not found for agent', { agentId });
353
+ return;
354
+ }
355
+ try {
356
+ // Create QUIC stream
357
+ const stream = await this.config.quicClient.createStream(connection.id);
358
+ // Serialize message
359
+ const messageBytes = this.serializeMessage(message);
360
+ // Send message
361
+ await stream.send(messageBytes);
362
+ // Close stream
363
+ await stream.close();
364
+ logger.debug('Message sent to agent', { agentId, messageId: message.id });
365
+ }
366
+ catch (error) {
367
+ logger.error('Failed to send message to agent', { agentId, error });
368
+ throw error;
369
+ }
370
+ }
371
+ /**
372
+ * Serialize message to bytes
373
+ */
374
+ serializeMessage(message) {
375
+ const json = JSON.stringify(message);
376
+ const encoder = new TextEncoder();
377
+ return encoder.encode(json);
378
+ }
379
+ /**
380
+ * Start heartbeat timer
381
+ */
382
+ startHeartbeat() {
383
+ this.heartbeatTimer = setInterval(async () => {
384
+ logger.debug('Sending heartbeat', { swarmId: this.config.swarmId });
385
+ const heartbeat = {
386
+ id: `heartbeat-${Date.now()}`,
387
+ from: 'coordinator',
388
+ to: '*',
389
+ type: 'heartbeat',
390
+ payload: { timestamp: Date.now() },
391
+ timestamp: Date.now()
392
+ };
393
+ try {
394
+ await this.broadcast(heartbeat);
395
+ }
396
+ catch (error) {
397
+ logger.error('Heartbeat failed', { error });
398
+ }
399
+ }, this.config.heartbeatInterval);
400
+ }
401
+ /**
402
+ * Start state sync timer
403
+ */
404
+ startStateSync() {
405
+ this.syncTimer = setInterval(async () => {
406
+ try {
407
+ await this.syncState();
408
+ }
409
+ catch (error) {
410
+ logger.error('State sync failed', { error });
411
+ }
412
+ }, this.config.statesSyncInterval);
413
+ }
414
+ /**
415
+ * Update message statistics
416
+ */
417
+ updateMessageStats(agentId, type, latency) {
418
+ const stats = this.messageStats.get(agentId);
419
+ if (!stats) {
420
+ return;
421
+ }
422
+ if (type === 'sent') {
423
+ stats.sent++;
424
+ }
425
+ else {
426
+ stats.received++;
427
+ }
428
+ stats.latency.push(latency);
429
+ // Keep only last 100 latency measurements
430
+ if (stats.latency.length > 100) {
431
+ stats.latency.shift();
432
+ }
433
+ }
434
+ /**
435
+ * Calculate current swarm statistics
436
+ */
437
+ async calculateStats() {
438
+ const quicStats = await this.config.quicClient.getStats();
439
+ // Calculate messages per second (last minute average)
440
+ const messagesPerSecond = this.state.stats.totalMessages / 60;
441
+ // Calculate average latency across all agents
442
+ let totalLatency = 0;
443
+ let latencyCount = 0;
444
+ for (const stats of this.messageStats.values()) {
445
+ if (stats.latency.length > 0) {
446
+ totalLatency += stats.latency.reduce((sum, l) => sum + l, 0);
447
+ latencyCount += stats.latency.length;
448
+ }
449
+ }
450
+ const averageLatency = latencyCount > 0 ? totalLatency / latencyCount : 0;
451
+ return {
452
+ totalAgents: this.state.agents.size,
453
+ activeAgents: this.state.agents.size,
454
+ totalMessages: this.state.stats.totalMessages,
455
+ messagesPerSecond,
456
+ averageLatency,
457
+ quicStats
458
+ };
459
+ }
460
+ }