@sparkleideas/swarm 3.0.0-alpha.7

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.
Files changed (65) hide show
  1. package/MIGRATION.md +472 -0
  2. package/README.md +634 -0
  3. package/__tests__/consensus.test.ts +577 -0
  4. package/__tests__/coordinator.test.ts +501 -0
  5. package/__tests__/queen-coordinator.test.ts +1335 -0
  6. package/__tests__/topology.test.ts +621 -0
  7. package/package.json +32 -0
  8. package/src/agent-pool.ts +476 -0
  9. package/src/application/commands/create-task.command.ts +124 -0
  10. package/src/application/commands/spawn-agent.command.ts +122 -0
  11. package/src/application/index.ts +30 -0
  12. package/src/application/services/swarm-application-service.ts +200 -0
  13. package/src/attention-coordinator.ts +1000 -0
  14. package/src/consensus/byzantine.ts +431 -0
  15. package/src/consensus/gossip.ts +513 -0
  16. package/src/consensus/index.ts +267 -0
  17. package/src/consensus/raft.ts +443 -0
  18. package/src/coordination/agent-registry.ts +544 -0
  19. package/src/coordination/index.ts +23 -0
  20. package/src/coordination/swarm-hub.ts +776 -0
  21. package/src/coordination/task-orchestrator.ts +605 -0
  22. package/src/domain/entities/agent.d.ts +151 -0
  23. package/src/domain/entities/agent.d.ts.map +1 -0
  24. package/src/domain/entities/agent.js +280 -0
  25. package/src/domain/entities/agent.js.map +1 -0
  26. package/src/domain/entities/agent.ts +370 -0
  27. package/src/domain/entities/task.d.ts +133 -0
  28. package/src/domain/entities/task.d.ts.map +1 -0
  29. package/src/domain/entities/task.js +261 -0
  30. package/src/domain/entities/task.js.map +1 -0
  31. package/src/domain/entities/task.ts +319 -0
  32. package/src/domain/index.ts +41 -0
  33. package/src/domain/repositories/agent-repository.interface.d.ts +57 -0
  34. package/src/domain/repositories/agent-repository.interface.d.ts.map +1 -0
  35. package/src/domain/repositories/agent-repository.interface.js +9 -0
  36. package/src/domain/repositories/agent-repository.interface.js.map +1 -0
  37. package/src/domain/repositories/agent-repository.interface.ts +69 -0
  38. package/src/domain/repositories/task-repository.interface.d.ts +61 -0
  39. package/src/domain/repositories/task-repository.interface.d.ts.map +1 -0
  40. package/src/domain/repositories/task-repository.interface.js +9 -0
  41. package/src/domain/repositories/task-repository.interface.js.map +1 -0
  42. package/src/domain/repositories/task-repository.interface.ts +75 -0
  43. package/src/domain/services/coordination-service.ts +320 -0
  44. package/src/federation-hub.d.ts +284 -0
  45. package/src/federation-hub.d.ts.map +1 -0
  46. package/src/federation-hub.js +692 -0
  47. package/src/federation-hub.js.map +1 -0
  48. package/src/federation-hub.ts +979 -0
  49. package/src/index.ts +348 -0
  50. package/src/message-bus.ts +607 -0
  51. package/src/queen-coordinator.ts +2025 -0
  52. package/src/shared/events.ts +285 -0
  53. package/src/shared/types.ts +389 -0
  54. package/src/topology-manager.ts +656 -0
  55. package/src/types.ts +545 -0
  56. package/src/unified-coordinator.ts +1844 -0
  57. package/src/workers/index.ts +65 -0
  58. package/src/workers/worker-dispatch.d.ts +234 -0
  59. package/src/workers/worker-dispatch.d.ts.map +1 -0
  60. package/src/workers/worker-dispatch.js +842 -0
  61. package/src/workers/worker-dispatch.js.map +1 -0
  62. package/src/workers/worker-dispatch.ts +1076 -0
  63. package/tmp.json +0 -0
  64. package/tsconfig.json +9 -0
  65. package/vitest.config.ts +20 -0
