@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,513 @@
1
+ /**
2
+ * V3 Gossip Protocol Consensus
3
+ * Eventually consistent consensus for large-scale distributed systems
4
+ */
5
+
6
+ import { EventEmitter } from 'events';
7
+ import {
8
+ ConsensusProposal,
9
+ ConsensusVote,
10
+ ConsensusResult,
11
+ ConsensusConfig,
12
+ SWARM_CONSTANTS,
13
+ } from '../types.js';
14
+
15
+ export interface GossipMessage {
16
+ id: string;
17
+ type: 'proposal' | 'vote' | 'state' | 'ack';
18
+ senderId: string;
19
+ version: number;
20
+ payload: unknown;
21
+ timestamp: Date;
22
+ ttl: number;
23
+ hops: number;
24
+ path: string[];
25
+ }
26
+
27
+ export interface GossipNode {
28
+ id: string;
29
+ state: Map<string, unknown>;
30
+ version: number;
31
+ neighbors: Set<string>;
32
+ seenMessages: Set<string>;
33
+ lastSync: Date;
34
+ }
35
+
36
+ export interface GossipConfig extends Partial<ConsensusConfig> {
37
+ fanout?: number;
38
+ gossipIntervalMs?: number;
39
+ maxHops?: number;
40
+ convergenceThreshold?: number;
41
+ }
42
+
43
+ export class GossipConsensus extends EventEmitter {
44
+ private config: GossipConfig;
45
+ private node: GossipNode;
46
+ private nodes: Map<string, GossipNode> = new Map();
47
+ private proposals: Map<string, ConsensusProposal> = new Map();
48
+ private messageQueue: GossipMessage[] = [];
49
+ private gossipInterval?: NodeJS.Timeout;
50
+ private proposalCounter: number = 0;
51
+
52
+ constructor(nodeId: string, config: GossipConfig = {}) {
53
+ super();
54
+ this.config = {
55
+ threshold: config.threshold ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_THRESHOLD,
56
+ timeoutMs: config.timeoutMs ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_TIMEOUT_MS,
57
+ maxRounds: config.maxRounds ?? 10,
58
+ requireQuorum: config.requireQuorum ?? false, // Gossip is eventually consistent
59
+ fanout: config.fanout ?? 3,
60
+ gossipIntervalMs: config.gossipIntervalMs ?? 100,
61
+ maxHops: config.maxHops ?? 10,
62
+ convergenceThreshold: config.convergenceThreshold ?? 0.9,
63
+ };
64
+
65
+ this.node = {
66
+ id: nodeId,
67
+ state: new Map(),
68
+ version: 0,
69
+ neighbors: new Set(),
70
+ seenMessages: new Set(),
71
+ lastSync: new Date(),
72
+ };
73
+ }
74
+
75
+ async initialize(): Promise<void> {
76
+ this.startGossipLoop();
77
+ this.emit('initialized', { nodeId: this.node.id });
78
+ }
79
+
80
+ async shutdown(): Promise<void> {
81
+ if (this.gossipInterval) {
82
+ clearInterval(this.gossipInterval);
83
+ }
84
+ this.emit('shutdown');
85
+ }
86
+
87
+ addNode(nodeId: string): void {
88
+ this.nodes.set(nodeId, {
89
+ id: nodeId,
90
+ state: new Map(),
91
+ version: 0,
92
+ neighbors: new Set(),
93
+ seenMessages: new Set(),
94
+ lastSync: new Date(),
95
+ });
96
+
97
+ // Add as neighbor with some probability (random mesh)
98
+ if (Math.random() < 0.5) {
99
+ this.node.neighbors.add(nodeId);
100
+ this.nodes.get(nodeId)!.neighbors.add(this.node.id);
101
+ }
102
+ }
103
+
104
+ removeNode(nodeId: string): void {
105
+ this.nodes.delete(nodeId);
106
+ this.node.neighbors.delete(nodeId);
107
+
108
+ for (const node of this.nodes.values()) {
109
+ node.neighbors.delete(nodeId);
110
+ }
111
+ }
112
+
113
+ addNeighbor(nodeId: string): void {
114
+ if (this.nodes.has(nodeId)) {
115
+ this.node.neighbors.add(nodeId);
116
+ }
117
+ }
118
+
119
+ removeNeighbor(nodeId: string): void {
120
+ this.node.neighbors.delete(nodeId);
121
+ }
122
+
123
+ async propose(value: unknown): Promise<ConsensusProposal> {
124
+ this.proposalCounter++;
125
+ const proposalId = `gossip_${this.node.id}_${this.proposalCounter}`;
126
+
127
+ const proposal: ConsensusProposal = {
128
+ id: proposalId,
129
+ proposerId: this.node.id,
130
+ value,
131
+ term: this.node.version,
132
+ timestamp: new Date(),
133
+ votes: new Map(),
134
+ status: 'pending',
135
+ };
136
+
137
+ this.proposals.set(proposalId, proposal);
138
+
139
+ // Self-vote
140
+ proposal.votes.set(this.node.id, {
141
+ voterId: this.node.id,
142
+ approve: true,
143
+ confidence: 1.0,
144
+ timestamp: new Date(),
145
+ });
146
+
147
+ // Create gossip message
148
+ const message: GossipMessage = {
149
+ id: `msg_${proposalId}`,
150
+ type: 'proposal',
151
+ senderId: this.node.id,
152
+ version: ++this.node.version,
153
+ payload: { proposalId, value },
154
+ timestamp: new Date(),
155
+ ttl: this.config.maxHops ?? 10,
156
+ hops: 0,
157
+ path: [this.node.id],
158
+ };
159
+
160
+ // Queue for gossip
161
+ this.queueMessage(message);
162
+
163
+ return proposal;
164
+ }
165
+
166
+ async vote(proposalId: string, vote: ConsensusVote): Promise<void> {
167
+ const proposal = this.proposals.get(proposalId);
168
+ if (!proposal) {
169
+ return;
170
+ }
171
+
172
+ proposal.votes.set(vote.voterId, vote);
173
+
174
+ // Create vote gossip message
175
+ const message: GossipMessage = {
176
+ id: `vote_${proposalId}_${vote.voterId}`,
177
+ type: 'vote',
178
+ senderId: this.node.id,
179
+ version: ++this.node.version,
180
+ payload: { proposalId, vote },
181
+ timestamp: new Date(),
182
+ ttl: this.config.maxHops ?? 10,
183
+ hops: 0,
184
+ path: [this.node.id],
185
+ };
186
+
187
+ this.queueMessage(message);
188
+
189
+ // Check convergence
190
+ await this.checkConvergence(proposalId);
191
+ }
192
+
193
+ async awaitConsensus(proposalId: string): Promise<ConsensusResult> {
194
+ const startTime = Date.now();
195
+ const maxWait = this.config.timeoutMs ?? 30000;
196
+
197
+ return new Promise((resolve, reject) => {
198
+ const checkInterval = setInterval(() => {
199
+ const proposal = this.proposals.get(proposalId);
200
+ if (!proposal) {
201
+ clearInterval(checkInterval);
202
+ reject(new Error(`Proposal ${proposalId} not found`));
203
+ return;
204
+ }
205
+
206
+ if (proposal.status !== 'pending') {
207
+ clearInterval(checkInterval);
208
+ resolve(this.createResult(proposal, Date.now() - startTime));
209
+ return;
210
+ }
211
+
212
+ // Check convergence
213
+ this.checkConvergence(proposalId);
214
+
215
+ if (Date.now() - startTime > maxWait) {
216
+ clearInterval(checkInterval);
217
+ // Gossip is eventually consistent, so mark as accepted if threshold met
218
+ const totalNodes = this.nodes.size + 1;
219
+ const votes = proposal.votes.size;
220
+ const threshold = this.config.convergenceThreshold ?? 0.9;
221
+
222
+ if (votes / totalNodes >= threshold) {
223
+ proposal.status = 'accepted';
224
+ } else {
225
+ proposal.status = 'expired';
226
+ }
227
+
228
+ resolve(this.createResult(proposal, Date.now() - startTime));
229
+ }
230
+ }, 50);
231
+ });
232
+ }
233
+
234
+ // ===== GOSSIP PROTOCOL =====
235
+
236
+ private startGossipLoop(): void {
237
+ this.gossipInterval = setInterval(() => {
238
+ this.gossipRound();
239
+ }, this.config.gossipIntervalMs ?? 100);
240
+ }
241
+
242
+ private async gossipRound(): Promise<void> {
243
+ if (this.messageQueue.length === 0) {
244
+ return;
245
+ }
246
+
247
+ // Select random neighbors (fanout)
248
+ const fanout = Math.min(
249
+ this.config.fanout ?? 3,
250
+ this.node.neighbors.size
251
+ );
252
+ const neighbors = this.selectRandomNeighbors(fanout);
253
+
254
+ // Send queued messages to selected neighbors
255
+ const messages = this.messageQueue.splice(0, 10); // Process up to 10 per round
256
+
257
+ for (const message of messages) {
258
+ for (const neighborId of neighbors) {
259
+ await this.sendToNeighbor(neighborId, message);
260
+ }
261
+ }
262
+
263
+ this.node.lastSync = new Date();
264
+ }
265
+
266
+ private selectRandomNeighbors(count: number): string[] {
267
+ const neighbors = Array.from(this.node.neighbors);
268
+ const selected: string[] = [];
269
+
270
+ while (selected.length < count && neighbors.length > 0) {
271
+ const idx = Math.floor(Math.random() * neighbors.length);
272
+ selected.push(neighbors.splice(idx, 1)[0]);
273
+ }
274
+
275
+ return selected;
276
+ }
277
+
278
+ private async sendToNeighbor(neighborId: string, message: GossipMessage): Promise<void> {
279
+ const neighbor = this.nodes.get(neighborId);
280
+ if (!neighbor) {
281
+ return;
282
+ }
283
+
284
+ // Check if already seen
285
+ if (neighbor.seenMessages.has(message.id)) {
286
+ return;
287
+ }
288
+
289
+ // Deliver message to neighbor node
290
+ const deliveredMessage: GossipMessage = {
291
+ ...message,
292
+ hops: message.hops + 1,
293
+ path: [...message.path, neighborId],
294
+ };
295
+
296
+ // Process at neighbor
297
+ await this.processReceivedMessage(neighbor, deliveredMessage);
298
+
299
+ this.emit('message.sent', { to: neighborId, message: deliveredMessage });
300
+ }
301
+
302
+ private async processReceivedMessage(
303
+ node: GossipNode,
304
+ message: GossipMessage
305
+ ): Promise<void> {
306
+ // Mark as seen
307
+ node.seenMessages.add(message.id);
308
+
309
+ // Check TTL
310
+ if (message.ttl <= 0 || message.hops >= (this.config.maxHops ?? 10)) {
311
+ return;
312
+ }
313
+
314
+ switch (message.type) {
315
+ case 'proposal':
316
+ await this.handleProposalMessage(node, message);
317
+ break;
318
+ case 'vote':
319
+ await this.handleVoteMessage(node, message);
320
+ break;
321
+ case 'state':
322
+ await this.handleStateMessage(node, message);
323
+ break;
324
+ }
325
+
326
+ // Propagate to neighbors (gossip)
327
+ if (message.hops < (this.config.maxHops ?? 10)) {
328
+ const propagateMessage: GossipMessage = {
329
+ ...message,
330
+ ttl: message.ttl - 1,
331
+ };
332
+
333
+ // Add to queue if this is our node
334
+ if (node.id === this.node.id) {
335
+ this.queueMessage(propagateMessage);
336
+ }
337
+ }
338
+ }
339
+
340
+ private async handleProposalMessage(
341
+ node: GossipNode,
342
+ message: GossipMessage
343
+ ): Promise<void> {
344
+ const { proposalId, value } = message.payload as {
345
+ proposalId: string;
346
+ value: unknown;
347
+ };
348
+
349
+ if (!this.proposals.has(proposalId)) {
350
+ const proposal: ConsensusProposal = {
351
+ id: proposalId,
352
+ proposerId: message.senderId,
353
+ value,
354
+ term: message.version,
355
+ timestamp: message.timestamp,
356
+ votes: new Map(),
357
+ status: 'pending',
358
+ };
359
+
360
+ this.proposals.set(proposalId, proposal);
361
+
362
+ // Auto-vote (simplified)
363
+ if (node.id === this.node.id) {
364
+ await this.vote(proposalId, {
365
+ voterId: this.node.id,
366
+ approve: true,
367
+ confidence: 0.9,
368
+ timestamp: new Date(),
369
+ });
370
+ }
371
+ }
372
+ }
373
+
374
+ private async handleVoteMessage(
375
+ node: GossipNode,
376
+ message: GossipMessage
377
+ ): Promise<void> {
378
+ const { proposalId, vote } = message.payload as {
379
+ proposalId: string;
380
+ vote: ConsensusVote;
381
+ };
382
+
383
+ const proposal = this.proposals.get(proposalId);
384
+ if (proposal && !proposal.votes.has(vote.voterId)) {
385
+ proposal.votes.set(vote.voterId, vote);
386
+ await this.checkConvergence(proposalId);
387
+ }
388
+ }
389
+
390
+ private async handleStateMessage(
391
+ node: GossipNode,
392
+ message: GossipMessage
393
+ ): Promise<void> {
394
+ const state = message.payload as Record<string, unknown>;
395
+
396
+ // Merge state (last-writer-wins)
397
+ if (message.version > node.version) {
398
+ for (const [key, value] of Object.entries(state)) {
399
+ node.state.set(key, value);
400
+ }
401
+ node.version = message.version;
402
+ }
403
+ }
404
+
405
+ private queueMessage(message: GossipMessage): void {
406
+ // Avoid duplicates
407
+ if (!this.node.seenMessages.has(message.id)) {
408
+ this.node.seenMessages.add(message.id);
409
+ this.messageQueue.push(message);
410
+ }
411
+ }
412
+
413
+ private async checkConvergence(proposalId: string): Promise<void> {
414
+ const proposal = this.proposals.get(proposalId);
415
+ if (!proposal || proposal.status !== 'pending') {
416
+ return;
417
+ }
418
+
419
+ const totalNodes = this.nodes.size + 1;
420
+ const votes = proposal.votes.size;
421
+ const threshold = this.config.convergenceThreshold ?? 0.9;
422
+ const approvalThreshold = this.config.threshold ?? 0.66;
423
+
424
+ // Check if we've converged (enough nodes have voted)
425
+ if (votes / totalNodes >= threshold) {
426
+ const approvingVotes = Array.from(proposal.votes.values()).filter(
427
+ v => v.approve
428
+ ).length;
429
+
430
+ if (approvingVotes / votes >= approvalThreshold) {
431
+ proposal.status = 'accepted';
432
+ this.emit('consensus.achieved', { proposalId, approved: true });
433
+ } else {
434
+ proposal.status = 'rejected';
435
+ this.emit('consensus.achieved', { proposalId, approved: false });
436
+ }
437
+ }
438
+ }
439
+
440
+ private createResult(proposal: ConsensusProposal, durationMs: number): ConsensusResult {
441
+ const totalNodes = this.nodes.size + 1;
442
+ const approvingVotes = Array.from(proposal.votes.values()).filter(
443
+ v => v.approve
444
+ ).length;
445
+
446
+ return {
447
+ proposalId: proposal.id,
448
+ approved: proposal.status === 'accepted',
449
+ approvalRate: proposal.votes.size > 0
450
+ ? approvingVotes / proposal.votes.size
451
+ : 0,
452
+ participationRate: proposal.votes.size / totalNodes,
453
+ finalValue: proposal.value,
454
+ rounds: this.node.version,
455
+ durationMs,
456
+ };
457
+ }
458
+
459
+ // ===== STATE QUERIES =====
460
+
461
+ getConvergence(proposalId: string): number {
462
+ const proposal = this.proposals.get(proposalId);
463
+ if (!proposal) return 0;
464
+
465
+ const totalNodes = this.nodes.size + 1;
466
+ return proposal.votes.size / totalNodes;
467
+ }
468
+
469
+ getVersion(): number {
470
+ return this.node.version;
471
+ }
472
+
473
+ getNeighborCount(): number {
474
+ return this.node.neighbors.size;
475
+ }
476
+
477
+ getSeenMessageCount(): number {
478
+ return this.node.seenMessages.size;
479
+ }
480
+
481
+ getQueueDepth(): number {
482
+ return this.messageQueue.length;
483
+ }
484
+
485
+ // Anti-entropy: sync full state with a random neighbor
486
+ async antiEntropy(): Promise<void> {
487
+ if (this.node.neighbors.size === 0) return;
488
+
489
+ const neighbors = Array.from(this.node.neighbors);
490
+ const randomNeighbor = neighbors[Math.floor(Math.random() * neighbors.length)];
491
+
492
+ const stateMessage: GossipMessage = {
493
+ id: `state_${this.node.id}_${Date.now()}`,
494
+ type: 'state',
495
+ senderId: this.node.id,
496
+ version: this.node.version,
497
+ payload: Object.fromEntries(this.node.state),
498
+ timestamp: new Date(),
499
+ ttl: 1,
500
+ hops: 0,
501
+ path: [this.node.id],
502
+ };
503
+
504
+ await this.sendToNeighbor(randomNeighbor, stateMessage);
505
+ }
506
+ }
507
+
508
+ export function createGossipConsensus(
509
+ nodeId: string,
510
+ config?: GossipConfig
511
+ ): GossipConsensus {
512
+ return new GossipConsensus(nodeId, config);
513
+ }