@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,692 @@
|
|
|
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
|
+
import { EventEmitter } from 'events';
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Default Configuration
|
|
29
|
+
// ============================================================================
|
|
30
|
+
const DEFAULT_CONFIG = {
|
|
31
|
+
federationId: `federation_${Date.now()}`,
|
|
32
|
+
maxEphemeralAgents: 100,
|
|
33
|
+
defaultTTL: 300000, // 5 minutes
|
|
34
|
+
syncIntervalMs: 30000, // 30 seconds
|
|
35
|
+
autoCleanup: true,
|
|
36
|
+
cleanupIntervalMs: 60000, // 1 minute
|
|
37
|
+
communicationTimeoutMs: 5000,
|
|
38
|
+
enableConsensus: true,
|
|
39
|
+
consensusQuorum: 0.66,
|
|
40
|
+
};
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Federation Hub Implementation
|
|
43
|
+
// ============================================================================
|
|
44
|
+
export class FederationHub extends EventEmitter {
|
|
45
|
+
config;
|
|
46
|
+
swarms = new Map();
|
|
47
|
+
ephemeralAgents = new Map();
|
|
48
|
+
messages = [];
|
|
49
|
+
proposals = new Map();
|
|
50
|
+
syncInterval;
|
|
51
|
+
cleanupInterval;
|
|
52
|
+
startTime;
|
|
53
|
+
stats;
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Secondary Indexes for O(1) Lookups (Performance Optimization)
|
|
56
|
+
// ============================================================================
|
|
57
|
+
/** Index: swarmId -> Set of agentIds */
|
|
58
|
+
agentsBySwarm = new Map();
|
|
59
|
+
/** Index: status -> Set of agentIds */
|
|
60
|
+
agentsByStatus = new Map();
|
|
61
|
+
constructor(config) {
|
|
62
|
+
super();
|
|
63
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
64
|
+
this.startTime = new Date();
|
|
65
|
+
this.stats = {
|
|
66
|
+
messagesExchanged: 0,
|
|
67
|
+
consensusProposals: 0,
|
|
68
|
+
completedAgents: 0,
|
|
69
|
+
failedAgents: 0,
|
|
70
|
+
totalAgentLifespanMs: 0,
|
|
71
|
+
};
|
|
72
|
+
// Initialize status index sets
|
|
73
|
+
this.agentsByStatus.set('spawning', new Set());
|
|
74
|
+
this.agentsByStatus.set('active', new Set());
|
|
75
|
+
this.agentsByStatus.set('completing', new Set());
|
|
76
|
+
this.agentsByStatus.set('terminated', new Set());
|
|
77
|
+
}
|
|
78
|
+
// ==========================================================================
|
|
79
|
+
// Index Maintenance Helpers
|
|
80
|
+
// ==========================================================================
|
|
81
|
+
/**
|
|
82
|
+
* Add agent to indexes - O(1)
|
|
83
|
+
*/
|
|
84
|
+
addAgentToIndexes(agent) {
|
|
85
|
+
// Add to swarm index
|
|
86
|
+
if (!this.agentsBySwarm.has(agent.swarmId)) {
|
|
87
|
+
this.agentsBySwarm.set(agent.swarmId, new Set());
|
|
88
|
+
}
|
|
89
|
+
this.agentsBySwarm.get(agent.swarmId).add(agent.id);
|
|
90
|
+
// Add to status index
|
|
91
|
+
this.agentsByStatus.get(agent.status).add(agent.id);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Remove agent from indexes - O(1)
|
|
95
|
+
*/
|
|
96
|
+
removeAgentFromIndexes(agent) {
|
|
97
|
+
// Remove from swarm index
|
|
98
|
+
const swarmSet = this.agentsBySwarm.get(agent.swarmId);
|
|
99
|
+
if (swarmSet) {
|
|
100
|
+
swarmSet.delete(agent.id);
|
|
101
|
+
if (swarmSet.size === 0) {
|
|
102
|
+
this.agentsBySwarm.delete(agent.swarmId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Remove from status index
|
|
106
|
+
this.agentsByStatus.get(agent.status)?.delete(agent.id);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Update agent status in index - O(1)
|
|
110
|
+
*/
|
|
111
|
+
updateAgentStatusIndex(agent, oldStatus) {
|
|
112
|
+
this.agentsByStatus.get(oldStatus)?.delete(agent.id);
|
|
113
|
+
this.agentsByStatus.get(agent.status).add(agent.id);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get agents by swarm using index - O(k) where k is agents in swarm
|
|
117
|
+
*/
|
|
118
|
+
getAgentIdsBySwarm(swarmId) {
|
|
119
|
+
const agentIds = this.agentsBySwarm.get(swarmId);
|
|
120
|
+
return agentIds ? Array.from(agentIds) : [];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get agents by status using index - O(k) where k is agents with status
|
|
124
|
+
*/
|
|
125
|
+
getAgentIdsByStatus(status) {
|
|
126
|
+
const agentIds = this.agentsByStatus.get(status);
|
|
127
|
+
return agentIds ? Array.from(agentIds) : [];
|
|
128
|
+
}
|
|
129
|
+
// ==========================================================================
|
|
130
|
+
// Lifecycle
|
|
131
|
+
// ==========================================================================
|
|
132
|
+
/**
|
|
133
|
+
* Initialize the federation hub
|
|
134
|
+
*/
|
|
135
|
+
async initialize() {
|
|
136
|
+
// Start sync interval
|
|
137
|
+
this.syncInterval = setInterval(() => this.syncFederation(), this.config.syncIntervalMs);
|
|
138
|
+
// Start cleanup interval if enabled
|
|
139
|
+
if (this.config.autoCleanup) {
|
|
140
|
+
this.cleanupInterval = setInterval(() => this.cleanupExpiredAgents(), this.config.cleanupIntervalMs);
|
|
141
|
+
}
|
|
142
|
+
this.emitEvent('federation_synced');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Shutdown the federation hub
|
|
146
|
+
*/
|
|
147
|
+
async shutdown() {
|
|
148
|
+
if (this.syncInterval) {
|
|
149
|
+
clearInterval(this.syncInterval);
|
|
150
|
+
}
|
|
151
|
+
if (this.cleanupInterval) {
|
|
152
|
+
clearInterval(this.cleanupInterval);
|
|
153
|
+
}
|
|
154
|
+
// Terminate all active ephemeral agents using index - O(k) where k = active + spawning
|
|
155
|
+
const activeIds = this.getAgentIdsByStatus('active');
|
|
156
|
+
const spawningIds = this.getAgentIdsByStatus('spawning');
|
|
157
|
+
const toTerminate = [...activeIds, ...spawningIds];
|
|
158
|
+
await Promise.all(toTerminate.map(id => this.terminateAgent(id)));
|
|
159
|
+
// Clear all data structures and indexes
|
|
160
|
+
this.swarms.clear();
|
|
161
|
+
this.ephemeralAgents.clear();
|
|
162
|
+
this.proposals.clear();
|
|
163
|
+
this.agentsBySwarm.clear();
|
|
164
|
+
for (const status of this.agentsByStatus.values()) {
|
|
165
|
+
status.clear();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ==========================================================================
|
|
169
|
+
// Swarm Registration
|
|
170
|
+
// ==========================================================================
|
|
171
|
+
/**
|
|
172
|
+
* Register a swarm with the federation
|
|
173
|
+
*/
|
|
174
|
+
registerSwarm(registration) {
|
|
175
|
+
const fullRegistration = {
|
|
176
|
+
...registration,
|
|
177
|
+
registeredAt: new Date(),
|
|
178
|
+
lastHeartbeat: new Date(),
|
|
179
|
+
};
|
|
180
|
+
this.swarms.set(registration.swarmId, fullRegistration);
|
|
181
|
+
this.emitEvent('swarm_joined', registration.swarmId);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Unregister a swarm from the federation
|
|
185
|
+
*/
|
|
186
|
+
unregisterSwarm(swarmId) {
|
|
187
|
+
const removed = this.swarms.delete(swarmId);
|
|
188
|
+
if (removed) {
|
|
189
|
+
// Terminate all ephemeral agents in this swarm using index - O(k)
|
|
190
|
+
const agentIds = this.getAgentIdsBySwarm(swarmId);
|
|
191
|
+
for (const agentId of agentIds) {
|
|
192
|
+
this.terminateAgent(agentId);
|
|
193
|
+
}
|
|
194
|
+
this.emitEvent('swarm_left', swarmId);
|
|
195
|
+
}
|
|
196
|
+
return removed;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Update swarm heartbeat
|
|
200
|
+
*/
|
|
201
|
+
heartbeat(swarmId, currentAgents) {
|
|
202
|
+
const swarm = this.swarms.get(swarmId);
|
|
203
|
+
if (!swarm)
|
|
204
|
+
return false;
|
|
205
|
+
swarm.lastHeartbeat = new Date();
|
|
206
|
+
if (currentAgents !== undefined) {
|
|
207
|
+
swarm.currentAgents = currentAgents;
|
|
208
|
+
}
|
|
209
|
+
if (swarm.status === 'inactive') {
|
|
210
|
+
swarm.status = 'active';
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get all registered swarms
|
|
216
|
+
*/
|
|
217
|
+
getSwarms() {
|
|
218
|
+
return Array.from(this.swarms.values());
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get swarm by ID
|
|
222
|
+
*/
|
|
223
|
+
getSwarm(swarmId) {
|
|
224
|
+
return this.swarms.get(swarmId);
|
|
225
|
+
}
|
|
226
|
+
// ==========================================================================
|
|
227
|
+
// Ephemeral Agent Management
|
|
228
|
+
// ==========================================================================
|
|
229
|
+
/**
|
|
230
|
+
* Spawn an ephemeral agent
|
|
231
|
+
*/
|
|
232
|
+
async spawnEphemeralAgent(options) {
|
|
233
|
+
// Select target swarm
|
|
234
|
+
const targetSwarmId = options.swarmId || this.selectOptimalSwarm(options);
|
|
235
|
+
if (!targetSwarmId) {
|
|
236
|
+
return {
|
|
237
|
+
agentId: '',
|
|
238
|
+
swarmId: '',
|
|
239
|
+
status: 'failed',
|
|
240
|
+
estimatedTTL: 0,
|
|
241
|
+
error: 'No suitable swarm available',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const swarm = this.swarms.get(targetSwarmId);
|
|
245
|
+
if (!swarm) {
|
|
246
|
+
return {
|
|
247
|
+
agentId: '',
|
|
248
|
+
swarmId: targetSwarmId,
|
|
249
|
+
status: 'failed',
|
|
250
|
+
estimatedTTL: 0,
|
|
251
|
+
error: 'Swarm not found',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// Check capacity
|
|
255
|
+
const swarmAgentCount = this.getSwarmAgentCount(targetSwarmId);
|
|
256
|
+
if (swarmAgentCount >= this.config.maxEphemeralAgents) {
|
|
257
|
+
return {
|
|
258
|
+
agentId: '',
|
|
259
|
+
swarmId: targetSwarmId,
|
|
260
|
+
status: 'failed',
|
|
261
|
+
estimatedTTL: 0,
|
|
262
|
+
error: 'Swarm at capacity',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
// Create ephemeral agent
|
|
266
|
+
const ttl = options.ttl || this.config.defaultTTL;
|
|
267
|
+
const agentId = `ephemeral_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
268
|
+
const now = new Date();
|
|
269
|
+
const agent = {
|
|
270
|
+
id: agentId,
|
|
271
|
+
swarmId: targetSwarmId,
|
|
272
|
+
type: options.type,
|
|
273
|
+
task: options.task,
|
|
274
|
+
status: 'spawning',
|
|
275
|
+
ttl,
|
|
276
|
+
createdAt: now,
|
|
277
|
+
expiresAt: new Date(now.getTime() + ttl),
|
|
278
|
+
metadata: options.metadata,
|
|
279
|
+
};
|
|
280
|
+
this.ephemeralAgents.set(agentId, agent);
|
|
281
|
+
this.addAgentToIndexes(agent);
|
|
282
|
+
// Simulate spawn (in real implementation, this would call the swarm coordinator)
|
|
283
|
+
setTimeout(() => {
|
|
284
|
+
const a = this.ephemeralAgents.get(agentId);
|
|
285
|
+
if (a && a.status === 'spawning') {
|
|
286
|
+
this.updateAgentStatusIndex(a, 'spawning');
|
|
287
|
+
a.status = 'active';
|
|
288
|
+
this.emitEvent('agent_spawned', targetSwarmId, agentId);
|
|
289
|
+
}
|
|
290
|
+
}, 50);
|
|
291
|
+
// If waiting for completion
|
|
292
|
+
if (options.waitForCompletion) {
|
|
293
|
+
const timeout = options.completionTimeout || ttl;
|
|
294
|
+
const result = await this.waitForAgentCompletion(agentId, timeout);
|
|
295
|
+
return {
|
|
296
|
+
agentId,
|
|
297
|
+
swarmId: targetSwarmId,
|
|
298
|
+
status: result ? 'spawned' : 'failed',
|
|
299
|
+
estimatedTTL: ttl,
|
|
300
|
+
result: result?.result,
|
|
301
|
+
error: result?.error?.message,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
agentId,
|
|
306
|
+
swarmId: targetSwarmId,
|
|
307
|
+
status: 'spawned',
|
|
308
|
+
estimatedTTL: ttl,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Complete an ephemeral agent's task
|
|
313
|
+
*/
|
|
314
|
+
completeAgent(agentId, result) {
|
|
315
|
+
const agent = this.ephemeralAgents.get(agentId);
|
|
316
|
+
if (!agent)
|
|
317
|
+
return false;
|
|
318
|
+
const oldStatus = agent.status;
|
|
319
|
+
agent.status = 'completing';
|
|
320
|
+
this.updateAgentStatusIndex(agent, oldStatus);
|
|
321
|
+
agent.result = result;
|
|
322
|
+
agent.completedAt = new Date();
|
|
323
|
+
const lifespan = agent.completedAt.getTime() - agent.createdAt.getTime();
|
|
324
|
+
this.stats.completedAgents++;
|
|
325
|
+
this.stats.totalAgentLifespanMs += lifespan;
|
|
326
|
+
// Mark as terminated after a brief delay
|
|
327
|
+
setTimeout(() => {
|
|
328
|
+
const a = this.ephemeralAgents.get(agentId);
|
|
329
|
+
if (a) {
|
|
330
|
+
this.updateAgentStatusIndex(a, 'completing');
|
|
331
|
+
a.status = 'terminated';
|
|
332
|
+
this.emitEvent('agent_completed', a.swarmId, agentId);
|
|
333
|
+
}
|
|
334
|
+
}, 100);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Terminate an ephemeral agent
|
|
339
|
+
*/
|
|
340
|
+
async terminateAgent(agentId, error) {
|
|
341
|
+
const agent = this.ephemeralAgents.get(agentId);
|
|
342
|
+
if (!agent)
|
|
343
|
+
return false;
|
|
344
|
+
const oldStatus = agent.status;
|
|
345
|
+
agent.status = 'terminated';
|
|
346
|
+
this.updateAgentStatusIndex(agent, oldStatus);
|
|
347
|
+
agent.completedAt = new Date();
|
|
348
|
+
if (error) {
|
|
349
|
+
agent.error = error;
|
|
350
|
+
this.stats.failedAgents++;
|
|
351
|
+
this.emitEvent('agent_failed', agent.swarmId, agentId);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
this.stats.completedAgents++;
|
|
355
|
+
this.emitEvent('agent_completed', agent.swarmId, agentId);
|
|
356
|
+
}
|
|
357
|
+
const lifespan = agent.completedAt.getTime() - agent.createdAt.getTime();
|
|
358
|
+
this.stats.totalAgentLifespanMs += lifespan;
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Get ephemeral agent by ID
|
|
363
|
+
*/
|
|
364
|
+
getAgent(agentId) {
|
|
365
|
+
return this.ephemeralAgents.get(agentId);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Get all ephemeral agents
|
|
369
|
+
*/
|
|
370
|
+
getAgents(swarmId) {
|
|
371
|
+
const agents = Array.from(this.ephemeralAgents.values());
|
|
372
|
+
return swarmId ? agents.filter(a => a.swarmId === swarmId) : agents;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Get active ephemeral agents
|
|
376
|
+
*/
|
|
377
|
+
getActiveAgents(swarmId) {
|
|
378
|
+
return this.getAgents(swarmId).filter(a => a.status === 'active' || a.status === 'spawning');
|
|
379
|
+
}
|
|
380
|
+
// ==========================================================================
|
|
381
|
+
// Cross-Swarm Communication
|
|
382
|
+
// ==========================================================================
|
|
383
|
+
/**
|
|
384
|
+
* Send a message to another swarm
|
|
385
|
+
*/
|
|
386
|
+
async sendMessage(sourceSwarmId, targetSwarmId, payload) {
|
|
387
|
+
const targetSwarm = this.swarms.get(targetSwarmId);
|
|
388
|
+
if (!targetSwarm || targetSwarm.status === 'inactive') {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
const message = {
|
|
392
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
393
|
+
type: 'direct',
|
|
394
|
+
sourceSwarmId,
|
|
395
|
+
targetSwarmId,
|
|
396
|
+
payload,
|
|
397
|
+
timestamp: new Date(),
|
|
398
|
+
};
|
|
399
|
+
this.messages.push(message);
|
|
400
|
+
this.stats.messagesExchanged++;
|
|
401
|
+
this.emitEvent('message_sent', sourceSwarmId);
|
|
402
|
+
// In real implementation, this would send to the target swarm's endpoint
|
|
403
|
+
// For now, we emit an event that can be listened to
|
|
404
|
+
this.emit('message', message);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Broadcast a message to all swarms
|
|
409
|
+
*/
|
|
410
|
+
async broadcast(sourceSwarmId, payload) {
|
|
411
|
+
let sent = 0;
|
|
412
|
+
for (const swarm of this.swarms.values()) {
|
|
413
|
+
if (swarm.swarmId !== sourceSwarmId && swarm.status === 'active') {
|
|
414
|
+
const success = await this.sendMessage(sourceSwarmId, swarm.swarmId, payload);
|
|
415
|
+
if (success)
|
|
416
|
+
sent++;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return sent;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Get recent messages
|
|
423
|
+
*/
|
|
424
|
+
getMessages(limit = 100) {
|
|
425
|
+
return this.messages.slice(-limit);
|
|
426
|
+
}
|
|
427
|
+
// ==========================================================================
|
|
428
|
+
// Federation Consensus
|
|
429
|
+
// ==========================================================================
|
|
430
|
+
/**
|
|
431
|
+
* Propose a value for federation-wide consensus
|
|
432
|
+
*/
|
|
433
|
+
async propose(proposerId, type, value, timeoutMs = 30000) {
|
|
434
|
+
if (!this.config.enableConsensus) {
|
|
435
|
+
throw new Error('Consensus is disabled');
|
|
436
|
+
}
|
|
437
|
+
const proposal = {
|
|
438
|
+
id: `proposal_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
439
|
+
proposerId,
|
|
440
|
+
type,
|
|
441
|
+
value,
|
|
442
|
+
votes: new Map([[proposerId, true]]),
|
|
443
|
+
status: 'pending',
|
|
444
|
+
createdAt: new Date(),
|
|
445
|
+
expiresAt: new Date(Date.now() + timeoutMs),
|
|
446
|
+
};
|
|
447
|
+
this.proposals.set(proposal.id, proposal);
|
|
448
|
+
this.stats.consensusProposals++;
|
|
449
|
+
this.emitEvent('consensus_started', proposerId);
|
|
450
|
+
// Request votes from all active swarms
|
|
451
|
+
await this.broadcast(proposerId, {
|
|
452
|
+
type: 'vote_request',
|
|
453
|
+
proposalId: proposal.id,
|
|
454
|
+
proposalType: type,
|
|
455
|
+
value,
|
|
456
|
+
});
|
|
457
|
+
return proposal;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Vote on a proposal
|
|
461
|
+
*/
|
|
462
|
+
vote(swarmId, proposalId, approve) {
|
|
463
|
+
const proposal = this.proposals.get(proposalId);
|
|
464
|
+
if (!proposal || proposal.status !== 'pending') {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
if (new Date() > proposal.expiresAt) {
|
|
468
|
+
proposal.status = 'rejected';
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
proposal.votes.set(swarmId, approve);
|
|
472
|
+
// Check if quorum reached
|
|
473
|
+
const activeSwarms = this.getActiveSwarmCount();
|
|
474
|
+
const approvals = Array.from(proposal.votes.values()).filter(v => v).length;
|
|
475
|
+
const rejections = Array.from(proposal.votes.values()).filter(v => !v).length;
|
|
476
|
+
const quorumThreshold = Math.ceil(activeSwarms * this.config.consensusQuorum);
|
|
477
|
+
if (approvals >= quorumThreshold) {
|
|
478
|
+
proposal.status = 'accepted';
|
|
479
|
+
this.emitEvent('consensus_completed', proposal.proposerId);
|
|
480
|
+
}
|
|
481
|
+
else if (rejections > activeSwarms - quorumThreshold) {
|
|
482
|
+
proposal.status = 'rejected';
|
|
483
|
+
this.emitEvent('consensus_completed', proposal.proposerId);
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Get proposal by ID
|
|
489
|
+
*/
|
|
490
|
+
getProposal(proposalId) {
|
|
491
|
+
return this.proposals.get(proposalId);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Get all pending proposals
|
|
495
|
+
*/
|
|
496
|
+
getPendingProposals() {
|
|
497
|
+
return Array.from(this.proposals.values()).filter(p => p.status === 'pending');
|
|
498
|
+
}
|
|
499
|
+
// ==========================================================================
|
|
500
|
+
// Statistics & Monitoring
|
|
501
|
+
// ==========================================================================
|
|
502
|
+
/**
|
|
503
|
+
* Get federation statistics
|
|
504
|
+
*/
|
|
505
|
+
getStats() {
|
|
506
|
+
const activeAgents = this.getActiveAgents().length;
|
|
507
|
+
const avgLifespan = this.stats.completedAgents > 0
|
|
508
|
+
? this.stats.totalAgentLifespanMs / this.stats.completedAgents
|
|
509
|
+
: 0;
|
|
510
|
+
return {
|
|
511
|
+
federationId: this.config.federationId,
|
|
512
|
+
totalSwarms: this.swarms.size,
|
|
513
|
+
activeSwarms: this.getActiveSwarmCount(),
|
|
514
|
+
totalEphemeralAgents: this.ephemeralAgents.size,
|
|
515
|
+
activeEphemeralAgents: activeAgents,
|
|
516
|
+
completedAgents: this.stats.completedAgents,
|
|
517
|
+
failedAgents: this.stats.failedAgents,
|
|
518
|
+
avgAgentLifespanMs: avgLifespan,
|
|
519
|
+
messagesExchanged: this.stats.messagesExchanged,
|
|
520
|
+
consensusProposals: this.stats.consensusProposals,
|
|
521
|
+
uptime: Date.now() - this.startTime.getTime(),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// ==========================================================================
|
|
525
|
+
// Private Helpers
|
|
526
|
+
// ==========================================================================
|
|
527
|
+
selectOptimalSwarm(options) {
|
|
528
|
+
const candidates = [];
|
|
529
|
+
for (const swarm of this.swarms.values()) {
|
|
530
|
+
if (swarm.status !== 'active')
|
|
531
|
+
continue;
|
|
532
|
+
// Check capacity
|
|
533
|
+
const agentCount = this.getSwarmAgentCount(swarm.swarmId);
|
|
534
|
+
if (agentCount >= swarm.maxAgents)
|
|
535
|
+
continue;
|
|
536
|
+
// Check capabilities
|
|
537
|
+
if (options.capabilities) {
|
|
538
|
+
const hasAllCapabilities = options.capabilities.every(cap => swarm.capabilities.includes(cap));
|
|
539
|
+
if (!hasAllCapabilities)
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
// Calculate score (higher is better)
|
|
543
|
+
let score = 100;
|
|
544
|
+
// Prefer swarms with more available capacity
|
|
545
|
+
const availableCapacity = swarm.maxAgents - agentCount;
|
|
546
|
+
score += availableCapacity * 5;
|
|
547
|
+
// Prefer recently active swarms
|
|
548
|
+
const lastHeartbeatAge = Date.now() - swarm.lastHeartbeat.getTime();
|
|
549
|
+
score -= lastHeartbeatAge / 10000;
|
|
550
|
+
candidates.push({ swarmId: swarm.swarmId, score });
|
|
551
|
+
}
|
|
552
|
+
if (candidates.length === 0)
|
|
553
|
+
return null;
|
|
554
|
+
// Sort by score and return best
|
|
555
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
556
|
+
return candidates[0].swarmId;
|
|
557
|
+
}
|
|
558
|
+
getSwarmAgentCount(swarmId) {
|
|
559
|
+
// Use index for O(1) lookup instead of O(n) filter
|
|
560
|
+
const swarmAgents = this.agentsBySwarm.get(swarmId);
|
|
561
|
+
if (!swarmAgents)
|
|
562
|
+
return 0;
|
|
563
|
+
// Count only active and spawning agents
|
|
564
|
+
let count = 0;
|
|
565
|
+
for (const agentId of swarmAgents) {
|
|
566
|
+
const agent = this.ephemeralAgents.get(agentId);
|
|
567
|
+
if (agent && (agent.status === 'active' || agent.status === 'spawning')) {
|
|
568
|
+
count++;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return count;
|
|
572
|
+
}
|
|
573
|
+
getActiveSwarmCount() {
|
|
574
|
+
return Array.from(this.swarms.values()).filter(s => s.status === 'active').length;
|
|
575
|
+
}
|
|
576
|
+
async waitForAgentCompletion(agentId, timeout) {
|
|
577
|
+
return new Promise((resolve) => {
|
|
578
|
+
const startTime = Date.now();
|
|
579
|
+
const check = () => {
|
|
580
|
+
const agent = this.ephemeralAgents.get(agentId);
|
|
581
|
+
if (!agent) {
|
|
582
|
+
resolve(null);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (agent.status === 'terminated' || agent.status === 'completing') {
|
|
586
|
+
resolve(agent);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (Date.now() - startTime > timeout) {
|
|
590
|
+
resolve(null);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
setTimeout(check, 100);
|
|
594
|
+
};
|
|
595
|
+
check();
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
syncFederation() {
|
|
599
|
+
const now = new Date();
|
|
600
|
+
const heartbeatTimeout = this.config.syncIntervalMs * 3;
|
|
601
|
+
// Check for inactive swarms
|
|
602
|
+
for (const swarm of this.swarms.values()) {
|
|
603
|
+
const age = now.getTime() - swarm.lastHeartbeat.getTime();
|
|
604
|
+
if (age > heartbeatTimeout && swarm.status === 'active') {
|
|
605
|
+
swarm.status = 'degraded';
|
|
606
|
+
this.emitEvent('swarm_degraded', swarm.swarmId);
|
|
607
|
+
}
|
|
608
|
+
else if (age > heartbeatTimeout * 2 && swarm.status === 'degraded') {
|
|
609
|
+
swarm.status = 'inactive';
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Check for expired proposals
|
|
613
|
+
for (const proposal of this.proposals.values()) {
|
|
614
|
+
if (proposal.status === 'pending' && now > proposal.expiresAt) {
|
|
615
|
+
proposal.status = 'rejected';
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
this.emitEvent('federation_synced');
|
|
619
|
+
}
|
|
620
|
+
cleanupExpiredAgents() {
|
|
621
|
+
const now = new Date();
|
|
622
|
+
// Use status index to only check active agents - O(k) instead of O(n)
|
|
623
|
+
const activeIds = this.getAgentIdsByStatus('active');
|
|
624
|
+
for (const agentId of activeIds) {
|
|
625
|
+
const agent = this.ephemeralAgents.get(agentId);
|
|
626
|
+
if (agent && now > agent.expiresAt) {
|
|
627
|
+
this.updateAgentStatusIndex(agent, 'active');
|
|
628
|
+
agent.status = 'terminated';
|
|
629
|
+
agent.completedAt = now;
|
|
630
|
+
agent.error = new Error('Agent TTL expired');
|
|
631
|
+
this.stats.failedAgents++;
|
|
632
|
+
this.emitEvent('agent_expired', agent.swarmId, agent.id);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// Clean up old terminated agents using index - O(k)
|
|
636
|
+
const cleanupThreshold = 5 * 60 * 1000;
|
|
637
|
+
const terminatedIds = this.getAgentIdsByStatus('terminated');
|
|
638
|
+
for (const agentId of terminatedIds) {
|
|
639
|
+
const agent = this.ephemeralAgents.get(agentId);
|
|
640
|
+
if (agent &&
|
|
641
|
+
agent.completedAt &&
|
|
642
|
+
now.getTime() - agent.completedAt.getTime() > cleanupThreshold) {
|
|
643
|
+
this.removeAgentFromIndexes(agent);
|
|
644
|
+
this.ephemeralAgents.delete(agentId);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
emitEvent(type, swarmId, agentId, data) {
|
|
649
|
+
const event = {
|
|
650
|
+
type,
|
|
651
|
+
federationId: this.config.federationId,
|
|
652
|
+
swarmId,
|
|
653
|
+
agentId,
|
|
654
|
+
data,
|
|
655
|
+
timestamp: new Date(),
|
|
656
|
+
};
|
|
657
|
+
this.emit('event', event);
|
|
658
|
+
this.emit(type, event);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// ============================================================================
|
|
662
|
+
// Factory Functions
|
|
663
|
+
// ============================================================================
|
|
664
|
+
/**
|
|
665
|
+
* Create a new Federation Hub instance
|
|
666
|
+
*/
|
|
667
|
+
export function createFederationHub(config) {
|
|
668
|
+
return new FederationHub(config);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Global federation hub instance
|
|
672
|
+
*/
|
|
673
|
+
let defaultFederationHub = null;
|
|
674
|
+
/**
|
|
675
|
+
* Get or create the default federation hub
|
|
676
|
+
*/
|
|
677
|
+
export function getDefaultFederationHub() {
|
|
678
|
+
if (!defaultFederationHub) {
|
|
679
|
+
defaultFederationHub = createFederationHub();
|
|
680
|
+
}
|
|
681
|
+
return defaultFederationHub;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Reset the default federation hub
|
|
685
|
+
*/
|
|
686
|
+
export async function resetDefaultFederationHub() {
|
|
687
|
+
if (defaultFederationHub) {
|
|
688
|
+
await defaultFederationHub.shutdown();
|
|
689
|
+
defaultFederationHub = null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
//# sourceMappingURL=federation-hub.js.map
|