@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,979 @@
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
+
27
+ import { EventEmitter } from 'events';
28
+
29
+ // ============================================================================
30
+ // Types
31
+ // ============================================================================
32
+
33
+ export type FederationId = string;
34
+ export type SwarmId = string;
35
+ export type EphemeralAgentId = string;
36
+
37
+ export interface FederationConfig {
38
+ /** Federation identifier */
39
+ federationId?: FederationId;
40
+ /** Maximum ephemeral agents per swarm */
41
+ maxEphemeralAgents?: number;
42
+ /** Default TTL for ephemeral agents (ms) */
43
+ defaultTTL?: number;
44
+ /** Sync interval for federation state (ms) */
45
+ syncIntervalMs?: number;
46
+ /** Enable auto-cleanup of expired agents */
47
+ autoCleanup?: boolean;
48
+ /** Cleanup check interval (ms) */
49
+ cleanupIntervalMs?: number;
50
+ /** Cross-swarm communication timeout (ms) */
51
+ communicationTimeoutMs?: number;
52
+ /** Enable federation-wide consensus */
53
+ enableConsensus?: boolean;
54
+ /** Consensus quorum percentage */
55
+ consensusQuorum?: number;
56
+ }
57
+
58
+ export interface SwarmRegistration {
59
+ swarmId: SwarmId;
60
+ name: string;
61
+ endpoint?: string;
62
+ capabilities: string[];
63
+ maxAgents: number;
64
+ currentAgents: number;
65
+ status: 'active' | 'inactive' | 'degraded';
66
+ registeredAt: Date;
67
+ lastHeartbeat: Date;
68
+ metadata?: Record<string, unknown>;
69
+ }
70
+
71
+ export interface EphemeralAgent {
72
+ id: EphemeralAgentId;
73
+ swarmId: SwarmId;
74
+ type: string;
75
+ task: string;
76
+ status: 'spawning' | 'active' | 'completing' | 'terminated';
77
+ ttl: number;
78
+ createdAt: Date;
79
+ expiresAt: Date;
80
+ completedAt?: Date;
81
+ result?: unknown;
82
+ error?: Error;
83
+ metadata?: Record<string, unknown>;
84
+ }
85
+
86
+ export interface SpawnEphemeralOptions {
87
+ /** Target swarm (auto-select if not specified) */
88
+ swarmId?: SwarmId;
89
+ /** Agent type */
90
+ type: string;
91
+ /** Task description */
92
+ task: string;
93
+ /** Time-to-live in ms (default from config) */
94
+ ttl?: number;
95
+ /** Required capabilities */
96
+ capabilities?: string[];
97
+ /** Priority for swarm selection */
98
+ priority?: 'low' | 'normal' | 'high' | 'critical';
99
+ /** Wait for completion */
100
+ waitForCompletion?: boolean;
101
+ /** Completion timeout (ms) */
102
+ completionTimeout?: number;
103
+ /** Additional metadata */
104
+ metadata?: Record<string, unknown>;
105
+ }
106
+
107
+ export interface SpawnResult {
108
+ agentId: EphemeralAgentId;
109
+ swarmId: SwarmId;
110
+ status: 'spawned' | 'queued' | 'failed';
111
+ estimatedTTL: number;
112
+ result?: unknown;
113
+ error?: string;
114
+ }
115
+
116
+ export interface FederationMessage {
117
+ id: string;
118
+ type: 'broadcast' | 'direct' | 'consensus' | 'heartbeat';
119
+ sourceSwarmId: SwarmId;
120
+ targetSwarmId?: SwarmId;
121
+ payload: unknown;
122
+ timestamp: Date;
123
+ ttl?: number;
124
+ }
125
+
126
+ export interface ConsensusProposal {
127
+ id: string;
128
+ proposerId: SwarmId;
129
+ type: string;
130
+ value: unknown;
131
+ votes: Map<SwarmId, boolean>;
132
+ status: 'pending' | 'accepted' | 'rejected';
133
+ createdAt: Date;
134
+ expiresAt: Date;
135
+ }
136
+
137
+ export interface FederationStats {
138
+ federationId: FederationId;
139
+ totalSwarms: number;
140
+ activeSwarms: number;
141
+ totalEphemeralAgents: number;
142
+ activeEphemeralAgents: number;
143
+ completedAgents: number;
144
+ failedAgents: number;
145
+ avgAgentLifespanMs: number;
146
+ messagesExchanged: number;
147
+ consensusProposals: number;
148
+ uptime: number;
149
+ }
150
+
151
+ export interface FederationEvent {
152
+ type: FederationEventType;
153
+ federationId: FederationId;
154
+ swarmId?: SwarmId;
155
+ agentId?: EphemeralAgentId;
156
+ data?: unknown;
157
+ timestamp: Date;
158
+ }
159
+
160
+ export type FederationEventType =
161
+ | 'swarm_joined'
162
+ | 'swarm_left'
163
+ | 'swarm_degraded'
164
+ | 'agent_spawned'
165
+ | 'agent_completed'
166
+ | 'agent_failed'
167
+ | 'agent_expired'
168
+ | 'message_sent'
169
+ | 'message_received'
170
+ | 'consensus_started'
171
+ | 'consensus_completed'
172
+ | 'federation_synced';
173
+
174
+ // ============================================================================
175
+ // Default Configuration
176
+ // ============================================================================
177
+
178
+ const DEFAULT_CONFIG: Required<FederationConfig> = {
179
+ federationId: `federation_${Date.now()}`,
180
+ maxEphemeralAgents: 100,
181
+ defaultTTL: 300000, // 5 minutes
182
+ syncIntervalMs: 30000, // 30 seconds
183
+ autoCleanup: true,
184
+ cleanupIntervalMs: 60000, // 1 minute
185
+ communicationTimeoutMs: 5000,
186
+ enableConsensus: true,
187
+ consensusQuorum: 0.66,
188
+ };
189
+
190
+ // ============================================================================
191
+ // Federation Hub Implementation
192
+ // ============================================================================
193
+
194
+ export class FederationHub extends EventEmitter {
195
+ private config: Required<FederationConfig>;
196
+ private swarms: Map<SwarmId, SwarmRegistration> = new Map();
197
+ private ephemeralAgents: Map<EphemeralAgentId, EphemeralAgent> = new Map();
198
+ private messages: FederationMessage[] = [];
199
+ private proposals: Map<string, ConsensusProposal> = new Map();
200
+ private syncInterval?: ReturnType<typeof setInterval>;
201
+ private cleanupInterval?: ReturnType<typeof setInterval>;
202
+ private startTime: Date;
203
+ private stats: {
204
+ messagesExchanged: number;
205
+ consensusProposals: number;
206
+ completedAgents: number;
207
+ failedAgents: number;
208
+ totalAgentLifespanMs: number;
209
+ };
210
+
211
+ // ============================================================================
212
+ // Secondary Indexes for O(1) Lookups (Performance Optimization)
213
+ // ============================================================================
214
+
215
+ /** Index: swarmId -> Set of agentIds */
216
+ private agentsBySwarm: Map<SwarmId, Set<EphemeralAgentId>> = new Map();
217
+
218
+ /** Index: status -> Set of agentIds */
219
+ private agentsByStatus: Map<EphemeralAgent['status'], Set<EphemeralAgentId>> = new Map();
220
+
221
+ constructor(config?: FederationConfig) {
222
+ super();
223
+ this.config = { ...DEFAULT_CONFIG, ...config };
224
+ this.startTime = new Date();
225
+ this.stats = {
226
+ messagesExchanged: 0,
227
+ consensusProposals: 0,
228
+ completedAgents: 0,
229
+ failedAgents: 0,
230
+ totalAgentLifespanMs: 0,
231
+ };
232
+
233
+ // Initialize status index sets
234
+ this.agentsByStatus.set('spawning', new Set());
235
+ this.agentsByStatus.set('active', new Set());
236
+ this.agentsByStatus.set('completing', new Set());
237
+ this.agentsByStatus.set('terminated', new Set());
238
+ }
239
+
240
+ // ==========================================================================
241
+ // Index Maintenance Helpers
242
+ // ==========================================================================
243
+
244
+ /**
245
+ * Add agent to indexes - O(1)
246
+ */
247
+ private addAgentToIndexes(agent: EphemeralAgent): void {
248
+ // Add to swarm index
249
+ if (!this.agentsBySwarm.has(agent.swarmId)) {
250
+ this.agentsBySwarm.set(agent.swarmId, new Set());
251
+ }
252
+ this.agentsBySwarm.get(agent.swarmId)!.add(agent.id);
253
+
254
+ // Add to status index
255
+ this.agentsByStatus.get(agent.status)!.add(agent.id);
256
+ }
257
+
258
+ /**
259
+ * Remove agent from indexes - O(1)
260
+ */
261
+ private removeAgentFromIndexes(agent: EphemeralAgent): void {
262
+ // Remove from swarm index
263
+ const swarmSet = this.agentsBySwarm.get(agent.swarmId);
264
+ if (swarmSet) {
265
+ swarmSet.delete(agent.id);
266
+ if (swarmSet.size === 0) {
267
+ this.agentsBySwarm.delete(agent.swarmId);
268
+ }
269
+ }
270
+
271
+ // Remove from status index
272
+ this.agentsByStatus.get(agent.status)?.delete(agent.id);
273
+ }
274
+
275
+ /**
276
+ * Update agent status in index - O(1)
277
+ */
278
+ private updateAgentStatusIndex(agent: EphemeralAgent, oldStatus: EphemeralAgent['status']): void {
279
+ this.agentsByStatus.get(oldStatus)?.delete(agent.id);
280
+ this.agentsByStatus.get(agent.status)!.add(agent.id);
281
+ }
282
+
283
+ /**
284
+ * Get agents by swarm using index - O(k) where k is agents in swarm
285
+ */
286
+ private getAgentIdsBySwarm(swarmId: SwarmId): EphemeralAgentId[] {
287
+ const agentIds = this.agentsBySwarm.get(swarmId);
288
+ return agentIds ? Array.from(agentIds) : [];
289
+ }
290
+
291
+ /**
292
+ * Get agents by status using index - O(k) where k is agents with status
293
+ */
294
+ private getAgentIdsByStatus(status: EphemeralAgent['status']): EphemeralAgentId[] {
295
+ const agentIds = this.agentsByStatus.get(status);
296
+ return agentIds ? Array.from(agentIds) : [];
297
+ }
298
+
299
+ // ==========================================================================
300
+ // Lifecycle
301
+ // ==========================================================================
302
+
303
+ /**
304
+ * Initialize the federation hub
305
+ */
306
+ async initialize(): Promise<void> {
307
+ // Start sync interval
308
+ this.syncInterval = setInterval(
309
+ () => this.syncFederation(),
310
+ this.config.syncIntervalMs
311
+ );
312
+
313
+ // Start cleanup interval if enabled
314
+ if (this.config.autoCleanup) {
315
+ this.cleanupInterval = setInterval(
316
+ () => this.cleanupExpiredAgents(),
317
+ this.config.cleanupIntervalMs
318
+ );
319
+ }
320
+
321
+ this.emitEvent('federation_synced');
322
+ }
323
+
324
+ /**
325
+ * Shutdown the federation hub
326
+ */
327
+ async shutdown(): Promise<void> {
328
+ if (this.syncInterval) {
329
+ clearInterval(this.syncInterval);
330
+ }
331
+ if (this.cleanupInterval) {
332
+ clearInterval(this.cleanupInterval);
333
+ }
334
+
335
+ // Terminate all active ephemeral agents using index - O(k) where k = active + spawning
336
+ const activeIds = this.getAgentIdsByStatus('active');
337
+ const spawningIds = this.getAgentIdsByStatus('spawning');
338
+ const toTerminate = [...activeIds, ...spawningIds];
339
+
340
+ await Promise.all(toTerminate.map(id => this.terminateAgent(id)));
341
+
342
+ // Clear all data structures and indexes
343
+ this.swarms.clear();
344
+ this.ephemeralAgents.clear();
345
+ this.proposals.clear();
346
+ this.agentsBySwarm.clear();
347
+ for (const status of this.agentsByStatus.values()) {
348
+ status.clear();
349
+ }
350
+ }
351
+
352
+ // ==========================================================================
353
+ // Swarm Registration
354
+ // ==========================================================================
355
+
356
+ /**
357
+ * Register a swarm with the federation
358
+ */
359
+ registerSwarm(registration: Omit<SwarmRegistration, 'registeredAt' | 'lastHeartbeat'>): void {
360
+ const fullRegistration: SwarmRegistration = {
361
+ ...registration,
362
+ registeredAt: new Date(),
363
+ lastHeartbeat: new Date(),
364
+ };
365
+
366
+ this.swarms.set(registration.swarmId, fullRegistration);
367
+ this.emitEvent('swarm_joined', registration.swarmId);
368
+ }
369
+
370
+ /**
371
+ * Unregister a swarm from the federation
372
+ */
373
+ unregisterSwarm(swarmId: SwarmId): boolean {
374
+ const removed = this.swarms.delete(swarmId);
375
+ if (removed) {
376
+ // Terminate all ephemeral agents in this swarm using index - O(k)
377
+ const agentIds = this.getAgentIdsBySwarm(swarmId);
378
+ for (const agentId of agentIds) {
379
+ this.terminateAgent(agentId);
380
+ }
381
+ this.emitEvent('swarm_left', swarmId);
382
+ }
383
+ return removed;
384
+ }
385
+
386
+ /**
387
+ * Update swarm heartbeat
388
+ */
389
+ heartbeat(swarmId: SwarmId, currentAgents?: number): boolean {
390
+ const swarm = this.swarms.get(swarmId);
391
+ if (!swarm) return false;
392
+
393
+ swarm.lastHeartbeat = new Date();
394
+ if (currentAgents !== undefined) {
395
+ swarm.currentAgents = currentAgents;
396
+ }
397
+ if (swarm.status === 'inactive') {
398
+ swarm.status = 'active';
399
+ }
400
+ return true;
401
+ }
402
+
403
+ /**
404
+ * Get all registered swarms
405
+ */
406
+ getSwarms(): SwarmRegistration[] {
407
+ return Array.from(this.swarms.values());
408
+ }
409
+
410
+ /**
411
+ * Get swarm by ID
412
+ */
413
+ getSwarm(swarmId: SwarmId): SwarmRegistration | undefined {
414
+ return this.swarms.get(swarmId);
415
+ }
416
+
417
+ // ==========================================================================
418
+ // Ephemeral Agent Management
419
+ // ==========================================================================
420
+
421
+ /**
422
+ * Spawn an ephemeral agent
423
+ */
424
+ async spawnEphemeralAgent(options: SpawnEphemeralOptions): Promise<SpawnResult> {
425
+ // Select target swarm
426
+ const targetSwarmId = options.swarmId || this.selectOptimalSwarm(options);
427
+
428
+ if (!targetSwarmId) {
429
+ return {
430
+ agentId: '',
431
+ swarmId: '',
432
+ status: 'failed',
433
+ estimatedTTL: 0,
434
+ error: 'No suitable swarm available',
435
+ };
436
+ }
437
+
438
+ const swarm = this.swarms.get(targetSwarmId);
439
+ if (!swarm) {
440
+ return {
441
+ agentId: '',
442
+ swarmId: targetSwarmId,
443
+ status: 'failed',
444
+ estimatedTTL: 0,
445
+ error: 'Swarm not found',
446
+ };
447
+ }
448
+
449
+ // Check capacity
450
+ const swarmAgentCount = this.getSwarmAgentCount(targetSwarmId);
451
+ if (swarmAgentCount >= this.config.maxEphemeralAgents) {
452
+ return {
453
+ agentId: '',
454
+ swarmId: targetSwarmId,
455
+ status: 'failed',
456
+ estimatedTTL: 0,
457
+ error: 'Swarm at capacity',
458
+ };
459
+ }
460
+
461
+ // Create ephemeral agent
462
+ const ttl = options.ttl || this.config.defaultTTL;
463
+ const agentId = `ephemeral_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
464
+ const now = new Date();
465
+
466
+ const agent: EphemeralAgent = {
467
+ id: agentId,
468
+ swarmId: targetSwarmId,
469
+ type: options.type,
470
+ task: options.task,
471
+ status: 'spawning',
472
+ ttl,
473
+ createdAt: now,
474
+ expiresAt: new Date(now.getTime() + ttl),
475
+ metadata: options.metadata,
476
+ };
477
+
478
+ this.ephemeralAgents.set(agentId, agent);
479
+ this.addAgentToIndexes(agent);
480
+
481
+ // Async spawn with status transition (spawning -> active)
482
+ setTimeout(() => {
483
+ const a = this.ephemeralAgents.get(agentId);
484
+ if (a && a.status === 'spawning') {
485
+ this.updateAgentStatusIndex(a, 'spawning');
486
+ a.status = 'active';
487
+ this.emitEvent('agent_spawned', targetSwarmId, agentId);
488
+ }
489
+ }, 50);
490
+
491
+ // If waiting for completion
492
+ if (options.waitForCompletion) {
493
+ const timeout = options.completionTimeout || ttl;
494
+ const result = await this.waitForAgentCompletion(agentId, timeout);
495
+ return {
496
+ agentId,
497
+ swarmId: targetSwarmId,
498
+ status: result ? 'spawned' : 'failed',
499
+ estimatedTTL: ttl,
500
+ result: result?.result,
501
+ error: result?.error?.message,
502
+ };
503
+ }
504
+
505
+ return {
506
+ agentId,
507
+ swarmId: targetSwarmId,
508
+ status: 'spawned',
509
+ estimatedTTL: ttl,
510
+ };
511
+ }
512
+
513
+ /**
514
+ * Complete an ephemeral agent's task
515
+ */
516
+ completeAgent(agentId: EphemeralAgentId, result?: unknown): boolean {
517
+ const agent = this.ephemeralAgents.get(agentId);
518
+ if (!agent) return false;
519
+
520
+ const oldStatus = agent.status;
521
+ agent.status = 'completing';
522
+ this.updateAgentStatusIndex(agent, oldStatus);
523
+ agent.result = result;
524
+ agent.completedAt = new Date();
525
+
526
+ const lifespan = agent.completedAt.getTime() - agent.createdAt.getTime();
527
+ this.stats.completedAgents++;
528
+ this.stats.totalAgentLifespanMs += lifespan;
529
+
530
+ // Mark as terminated after a brief delay
531
+ setTimeout(() => {
532
+ const a = this.ephemeralAgents.get(agentId);
533
+ if (a) {
534
+ this.updateAgentStatusIndex(a, 'completing');
535
+ a.status = 'terminated';
536
+ this.emitEvent('agent_completed', a.swarmId, agentId);
537
+ }
538
+ }, 100);
539
+
540
+ return true;
541
+ }
542
+
543
+ /**
544
+ * Terminate an ephemeral agent
545
+ */
546
+ async terminateAgent(agentId: EphemeralAgentId, error?: Error): Promise<boolean> {
547
+ const agent = this.ephemeralAgents.get(agentId);
548
+ if (!agent) return false;
549
+
550
+ const oldStatus = agent.status;
551
+ agent.status = 'terminated';
552
+ this.updateAgentStatusIndex(agent, oldStatus);
553
+ agent.completedAt = new Date();
554
+
555
+ if (error) {
556
+ agent.error = error;
557
+ this.stats.failedAgents++;
558
+ this.emitEvent('agent_failed', agent.swarmId, agentId);
559
+ } else {
560
+ this.stats.completedAgents++;
561
+ this.emitEvent('agent_completed', agent.swarmId, agentId);
562
+ }
563
+
564
+ const lifespan = agent.completedAt.getTime() - agent.createdAt.getTime();
565
+ this.stats.totalAgentLifespanMs += lifespan;
566
+
567
+ return true;
568
+ }
569
+
570
+ /**
571
+ * Get ephemeral agent by ID
572
+ */
573
+ getAgent(agentId: EphemeralAgentId): EphemeralAgent | undefined {
574
+ return this.ephemeralAgents.get(agentId);
575
+ }
576
+
577
+ /**
578
+ * Get all ephemeral agents
579
+ */
580
+ getAgents(swarmId?: SwarmId): EphemeralAgent[] {
581
+ const agents = Array.from(this.ephemeralAgents.values());
582
+ return swarmId ? agents.filter(a => a.swarmId === swarmId) : agents;
583
+ }
584
+
585
+ /**
586
+ * Get active ephemeral agents
587
+ */
588
+ getActiveAgents(swarmId?: SwarmId): EphemeralAgent[] {
589
+ return this.getAgents(swarmId).filter(
590
+ a => a.status === 'active' || a.status === 'spawning'
591
+ );
592
+ }
593
+
594
+ // ==========================================================================
595
+ // Cross-Swarm Communication
596
+ // ==========================================================================
597
+
598
+ /**
599
+ * Send a message to another swarm
600
+ */
601
+ async sendMessage(
602
+ sourceSwarmId: SwarmId,
603
+ targetSwarmId: SwarmId,
604
+ payload: unknown
605
+ ): Promise<boolean> {
606
+ const targetSwarm = this.swarms.get(targetSwarmId);
607
+ if (!targetSwarm || targetSwarm.status === 'inactive') {
608
+ return false;
609
+ }
610
+
611
+ const message: FederationMessage = {
612
+ id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
613
+ type: 'direct',
614
+ sourceSwarmId,
615
+ targetSwarmId,
616
+ payload,
617
+ timestamp: new Date(),
618
+ };
619
+
620
+ this.messages.push(message);
621
+ this.stats.messagesExchanged++;
622
+ this.emitEvent('message_sent', sourceSwarmId);
623
+
624
+ // In real implementation, this would send to the target swarm's endpoint
625
+ // For now, we emit an event that can be listened to
626
+ this.emit('message', message);
627
+
628
+ return true;
629
+ }
630
+
631
+ /**
632
+ * Broadcast a message to all swarms
633
+ */
634
+ async broadcast(sourceSwarmId: SwarmId, payload: unknown): Promise<number> {
635
+ let sent = 0;
636
+
637
+ for (const swarm of this.swarms.values()) {
638
+ if (swarm.swarmId !== sourceSwarmId && swarm.status === 'active') {
639
+ const success = await this.sendMessage(sourceSwarmId, swarm.swarmId, payload);
640
+ if (success) sent++;
641
+ }
642
+ }
643
+
644
+ return sent;
645
+ }
646
+
647
+ /**
648
+ * Get recent messages
649
+ */
650
+ getMessages(limit: number = 100): FederationMessage[] {
651
+ return this.messages.slice(-limit);
652
+ }
653
+
654
+ // ==========================================================================
655
+ // Federation Consensus
656
+ // ==========================================================================
657
+
658
+ /**
659
+ * Propose a value for federation-wide consensus
660
+ */
661
+ async propose(
662
+ proposerId: SwarmId,
663
+ type: string,
664
+ value: unknown,
665
+ timeoutMs: number = 30000
666
+ ): Promise<ConsensusProposal> {
667
+ if (!this.config.enableConsensus) {
668
+ throw new Error('Consensus is disabled');
669
+ }
670
+
671
+ const proposal: ConsensusProposal = {
672
+ id: `proposal_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
673
+ proposerId,
674
+ type,
675
+ value,
676
+ votes: new Map([[proposerId, true]]),
677
+ status: 'pending',
678
+ createdAt: new Date(),
679
+ expiresAt: new Date(Date.now() + timeoutMs),
680
+ };
681
+
682
+ this.proposals.set(proposal.id, proposal);
683
+ this.stats.consensusProposals++;
684
+ this.emitEvent('consensus_started', proposerId);
685
+
686
+ // Request votes from all active swarms
687
+ await this.broadcast(proposerId, {
688
+ type: 'vote_request',
689
+ proposalId: proposal.id,
690
+ proposalType: type,
691
+ value,
692
+ });
693
+
694
+ return proposal;
695
+ }
696
+
697
+ /**
698
+ * Vote on a proposal
699
+ */
700
+ vote(swarmId: SwarmId, proposalId: string, approve: boolean): boolean {
701
+ const proposal = this.proposals.get(proposalId);
702
+ if (!proposal || proposal.status !== 'pending') {
703
+ return false;
704
+ }
705
+
706
+ if (new Date() > proposal.expiresAt) {
707
+ proposal.status = 'rejected';
708
+ return false;
709
+ }
710
+
711
+ proposal.votes.set(swarmId, approve);
712
+
713
+ // Check if quorum reached
714
+ const activeSwarms = this.getActiveSwarmCount();
715
+ const approvals = Array.from(proposal.votes.values()).filter(v => v).length;
716
+ const rejections = Array.from(proposal.votes.values()).filter(v => !v).length;
717
+ const quorumThreshold = Math.ceil(activeSwarms * this.config.consensusQuorum);
718
+
719
+ if (approvals >= quorumThreshold) {
720
+ proposal.status = 'accepted';
721
+ this.emitEvent('consensus_completed', proposal.proposerId);
722
+ } else if (rejections > activeSwarms - quorumThreshold) {
723
+ proposal.status = 'rejected';
724
+ this.emitEvent('consensus_completed', proposal.proposerId);
725
+ }
726
+
727
+ return true;
728
+ }
729
+
730
+ /**
731
+ * Get proposal by ID
732
+ */
733
+ getProposal(proposalId: string): ConsensusProposal | undefined {
734
+ return this.proposals.get(proposalId);
735
+ }
736
+
737
+ /**
738
+ * Get all pending proposals
739
+ */
740
+ getPendingProposals(): ConsensusProposal[] {
741
+ return Array.from(this.proposals.values()).filter(p => p.status === 'pending');
742
+ }
743
+
744
+ // ==========================================================================
745
+ // Statistics & Monitoring
746
+ // ==========================================================================
747
+
748
+ /**
749
+ * Get federation statistics
750
+ */
751
+ getStats(): FederationStats {
752
+ const activeAgents = this.getActiveAgents().length;
753
+ const avgLifespan = this.stats.completedAgents > 0
754
+ ? this.stats.totalAgentLifespanMs / this.stats.completedAgents
755
+ : 0;
756
+
757
+ return {
758
+ federationId: this.config.federationId,
759
+ totalSwarms: this.swarms.size,
760
+ activeSwarms: this.getActiveSwarmCount(),
761
+ totalEphemeralAgents: this.ephemeralAgents.size,
762
+ activeEphemeralAgents: activeAgents,
763
+ completedAgents: this.stats.completedAgents,
764
+ failedAgents: this.stats.failedAgents,
765
+ avgAgentLifespanMs: avgLifespan,
766
+ messagesExchanged: this.stats.messagesExchanged,
767
+ consensusProposals: this.stats.consensusProposals,
768
+ uptime: Date.now() - this.startTime.getTime(),
769
+ };
770
+ }
771
+
772
+ // ==========================================================================
773
+ // Private Helpers
774
+ // ==========================================================================
775
+
776
+ private selectOptimalSwarm(options: SpawnEphemeralOptions): SwarmId | null {
777
+ const candidates: Array<{ swarmId: SwarmId; score: number }> = [];
778
+
779
+ for (const swarm of this.swarms.values()) {
780
+ if (swarm.status !== 'active') continue;
781
+
782
+ // Check capacity
783
+ const agentCount = this.getSwarmAgentCount(swarm.swarmId);
784
+ if (agentCount >= swarm.maxAgents) continue;
785
+
786
+ // Check capabilities
787
+ if (options.capabilities) {
788
+ const hasAllCapabilities = options.capabilities.every(
789
+ cap => swarm.capabilities.includes(cap)
790
+ );
791
+ if (!hasAllCapabilities) continue;
792
+ }
793
+
794
+ // Calculate score (higher is better)
795
+ let score = 100;
796
+
797
+ // Prefer swarms with more available capacity
798
+ const availableCapacity = swarm.maxAgents - agentCount;
799
+ score += availableCapacity * 5;
800
+
801
+ // Prefer recently active swarms
802
+ const lastHeartbeatAge = Date.now() - swarm.lastHeartbeat.getTime();
803
+ score -= lastHeartbeatAge / 10000;
804
+
805
+ candidates.push({ swarmId: swarm.swarmId, score });
806
+ }
807
+
808
+ if (candidates.length === 0) return null;
809
+
810
+ // Sort by score and return best
811
+ candidates.sort((a, b) => b.score - a.score);
812
+ return candidates[0].swarmId;
813
+ }
814
+
815
+ private getSwarmAgentCount(swarmId: SwarmId): number {
816
+ // Use index for O(1) lookup instead of O(n) filter
817
+ const swarmAgents = this.agentsBySwarm.get(swarmId);
818
+ if (!swarmAgents) return 0;
819
+
820
+ // Count only active and spawning agents
821
+ let count = 0;
822
+ for (const agentId of swarmAgents) {
823
+ const agent = this.ephemeralAgents.get(agentId);
824
+ if (agent && (agent.status === 'active' || agent.status === 'spawning')) {
825
+ count++;
826
+ }
827
+ }
828
+ return count;
829
+ }
830
+
831
+ private getActiveSwarmCount(): number {
832
+ return Array.from(this.swarms.values()).filter(s => s.status === 'active').length;
833
+ }
834
+
835
+ private async waitForAgentCompletion(
836
+ agentId: EphemeralAgentId,
837
+ timeout: number
838
+ ): Promise<EphemeralAgent | null> {
839
+ return new Promise((resolve) => {
840
+ const startTime = Date.now();
841
+
842
+ const check = () => {
843
+ const agent = this.ephemeralAgents.get(agentId);
844
+
845
+ if (!agent) {
846
+ resolve(null);
847
+ return;
848
+ }
849
+
850
+ if (agent.status === 'terminated' || agent.status === 'completing') {
851
+ resolve(agent);
852
+ return;
853
+ }
854
+
855
+ if (Date.now() - startTime > timeout) {
856
+ resolve(null);
857
+ return;
858
+ }
859
+
860
+ setTimeout(check, 100);
861
+ };
862
+
863
+ check();
864
+ });
865
+ }
866
+
867
+ private syncFederation(): void {
868
+ const now = new Date();
869
+ const heartbeatTimeout = this.config.syncIntervalMs * 3;
870
+
871
+ // Check for inactive swarms
872
+ for (const swarm of this.swarms.values()) {
873
+ const age = now.getTime() - swarm.lastHeartbeat.getTime();
874
+
875
+ if (age > heartbeatTimeout && swarm.status === 'active') {
876
+ swarm.status = 'degraded';
877
+ this.emitEvent('swarm_degraded', swarm.swarmId);
878
+ } else if (age > heartbeatTimeout * 2 && swarm.status === 'degraded') {
879
+ swarm.status = 'inactive';
880
+ }
881
+ }
882
+
883
+ // Check for expired proposals
884
+ for (const proposal of this.proposals.values()) {
885
+ if (proposal.status === 'pending' && now > proposal.expiresAt) {
886
+ proposal.status = 'rejected';
887
+ }
888
+ }
889
+
890
+ this.emitEvent('federation_synced');
891
+ }
892
+
893
+ private cleanupExpiredAgents(): void {
894
+ const now = new Date();
895
+
896
+ // Use status index to only check active agents - O(k) instead of O(n)
897
+ const activeIds = this.getAgentIdsByStatus('active');
898
+ for (const agentId of activeIds) {
899
+ const agent = this.ephemeralAgents.get(agentId);
900
+ if (agent && now > agent.expiresAt) {
901
+ this.updateAgentStatusIndex(agent, 'active');
902
+ agent.status = 'terminated';
903
+ agent.completedAt = now;
904
+ agent.error = new Error('Agent TTL expired');
905
+ this.stats.failedAgents++;
906
+ this.emitEvent('agent_expired', agent.swarmId, agent.id);
907
+ }
908
+ }
909
+
910
+ // Clean up old terminated agents using index - O(k)
911
+ const cleanupThreshold = 5 * 60 * 1000;
912
+ const terminatedIds = this.getAgentIdsByStatus('terminated');
913
+ for (const agentId of terminatedIds) {
914
+ const agent = this.ephemeralAgents.get(agentId);
915
+ if (
916
+ agent &&
917
+ agent.completedAt &&
918
+ now.getTime() - agent.completedAt.getTime() > cleanupThreshold
919
+ ) {
920
+ this.removeAgentFromIndexes(agent);
921
+ this.ephemeralAgents.delete(agentId);
922
+ }
923
+ }
924
+ }
925
+
926
+ private emitEvent(
927
+ type: FederationEventType,
928
+ swarmId?: SwarmId,
929
+ agentId?: EphemeralAgentId,
930
+ data?: unknown
931
+ ): void {
932
+ const event: FederationEvent = {
933
+ type,
934
+ federationId: this.config.federationId,
935
+ swarmId,
936
+ agentId,
937
+ data,
938
+ timestamp: new Date(),
939
+ };
940
+ this.emit('event', event);
941
+ this.emit(type, event);
942
+ }
943
+ }
944
+
945
+ // ============================================================================
946
+ // Factory Functions
947
+ // ============================================================================
948
+
949
+ /**
950
+ * Create a new Federation Hub instance
951
+ */
952
+ export function createFederationHub(config?: FederationConfig): FederationHub {
953
+ return new FederationHub(config);
954
+ }
955
+
956
+ /**
957
+ * Global federation hub instance
958
+ */
959
+ let defaultFederationHub: FederationHub | null = null;
960
+
961
+ /**
962
+ * Get or create the default federation hub
963
+ */
964
+ export function getDefaultFederationHub(): FederationHub {
965
+ if (!defaultFederationHub) {
966
+ defaultFederationHub = createFederationHub();
967
+ }
968
+ return defaultFederationHub;
969
+ }
970
+
971
+ /**
972
+ * Reset the default federation hub
973
+ */
974
+ export async function resetDefaultFederationHub(): Promise<void> {
975
+ if (defaultFederationHub) {
976
+ await defaultFederationHub.shutdown();
977
+ defaultFederationHub = null;
978
+ }
979
+ }