@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,656 @@
1
+ /**
2
+ * V3 Topology Manager
3
+ * Manages swarm network topology with support for mesh, hierarchical, centralized, and hybrid modes
4
+ */
5
+
6
+ import { EventEmitter } from 'events';
7
+ import {
8
+ TopologyConfig,
9
+ TopologyState,
10
+ TopologyNode,
11
+ TopologyEdge,
12
+ TopologyPartition,
13
+ TopologyType,
14
+ ITopologyManager,
15
+ } from './types.js';
16
+
17
+ export class TopologyManager extends EventEmitter implements ITopologyManager {
18
+ private config: TopologyConfig;
19
+ private state: TopologyState;
20
+ private nodeIndex: Map<string, TopologyNode> = new Map();
21
+ private adjacencyList: Map<string, Set<string>> = new Map();
22
+ private lastRebalance: Date = new Date();
23
+
24
+ // O(1) role-based indexes for performance (fixes O(n) find operations)
25
+ private roleIndex: Map<TopologyNode['role'], Set<string>> = new Map();
26
+ private queenNode: TopologyNode | null = null;
27
+ private coordinatorNode: TopologyNode | null = null;
28
+
29
+ constructor(config: Partial<TopologyConfig> = {}) {
30
+ super();
31
+ this.config = {
32
+ type: config.type ?? 'mesh',
33
+ maxAgents: config.maxAgents ?? 100,
34
+ replicationFactor: config.replicationFactor ?? 2,
35
+ partitionStrategy: config.partitionStrategy ?? 'hash',
36
+ failoverEnabled: config.failoverEnabled ?? true,
37
+ autoRebalance: config.autoRebalance ?? true,
38
+ };
39
+
40
+ this.state = {
41
+ type: this.config.type,
42
+ nodes: [],
43
+ edges: [],
44
+ leader: undefined,
45
+ partitions: [],
46
+ };
47
+ }
48
+
49
+ async initialize(config?: TopologyConfig): Promise<void> {
50
+ if (config) {
51
+ this.config = { ...this.config, ...config };
52
+ this.state.type = this.config.type;
53
+ }
54
+
55
+ this.emit('initialized', { type: this.config.type });
56
+ }
57
+
58
+ getState(): TopologyState {
59
+ return {
60
+ ...this.state,
61
+ nodes: [...this.state.nodes],
62
+ edges: [...this.state.edges],
63
+ partitions: [...this.state.partitions],
64
+ };
65
+ }
66
+
67
+ async addNode(agentId: string, role: TopologyNode['role']): Promise<TopologyNode> {
68
+ if (this.nodeIndex.has(agentId)) {
69
+ throw new Error(`Node ${agentId} already exists in topology`);
70
+ }
71
+
72
+ if (this.nodeIndex.size >= this.config.maxAgents) {
73
+ throw new Error(`Maximum agents (${this.config.maxAgents}) reached`);
74
+ }
75
+
76
+ // Create node with connections based on topology type
77
+ const connections = this.calculateInitialConnections(agentId, role);
78
+
79
+ const node: TopologyNode = {
80
+ id: `node_${agentId}`,
81
+ agentId,
82
+ role: this.determineRole(role),
83
+ status: 'syncing',
84
+ connections,
85
+ metadata: {
86
+ joinedAt: new Date().toISOString(),
87
+ version: '1.0.0',
88
+ },
89
+ };
90
+
91
+ // Add to state
92
+ this.nodeIndex.set(agentId, node);
93
+ this.state.nodes.push(node);
94
+
95
+ // Update role index for O(1) role-based lookups
96
+ this.addToRoleIndex(node);
97
+
98
+ // Initialize adjacency list
99
+ this.adjacencyList.set(agentId, new Set(connections));
100
+
101
+ // Create edges
102
+ await this.createEdgesForNode(node);
103
+
104
+ // Update partitions if needed
105
+ await this.updatePartitions(node);
106
+
107
+ // Mark as active after sync
108
+ node.status = 'active';
109
+
110
+ this.emit('node.added', { node });
111
+
112
+ // Trigger rebalance if needed
113
+ if (this.config.autoRebalance && this.shouldRebalance()) {
114
+ await this.rebalance();
115
+ }
116
+
117
+ return node;
118
+ }
119
+
120
+ async removeNode(agentId: string): Promise<void> {
121
+ const node = this.nodeIndex.get(agentId);
122
+ if (!node) {
123
+ return;
124
+ }
125
+
126
+ // Remove from state
127
+ this.state.nodes = this.state.nodes.filter(n => n.agentId !== agentId);
128
+ this.nodeIndex.delete(agentId);
129
+
130
+ // Update role index
131
+ this.removeFromRoleIndex(node);
132
+
133
+ // Remove all edges connected to this node
134
+ this.state.edges = this.state.edges.filter(
135
+ e => e.from !== agentId && e.to !== agentId
136
+ );
137
+
138
+ // Update adjacency list
139
+ this.adjacencyList.delete(agentId);
140
+ for (const neighbors of this.adjacencyList.values()) {
141
+ neighbors.delete(agentId);
142
+ }
143
+
144
+ // Update all nodes' connections
145
+ for (const n of this.state.nodes) {
146
+ n.connections = n.connections.filter(c => c !== agentId);
147
+ }
148
+
149
+ // If this was the leader, elect new one
150
+ if (this.state.leader === agentId) {
151
+ await this.electLeader();
152
+ }
153
+
154
+ // Update partitions
155
+ for (const partition of this.state.partitions) {
156
+ partition.nodes = partition.nodes.filter(n => n !== agentId);
157
+ if (partition.leader === agentId) {
158
+ partition.leader = partition.nodes[0] || '';
159
+ }
160
+ }
161
+
162
+ this.emit('node.removed', { agentId });
163
+
164
+ // Trigger rebalance if needed
165
+ if (this.config.autoRebalance) {
166
+ await this.rebalance();
167
+ }
168
+ }
169
+
170
+ async updateNode(agentId: string, updates: Partial<TopologyNode>): Promise<void> {
171
+ const node = this.nodeIndex.get(agentId);
172
+ if (!node) {
173
+ throw new Error(`Node ${agentId} not found`);
174
+ }
175
+
176
+ // Apply updates
177
+ if (updates.role !== undefined) node.role = updates.role;
178
+ if (updates.status !== undefined) node.status = updates.status;
179
+ if (updates.connections !== undefined) {
180
+ node.connections = updates.connections;
181
+ this.adjacencyList.set(agentId, new Set(updates.connections));
182
+ }
183
+ if (updates.metadata !== undefined) {
184
+ node.metadata = { ...node.metadata, ...updates.metadata };
185
+ }
186
+
187
+ this.emit('node.updated', { agentId, updates });
188
+ }
189
+
190
+ getLeader(): string | undefined {
191
+ return this.state.leader;
192
+ }
193
+
194
+ async electLeader(): Promise<string> {
195
+ if (this.state.nodes.length === 0) {
196
+ throw new Error('No nodes available for leader election');
197
+ }
198
+
199
+ // For hierarchical topology, the queen is the leader (O(1) lookup)
200
+ if (this.config.type === 'hierarchical') {
201
+ const queen = this.queenNode;
202
+ if (queen) {
203
+ this.state.leader = queen.agentId;
204
+ return queen.agentId;
205
+ }
206
+ }
207
+
208
+ // For centralized topology, the coordinator is the leader (O(1) lookup)
209
+ if (this.config.type === 'centralized') {
210
+ const coordinator = this.coordinatorNode;
211
+ if (coordinator) {
212
+ this.state.leader = coordinator.agentId;
213
+ return coordinator.agentId;
214
+ }
215
+ }
216
+
217
+ // For mesh/hybrid, elect based on node capabilities
218
+ const candidates = this.state.nodes
219
+ .filter(n => n.status === 'active')
220
+ .sort((a, b) => {
221
+ // Prefer coordinators, then queens
222
+ const roleOrder: Record<TopologyNode['role'], number> = {
223
+ queen: 0,
224
+ coordinator: 1,
225
+ worker: 2,
226
+ peer: 2,
227
+ };
228
+ return roleOrder[a.role] - roleOrder[b.role];
229
+ });
230
+
231
+ if (candidates.length === 0) {
232
+ throw new Error('No active nodes available for leader election');
233
+ }
234
+
235
+ const leader = candidates[0];
236
+ this.state.leader = leader.agentId;
237
+
238
+ this.emit('leader.elected', { leaderId: leader.agentId });
239
+
240
+ return leader.agentId;
241
+ }
242
+
243
+ async rebalance(): Promise<void> {
244
+ const now = new Date();
245
+ const timeSinceLastRebalance = now.getTime() - this.lastRebalance.getTime();
246
+
247
+ // Prevent too frequent rebalancing
248
+ if (timeSinceLastRebalance < 5000) {
249
+ return;
250
+ }
251
+
252
+ this.lastRebalance = now;
253
+
254
+ switch (this.config.type) {
255
+ case 'mesh':
256
+ await this.rebalanceMesh();
257
+ break;
258
+ case 'hierarchical':
259
+ await this.rebalanceHierarchical();
260
+ break;
261
+ case 'centralized':
262
+ await this.rebalanceCentralized();
263
+ break;
264
+ case 'hybrid':
265
+ await this.rebalanceHybrid();
266
+ break;
267
+ }
268
+
269
+ this.emit('topology.rebalanced', { type: this.config.type });
270
+ }
271
+
272
+ getNeighbors(agentId: string): string[] {
273
+ return Array.from(this.adjacencyList.get(agentId) || []);
274
+ }
275
+
276
+ findOptimalPath(from: string, to: string): string[] {
277
+ if (from === to) {
278
+ return [from];
279
+ }
280
+
281
+ // BFS for shortest path
282
+ const visited = new Set<string>();
283
+ const queue: { node: string; path: string[] }[] = [{ node: from, path: [from] }];
284
+
285
+ while (queue.length > 0) {
286
+ const { node, path } = queue.shift()!;
287
+
288
+ if (node === to) {
289
+ return path;
290
+ }
291
+
292
+ if (visited.has(node)) {
293
+ continue;
294
+ }
295
+ visited.add(node);
296
+
297
+ const neighbors = this.adjacencyList.get(node) || new Set();
298
+ for (const neighbor of neighbors) {
299
+ if (!visited.has(neighbor)) {
300
+ queue.push({ node: neighbor, path: [...path, neighbor] });
301
+ }
302
+ }
303
+ }
304
+
305
+ // No path found
306
+ return [];
307
+ }
308
+
309
+ // ===== PRIVATE METHODS =====
310
+
311
+ private determineRole(requestedRole: TopologyNode['role']): TopologyNode['role'] {
312
+ switch (this.config.type) {
313
+ case 'mesh':
314
+ return 'peer';
315
+ case 'hierarchical':
316
+ // First node becomes queen
317
+ if (this.state.nodes.length === 0) {
318
+ return 'queen';
319
+ }
320
+ return requestedRole === 'queen' && !this.hasQueen() ? 'queen' : 'worker';
321
+ case 'centralized':
322
+ // First node becomes coordinator
323
+ if (this.state.nodes.length === 0) {
324
+ return 'coordinator';
325
+ }
326
+ return 'worker';
327
+ case 'hybrid':
328
+ return requestedRole;
329
+ }
330
+ }
331
+
332
+ private hasQueen(): boolean {
333
+ return this.queenNode !== null; // O(1) check using cached queen
334
+ }
335
+
336
+ private calculateInitialConnections(agentId: string, role: TopologyNode['role']): string[] {
337
+ const existingNodes = Array.from(this.nodeIndex.keys());
338
+
339
+ switch (this.config.type) {
340
+ case 'mesh':
341
+ // In mesh, connect to all existing nodes (up to a limit)
342
+ const maxMeshConnections = Math.min(10, existingNodes.length);
343
+ return existingNodes.slice(0, maxMeshConnections);
344
+
345
+ case 'hierarchical':
346
+ // Workers connect to queen, queen connects to workers (O(1) lookup)
347
+ if (role === 'queen' || existingNodes.length === 0) {
348
+ return existingNodes;
349
+ }
350
+ return this.queenNode ? [this.queenNode.agentId] : [];
351
+
352
+ case 'centralized':
353
+ // All nodes connect to coordinator (O(1) lookup)
354
+ if (role === 'coordinator' || existingNodes.length === 0) {
355
+ return existingNodes;
356
+ }
357
+ return this.coordinatorNode ? [this.coordinatorNode.agentId] : [];
358
+
359
+ case 'hybrid':
360
+ // Mix of mesh and hierarchical
361
+ const leaders = this.state.nodes.filter(n =>
362
+ n.role === 'queen' || n.role === 'coordinator'
363
+ );
364
+ const peers = existingNodes.slice(0, 3);
365
+ return [...new Set([...leaders.map(l => l.agentId), ...peers])];
366
+ }
367
+ }
368
+
369
+ private async createEdgesForNode(node: TopologyNode): Promise<void> {
370
+ for (const connectionId of node.connections) {
371
+ const edge: TopologyEdge = {
372
+ from: node.agentId,
373
+ to: connectionId,
374
+ weight: 1,
375
+ bidirectional: this.config.type === 'mesh',
376
+ latencyMs: 0,
377
+ };
378
+
379
+ this.state.edges.push(edge);
380
+
381
+ // Add bidirectional edge
382
+ if (edge.bidirectional) {
383
+ const existingNode = this.nodeIndex.get(connectionId);
384
+ if (existingNode && !existingNode.connections.includes(node.agentId)) {
385
+ existingNode.connections.push(node.agentId);
386
+ this.adjacencyList.get(connectionId)?.add(node.agentId);
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ private async updatePartitions(node: TopologyNode): Promise<void> {
393
+ if (this.config.type !== 'mesh' && this.config.type !== 'hybrid') {
394
+ return;
395
+ }
396
+
397
+ // Create partitions based on strategy
398
+ const nodesPerPartition = Math.ceil(this.config.maxAgents / 10);
399
+ const partitionIndex = Math.floor(this.state.nodes.length / nodesPerPartition);
400
+
401
+ if (this.state.partitions.length <= partitionIndex) {
402
+ // Create new partition
403
+ const partition: TopologyPartition = {
404
+ id: `partition_${partitionIndex}`,
405
+ nodes: [node.agentId],
406
+ leader: node.agentId,
407
+ replicaCount: 1,
408
+ };
409
+ this.state.partitions.push(partition);
410
+ } else {
411
+ // Add to existing partition
412
+ const partition = this.state.partitions[partitionIndex];
413
+ partition.nodes.push(node.agentId);
414
+ partition.replicaCount = Math.min(
415
+ partition.nodes.length,
416
+ this.config.replicationFactor ?? 2
417
+ );
418
+ }
419
+ }
420
+
421
+ private shouldRebalance(): boolean {
422
+ // Check for uneven distribution
423
+ if (this.config.type === 'mesh') {
424
+ const avgConnections = this.state.nodes.reduce(
425
+ (sum, n) => sum + n.connections.length, 0
426
+ ) / Math.max(1, this.state.nodes.length);
427
+
428
+ for (const node of this.state.nodes) {
429
+ if (Math.abs(node.connections.length - avgConnections) > avgConnections * 0.5) {
430
+ return true;
431
+ }
432
+ }
433
+ }
434
+
435
+ return false;
436
+ }
437
+
438
+ private async rebalanceMesh(): Promise<void> {
439
+ const targetConnections = Math.min(5, this.state.nodes.length - 1);
440
+
441
+ for (const node of this.state.nodes) {
442
+ // Ensure minimum connections
443
+ while (node.connections.length < targetConnections) {
444
+ const candidates = this.state.nodes
445
+ .filter(n =>
446
+ n.agentId !== node.agentId &&
447
+ !node.connections.includes(n.agentId)
448
+ )
449
+ .sort(() => Math.random() - 0.5);
450
+
451
+ if (candidates.length > 0) {
452
+ node.connections.push(candidates[0].agentId);
453
+ this.adjacencyList.get(node.agentId)?.add(candidates[0].agentId);
454
+
455
+ // Bidirectional
456
+ candidates[0].connections.push(node.agentId);
457
+ this.adjacencyList.get(candidates[0].agentId)?.add(node.agentId);
458
+ } else {
459
+ break;
460
+ }
461
+ }
462
+ }
463
+ }
464
+
465
+ private async rebalanceHierarchical(): Promise<void> {
466
+ // O(1) queen lookup
467
+ let queen = this.queenNode;
468
+ if (!queen) {
469
+ // Elect a queen if missing
470
+ if (this.state.nodes.length > 0) {
471
+ const newQueen = this.state.nodes[0]!;
472
+ newQueen.role = 'queen';
473
+ this.addToRoleIndex(newQueen);
474
+ queen = newQueen;
475
+ } else {
476
+ return;
477
+ }
478
+ }
479
+
480
+ // Ensure all workers are connected to queen
481
+ for (const node of this.state.nodes) {
482
+ if (node.role === 'worker' && !node.connections.includes(queen.agentId)) {
483
+ node.connections.push(queen.agentId);
484
+ this.adjacencyList.get(node.agentId)?.add(queen.agentId);
485
+ queen.connections.push(node.agentId);
486
+ this.adjacencyList.get(queen.agentId)?.add(node.agentId);
487
+ }
488
+ }
489
+ }
490
+
491
+ private async rebalanceCentralized(): Promise<void> {
492
+ // O(1) coordinator lookup
493
+ let coordinator = this.coordinatorNode;
494
+ if (!coordinator) {
495
+ if (this.state.nodes.length > 0) {
496
+ const newCoord = this.state.nodes[0]!;
497
+ newCoord.role = 'coordinator';
498
+ this.addToRoleIndex(newCoord);
499
+ coordinator = newCoord;
500
+ } else {
501
+ return;
502
+ }
503
+ }
504
+
505
+ // Ensure all nodes are connected to coordinator
506
+ for (const node of this.state.nodes) {
507
+ if (node.role !== 'coordinator' && !node.connections.includes(coordinator.agentId)) {
508
+ node.connections = [coordinator.agentId];
509
+ this.adjacencyList.set(node.agentId, new Set([coordinator.agentId]));
510
+ coordinator.connections.push(node.agentId);
511
+ this.adjacencyList.get(coordinator.agentId)?.add(node.agentId);
512
+ }
513
+ }
514
+ }
515
+
516
+ private async rebalanceHybrid(): Promise<void> {
517
+ // Hybrid combines mesh for workers and hierarchical for coordinators
518
+ const coordinators = this.state.nodes.filter(
519
+ n => n.role === 'queen' || n.role === 'coordinator'
520
+ );
521
+ const workers = this.state.nodes.filter(n => n.role === 'worker' || n.role === 'peer');
522
+
523
+ // Connect workers in mesh (limited connections)
524
+ for (const worker of workers) {
525
+ const targetConnections = Math.min(3, workers.length - 1);
526
+ const currentWorkerConnections = worker.connections.filter(
527
+ c => workers.some(w => w.agentId === c)
528
+ );
529
+
530
+ while (currentWorkerConnections.length < targetConnections) {
531
+ const candidates = workers.filter(
532
+ w => w.agentId !== worker.agentId && !currentWorkerConnections.includes(w.agentId)
533
+ );
534
+ if (candidates.length === 0) break;
535
+
536
+ const target = candidates[Math.floor(Math.random() * candidates.length)];
537
+ worker.connections.push(target.agentId);
538
+ currentWorkerConnections.push(target.agentId);
539
+ this.adjacencyList.get(worker.agentId)?.add(target.agentId);
540
+ }
541
+ }
542
+
543
+ // Connect all workers to at least one coordinator
544
+ if (coordinators.length > 0) {
545
+ for (const worker of workers) {
546
+ const hasCoordinator = worker.connections.some(
547
+ c => coordinators.some(coord => coord.agentId === c)
548
+ );
549
+ if (!hasCoordinator) {
550
+ const coord = coordinators[Math.floor(Math.random() * coordinators.length)];
551
+ worker.connections.push(coord.agentId);
552
+ this.adjacencyList.get(worker.agentId)?.add(coord.agentId);
553
+ coord.connections.push(worker.agentId);
554
+ this.adjacencyList.get(coord.agentId)?.add(worker.agentId);
555
+ }
556
+ }
557
+ }
558
+ }
559
+
560
+ // ===== ROLE INDEX METHODS (O(1) lookups) =====
561
+
562
+ /**
563
+ * Add node to role index
564
+ */
565
+ private addToRoleIndex(node: TopologyNode): void {
566
+ let roleSet = this.roleIndex.get(node.role);
567
+ if (!roleSet) {
568
+ roleSet = new Set();
569
+ this.roleIndex.set(node.role, roleSet);
570
+ }
571
+ roleSet.add(node.agentId);
572
+
573
+ // Cache queen/coordinator for O(1) access
574
+ if (node.role === 'queen') {
575
+ this.queenNode = node;
576
+ } else if (node.role === 'coordinator') {
577
+ this.coordinatorNode = node;
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Remove node from role index
583
+ */
584
+ private removeFromRoleIndex(node: TopologyNode): void {
585
+ const roleSet = this.roleIndex.get(node.role);
586
+ if (roleSet) {
587
+ roleSet.delete(node.agentId);
588
+ }
589
+
590
+ // Clear cached queen/coordinator
591
+ if (node.role === 'queen' && this.queenNode?.agentId === node.agentId) {
592
+ this.queenNode = null;
593
+ } else if (node.role === 'coordinator' && this.coordinatorNode?.agentId === node.agentId) {
594
+ this.coordinatorNode = null;
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Get queen node with O(1) lookup
600
+ */
601
+ getQueen(): TopologyNode | undefined {
602
+ return this.queenNode ?? undefined;
603
+ }
604
+
605
+ /**
606
+ * Get coordinator node with O(1) lookup
607
+ */
608
+ getCoordinator(): TopologyNode | undefined {
609
+ return this.coordinatorNode ?? undefined;
610
+ }
611
+
612
+ // ===== UTILITY METHODS =====
613
+
614
+ getNode(agentId: string): TopologyNode | undefined {
615
+ return this.nodeIndex.get(agentId);
616
+ }
617
+
618
+ getNodesByRole(role: TopologyNode['role']): TopologyNode[] {
619
+ // Use role index for O(1) id lookup, then O(k) node retrieval where k = nodes with role
620
+ const roleSet = this.roleIndex.get(role);
621
+ if (!roleSet) return [];
622
+
623
+ const nodes: TopologyNode[] = [];
624
+ for (const agentId of roleSet) {
625
+ const node = this.nodeIndex.get(agentId);
626
+ if (node) nodes.push(node);
627
+ }
628
+ return nodes;
629
+ }
630
+
631
+ getActiveNodes(): TopologyNode[] {
632
+ return this.state.nodes.filter(n => n.status === 'active');
633
+ }
634
+
635
+ getPartition(partitionId: string): TopologyPartition | undefined {
636
+ return this.state.partitions.find(p => p.id === partitionId);
637
+ }
638
+
639
+ isConnected(from: string, to: string): boolean {
640
+ return this.adjacencyList.get(from)?.has(to) ?? false;
641
+ }
642
+
643
+ getConnectionCount(): number {
644
+ return this.state.edges.length;
645
+ }
646
+
647
+ getAverageConnections(): number {
648
+ if (this.state.nodes.length === 0) return 0;
649
+ const total = this.state.nodes.reduce((sum, n) => sum + n.connections.length, 0);
650
+ return total / this.state.nodes.length;
651
+ }
652
+ }
653
+
654
+ export function createTopologyManager(config?: Partial<TopologyConfig>): TopologyManager {
655
+ return new TopologyManager(config);
656
+ }