@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,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 Agent Pool
|
|
3
|
+
* Manages agent lifecycle, pooling, and auto-scaling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import {
|
|
8
|
+
AgentState,
|
|
9
|
+
AgentType,
|
|
10
|
+
AgentStatus,
|
|
11
|
+
AgentCapabilities,
|
|
12
|
+
AgentMetrics,
|
|
13
|
+
AgentPoolConfig,
|
|
14
|
+
AgentPoolState,
|
|
15
|
+
IAgentPool,
|
|
16
|
+
SWARM_CONSTANTS,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
interface PooledAgent {
|
|
20
|
+
agent: AgentState;
|
|
21
|
+
acquiredAt?: Date;
|
|
22
|
+
lastUsed: Date;
|
|
23
|
+
usageCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class AgentPool extends EventEmitter implements IAgentPool {
|
|
27
|
+
private config: AgentPoolConfig;
|
|
28
|
+
private pooledAgents: Map<string, PooledAgent> = new Map();
|
|
29
|
+
private available: Set<string> = new Set();
|
|
30
|
+
private busy: Set<string> = new Set();
|
|
31
|
+
private pendingScale: number = 0;
|
|
32
|
+
private lastScaleOperation?: Date;
|
|
33
|
+
private healthCheckInterval?: NodeJS.Timeout;
|
|
34
|
+
private agentCounter: number = 0;
|
|
35
|
+
|
|
36
|
+
constructor(config: Partial<AgentPoolConfig> = {}) {
|
|
37
|
+
super();
|
|
38
|
+
this.config = {
|
|
39
|
+
name: config.name ?? 'default-pool',
|
|
40
|
+
type: config.type ?? 'worker',
|
|
41
|
+
minSize: config.minSize ?? 1,
|
|
42
|
+
maxSize: config.maxSize ?? 10,
|
|
43
|
+
scaleUpThreshold: config.scaleUpThreshold ?? 0.8,
|
|
44
|
+
scaleDownThreshold: config.scaleDownThreshold ?? 0.2,
|
|
45
|
+
cooldownMs: config.cooldownMs ?? 30000,
|
|
46
|
+
healthCheckIntervalMs: config.healthCheckIntervalMs ?? SWARM_CONSTANTS.DEFAULT_HEALTH_CHECK_INTERVAL_MS,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async initialize(config?: AgentPoolConfig): Promise<void> {
|
|
51
|
+
if (config) {
|
|
52
|
+
this.config = { ...this.config, ...config };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Start with minimum pool size - create agents in parallel
|
|
56
|
+
const createPromises = Array.from(
|
|
57
|
+
{ length: this.config.minSize },
|
|
58
|
+
() => this.createPooledAgent()
|
|
59
|
+
);
|
|
60
|
+
await Promise.all(createPromises);
|
|
61
|
+
|
|
62
|
+
// Start health checks
|
|
63
|
+
this.startHealthChecks();
|
|
64
|
+
|
|
65
|
+
this.emit('initialized', {
|
|
66
|
+
poolName: this.config.name,
|
|
67
|
+
size: this.pooledAgents.size
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async shutdown(): Promise<void> {
|
|
72
|
+
if (this.healthCheckInterval) {
|
|
73
|
+
clearInterval(this.healthCheckInterval);
|
|
74
|
+
this.healthCheckInterval = undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Mark all agents as terminated
|
|
78
|
+
for (const pooled of this.pooledAgents.values()) {
|
|
79
|
+
pooled.agent.status = 'terminated';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.pooledAgents.clear();
|
|
83
|
+
this.available.clear();
|
|
84
|
+
this.busy.clear();
|
|
85
|
+
|
|
86
|
+
this.emit('shutdown');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async acquire(): Promise<AgentState | undefined> {
|
|
90
|
+
// Try to get an available agent
|
|
91
|
+
const availableId = this.available.values().next().value as string | undefined;
|
|
92
|
+
|
|
93
|
+
if (availableId) {
|
|
94
|
+
const pooled = this.pooledAgents.get(availableId);
|
|
95
|
+
if (pooled) {
|
|
96
|
+
this.available.delete(availableId);
|
|
97
|
+
this.busy.add(availableId);
|
|
98
|
+
pooled.acquiredAt = new Date();
|
|
99
|
+
pooled.usageCount++;
|
|
100
|
+
pooled.agent.status = 'busy';
|
|
101
|
+
|
|
102
|
+
this.emit('agent.acquired', { agentId: availableId });
|
|
103
|
+
|
|
104
|
+
// Check if we need to scale up
|
|
105
|
+
await this.checkScaling();
|
|
106
|
+
|
|
107
|
+
return pooled.agent;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// No available agents, try to scale up
|
|
112
|
+
if (this.pooledAgents.size < this.config.maxSize) {
|
|
113
|
+
const agent = await this.createPooledAgent();
|
|
114
|
+
if (agent) {
|
|
115
|
+
const pooled = this.pooledAgents.get(agent.id.id)!;
|
|
116
|
+
this.available.delete(agent.id.id);
|
|
117
|
+
this.busy.add(agent.id.id);
|
|
118
|
+
pooled.acquiredAt = new Date();
|
|
119
|
+
pooled.usageCount++;
|
|
120
|
+
agent.status = 'busy';
|
|
121
|
+
|
|
122
|
+
this.emit('agent.acquired', { agentId: agent.id.id });
|
|
123
|
+
|
|
124
|
+
return agent;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Pool exhausted
|
|
129
|
+
this.emit('pool.exhausted');
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async release(agentId: string): Promise<void> {
|
|
134
|
+
const pooled = this.pooledAgents.get(agentId);
|
|
135
|
+
if (!pooled) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.busy.delete(agentId);
|
|
140
|
+
this.available.add(agentId);
|
|
141
|
+
pooled.acquiredAt = undefined;
|
|
142
|
+
pooled.lastUsed = new Date();
|
|
143
|
+
pooled.agent.status = 'idle';
|
|
144
|
+
pooled.agent.currentTask = undefined;
|
|
145
|
+
|
|
146
|
+
this.emit('agent.released', { agentId });
|
|
147
|
+
|
|
148
|
+
// Check if we need to scale down
|
|
149
|
+
await this.checkScaling();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async add(agent: AgentState): Promise<void> {
|
|
153
|
+
if (this.pooledAgents.size >= this.config.maxSize) {
|
|
154
|
+
throw new Error(`Pool ${this.config.name} is at maximum capacity`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const pooled: PooledAgent = {
|
|
158
|
+
agent,
|
|
159
|
+
lastUsed: new Date(),
|
|
160
|
+
usageCount: 0,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
this.pooledAgents.set(agent.id.id, pooled);
|
|
164
|
+
this.available.add(agent.id.id);
|
|
165
|
+
|
|
166
|
+
this.emit('agent.added', { agentId: agent.id.id });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async remove(agentId: string): Promise<void> {
|
|
170
|
+
const pooled = this.pooledAgents.get(agentId);
|
|
171
|
+
if (!pooled) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.pooledAgents.delete(agentId);
|
|
176
|
+
this.available.delete(agentId);
|
|
177
|
+
this.busy.delete(agentId);
|
|
178
|
+
pooled.agent.status = 'terminated';
|
|
179
|
+
|
|
180
|
+
this.emit('agent.removed', { agentId });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async scale(delta: number): Promise<void> {
|
|
184
|
+
const now = new Date();
|
|
185
|
+
|
|
186
|
+
// Check cooldown
|
|
187
|
+
if (this.lastScaleOperation) {
|
|
188
|
+
const timeSinceLastScale = now.getTime() - this.lastScaleOperation.getTime();
|
|
189
|
+
if (timeSinceLastScale < this.config.cooldownMs) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (delta > 0) {
|
|
195
|
+
// Scale up - create agents in parallel
|
|
196
|
+
const targetSize = Math.min(
|
|
197
|
+
this.pooledAgents.size + delta,
|
|
198
|
+
this.config.maxSize
|
|
199
|
+
);
|
|
200
|
+
const toCreate = targetSize - this.pooledAgents.size;
|
|
201
|
+
|
|
202
|
+
const createPromises = Array.from(
|
|
203
|
+
{ length: toCreate },
|
|
204
|
+
() => this.createPooledAgent()
|
|
205
|
+
);
|
|
206
|
+
await Promise.all(createPromises);
|
|
207
|
+
|
|
208
|
+
this.emit('pool.scaled_up', { added: toCreate });
|
|
209
|
+
} else if (delta < 0) {
|
|
210
|
+
// Scale down - remove agents in parallel
|
|
211
|
+
const targetSize = Math.max(
|
|
212
|
+
this.pooledAgents.size + delta,
|
|
213
|
+
this.config.minSize
|
|
214
|
+
);
|
|
215
|
+
const toRemove = this.pooledAgents.size - targetSize;
|
|
216
|
+
|
|
217
|
+
// Remove least recently used available agents
|
|
218
|
+
const sortedAvailable = Array.from(this.available)
|
|
219
|
+
.map(id => this.pooledAgents.get(id)!)
|
|
220
|
+
.filter(p => p !== undefined)
|
|
221
|
+
.sort((a, b) => a.lastUsed.getTime() - b.lastUsed.getTime());
|
|
222
|
+
|
|
223
|
+
const agentsToRemove = sortedAvailable.slice(0, toRemove);
|
|
224
|
+
await Promise.all(agentsToRemove.map(pooled => this.remove(pooled.agent.id.id)));
|
|
225
|
+
|
|
226
|
+
this.emit('pool.scaled_down', { removed: agentsToRemove.length });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.lastScaleOperation = now;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
getState(): AgentPoolState {
|
|
233
|
+
return {
|
|
234
|
+
id: `pool_${this.config.name}`,
|
|
235
|
+
config: { ...this.config },
|
|
236
|
+
agents: new Map(
|
|
237
|
+
Array.from(this.pooledAgents.entries()).map(
|
|
238
|
+
([id, pooled]) => [id, pooled.agent]
|
|
239
|
+
)
|
|
240
|
+
),
|
|
241
|
+
availableAgents: Array.from(this.available),
|
|
242
|
+
busyAgents: Array.from(this.busy),
|
|
243
|
+
pendingScale: this.pendingScale,
|
|
244
|
+
lastScaleOperation: this.lastScaleOperation,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
getAvailableCount(): number {
|
|
249
|
+
return this.available.size;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
getBusyCount(): number {
|
|
253
|
+
return this.busy.size;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
getTotalCount(): number {
|
|
257
|
+
return this.pooledAgents.size;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getUtilization(): number {
|
|
261
|
+
if (this.pooledAgents.size === 0) return 0;
|
|
262
|
+
return this.busy.size / this.pooledAgents.size;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ===== PRIVATE METHODS =====
|
|
266
|
+
|
|
267
|
+
private async createPooledAgent(): Promise<AgentState | undefined> {
|
|
268
|
+
if (this.pooledAgents.size >= this.config.maxSize) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.agentCounter++;
|
|
273
|
+
const agentId = `${this.config.name}_agent_${this.agentCounter}`;
|
|
274
|
+
|
|
275
|
+
const agent: AgentState = {
|
|
276
|
+
id: {
|
|
277
|
+
id: agentId,
|
|
278
|
+
swarmId: 'pool',
|
|
279
|
+
type: this.config.type,
|
|
280
|
+
instance: this.agentCounter,
|
|
281
|
+
},
|
|
282
|
+
name: `${this.config.name}-${this.agentCounter}`,
|
|
283
|
+
type: this.config.type,
|
|
284
|
+
status: 'idle',
|
|
285
|
+
capabilities: this.createDefaultCapabilities(),
|
|
286
|
+
metrics: this.createDefaultMetrics(),
|
|
287
|
+
workload: 0,
|
|
288
|
+
health: 1.0,
|
|
289
|
+
lastHeartbeat: new Date(),
|
|
290
|
+
connections: [],
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const pooled: PooledAgent = {
|
|
294
|
+
agent,
|
|
295
|
+
lastUsed: new Date(),
|
|
296
|
+
usageCount: 0,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
this.pooledAgents.set(agentId, pooled);
|
|
300
|
+
this.available.add(agentId);
|
|
301
|
+
|
|
302
|
+
this.emit('agent.created', { agentId });
|
|
303
|
+
|
|
304
|
+
return agent;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private createDefaultCapabilities(): AgentCapabilities {
|
|
308
|
+
return {
|
|
309
|
+
codeGeneration: true,
|
|
310
|
+
codeReview: true,
|
|
311
|
+
testing: true,
|
|
312
|
+
documentation: true,
|
|
313
|
+
research: true,
|
|
314
|
+
analysis: true,
|
|
315
|
+
coordination: this.config.type === 'coordinator',
|
|
316
|
+
languages: ['typescript', 'javascript', 'python'],
|
|
317
|
+
frameworks: ['node', 'deno', 'react'],
|
|
318
|
+
domains: ['development', 'testing', 'analysis'],
|
|
319
|
+
tools: ['git', 'npm', 'editor'],
|
|
320
|
+
maxConcurrentTasks: 3,
|
|
321
|
+
maxMemoryUsage: 512 * 1024 * 1024,
|
|
322
|
+
maxExecutionTime: SWARM_CONSTANTS.DEFAULT_TASK_TIMEOUT_MS,
|
|
323
|
+
reliability: 0.95,
|
|
324
|
+
speed: 1.0,
|
|
325
|
+
quality: 0.9,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private createDefaultMetrics(): AgentMetrics {
|
|
330
|
+
return {
|
|
331
|
+
tasksCompleted: 0,
|
|
332
|
+
tasksFailed: 0,
|
|
333
|
+
averageExecutionTime: 0,
|
|
334
|
+
successRate: 1.0,
|
|
335
|
+
cpuUsage: 0,
|
|
336
|
+
memoryUsage: 0,
|
|
337
|
+
messagesProcessed: 0,
|
|
338
|
+
lastActivity: new Date(),
|
|
339
|
+
responseTime: 0,
|
|
340
|
+
health: 1.0,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private async checkScaling(): Promise<void> {
|
|
345
|
+
const utilization = this.getUtilization();
|
|
346
|
+
|
|
347
|
+
if (utilization >= this.config.scaleUpThreshold &&
|
|
348
|
+
this.pooledAgents.size < this.config.maxSize) {
|
|
349
|
+
// Scale up by 1
|
|
350
|
+
this.pendingScale = 1;
|
|
351
|
+
await this.scale(1);
|
|
352
|
+
this.pendingScale = 0;
|
|
353
|
+
} else if (utilization <= this.config.scaleDownThreshold &&
|
|
354
|
+
this.pooledAgents.size > this.config.minSize) {
|
|
355
|
+
// Scale down by 1
|
|
356
|
+
this.pendingScale = -1;
|
|
357
|
+
await this.scale(-1);
|
|
358
|
+
this.pendingScale = 0;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private startHealthChecks(): void {
|
|
363
|
+
this.healthCheckInterval = setInterval(() => {
|
|
364
|
+
this.performHealthChecks();
|
|
365
|
+
}, this.config.healthCheckIntervalMs);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private performHealthChecks(): void {
|
|
369
|
+
const now = new Date();
|
|
370
|
+
const unhealthyThresholdMs = this.config.healthCheckIntervalMs * 3;
|
|
371
|
+
|
|
372
|
+
for (const [agentId, pooled] of this.pooledAgents) {
|
|
373
|
+
const timeSinceLastActivity = now.getTime() - pooled.agent.lastHeartbeat.getTime();
|
|
374
|
+
|
|
375
|
+
if (timeSinceLastActivity > unhealthyThresholdMs) {
|
|
376
|
+
// Agent is unhealthy
|
|
377
|
+
pooled.agent.health = Math.max(0, pooled.agent.health - 0.2);
|
|
378
|
+
pooled.agent.status = 'error';
|
|
379
|
+
|
|
380
|
+
this.emit('agent.unhealthy', { agentId, health: pooled.agent.health });
|
|
381
|
+
|
|
382
|
+
// If completely unhealthy, remove and replace
|
|
383
|
+
if (pooled.agent.health <= 0) {
|
|
384
|
+
this.replaceUnhealthyAgent(agentId);
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// Update health positively
|
|
388
|
+
pooled.agent.health = Math.min(1.0, pooled.agent.health + 0.1);
|
|
389
|
+
pooled.agent.lastHeartbeat = now;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private async replaceUnhealthyAgent(agentId: string): Promise<void> {
|
|
395
|
+
const pooled = this.pooledAgents.get(agentId);
|
|
396
|
+
if (!pooled) return;
|
|
397
|
+
|
|
398
|
+
const wasBusy = this.busy.has(agentId);
|
|
399
|
+
await this.remove(agentId);
|
|
400
|
+
|
|
401
|
+
// Create replacement if below min size or was busy
|
|
402
|
+
if (this.pooledAgents.size < this.config.minSize || wasBusy) {
|
|
403
|
+
await this.createPooledAgent();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.emit('agent.replaced', { oldAgentId: agentId });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ===== UTILITY METHODS =====
|
|
410
|
+
|
|
411
|
+
getAgent(agentId: string): AgentState | undefined {
|
|
412
|
+
return this.pooledAgents.get(agentId)?.agent;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
getAllAgents(): AgentState[] {
|
|
416
|
+
return Array.from(this.pooledAgents.values()).map(p => p.agent);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
getAvailableAgents(): AgentState[] {
|
|
420
|
+
return Array.from(this.available)
|
|
421
|
+
.map(id => this.pooledAgents.get(id)?.agent)
|
|
422
|
+
.filter((a): a is AgentState => a !== undefined);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
getBusyAgents(): AgentState[] {
|
|
426
|
+
return Array.from(this.busy)
|
|
427
|
+
.map(id => this.pooledAgents.get(id)?.agent)
|
|
428
|
+
.filter((a): a is AgentState => a !== undefined);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
updateAgentHeartbeat(agentId: string): void {
|
|
432
|
+
const pooled = this.pooledAgents.get(agentId);
|
|
433
|
+
if (pooled) {
|
|
434
|
+
pooled.agent.lastHeartbeat = new Date();
|
|
435
|
+
pooled.agent.health = Math.min(1.0, pooled.agent.health + 0.05);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
updateAgentMetrics(agentId: string, metrics: Partial<AgentMetrics>): void {
|
|
440
|
+
const pooled = this.pooledAgents.get(agentId);
|
|
441
|
+
if (pooled) {
|
|
442
|
+
pooled.agent.metrics = { ...pooled.agent.metrics, ...metrics };
|
|
443
|
+
pooled.agent.lastHeartbeat = new Date();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
getPoolStats(): {
|
|
448
|
+
total: number;
|
|
449
|
+
available: number;
|
|
450
|
+
busy: number;
|
|
451
|
+
utilization: number;
|
|
452
|
+
avgHealth: number;
|
|
453
|
+
avgUsageCount: number;
|
|
454
|
+
} {
|
|
455
|
+
const agents = Array.from(this.pooledAgents.values());
|
|
456
|
+
const avgHealth = agents.length > 0
|
|
457
|
+
? agents.reduce((sum, p) => sum + p.agent.health, 0) / agents.length
|
|
458
|
+
: 1.0;
|
|
459
|
+
const avgUsageCount = agents.length > 0
|
|
460
|
+
? agents.reduce((sum, p) => sum + p.usageCount, 0) / agents.length
|
|
461
|
+
: 0;
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
total: this.pooledAgents.size,
|
|
465
|
+
available: this.available.size,
|
|
466
|
+
busy: this.busy.size,
|
|
467
|
+
utilization: this.getUtilization(),
|
|
468
|
+
avgHealth,
|
|
469
|
+
avgUsageCount,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function createAgentPool(config?: Partial<AgentPoolConfig>): AgentPool {
|
|
475
|
+
return new AgentPool(config);
|
|
476
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create Task Command - Application Layer (CQRS)
|
|
3
|
+
*
|
|
4
|
+
* Command for creating new tasks in the swarm.
|
|
5
|
+
*
|
|
6
|
+
* @module v3/swarm/application/commands
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Task, TaskPriority, TaskProps } from '../../domain/entities/task.js';
|
|
10
|
+
import { ITaskRepository } from '../../domain/repositories/task-repository.interface.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create Task Command Input
|
|
14
|
+
*/
|
|
15
|
+
export interface CreateTaskInput {
|
|
16
|
+
title: string;
|
|
17
|
+
description: string;
|
|
18
|
+
type: string;
|
|
19
|
+
priority?: TaskPriority;
|
|
20
|
+
dependencies?: string[];
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
input?: unknown;
|
|
23
|
+
timeout?: number;
|
|
24
|
+
maxRetries?: number;
|
|
25
|
+
autoQueue?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create Task Command Result
|
|
30
|
+
*/
|
|
31
|
+
export interface CreateTaskResult {
|
|
32
|
+
success: boolean;
|
|
33
|
+
taskId: string;
|
|
34
|
+
task: Task;
|
|
35
|
+
queuedAutomatically: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create Task Command Handler
|
|
40
|
+
*/
|
|
41
|
+
export class CreateTaskCommandHandler {
|
|
42
|
+
constructor(private readonly repository: ITaskRepository) {}
|
|
43
|
+
|
|
44
|
+
async execute(input: CreateTaskInput): Promise<CreateTaskResult> {
|
|
45
|
+
// Validate dependencies exist
|
|
46
|
+
if (input.dependencies && input.dependencies.length > 0) {
|
|
47
|
+
for (const depId of input.dependencies) {
|
|
48
|
+
const exists = await this.repository.exists(depId);
|
|
49
|
+
if (!exists) {
|
|
50
|
+
throw new Error(`Dependency task '${depId}' not found`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create task
|
|
56
|
+
const task = Task.create({
|
|
57
|
+
title: input.title,
|
|
58
|
+
description: input.description,
|
|
59
|
+
type: input.type,
|
|
60
|
+
priority: input.priority,
|
|
61
|
+
dependencies: input.dependencies,
|
|
62
|
+
metadata: input.metadata,
|
|
63
|
+
input: input.input,
|
|
64
|
+
timeout: input.timeout,
|
|
65
|
+
maxRetries: input.maxRetries,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Auto-queue if requested and no dependencies
|
|
69
|
+
let queuedAutomatically = false;
|
|
70
|
+
if (input.autoQueue && (!input.dependencies || input.dependencies.length === 0)) {
|
|
71
|
+
task.queue();
|
|
72
|
+
queuedAutomatically = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await this.repository.save(task);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
taskId: task.id,
|
|
80
|
+
task,
|
|
81
|
+
queuedAutomatically,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Cancel Task Command Input
|
|
88
|
+
*/
|
|
89
|
+
export interface CancelTaskInput {
|
|
90
|
+
taskId: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Cancel Task Command Result
|
|
95
|
+
*/
|
|
96
|
+
export interface CancelTaskResult {
|
|
97
|
+
success: boolean;
|
|
98
|
+
taskId: string;
|
|
99
|
+
previousStatus: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Cancel Task Command Handler
|
|
104
|
+
*/
|
|
105
|
+
export class CancelTaskCommandHandler {
|
|
106
|
+
constructor(private readonly repository: ITaskRepository) {}
|
|
107
|
+
|
|
108
|
+
async execute(input: CancelTaskInput): Promise<CancelTaskResult> {
|
|
109
|
+
const task = await this.repository.findById(input.taskId);
|
|
110
|
+
if (!task) {
|
|
111
|
+
throw new Error(`Task '${input.taskId}' not found`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const previousStatus = task.status;
|
|
115
|
+
task.cancel();
|
|
116
|
+
await this.repository.save(task);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
taskId: input.taskId,
|
|
121
|
+
previousStatus,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn Agent Command - Application Layer (CQRS)
|
|
3
|
+
*
|
|
4
|
+
* Command for spawning a new agent in the swarm.
|
|
5
|
+
*
|
|
6
|
+
* @module v3/swarm/application/commands
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Agent, AgentRole, AgentProps } from '../../domain/entities/agent.js';
|
|
10
|
+
import { IAgentRepository } from '../../domain/repositories/agent-repository.interface.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Spawn Agent Command Input
|
|
14
|
+
*/
|
|
15
|
+
export interface SpawnAgentInput {
|
|
16
|
+
name: string;
|
|
17
|
+
role: AgentRole;
|
|
18
|
+
domain: string;
|
|
19
|
+
capabilities: string[];
|
|
20
|
+
parentId?: string;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
maxConcurrentTasks?: number;
|
|
23
|
+
autoStart?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Spawn Agent Command Result
|
|
28
|
+
*/
|
|
29
|
+
export interface SpawnAgentResult {
|
|
30
|
+
success: boolean;
|
|
31
|
+
agentId: string;
|
|
32
|
+
agent: Agent;
|
|
33
|
+
startedAutomatically: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Spawn Agent Command Handler
|
|
38
|
+
*/
|
|
39
|
+
export class SpawnAgentCommandHandler {
|
|
40
|
+
constructor(private readonly repository: IAgentRepository) {}
|
|
41
|
+
|
|
42
|
+
async execute(input: SpawnAgentInput): Promise<SpawnAgentResult> {
|
|
43
|
+
// Check if agent with same name exists
|
|
44
|
+
const existing = await this.repository.findByName(input.name);
|
|
45
|
+
if (existing) {
|
|
46
|
+
throw new Error(`Agent with name '${input.name}' already exists`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create agent
|
|
50
|
+
const agent = Agent.create({
|
|
51
|
+
name: input.name,
|
|
52
|
+
role: input.role,
|
|
53
|
+
domain: input.domain,
|
|
54
|
+
capabilities: input.capabilities,
|
|
55
|
+
parentId: input.parentId,
|
|
56
|
+
metadata: input.metadata,
|
|
57
|
+
maxConcurrentTasks: input.maxConcurrentTasks,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Auto-start if requested
|
|
61
|
+
let startedAutomatically = false;
|
|
62
|
+
if (input.autoStart) {
|
|
63
|
+
agent.start();
|
|
64
|
+
startedAutomatically = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await this.repository.save(agent);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
agentId: agent.id,
|
|
72
|
+
agent,
|
|
73
|
+
startedAutomatically,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Terminate Agent Command Input
|
|
80
|
+
*/
|
|
81
|
+
export interface TerminateAgentInput {
|
|
82
|
+
agentId: string;
|
|
83
|
+
force?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Terminate Agent Command Result
|
|
88
|
+
*/
|
|
89
|
+
export interface TerminateAgentResult {
|
|
90
|
+
success: boolean;
|
|
91
|
+
agentId: string;
|
|
92
|
+
tasksReassigned: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Terminate Agent Command Handler
|
|
97
|
+
*/
|
|
98
|
+
export class TerminateAgentCommandHandler {
|
|
99
|
+
constructor(private readonly repository: IAgentRepository) {}
|
|
100
|
+
|
|
101
|
+
async execute(input: TerminateAgentInput): Promise<TerminateAgentResult> {
|
|
102
|
+
const agent = await this.repository.findById(input.agentId);
|
|
103
|
+
if (!agent) {
|
|
104
|
+
throw new Error(`Agent '${input.agentId}' not found`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const currentTasks = agent.currentTaskCount;
|
|
108
|
+
|
|
109
|
+
if (currentTasks > 0 && !input.force) {
|
|
110
|
+
throw new Error(`Agent has ${currentTasks} active tasks. Use force=true to terminate anyway.`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
agent.terminate();
|
|
114
|
+
await this.repository.save(agent);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
success: true,
|
|
118
|
+
agentId: input.agentId,
|
|
119
|
+
tasksReassigned: currentTasks,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|