@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.
- package/MIGRATION.md +472 -0
- package/README.md +634 -0
- package/__tests__/consensus.test.ts +577 -0
- package/__tests__/coordinator.test.ts +501 -0
- package/__tests__/queen-coordinator.test.ts +1335 -0
- package/__tests__/topology.test.ts +621 -0
- package/package.json +32 -0
- package/src/agent-pool.ts +476 -0
- package/src/application/commands/create-task.command.ts +124 -0
- package/src/application/commands/spawn-agent.command.ts +122 -0
- package/src/application/index.ts +30 -0
- package/src/application/services/swarm-application-service.ts +200 -0
- package/src/attention-coordinator.ts +1000 -0
- package/src/consensus/byzantine.ts +431 -0
- package/src/consensus/gossip.ts +513 -0
- package/src/consensus/index.ts +267 -0
- package/src/consensus/raft.ts +443 -0
- package/src/coordination/agent-registry.ts +544 -0
- package/src/coordination/index.ts +23 -0
- package/src/coordination/swarm-hub.ts +776 -0
- package/src/coordination/task-orchestrator.ts +605 -0
- package/src/domain/entities/agent.d.ts +151 -0
- package/src/domain/entities/agent.d.ts.map +1 -0
- package/src/domain/entities/agent.js +280 -0
- package/src/domain/entities/agent.js.map +1 -0
- package/src/domain/entities/agent.ts +370 -0
- package/src/domain/entities/task.d.ts +133 -0
- package/src/domain/entities/task.d.ts.map +1 -0
- package/src/domain/entities/task.js +261 -0
- package/src/domain/entities/task.js.map +1 -0
- package/src/domain/entities/task.ts +319 -0
- package/src/domain/index.ts +41 -0
- package/src/domain/repositories/agent-repository.interface.d.ts +57 -0
- package/src/domain/repositories/agent-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/agent-repository.interface.js +9 -0
- package/src/domain/repositories/agent-repository.interface.js.map +1 -0
- package/src/domain/repositories/agent-repository.interface.ts +69 -0
- package/src/domain/repositories/task-repository.interface.d.ts +61 -0
- package/src/domain/repositories/task-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/task-repository.interface.js +9 -0
- package/src/domain/repositories/task-repository.interface.js.map +1 -0
- package/src/domain/repositories/task-repository.interface.ts +75 -0
- package/src/domain/services/coordination-service.ts +320 -0
- package/src/federation-hub.d.ts +284 -0
- package/src/federation-hub.d.ts.map +1 -0
- package/src/federation-hub.js +692 -0
- package/src/federation-hub.js.map +1 -0
- package/src/federation-hub.ts +979 -0
- package/src/index.ts +348 -0
- package/src/message-bus.ts +607 -0
- package/src/queen-coordinator.ts +2025 -0
- package/src/shared/events.ts +285 -0
- package/src/shared/types.ts +389 -0
- package/src/topology-manager.ts +656 -0
- package/src/types.ts +545 -0
- package/src/unified-coordinator.ts +1844 -0
- package/src/workers/index.ts +65 -0
- package/src/workers/worker-dispatch.d.ts +234 -0
- package/src/workers/worker-dispatch.d.ts.map +1 -0
- package/src/workers/worker-dispatch.js +842 -0
- package/src/workers/worker-dispatch.js.map +1 -0
- package/src/workers/worker-dispatch.ts +1076 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
- 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
|
+
}
|