@@ -0,0 +1,692 @@
1
+ /**
2
+ * Federation Hub - Ephemeral Agent Coordination
3
+ *
4
+ * Provides cross-swarm coordination and ephemeral agent management
5
+ * for distributed multi-swarm architectures.
6
+ *
7
+ * Features:
8
+ * - Ephemeral agent spawning (short-lived, task-specific)
9
+ * - Cross-swarm communication and coordination
10
+ * - Federation protocol for distributed consensus
11
+ * - Resource allocation and load balancing
12
+ * - Agent lifecycle management with auto-cleanup
13
+ *
14
+ * Performance Targets:
15
+ * - Agent spawn: <50ms
16
+ * - Cross-swarm message: <100ms
17
+ * - Federation sync: <500ms
18
+ * - Auto-cleanup: Background, non-blocking
19
+ *
20
+ * Implements ADR-001: @sparkleideas/agentic-flow@alpha compatibility
21
+ * Implements ADR-003: Unified coordination engine
22
+ *
23
+ * @module @sparkleideas/swarm/federation-hub
24
+ * @version 3.0.0-alpha.1
25
+ */
26
+ import { EventEmitter } from 'events';
27
+ // ============================================================================
28
+ // Default Configuration
29
+ // ============================================================================
30
+ const DEFAULT_CONFIG = {
31
+ federationId: `federation_${Date.now()}`,
32
+ maxEphemeralAgents: 100,
33
+ defaultTTL: 300000, // 5 minutes
34
+ syncIntervalMs: 30000, // 30 seconds
35
+ autoCleanup: true,
36
+ cleanupIntervalMs: 60000, // 1 minute
37
+ communicationTimeoutMs: 5000,
38
+ enableConsensus: true,
39
+ consensusQuorum: 0.66,
40
+ };
41
+ // ============================================================================
42
+ // Federation Hub Implementation
43
+ // ============================================================================
44
+ export class FederationHub extends EventEmitter {
45
+ config;
46
+ swarms = new Map();
47
+ ephemeralAgents = new Map();
48
+ messages = [];
49
+ proposals = new Map();
50
+ syncInterval;
51
+ cleanupInterval;
52
+ startTime;
53
+ stats;
54
+ // ============================================================================
55
+ // Secondary Indexes for O(1) Lookups (Performance Optimization)
56
+ // ============================================================================
57
+ /** Index: swarmId -> Set of agentIds */
58
+ agentsBySwarm = new Map();
59
+ /** Index: status -> Set of agentIds */
60
+ agentsByStatus = new Map();
61
+ constructor(config) {
62
+ super();
63
+ this.config = { ...DEFAULT_CONFIG, ...config };
64
+ this.startTime = new Date();
65
+ this.stats = {
66
+ messagesExchanged: 0,
67
+ consensusProposals: 0,
68
+ completedAgents: 0,
69
+ failedAgents: 0,
70
+ totalAgentLifespanMs: 0,
71
+ };
72
+ // Initialize status index sets
73
+ this.agentsByStatus.set('spawning', new Set());
74
+ this.agentsByStatus.set('active', new Set());
75
+ this.agentsByStatus.set('completing', new Set());
76
+ this.agentsByStatus.set('terminated', new Set());
77
+ }
78
+ // ==========================================================================
79
+ // Index Maintenance Helpers
80
+ // ==========================================================================
81
+ /**
82
+ * Add agent to indexes - O(1)
83
+ */
84
+ addAgentToIndexes(agent) {
85
+ // Add to swarm index
86
+ if (!this.agentsBySwarm.has(agent.swarmId)) {
87
+ this.agentsBySwarm.set(agent.swarmId, new Set());
88
+ }
89
+ this.agentsBySwarm.get(agent.swarmId).add(agent.id);
90
+ // Add to status index
91
+ this.agentsByStatus.get(agent.status).add(agent.id);
92
+ }
93
+ /**
94
+ * Remove agent from indexes - O(1)
95
+ */
96
+ removeAgentFromIndexes(agent) {
97
+ // Remove from swarm index
98
+ const swarmSet = this.agentsBySwarm.get(agent.swarmId);
99
+ if (swarmSet) {
100
+ swarmSet.delete(agent.id);
101
+ if (swarmSet.size === 0) {
102
+ this.agentsBySwarm.delete(agent.swarmId);
103
+ }
104
+ }
105
+ // Remove from status index
106
+ this.agentsByStatus.get(agent.status)?.delete(agent.id);
107
+ }
108
+ /**
109
+ * Update agent status in index - O(1)
110
+ */
111
+ updateAgentStatusIndex(agent, oldStatus) {
112
+ this.agentsByStatus.get(oldStatus)?.delete(agent.id);
113
+ this.agentsByStatus.get(agent.status).add(agent.id);
114
+ }
115
+ /**
116
+ * Get agents by swarm using index - O(k) where k is agents in swarm
117
+ */
118
+ getAgentIdsBySwarm(swarmId) {
119
+ const agentIds = this.agentsBySwarm.get(swarmId);
120
+ return agentIds ? Array.from(agentIds) : [];
121
+ }
122
+ /**
123
+ * Get agents by status using index - O(k) where k is agents with status
124
+ */
125
+ getAgentIdsByStatus(status) {
126
+ const agentIds = this.agentsByStatus.get(status);
127
+ return agentIds ? Array.from(agentIds) : [];
128
+ }
129
+ // ==========================================================================
130
+ // Lifecycle
131
+ // ==========================================================================
132
+ /**
133
+ * Initialize the federation hub
134
+ */
135
+ async initialize() {
136
+ // Start sync interval
137
+ this.syncInterval = setInterval(() => this.syncFederation(), this.config.syncIntervalMs);
138
+ // Start cleanup interval if enabled
139
+ if (this.config.autoCleanup) {
140
+ this.cleanupInterval = setInterval(() => this.cleanupExpiredAgents(), this.config.cleanupIntervalMs);
141
+ }
142
+ this.emitEvent('federation_synced');
143
+ }
144
+ /**
145
+ * Shutdown the federation hub
146
+ */
147
+ async shutdown() {
148
+ if (this.syncInterval) {
149
+ clearInterval(this.syncInterval);
150
+ }
151
+ if (this.cleanupInterval) {
152
+ clearInterval(this.cleanupInterval);
153
+ }
154
+ // Terminate all active ephemeral agents using index - O(k) where k = active + spawning
155
+ const activeIds = this.getAgentIdsByStatus('active');
156
+ const spawningIds = this.getAgentIdsByStatus('spawning');
157
+ const toTerminate = [...activeIds, ...spawningIds];
158
+ await Promise.all(toTerminate.map(id => this.terminateAgent(id)));
159
+ // Clear all data structures and indexes
160
+ this.swarms.clear();
161
+ this.ephemeralAgents.clear();
162
+ this.proposals.clear();
163
+ this.agentsBySwarm.clear();
164
+ for (const status of this.agentsByStatus.values()) {
165
+ status.clear();
166
+ }
167
+ }
168
+ // ==========================================================================
169
+ // Swarm Registration
170
+ // ==========================================================================
171
+ /**
172
+ * Register a swarm with the federation
173
+ */
174
+ registerSwarm(registration) {
175
+ const fullRegistration = {
176
+ ...registration,
177
+ registeredAt: new Date(),
178
+ lastHeartbeat: new Date(),
179
+ };
180
+ this.swarms.set(registration.swarmId, fullRegistration);
181
+ this.emitEvent('swarm_joined', registration.swarmId);
182
+ }
183
+ /**
184
+ * Unregister a swarm from the federation
185
+ */
186
+ unregisterSwarm(swarmId) {
187
+ const removed = this.swarms.delete(swarmId);
188
+ if (removed) {
189
+ // Terminate all ephemeral agents in this swarm using index - O(k)
190
+ const agentIds = this.getAgentIdsBySwarm(swarmId);
191
+ for (const agentId of agentIds) {
192
+ this.terminateAgent(agentId);
193
+ }
194
+ this.emitEvent('swarm_left', swarmId);
195
+ }
196
+ return removed;
197
+ }
198
+ /**
199
+ * Update swarm heartbeat
200
+ */
201
+ heartbeat(swarmId, currentAgents) {
202
+ const swarm = this.swarms.get(swarmId);
203
+ if (!swarm)
204
+ return false;
205
+ swarm.lastHeartbeat = new Date();
206
+ if (currentAgents !== undefined) {
207
+ swarm.currentAgents = currentAgents;
208
+ }
209
+ if (swarm.status === 'inactive') {
210
+ swarm.status = 'active';
211
+ }
212
+ return true;
213
+ }
214
+ /**
215
+ * Get all registered swarms
216
+ */
217
+ getSwarms() {
218
+ return Array.from(this.swarms.values());
219
+ }
220
+ /**
221
+ * Get swarm by ID
222
+ */
223
+ getSwarm(swarmId) {
224
+ return this.swarms.get(swarmId);
225
+ }
226
+ // ==========================================================================
227
+ // Ephemeral Agent Management
228
+ // ==========================================================================
229
+ /**
230
+ * Spawn an ephemeral agent
231
+ */
232
+ async spawnEphemeralAgent(options) {
233
+ // Select target swarm
234
+ const targetSwarmId = options.swarmId || this.selectOptimalSwarm(options);
235
+ if (!targetSwarmId) {
236
+ return {
237
+ agentId: '',
238
+ swarmId: '',
239
+ status: 'failed',
240
+ estimatedTTL: 0,
241
+ error: 'No suitable swarm available',
242
+ };
243
+ }
244
+ const swarm = this.swarms.get(targetSwarmId);
245
+ if (!swarm) {
246
+ return {
247
+ agentId: '',
248
+ swarmId: targetSwarmId,
249
+ status: 'failed',
250
+ estimatedTTL: 0,
251
+ error: 'Swarm not found',
252
+ };
253
+ }
254
+ // Check capacity
255
+ const swarmAgentCount = this.getSwarmAgentCount(targetSwarmId);
256
+ if (swarmAgentCount >= this.config.maxEphemeralAgents) {
257
+ return {
258
+ agentId: '',
259
+ swarmId: targetSwarmId,
260
+ status: 'failed',
261
+ estimatedTTL: 0,
262
+ error: 'Swarm at capacity',
263
+ };
264
+ }
265
+ // Create ephemeral agent
266
+ const ttl = options.ttl || this.config.defaultTTL;
267
+ const agentId = `ephemeral_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
268
+ const now = new Date();
269
+ const agent = {
270
+ id: agentId,
271
+ swarmId: targetSwarmId,
272
+ type: options.type,
273
+ task: options.task,
274
+ status: 'spawning',
275
+ ttl,
276
+ createdAt: now,
277
+ expiresAt: new Date(now.getTime() + ttl),
278
+ metadata: options.metadata,
279
+ };
280
+ this.ephemeralAgents.set(agentId, agent);
281
+ this.addAgentToIndexes(agent);
282
+ // Simulate spawn (in real implementation, this would call the swarm coordinator)
283
+ setTimeout(() => {
284
+ const a = this.ephemeralAgents.get(agentId);
285
+ if (a && a.status === 'spawning') {
286
+ this.updateAgentStatusIndex(a, 'spawning');
287
+ a.status = 'active';
288
+ this.emitEvent('agent_spawned', targetSwarmId, agentId);
289
+ }
290
+ }, 50);
291
+ // If waiting for completion
292
+ if (options.waitForCompletion) {
293
+ const timeout = options.completionTimeout || ttl;
294
+ const result = await this.waitForAgentCompletion(agentId, timeout);
295
+ return {
296
+ agentId,
297
+ swarmId: targetSwarmId,
298
+ status: result ? 'spawned' : 'failed',
299
+ estimatedTTL: ttl,
300
+ result: result?.result,
301
+ error: result?.error?.message,
302
+ };
303
+ }
304
+ return {
305
+ agentId,
306
+ swarmId: targetSwarmId,
307
+ status: 'spawned',
308
+ estimatedTTL: ttl,
309
+ };
310
+ }
311
+ /**
312
+ * Complete an ephemeral agent's task
313
+ */
314
+ completeAgent(agentId, result) {
315
+ const agent = this.ephemeralAgents.get(agentId);
316
+ if (!agent)
317
+ return false;
318
+ const oldStatus = agent.status;
319
+ agent.status = 'completing';
320
+ this.updateAgentStatusIndex(agent, oldStatus);
321
+ agent.result = result;
322
+ agent.completedAt = new Date();
323
+ const lifespan = agent.completedAt.getTime() - agent.createdAt.getTime();
324
+ this.stats.completedAgents++;
325
+ this.stats.totalAgentLifespanMs += lifespan;
326
+ // Mark as terminated after a brief delay
327
+ setTimeout(() => {
328
+ const a = this.ephemeralAgents.get(agentId);
329
+ if (a) {
330
+ this.updateAgentStatusIndex(a, 'completing');
331
+ a.status = 'terminated';
332
+ this.emitEvent('agent_completed', a.swarmId, agentId);
333
+ }
334
+ }, 100);
335
+ return true;
336
+ }
337
+ /**
338
+ * Terminate an ephemeral agent
339
+ */
340
+ async terminateAgent(agentId, error) {
341
+ const agent = this.ephemeralAgents.get(agentId);
342
+ if (!agent)
343
+ return false;
344
+ const oldStatus = agent.status;
345
+ agent.status = 'terminated';
346
+ this.updateAgentStatusIndex(agent, oldStatus);
347
+ agent.completedAt = new Date();
348
+ if (error) {
349
+ agent.error = error;
350
+ this.stats.failedAgents++;
351
+ this.emitEvent('agent_failed', agent.swarmId, agentId);
352
+ }
353
+ else {
354
+ this.stats.completedAgents++;
355
+ this.emitEvent('agent_completed', agent.swarmId, agentId);
356
+ }
357
+ const lifespan = agent.completedAt.getTime() - agent.createdAt.getTime();
358
+ this.stats.totalAgentLifespanMs += lifespan;
359
+ return true;
360
+ }
361
+ /**
362
+ * Get ephemeral agent by ID
363
+ */
364
+ getAgent(agentId) {
365
+ return this.ephemeralAgents.get(agentId);
366
+ }
367
+ /**
368
+ * Get all ephemeral agents
369
+ */
370
+ getAgents(swarmId) {
371
+ const agents = Array.from(this.ephemeralAgents.values());
372
+ return swarmId ? agents.filter(a => a.swarmId === swarmId) : agents;
373
+ }
374
+ /**
375
+ * Get active ephemeral agents
376
+ */
377
+ getActiveAgents(swarmId) {
378
+ return this.getAgents(swarmId).filter(a => a.status === 'active' || a.status === 'spawning');
379
+ }
380
+ // ==========================================================================
381
+ // Cross-Swarm Communication
382
+ // ==========================================================================
383
+ /**
384
+ * Send a message to another swarm
385
+ */
386
+ async sendMessage(sourceSwarmId, targetSwarmId, payload) {
387
+ const targetSwarm = this.swarms.get(targetSwarmId);
388
+ if (!targetSwarm || targetSwarm.status === 'inactive') {
389
+ return false;
390
+ }
391
+ const message = {
392
+ id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
393
+ type: 'direct',
394
+ sourceSwarmId,
395
+ targetSwarmId,
396
+ payload,
397
+ timestamp: new Date(),
398
+ };
399
+ this.messages.push(message);
400
+ this.stats.messagesExchanged++;
401
+ this.emitEvent('message_sent', sourceSwarmId);
402
+ // In real implementation, this would send to the target swarm's endpoint
403
+ // For now, we emit an event that can be listened to
404
+ this.emit('message', message);
405
+ return true;
406
+ }
407
+ /**
408
+ * Broadcast a message to all swarms
409
+ */
410
+ async broadcast(sourceSwarmId, payload) {
411
+ let sent = 0;
412
+ for (const swarm of this.swarms.values()) {
413
+ if (swarm.swarmId !== sourceSwarmId && swarm.status === 'active') {
414
+ const success = await this.sendMessage(sourceSwarmId, swarm.swarmId, payload);
415
+ if (success)
416
+ sent++;
417
+ }
418
+ }
419
+ return sent;
420
+ }
421
+ /**
422
+ * Get recent messages
423
+ */
424
+ getMessages(limit = 100) {
425
+ return this.messages.slice(-limit);
426
+ }
427
+ // ==========================================================================
428
+ // Federation Consensus
429
+ // ==========================================================================
430
+ /**
431
+ * Propose a value for federation-wide consensus
432
+ */
433
+ async propose(proposerId, type, value, timeoutMs = 30000) {
434
+ if (!this.config.enableConsensus) {
435
+ throw new Error('Consensus is disabled');
436
+ }
437
+ const proposal = {
438
+ id: `proposal_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
439
+ proposerId,
440
+ type,
441
+ value,
442
+ votes: new Map([[proposerId, true]]),
443
+ status: 'pending',
444
+ createdAt: new Date(),
445
+ expiresAt: new Date(Date.now() + timeoutMs),
446
+ };
447
+ this.proposals.set(proposal.id, proposal);
448
+ this.stats.consensusProposals++;
449
+ this.emitEvent('consensus_started', proposerId);
450
+ // Request votes from all active swarms
451
+ await this.broadcast(proposerId, {
452
+ type: 'vote_request',
453
+ proposalId: proposal.id,
454
+ proposalType: type,
455
+ value,
456
+ });
457
+ return proposal;
458
+ }
459
+ /**
460
+ * Vote on a proposal
461
+ */
462
+ vote(swarmId, proposalId, approve) {
463
+ const proposal = this.proposals.get(proposalId);
464
+ if (!proposal || proposal.status !== 'pending') {
465
+ return false;
466
+ }
467
+ if (new Date() > proposal.expiresAt) {
468
+ proposal.status = 'rejected';
469
+ return false;
470
+ }
471
+ proposal.votes.set(swarmId, approve);
472
+ // Check if quorum reached
473
+ const activeSwarms = this.getActiveSwarmCount();
474
+ const approvals = Array.from(proposal.votes.values()).filter(v => v).length;
475
+ const rejections = Array.from(proposal.votes.values()).filter(v => !v).length;
476
+ const quorumThreshold = Math.ceil(activeSwarms * this.config.consensusQuorum);
477
+ if (approvals >= quorumThreshold) {
478
+ proposal.status = 'accepted';
479
+ this.emitEvent('consensus_completed', proposal.proposerId);
480
+ }
481
+ else if (rejections > activeSwarms - quorumThreshold) {
482
+ proposal.status = 'rejected';
483
+ this.emitEvent('consensus_completed', proposal.proposerId);
484
+ }
485
+ return true;
486
+ }
487
+ /**
488
+ * Get proposal by ID
489
+ */
490
+ getProposal(proposalId) {
491
+ return this.proposals.get(proposalId);
492
+ }
493
+ /**
494
+ * Get all pending proposals
495
+ */
496
+ getPendingProposals() {
497
+ return Array.from(this.proposals.values()).filter(p => p.status === 'pending');
498
+ }
499
+ // ==========================================================================
500
+ // Statistics & Monitoring
501
+ // ==========================================================================
502
+ /**
503
+ * Get federation statistics
504
+ */
505
+ getStats() {
506
+ const activeAgents = this.getActiveAgents().length;
507
+ const avgLifespan = this.stats.completedAgents > 0
508
+ ? this.stats.totalAgentLifespanMs / this.stats.completedAgents
509
+ : 0;
510
+ return {
511
+ federationId: this.config.federationId,
512
+ totalSwarms: this.swarms.size,
513
+ activeSwarms: this.getActiveSwarmCount(),
514
+ totalEphemeralAgents: this.ephemeralAgents.size,
515
+ activeEphemeralAgents: activeAgents,
516
+ completedAgents: this.stats.completedAgents,
517
+ failedAgents: this.stats.failedAgents,
518
+ avgAgentLifespanMs: avgLifespan,
519
+ messagesExchanged: this.stats.messagesExchanged,
520
+ consensusProposals: this.stats.consensusProposals,
521
+ uptime: Date.now() - this.startTime.getTime(),
522
+ };
523
+ }
524
+ // ==========================================================================
525
+ // Private Helpers
526
+ // ==========================================================================
527
+ selectOptimalSwarm(options) {
528
+ const candidates = [];
529
+ for (const swarm of this.swarms.values()) {
530
+ if (swarm.status !== 'active')
531
+ continue;
532
+ // Check capacity
533
+ const agentCount = this.getSwarmAgentCount(swarm.swarmId);
534
+ if (agentCount >= swarm.maxAgents)
535
+ continue;
536
+ // Check capabilities
537
+ if (options.capabilities) {
538
+ const hasAllCapabilities = options.capabilities.every(cap => swarm.capabilities.includes(cap));
539
+ if (!hasAllCapabilities)
540
+ continue;
541
+ }
542
+ // Calculate score (higher is better)
543
+ let score = 100;
544
+ // Prefer swarms with more available capacity
545
+ const availableCapacity = swarm.maxAgents - agentCount;
546
+ score += availableCapacity * 5;
547
+ // Prefer recently active swarms
548
+ const lastHeartbeatAge = Date.now() - swarm.lastHeartbeat.getTime();
549
+ score -= lastHeartbeatAge / 10000;
550
+ candidates.push({ swarmId: swarm.swarmId, score });
551
+ }
552
+ if (candidates.length === 0)
553
+ return null;
554
+ // Sort by score and return best
555
+ candidates.sort((a, b) => b.score - a.score);
556
+ return candidates[0].swarmId;
557
+ }
558
+ getSwarmAgentCount(swarmId) {
559
+ // Use index for O(1) lookup instead of O(n) filter
560
+ const swarmAgents = this.agentsBySwarm.get(swarmId);
561
+ if (!swarmAgents)
562
+ return 0;
563
+ // Count only active and spawning agents
564
+ let count = 0;
565
+ for (const agentId of swarmAgents) {
566
+ const agent = this.ephemeralAgents.get(agentId);
567
+ if (agent && (agent.status === 'active' || agent.status === 'spawning')) {
568
+ count++;
569
+ }
570
+ }
571
+ return count;
572
+ }
573
+ getActiveSwarmCount() {
574
+ return Array.from(this.swarms.values()).filter(s => s.status === 'active').length;
575
+ }
576
+ async waitForAgentCompletion(agentId, timeout) {
577
+ return new Promise((resolve) => {
578
+ const startTime = Date.now();
579
+ const check = () => {
580
+ const agent = this.ephemeralAgents.get(agentId);
581
+ if (!agent) {
582
+ resolve(null);
583
+ return;
584
+ }
585
+ if (agent.status === 'terminated' || agent.status === 'completing') {
586
+ resolve(agent);
587
+ return;
588
+ }
589
+ if (Date.now() - startTime > timeout) {
590
+ resolve(null);
591
+ return;
592
+ }
593
+ setTimeout(check, 100);
594
+ };
595
+ check();
596
+ });
597
+ }
598
+ syncFederation() {
599
+ const now = new Date();
600
+ const heartbeatTimeout = this.config.syncIntervalMs * 3;
601
+ // Check for inactive swarms
602
+ for (const swarm of this.swarms.values()) {
603
+ const age = now.getTime() - swarm.lastHeartbeat.getTime();
604
+ if (age > heartbeatTimeout && swarm.status === 'active') {
605
+ swarm.status = 'degraded';
606
+ this.emitEvent('swarm_degraded', swarm.swarmId);
607
+ }
608
+ else if (age > heartbeatTimeout * 2 && swarm.status === 'degraded') {
609
+ swarm.status = 'inactive';
610
+ }
611
+ }
612
+ // Check for expired proposals
613
+ for (const proposal of this.proposals.values()) {
614
+ if (proposal.status === 'pending' && now > proposal.expiresAt) {
615
+ proposal.status = 'rejected';
616
+ }
617
+ }
618
+ this.emitEvent('federation_synced');
619
+ }
620
+ cleanupExpiredAgents() {
621
+ const now = new Date();
622
+ // Use status index to only check active agents - O(k) instead of O(n)
623
+ const activeIds = this.getAgentIdsByStatus('active');
624
+ for (const agentId of activeIds) {
625
+ const agent = this.ephemeralAgents.get(agentId);
626
+ if (agent && now > agent.expiresAt) {
627
+ this.updateAgentStatusIndex(agent, 'active');
628
+ agent.status = 'terminated';
629
+ agent.completedAt = now;
630
+ agent.error = new Error('Agent TTL expired');
631
+ this.stats.failedAgents++;
632
+ this.emitEvent('agent_expired', agent.swarmId, agent.id);
633
+ }
634
+ }
635
+ // Clean up old terminated agents using index - O(k)
636
+ const cleanupThreshold = 5 * 60 * 1000;
637
+ const terminatedIds = this.getAgentIdsByStatus('terminated');
638
+ for (const agentId of terminatedIds) {
639
+ const agent = this.ephemeralAgents.get(agentId);
640
+ if (agent &&
641
+ agent.completedAt &&
642
+ now.getTime() - agent.completedAt.getTime() > cleanupThreshold) {
643
+ this.removeAgentFromIndexes(agent);
644
+ this.ephemeralAgents.delete(agentId);
645
+ }
646
+ }
647
+ }
648
+ emitEvent(type, swarmId, agentId, data) {
649
+ const event = {
650
+ type,
651
+ federationId: this.config.federationId,
652
+ swarmId,
653
+ agentId,
654
+ data,
655
+ timestamp: new Date(),
656
+ };
657
+ this.emit('event', event);
658
+ this.emit(type, event);
659
+ }
660
+ }
661
+ // ============================================================================
662
+ // Factory Functions
663
+ // ============================================================================
664
+ /**
665
+ * Create a new Federation Hub instance
666
+ */
667
+ export function createFederationHub(config) {
668
+ return new FederationHub(config);
669
+ }
670
+ /**
671
+ * Global federation hub instance
672
+ */
673
+ let defaultFederationHub = null;
674
+ /**
675
+ * Get or create the default federation hub
676
+ */
677
+ export function getDefaultFederationHub() {
678
+ if (!defaultFederationHub) {
679
+ defaultFederationHub = createFederationHub();
680
+ }
681
+ return defaultFederationHub;
682
+ }
683
+ /**
684
+ * Reset the default federation hub
685
+ */
686
+ export async function resetDefaultFederationHub() {
687
+ if (defaultFederationHub) {
688
+ await defaultFederationHub.shutdown();
689
+ defaultFederationHub = null;
690
+ }
691
+ }
692
+ //# sourceMappingURL=federation-hub.js.map