@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,431 @@
1
+ /**
2
+ * V3 Byzantine Fault Tolerant Consensus
3
+ * PBFT-style consensus for handling malicious or faulty nodes
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 type ByzantinePhase = 'pre-prepare' | 'prepare' | 'commit' | 'reply';
16
+
17
+ export interface ByzantineMessage {
18
+ type: ByzantinePhase;
19
+ viewNumber: number;
20
+ sequenceNumber: number;
21
+ digest: string;
22
+ senderId: string;
23
+ timestamp: Date;
24
+ payload?: unknown;
25
+ signature?: string;
26
+ }
27
+
28
+ export interface ByzantineNode {
29
+ id: string;
30
+ isPrimary: boolean;
31
+ viewNumber: number;
32
+ sequenceNumber: number;
33
+ preparedMessages: Map<string, ByzantineMessage[]>;
34
+ committedMessages: Map<string, ByzantineMessage[]>;
35
+ }
36
+
37
+ export interface ByzantineConfig extends Partial<ConsensusConfig> {
38
+ maxFaultyNodes?: number;
39
+ viewChangeTimeoutMs?: number;
40
+ }
41
+
42
+ export class ByzantineConsensus extends EventEmitter {
43
+ private config: ByzantineConfig;
44
+ private node: ByzantineNode;
45
+ private nodes: Map<string, ByzantineNode> = new Map();
46
+ private proposals: Map<string, ConsensusProposal> = new Map();
47
+ private messageLog: Map<string, ByzantineMessage[]> = new Map();
48
+ private proposalCounter: number = 0;
49
+ private viewChangeTimeout?: NodeJS.Timeout;
50
+
51
+ constructor(nodeId: string, config: ByzantineConfig = {}) {
52
+ super();
53
+ this.config = {
54
+ threshold: config.threshold ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_THRESHOLD,
55
+ timeoutMs: config.timeoutMs ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_TIMEOUT_MS,
56
+ maxRounds: config.maxRounds ?? 10,
57
+ requireQuorum: config.requireQuorum ?? true,
58
+ maxFaultyNodes: config.maxFaultyNodes ?? 1,
59
+ viewChangeTimeoutMs: config.viewChangeTimeoutMs ?? 5000,
60
+ };
61
+
62
+ this.node = {
63
+ id: nodeId,
64
+ isPrimary: false,
65
+ viewNumber: 0,
66
+ sequenceNumber: 0,
67
+ preparedMessages: new Map(),
68
+ committedMessages: new Map(),
69
+ };
70
+ }
71
+
72
+ async initialize(): Promise<void> {
73
+ this.emit('initialized', { nodeId: this.node.id });
74
+ }
75
+
76
+ async shutdown(): Promise<void> {
77
+ if (this.viewChangeTimeout) {
78
+ clearTimeout(this.viewChangeTimeout);
79
+ }
80
+ this.emit('shutdown');
81
+ }
82
+
83
+ addNode(nodeId: string, isPrimary: boolean = false): void {
84
+ this.nodes.set(nodeId, {
85
+ id: nodeId,
86
+ isPrimary,
87
+ viewNumber: 0,
88
+ sequenceNumber: 0,
89
+ preparedMessages: new Map(),
90
+ committedMessages: new Map(),
91
+ });
92
+
93
+ if (isPrimary && this.node.id === nodeId) {
94
+ this.node.isPrimary = true;
95
+ }
96
+ }
97
+
98
+ removeNode(nodeId: string): void {
99
+ this.nodes.delete(nodeId);
100
+ }
101
+
102
+ electPrimary(): string {
103
+ const nodeIds = [this.node.id, ...Array.from(this.nodes.keys())];
104
+ const primaryIndex = this.node.viewNumber % nodeIds.length;
105
+ const primaryId = nodeIds[primaryIndex];
106
+
107
+ this.node.isPrimary = primaryId === this.node.id;
108
+
109
+ for (const [id, node] of this.nodes) {
110
+ node.isPrimary = id === primaryId;
111
+ }
112
+
113
+ this.emit('primary.elected', { primaryId, viewNumber: this.node.viewNumber });
114
+
115
+ return primaryId;
116
+ }
117
+
118
+ async propose(value: unknown): Promise<ConsensusProposal> {
119
+ if (!this.node.isPrimary) {
120
+ throw new Error('Only primary can propose values');
121
+ }
122
+
123
+ this.proposalCounter++;
124
+ const sequenceNumber = ++this.node.sequenceNumber;
125
+ const digest = this.computeDigest(value);
126
+ const proposalId = `bft_${this.node.viewNumber}_${sequenceNumber}`;
127
+
128
+ const proposal: ConsensusProposal = {
129
+ id: proposalId,
130
+ proposerId: this.node.id,
131
+ value,
132
+ term: this.node.viewNumber,
133
+ timestamp: new Date(),
134
+ votes: new Map(),
135
+ status: 'pending',
136
+ };
137
+
138
+ this.proposals.set(proposalId, proposal);
139
+
140
+ // Phase 1: Pre-prepare
141
+ const prePrepareMsg: ByzantineMessage = {
142
+ type: 'pre-prepare',
143
+ viewNumber: this.node.viewNumber,
144
+ sequenceNumber,
145
+ digest,
146
+ senderId: this.node.id,
147
+ timestamp: new Date(),
148
+ payload: value,
149
+ };
150
+
151
+ await this.broadcastMessage(prePrepareMsg);
152
+
153
+ // Self-prepare
154
+ await this.handlePrepare({
155
+ type: 'prepare',
156
+ viewNumber: this.node.viewNumber,
157
+ sequenceNumber,
158
+ digest,
159
+ senderId: this.node.id,
160
+ timestamp: new Date(),
161
+ });
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 || proposal.status !== 'pending') {
169
+ return;
170
+ }
171
+
172
+ proposal.votes.set(vote.voterId, vote);
173
+
174
+ // Check consensus
175
+ const f = this.config.maxFaultyNodes ?? 1;
176
+ const n = this.nodes.size + 1;
177
+ const requiredVotes = 2 * f + 1;
178
+
179
+ const approvingVotes = Array.from(proposal.votes.values()).filter(
180
+ v => v.approve
181
+ ).length;
182
+
183
+ if (approvingVotes >= requiredVotes) {
184
+ proposal.status = 'accepted';
185
+ this.emit('consensus.achieved', { proposalId, approved: true });
186
+ } else if (proposal.votes.size >= n && approvingVotes < requiredVotes) {
187
+ proposal.status = 'rejected';
188
+ this.emit('consensus.achieved', { proposalId, approved: false });
189
+ }
190
+ }
191
+
192
+ async awaitConsensus(proposalId: string): Promise<ConsensusResult> {
193
+ const startTime = Date.now();
194
+
195
+ return new Promise((resolve, reject) => {
196
+ const checkInterval = setInterval(() => {
197
+ const proposal = this.proposals.get(proposalId);
198
+ if (!proposal) {
199
+ clearInterval(checkInterval);
200
+ reject(new Error(`Proposal ${proposalId} not found`));
201
+ return;
202
+ }
203
+
204
+ if (proposal.status !== 'pending') {
205
+ clearInterval(checkInterval);
206
+ resolve(this.createResult(proposal, Date.now() - startTime));
207
+ return;
208
+ }
209
+
210
+ if (Date.now() - startTime > (this.config.timeoutMs ?? 30000)) {
211
+ clearInterval(checkInterval);
212
+ proposal.status = 'expired';
213
+ resolve(this.createResult(proposal, Date.now() - startTime));
214
+ }
215
+ }, 10);
216
+ });
217
+ }
218
+
219
+ // ===== MESSAGE HANDLERS =====
220
+
221
+ async handlePrePrepare(message: ByzantineMessage): Promise<void> {
222
+ // Validate message
223
+ if (message.viewNumber !== this.node.viewNumber) {
224
+ return;
225
+ }
226
+
227
+ // Accept pre-prepare from primary
228
+ const proposalId = `bft_${message.viewNumber}_${message.sequenceNumber}`;
229
+
230
+ if (!this.proposals.has(proposalId) && message.payload !== undefined) {
231
+ const proposal: ConsensusProposal = {
232
+ id: proposalId,
233
+ proposerId: message.senderId,
234
+ value: message.payload,
235
+ term: message.viewNumber,
236
+ timestamp: message.timestamp,
237
+ votes: new Map(),
238
+ status: 'pending',
239
+ };
240
+ this.proposals.set(proposalId, proposal);
241
+ }
242
+
243
+ // Send prepare message
244
+ const prepareMsg: ByzantineMessage = {
245
+ type: 'prepare',
246
+ viewNumber: message.viewNumber,
247
+ sequenceNumber: message.sequenceNumber,
248
+ digest: message.digest,
249
+ senderId: this.node.id,
250
+ timestamp: new Date(),
251
+ };
252
+
253
+ await this.broadcastMessage(prepareMsg);
254
+ await this.handlePrepare(prepareMsg);
255
+ }
256
+
257
+ async handlePrepare(message: ByzantineMessage): Promise<void> {
258
+ const key = `${message.viewNumber}_${message.sequenceNumber}`;
259
+
260
+ if (!this.messageLog.has(key)) {
261
+ this.messageLog.set(key, []);
262
+ }
263
+
264
+ const messages = this.messageLog.get(key)!;
265
+ const hasPrepare = messages.some(
266
+ m => m.type === 'prepare' && m.senderId === message.senderId
267
+ );
268
+
269
+ if (!hasPrepare) {
270
+ messages.push(message);
271
+ }
272
+
273
+ // Check if prepared (2f + 1 prepare messages)
274
+ const f = this.config.maxFaultyNodes ?? 1;
275
+ const prepareCount = messages.filter(m => m.type === 'prepare').length;
276
+
277
+ if (prepareCount >= 2 * f + 1) {
278
+ const proposalId = `bft_${message.viewNumber}_${message.sequenceNumber}`;
279
+ this.node.preparedMessages.set(key, messages);
280
+
281
+ // Send commit message
282
+ const commitMsg: ByzantineMessage = {
283
+ type: 'commit',
284
+ viewNumber: message.viewNumber,
285
+ sequenceNumber: message.sequenceNumber,
286
+ digest: message.digest,
287
+ senderId: this.node.id,
288
+ timestamp: new Date(),
289
+ };
290
+
291
+ await this.broadcastMessage(commitMsg);
292
+ await this.handleCommit(commitMsg);
293
+
294
+ // Record vote
295
+ const proposal = this.proposals.get(proposalId);
296
+ if (proposal) {
297
+ proposal.votes.set(this.node.id, {
298
+ voterId: this.node.id,
299
+ approve: true,
300
+ confidence: 1.0,
301
+ timestamp: new Date(),
302
+ });
303
+ }
304
+ }
305
+ }
306
+
307
+ async handleCommit(message: ByzantineMessage): Promise<void> {
308
+ const key = `${message.viewNumber}_${message.sequenceNumber}`;
309
+
310
+ if (!this.messageLog.has(key)) {
311
+ this.messageLog.set(key, []);
312
+ }
313
+
314
+ const messages = this.messageLog.get(key)!;
315
+ const hasCommit = messages.some(
316
+ m => m.type === 'commit' && m.senderId === message.senderId
317
+ );
318
+
319
+ if (!hasCommit) {
320
+ messages.push(message);
321
+ }
322
+
323
+ // Check if committed (2f + 1 commit messages)
324
+ const f = this.config.maxFaultyNodes ?? 1;
325
+ const commitCount = messages.filter(m => m.type === 'commit').length;
326
+
327
+ if (commitCount >= 2 * f + 1) {
328
+ const proposalId = `bft_${message.viewNumber}_${message.sequenceNumber}`;
329
+ this.node.committedMessages.set(key, messages);
330
+
331
+ const proposal = this.proposals.get(proposalId);
332
+ if (proposal && proposal.status === 'pending') {
333
+ proposal.status = 'accepted';
334
+ this.emit('consensus.achieved', { proposalId, approved: true });
335
+ }
336
+ }
337
+ }
338
+
339
+ // ===== VIEW CHANGE =====
340
+
341
+ async initiateViewChange(): Promise<void> {
342
+ this.node.viewNumber++;
343
+
344
+ this.emit('view.changing', { newViewNumber: this.node.viewNumber });
345
+
346
+ // Elect new primary
347
+ this.electPrimary();
348
+
349
+ this.emit('view.changed', { viewNumber: this.node.viewNumber });
350
+ }
351
+
352
+ // ===== PRIVATE METHODS =====
353
+
354
+ private async broadcastMessage(message: ByzantineMessage): Promise<void> {
355
+ this.emit('message.broadcast', { message });
356
+
357
+ // Broadcast to all registered nodes
358
+ for (const node of this.nodes.values()) {
359
+ this.emit('message.sent', { to: node.id, message });
360
+ }
361
+ }
362
+
363
+ private computeDigest(value: unknown): string {
364
+ // Simple hash for demonstration
365
+ const str = JSON.stringify(value);
366
+ let hash = 0;
367
+ for (let i = 0; i < str.length; i++) {
368
+ const char = str.charCodeAt(i);
369
+ hash = ((hash << 5) - hash) + char;
370
+ hash = hash & hash;
371
+ }
372
+ return hash.toString(16);
373
+ }
374
+
375
+ private createResult(proposal: ConsensusProposal, durationMs: number): ConsensusResult {
376
+ const n = this.nodes.size + 1;
377
+ const approvingVotes = Array.from(proposal.votes.values()).filter(
378
+ v => v.approve
379
+ ).length;
380
+
381
+ return {
382
+ proposalId: proposal.id,
383
+ approved: proposal.status === 'accepted',
384
+ approvalRate: proposal.votes.size > 0
385
+ ? approvingVotes / proposal.votes.size
386
+ : 0,
387
+ participationRate: proposal.votes.size / n,
388
+ finalValue: proposal.value,
389
+ rounds: 3, // pre-prepare, prepare, commit
390
+ durationMs,
391
+ };
392
+ }
393
+
394
+ // ===== STATE QUERIES =====
395
+
396
+ isPrimary(): boolean {
397
+ return this.node.isPrimary;
398
+ }
399
+
400
+ getViewNumber(): number {
401
+ return this.node.viewNumber;
402
+ }
403
+
404
+ getSequenceNumber(): number {
405
+ return this.node.sequenceNumber;
406
+ }
407
+
408
+ getPreparedCount(): number {
409
+ return this.node.preparedMessages.size;
410
+ }
411
+
412
+ getCommittedCount(): number {
413
+ return this.node.committedMessages.size;
414
+ }
415
+
416
+ getMaxFaultyNodes(): number {
417
+ const n = this.nodes.size + 1;
418
+ return Math.floor((n - 1) / 3);
419
+ }
420
+
421
+ canTolerate(faultyCount: number): boolean {
422
+ return faultyCount <= this.getMaxFaultyNodes();
423
+ }
424
+ }
425
+
426
+ export function createByzantineConsensus(
427
+ nodeId: string,
428
+ config?: ByzantineConfig
429
+ ): ByzantineConsensus {
430
+ return new ByzantineConsensus(nodeId, config);
431
+ }