@sparkleideas/integration 3.5.2-patch.1
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/README.md +270 -0
- package/package.json +55 -0
- package/src/__tests__/agent-adapter.test.ts +271 -0
- package/src/__tests__/agentic-flow-agent.test.ts +176 -0
- package/src/__tests__/token-optimizer.test.ts +176 -0
- package/src/agent-adapter.ts +651 -0
- package/src/agentic-flow-agent.ts +802 -0
- package/src/agentic-flow-bridge.ts +803 -0
- package/src/attention-coordinator.ts +679 -0
- package/src/feature-flags.ts +485 -0
- package/src/index.ts +466 -0
- package/src/long-running-worker.ts +871 -0
- package/src/multi-model-router.ts +1079 -0
- package/src/provider-adapter.ts +1168 -0
- package/src/sdk-bridge.ts +435 -0
- package/src/sona-adapter.ts +824 -0
- package/src/specialized-worker.ts +864 -0
- package/src/swarm-adapter.ts +1112 -0
- package/src/token-optimizer.ts +306 -0
- package/src/types.ts +494 -0
- package/src/worker-base.ts +822 -0
- package/src/worker-pool.ts +933 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkerPool - Worker Pool Management
|
|
3
|
+
*
|
|
4
|
+
* Manages a collection of workers with intelligent routing,
|
|
5
|
+
* load balancing, and lifecycle management.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Dynamic worker spawning and termination
|
|
9
|
+
* - Embedding-based task routing
|
|
10
|
+
* - Load balancing across workers
|
|
11
|
+
* - Health monitoring and auto-recovery
|
|
12
|
+
* - Type-safe worker registry
|
|
13
|
+
*
|
|
14
|
+
* Compatible with @sparkleideas/agentic-flow's worker pool patterns.
|
|
15
|
+
*
|
|
16
|
+
* @module v3/integration/worker-pool
|
|
17
|
+
* @version 3.0.0-alpha.1
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { EventEmitter } from 'events';
|
|
21
|
+
import {
|
|
22
|
+
WorkerBase,
|
|
23
|
+
WorkerConfig,
|
|
24
|
+
WorkerType,
|
|
25
|
+
WorkerMetrics,
|
|
26
|
+
WorkerHealth,
|
|
27
|
+
} from './worker-base.js';
|
|
28
|
+
import { SpecializedWorker, SpecializedWorkerConfig } from './specialized-worker.js';
|
|
29
|
+
import { LongRunningWorker, LongRunningWorkerConfig } from './long-running-worker.js';
|
|
30
|
+
import type { Task, TaskResult } from './agentic-flow-agent.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Worker pool configuration
|
|
34
|
+
*/
|
|
35
|
+
export interface WorkerPoolConfig {
|
|
36
|
+
/** Pool identifier */
|
|
37
|
+
id?: string;
|
|
38
|
+
/** Pool name */
|
|
39
|
+
name?: string;
|
|
40
|
+
/** Minimum workers to maintain */
|
|
41
|
+
minWorkers?: number;
|
|
42
|
+
/** Maximum workers allowed */
|
|
43
|
+
maxWorkers?: number;
|
|
44
|
+
/** Default worker configuration */
|
|
45
|
+
defaultWorkerConfig?: Partial<WorkerConfig>;
|
|
46
|
+
/** Enable auto-scaling */
|
|
47
|
+
autoScale?: boolean;
|
|
48
|
+
/** Scale up threshold (0.0-1.0 utilization) */
|
|
49
|
+
scaleUpThreshold?: number;
|
|
50
|
+
/** Scale down threshold (0.0-1.0 utilization) */
|
|
51
|
+
scaleDownThreshold?: number;
|
|
52
|
+
/** Health check interval in milliseconds */
|
|
53
|
+
healthCheckInterval?: number;
|
|
54
|
+
/** Enable automatic health recovery */
|
|
55
|
+
autoRecover?: boolean;
|
|
56
|
+
/** Routing strategy */
|
|
57
|
+
routingStrategy?: RoutingStrategy;
|
|
58
|
+
/** Load balancing strategy */
|
|
59
|
+
loadBalancingStrategy?: LoadBalancingStrategy;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Task routing strategy
|
|
64
|
+
*/
|
|
65
|
+
export type RoutingStrategy =
|
|
66
|
+
| 'round-robin'
|
|
67
|
+
| 'least-loaded'
|
|
68
|
+
| 'capability-match'
|
|
69
|
+
| 'embedding-similarity'
|
|
70
|
+
| 'priority-based'
|
|
71
|
+
| 'hybrid'
|
|
72
|
+
| 'custom';
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load balancing strategy
|
|
76
|
+
*/
|
|
77
|
+
export type LoadBalancingStrategy =
|
|
78
|
+
| 'equal'
|
|
79
|
+
| 'weighted'
|
|
80
|
+
| 'adaptive'
|
|
81
|
+
| 'capacity-based';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Worker routing result
|
|
85
|
+
*/
|
|
86
|
+
export interface RoutingResult {
|
|
87
|
+
/** Selected workers */
|
|
88
|
+
workers: WorkerBase[];
|
|
89
|
+
/** Routing scores for each worker */
|
|
90
|
+
scores: Map<string, number>;
|
|
91
|
+
/** Routing strategy used */
|
|
92
|
+
strategy: RoutingStrategy;
|
|
93
|
+
/** Routing metadata */
|
|
94
|
+
metadata: {
|
|
95
|
+
totalCandidates: number;
|
|
96
|
+
filtered: number;
|
|
97
|
+
matchThreshold: number;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Pool statistics
|
|
103
|
+
*/
|
|
104
|
+
export interface PoolStats {
|
|
105
|
+
/** Pool identifier */
|
|
106
|
+
poolId: string;
|
|
107
|
+
/** Total workers */
|
|
108
|
+
totalWorkers: number;
|
|
109
|
+
/** Available workers */
|
|
110
|
+
availableWorkers: number;
|
|
111
|
+
/** Busy workers */
|
|
112
|
+
busyWorkers: number;
|
|
113
|
+
/** Unhealthy workers */
|
|
114
|
+
unhealthyWorkers: number;
|
|
115
|
+
/** Average utilization */
|
|
116
|
+
avgUtilization: number;
|
|
117
|
+
/** Average health score */
|
|
118
|
+
avgHealthScore: number;
|
|
119
|
+
/** Tasks processed */
|
|
120
|
+
tasksProcessed: number;
|
|
121
|
+
/** Tasks failed */
|
|
122
|
+
tasksFailed: number;
|
|
123
|
+
/** Average task duration */
|
|
124
|
+
avgTaskDuration: number;
|
|
125
|
+
/** Worker types breakdown */
|
|
126
|
+
workerTypes: Record<WorkerType, number>;
|
|
127
|
+
/** Uptime in milliseconds */
|
|
128
|
+
uptime: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Worker spawn options
|
|
133
|
+
*/
|
|
134
|
+
export interface SpawnOptions {
|
|
135
|
+
/** Immediately initialize the worker */
|
|
136
|
+
initialize?: boolean;
|
|
137
|
+
/** Replace existing worker with same ID */
|
|
138
|
+
replace?: boolean;
|
|
139
|
+
/** Worker priority in pool */
|
|
140
|
+
poolPriority?: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* WorkerPool - Manages a collection of workers
|
|
145
|
+
*
|
|
146
|
+
* Usage:
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const pool = new WorkerPool({
|
|
149
|
+
* name: 'main-pool',
|
|
150
|
+
* minWorkers: 2,
|
|
151
|
+
* maxWorkers: 10,
|
|
152
|
+
* autoScale: true,
|
|
153
|
+
* routingStrategy: 'embedding-similarity',
|
|
154
|
+
* });
|
|
155
|
+
*
|
|
156
|
+
* await pool.initialize();
|
|
157
|
+
*
|
|
158
|
+
* // Spawn workers
|
|
159
|
+
* pool.spawn({
|
|
160
|
+
* id: 'coder-1',
|
|
161
|
+
* type: 'coder',
|
|
162
|
+
* capabilities: ['typescript', 'code-generation'],
|
|
163
|
+
* });
|
|
164
|
+
*
|
|
165
|
+
* // Route a task
|
|
166
|
+
* const workers = pool.routeTask(task, 3);
|
|
167
|
+
* for (const worker of workers) {
|
|
168
|
+
* const result = await worker.executeTask(task);
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
export class WorkerPool extends EventEmitter {
|
|
173
|
+
/** Pool identifier */
|
|
174
|
+
readonly id: string;
|
|
175
|
+
|
|
176
|
+
/** Pool name */
|
|
177
|
+
readonly name: string;
|
|
178
|
+
|
|
179
|
+
/** Worker registry */
|
|
180
|
+
workers: Map<string, WorkerBase>;
|
|
181
|
+
|
|
182
|
+
/** Pool configuration */
|
|
183
|
+
protected config: WorkerPoolConfig;
|
|
184
|
+
|
|
185
|
+
/** Pool initialized state */
|
|
186
|
+
protected initialized: boolean = false;
|
|
187
|
+
|
|
188
|
+
/** Health check timer */
|
|
189
|
+
private healthCheckTimer: NodeJS.Timeout | null = null;
|
|
190
|
+
|
|
191
|
+
/** Pool creation time */
|
|
192
|
+
private createdAt: number;
|
|
193
|
+
|
|
194
|
+
/** Round-robin index */
|
|
195
|
+
private roundRobinIndex: number = 0;
|
|
196
|
+
|
|
197
|
+
/** Pool-level metrics */
|
|
198
|
+
private poolMetrics: {
|
|
199
|
+
tasksProcessed: number;
|
|
200
|
+
tasksFailed: number;
|
|
201
|
+
totalTaskDuration: number;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Create a new WorkerPool instance
|
|
206
|
+
*
|
|
207
|
+
* @param config - Pool configuration
|
|
208
|
+
*/
|
|
209
|
+
constructor(config: WorkerPoolConfig = {}) {
|
|
210
|
+
super();
|
|
211
|
+
|
|
212
|
+
this.id = config.id || `pool_${Date.now()}`;
|
|
213
|
+
this.name = config.name || 'default-pool';
|
|
214
|
+
this.workers = new Map();
|
|
215
|
+
this.createdAt = Date.now();
|
|
216
|
+
|
|
217
|
+
this.config = {
|
|
218
|
+
minWorkers: config.minWorkers ?? 1,
|
|
219
|
+
maxWorkers: config.maxWorkers ?? 10,
|
|
220
|
+
defaultWorkerConfig: config.defaultWorkerConfig ?? {},
|
|
221
|
+
autoScale: config.autoScale ?? true,
|
|
222
|
+
scaleUpThreshold: config.scaleUpThreshold ?? 0.8,
|
|
223
|
+
scaleDownThreshold: config.scaleDownThreshold ?? 0.2,
|
|
224
|
+
healthCheckInterval: config.healthCheckInterval ?? 30000,
|
|
225
|
+
autoRecover: config.autoRecover ?? true,
|
|
226
|
+
routingStrategy: config.routingStrategy ?? 'hybrid',
|
|
227
|
+
loadBalancingStrategy: config.loadBalancingStrategy ?? 'adaptive',
|
|
228
|
+
...config,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
this.poolMetrics = {
|
|
232
|
+
tasksProcessed: 0,
|
|
233
|
+
tasksFailed: 0,
|
|
234
|
+
totalTaskDuration: 0,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
this.emit('pool-created', { poolId: this.id, name: this.name });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Initialize the pool
|
|
242
|
+
*/
|
|
243
|
+
async initialize(): Promise<void> {
|
|
244
|
+
if (this.initialized) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.emit('pool-initializing', { poolId: this.id });
|
|
249
|
+
|
|
250
|
+
// Initialize all existing workers
|
|
251
|
+
const initPromises = Array.from(this.workers.values()).map((worker) =>
|
|
252
|
+
worker.initialize().catch((error) => {
|
|
253
|
+
this.emit('worker-init-failed', {
|
|
254
|
+
poolId: this.id,
|
|
255
|
+
workerId: worker.id,
|
|
256
|
+
error,
|
|
257
|
+
});
|
|
258
|
+
})
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
await Promise.all(initPromises);
|
|
262
|
+
|
|
263
|
+
// Start health checks
|
|
264
|
+
this.startHealthChecks();
|
|
265
|
+
|
|
266
|
+
this.initialized = true;
|
|
267
|
+
|
|
268
|
+
this.emit('pool-initialized', {
|
|
269
|
+
poolId: this.id,
|
|
270
|
+
workerCount: this.workers.size,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Shutdown the pool
|
|
276
|
+
*/
|
|
277
|
+
async shutdown(): Promise<void> {
|
|
278
|
+
this.emit('pool-shutting-down', { poolId: this.id });
|
|
279
|
+
|
|
280
|
+
// Stop health checks
|
|
281
|
+
this.stopHealthChecks();
|
|
282
|
+
|
|
283
|
+
// Shutdown all workers
|
|
284
|
+
const shutdownPromises = Array.from(this.workers.values()).map((worker) =>
|
|
285
|
+
worker.shutdown().catch((error) => {
|
|
286
|
+
this.emit('worker-shutdown-failed', {
|
|
287
|
+
poolId: this.id,
|
|
288
|
+
workerId: worker.id,
|
|
289
|
+
error,
|
|
290
|
+
});
|
|
291
|
+
})
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
await Promise.all(shutdownPromises);
|
|
295
|
+
|
|
296
|
+
this.workers.clear();
|
|
297
|
+
this.initialized = false;
|
|
298
|
+
|
|
299
|
+
this.emit('pool-shutdown', { poolId: this.id });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Spawn a new worker in the pool
|
|
304
|
+
*
|
|
305
|
+
* @param config - Worker configuration
|
|
306
|
+
* @param options - Spawn options
|
|
307
|
+
* @returns Created worker
|
|
308
|
+
*/
|
|
309
|
+
spawn(
|
|
310
|
+
config: WorkerConfig | SpecializedWorkerConfig | LongRunningWorkerConfig,
|
|
311
|
+
options: SpawnOptions = {}
|
|
312
|
+
): WorkerBase {
|
|
313
|
+
// Check capacity
|
|
314
|
+
if (this.workers.size >= this.config.maxWorkers! && !options.replace) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Pool ${this.id} at maximum capacity (${this.config.maxWorkers} workers)`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Handle replacement
|
|
321
|
+
if (this.workers.has(config.id)) {
|
|
322
|
+
if (options.replace) {
|
|
323
|
+
this.terminate(config.id);
|
|
324
|
+
} else {
|
|
325
|
+
throw new Error(`Worker ${config.id} already exists in pool`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Merge with default config
|
|
330
|
+
const mergedConfig = {
|
|
331
|
+
...this.config.defaultWorkerConfig,
|
|
332
|
+
...config,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// Create appropriate worker type
|
|
336
|
+
let worker: WorkerBase;
|
|
337
|
+
|
|
338
|
+
if ('domain' in config) {
|
|
339
|
+
worker = new SpecializedWorker(config as SpecializedWorkerConfig);
|
|
340
|
+
} else if ('checkpointInterval' in config) {
|
|
341
|
+
worker = new LongRunningWorker(config as LongRunningWorkerConfig);
|
|
342
|
+
} else {
|
|
343
|
+
// Create a concrete implementation for generic workers
|
|
344
|
+
worker = new GenericWorker(mergedConfig);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Add to registry
|
|
348
|
+
this.workers.set(worker.id, worker);
|
|
349
|
+
|
|
350
|
+
// Forward worker events
|
|
351
|
+
this.forwardWorkerEvents(worker);
|
|
352
|
+
|
|
353
|
+
// Initialize if requested and pool is initialized
|
|
354
|
+
if (options.initialize && this.initialized) {
|
|
355
|
+
worker.initialize().catch((error) => {
|
|
356
|
+
this.emit('worker-init-failed', {
|
|
357
|
+
poolId: this.id,
|
|
358
|
+
workerId: worker.id,
|
|
359
|
+
error,
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.emit('worker-spawned', {
|
|
365
|
+
poolId: this.id,
|
|
366
|
+
workerId: worker.id,
|
|
367
|
+
type: worker.type,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
return worker;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Terminate a worker
|
|
375
|
+
*
|
|
376
|
+
* @param workerId - Worker ID to terminate
|
|
377
|
+
* @returns True if worker was terminated
|
|
378
|
+
*/
|
|
379
|
+
terminate(workerId: string): boolean {
|
|
380
|
+
const worker = this.workers.get(workerId);
|
|
381
|
+
|
|
382
|
+
if (!worker) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Shutdown worker
|
|
387
|
+
worker.shutdown().catch((error) => {
|
|
388
|
+
this.emit('worker-shutdown-failed', {
|
|
389
|
+
poolId: this.id,
|
|
390
|
+
workerId,
|
|
391
|
+
error,
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Remove from registry
|
|
396
|
+
this.workers.delete(workerId);
|
|
397
|
+
|
|
398
|
+
this.emit('worker-terminated', {
|
|
399
|
+
poolId: this.id,
|
|
400
|
+
workerId,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Route a task to the best workers
|
|
408
|
+
*
|
|
409
|
+
* @param task - Task to route
|
|
410
|
+
* @param topK - Number of workers to return (default: 1)
|
|
411
|
+
* @returns Array of best-matched workers
|
|
412
|
+
*/
|
|
413
|
+
routeTask(task: Task, topK: number = 1): WorkerBase[] {
|
|
414
|
+
const result = this.routeTaskWithDetails(task, topK);
|
|
415
|
+
return result.workers;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Route a task with detailed scoring information
|
|
420
|
+
*
|
|
421
|
+
* @param task - Task to route
|
|
422
|
+
* @param topK - Number of workers to return
|
|
423
|
+
* @returns Detailed routing result
|
|
424
|
+
*/
|
|
425
|
+
routeTaskWithDetails(task: Task, topK: number = 1): RoutingResult {
|
|
426
|
+
const availableWorkers = this.getAvailableWorkers();
|
|
427
|
+
const scores = new Map<string, number>();
|
|
428
|
+
|
|
429
|
+
// Score each worker
|
|
430
|
+
for (const worker of availableWorkers) {
|
|
431
|
+
const score = this.scoreWorkerForTask(worker, task);
|
|
432
|
+
scores.set(worker.id, score);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Sort by score and take top K
|
|
436
|
+
const sortedWorkers = availableWorkers
|
|
437
|
+
.sort((a, b) => (scores.get(b.id) || 0) - (scores.get(a.id) || 0))
|
|
438
|
+
.slice(0, topK);
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
workers: sortedWorkers,
|
|
442
|
+
scores,
|
|
443
|
+
strategy: this.config.routingStrategy!,
|
|
444
|
+
metadata: {
|
|
445
|
+
totalCandidates: availableWorkers.length,
|
|
446
|
+
filtered: availableWorkers.length - sortedWorkers.length,
|
|
447
|
+
matchThreshold: 0.5,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Balance load across workers
|
|
454
|
+
*
|
|
455
|
+
* Redistributes tasks or adjusts worker priorities based on
|
|
456
|
+
* the configured load balancing strategy.
|
|
457
|
+
*/
|
|
458
|
+
balanceLoad(): void {
|
|
459
|
+
const stats = this.getStats();
|
|
460
|
+
|
|
461
|
+
if (stats.avgUtilization > this.config.scaleUpThreshold! && this.config.autoScale) {
|
|
462
|
+
this.scaleUp();
|
|
463
|
+
} else if (
|
|
464
|
+
stats.avgUtilization < this.config.scaleDownThreshold! &&
|
|
465
|
+
this.config.autoScale
|
|
466
|
+
) {
|
|
467
|
+
this.scaleDown();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
this.emit('load-balanced', {
|
|
471
|
+
poolId: this.id,
|
|
472
|
+
avgUtilization: stats.avgUtilization,
|
|
473
|
+
workerCount: stats.totalWorkers,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get a worker by ID
|
|
479
|
+
*
|
|
480
|
+
* @param workerId - Worker ID
|
|
481
|
+
* @returns Worker or undefined
|
|
482
|
+
*/
|
|
483
|
+
getWorker(workerId: string): WorkerBase | undefined {
|
|
484
|
+
return this.workers.get(workerId);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get all workers
|
|
489
|
+
*/
|
|
490
|
+
getAllWorkers(): WorkerBase[] {
|
|
491
|
+
return Array.from(this.workers.values());
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Get available workers (not at capacity)
|
|
496
|
+
*/
|
|
497
|
+
getAvailableWorkers(): WorkerBase[] {
|
|
498
|
+
return Array.from(this.workers.values()).filter((worker) =>
|
|
499
|
+
worker.isAvailable()
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Get workers by type
|
|
505
|
+
*
|
|
506
|
+
* @param type - Worker type to filter
|
|
507
|
+
*/
|
|
508
|
+
getWorkersByType(type: WorkerType): WorkerBase[] {
|
|
509
|
+
return Array.from(this.workers.values()).filter(
|
|
510
|
+
(worker) => worker.type === type
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Get workers by capability
|
|
516
|
+
*
|
|
517
|
+
* @param capability - Required capability
|
|
518
|
+
*/
|
|
519
|
+
getWorkersByCapability(capability: string): WorkerBase[] {
|
|
520
|
+
return Array.from(this.workers.values()).filter((worker) =>
|
|
521
|
+
worker.capabilities.includes(capability)
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Get pool statistics
|
|
527
|
+
*/
|
|
528
|
+
getStats(): PoolStats {
|
|
529
|
+
const workers = Array.from(this.workers.values());
|
|
530
|
+
const availableWorkers = workers.filter((w) => w.isAvailable());
|
|
531
|
+
const busyWorkers = workers.filter(
|
|
532
|
+
(w) => w.status === 'busy' || w.load > 0.9
|
|
533
|
+
);
|
|
534
|
+
const unhealthyWorkers = workers.filter(
|
|
535
|
+
(w) => w.getHealth().status === 'unhealthy'
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
// Calculate averages
|
|
539
|
+
const avgUtilization =
|
|
540
|
+
workers.length > 0
|
|
541
|
+
? workers.reduce((sum, w) => sum + w.load, 0) / workers.length
|
|
542
|
+
: 0;
|
|
543
|
+
|
|
544
|
+
const avgHealthScore =
|
|
545
|
+
workers.length > 0
|
|
546
|
+
? workers.reduce((sum, w) => sum + w.getHealth().score, 0) / workers.length
|
|
547
|
+
: 1;
|
|
548
|
+
|
|
549
|
+
const avgTaskDuration =
|
|
550
|
+
this.poolMetrics.tasksProcessed > 0
|
|
551
|
+
? this.poolMetrics.totalTaskDuration / this.poolMetrics.tasksProcessed
|
|
552
|
+
: 0;
|
|
553
|
+
|
|
554
|
+
// Worker type breakdown
|
|
555
|
+
const workerTypes: Record<WorkerType, number> = {
|
|
556
|
+
coder: 0,
|
|
557
|
+
reviewer: 0,
|
|
558
|
+
tester: 0,
|
|
559
|
+
researcher: 0,
|
|
560
|
+
planner: 0,
|
|
561
|
+
architect: 0,
|
|
562
|
+
coordinator: 0,
|
|
563
|
+
security: 0,
|
|
564
|
+
performance: 0,
|
|
565
|
+
specialized: 0,
|
|
566
|
+
'long-running': 0,
|
|
567
|
+
generic: 0,
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
for (const worker of workers) {
|
|
571
|
+
if (worker.type in workerTypes) {
|
|
572
|
+
workerTypes[worker.type as WorkerType]++;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
poolId: this.id,
|
|
578
|
+
totalWorkers: workers.length,
|
|
579
|
+
availableWorkers: availableWorkers.length,
|
|
580
|
+
busyWorkers: busyWorkers.length,
|
|
581
|
+
unhealthyWorkers: unhealthyWorkers.length,
|
|
582
|
+
avgUtilization,
|
|
583
|
+
avgHealthScore,
|
|
584
|
+
tasksProcessed: this.poolMetrics.tasksProcessed,
|
|
585
|
+
tasksFailed: this.poolMetrics.tasksFailed,
|
|
586
|
+
avgTaskDuration,
|
|
587
|
+
workerTypes,
|
|
588
|
+
uptime: Date.now() - this.createdAt,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Execute a task on the best available worker
|
|
594
|
+
*
|
|
595
|
+
* @param task - Task to execute
|
|
596
|
+
* @returns Task result
|
|
597
|
+
*/
|
|
598
|
+
async executeTask(task: Task): Promise<TaskResult> {
|
|
599
|
+
const workers = this.routeTask(task, 1);
|
|
600
|
+
|
|
601
|
+
if (workers.length === 0) {
|
|
602
|
+
throw new Error('No available workers for task');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const worker = workers[0];
|
|
606
|
+
const startTime = Date.now();
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
const result = await worker.executeTask(task);
|
|
610
|
+
|
|
611
|
+
// Update pool metrics
|
|
612
|
+
this.poolMetrics.tasksProcessed++;
|
|
613
|
+
this.poolMetrics.totalTaskDuration += result.duration;
|
|
614
|
+
|
|
615
|
+
if (!result.success) {
|
|
616
|
+
this.poolMetrics.tasksFailed++;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return result;
|
|
620
|
+
} catch (error) {
|
|
621
|
+
this.poolMetrics.tasksProcessed++;
|
|
622
|
+
this.poolMetrics.tasksFailed++;
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
taskId: task.id,
|
|
626
|
+
success: false,
|
|
627
|
+
error: error as Error,
|
|
628
|
+
duration: Date.now() - startTime,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ===== Private Methods =====
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Score a worker for a specific task
|
|
637
|
+
*/
|
|
638
|
+
private scoreWorkerForTask(worker: WorkerBase, task: Task): number {
|
|
639
|
+
let score = 0;
|
|
640
|
+
|
|
641
|
+
switch (this.config.routingStrategy) {
|
|
642
|
+
case 'round-robin':
|
|
643
|
+
score = 1;
|
|
644
|
+
break;
|
|
645
|
+
|
|
646
|
+
case 'least-loaded':
|
|
647
|
+
score = 1 - worker.load;
|
|
648
|
+
break;
|
|
649
|
+
|
|
650
|
+
case 'capability-match':
|
|
651
|
+
score = this.calculateCapabilityScore(worker, task);
|
|
652
|
+
break;
|
|
653
|
+
|
|
654
|
+
case 'embedding-similarity':
|
|
655
|
+
score = this.calculateEmbeddingScore(worker, task);
|
|
656
|
+
break;
|
|
657
|
+
|
|
658
|
+
case 'priority-based':
|
|
659
|
+
score = (worker.config.priority || 5) / 10;
|
|
660
|
+
break;
|
|
661
|
+
|
|
662
|
+
case 'hybrid':
|
|
663
|
+
default:
|
|
664
|
+
// Weighted combination
|
|
665
|
+
const loadScore = (1 - worker.load) * 0.3;
|
|
666
|
+
const capabilityScore = this.calculateCapabilityScore(worker, task) * 0.3;
|
|
667
|
+
const embeddingScore = this.calculateEmbeddingScore(worker, task) * 0.25;
|
|
668
|
+
const healthScore = worker.getHealth().score * 0.15;
|
|
669
|
+
score = loadScore + capabilityScore + embeddingScore + healthScore;
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return score;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Calculate capability match score
|
|
678
|
+
*/
|
|
679
|
+
private calculateCapabilityScore(worker: WorkerBase, task: Task): number {
|
|
680
|
+
const requiredCapabilities = this.extractRequiredCapabilities(task);
|
|
681
|
+
|
|
682
|
+
if (requiredCapabilities.length === 0) {
|
|
683
|
+
return 1;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const matched = requiredCapabilities.filter((cap) =>
|
|
687
|
+
worker.capabilities.includes(cap)
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
return matched.length / requiredCapabilities.length;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Calculate embedding similarity score
|
|
695
|
+
*/
|
|
696
|
+
private calculateEmbeddingScore(worker: WorkerBase, task: Task): number {
|
|
697
|
+
if (worker instanceof SpecializedWorker) {
|
|
698
|
+
const matchResult = worker.matchTask(task);
|
|
699
|
+
return matchResult.breakdown.embeddingScore;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Generate task embedding and calculate similarity
|
|
703
|
+
const taskEmbedding = this.generateTaskEmbedding(task);
|
|
704
|
+
return worker.calculateSimilarity(taskEmbedding);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Extract required capabilities from task
|
|
709
|
+
*/
|
|
710
|
+
private extractRequiredCapabilities(task: Task): string[] {
|
|
711
|
+
if (task.metadata?.requiredCapabilities) {
|
|
712
|
+
return task.metadata.requiredCapabilities as string[];
|
|
713
|
+
}
|
|
714
|
+
return [];
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Generate a simple task embedding
|
|
719
|
+
*/
|
|
720
|
+
private generateTaskEmbedding(task: Task): Float32Array {
|
|
721
|
+
const dimension = 64;
|
|
722
|
+
const embedding = new Float32Array(dimension);
|
|
723
|
+
|
|
724
|
+
// Simple hash-based embedding from task description
|
|
725
|
+
const text = `${task.type} ${task.description}`;
|
|
726
|
+
let hash = 0;
|
|
727
|
+
for (let i = 0; i < text.length; i++) {
|
|
728
|
+
hash = ((hash << 5) - hash) + text.charCodeAt(i);
|
|
729
|
+
hash = hash & hash;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
for (let i = 0; i < dimension; i++) {
|
|
733
|
+
embedding[i] = ((hash >> (i % 32)) & 1) ? 0.1 : -0.1;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Normalize
|
|
737
|
+
const norm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
|
|
738
|
+
if (norm > 0) {
|
|
739
|
+
for (let i = 0; i < dimension; i++) {
|
|
740
|
+
embedding[i] /= norm;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return embedding;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Scale up the pool
|
|
749
|
+
*/
|
|
750
|
+
private scaleUp(): void {
|
|
751
|
+
if (this.workers.size >= this.config.maxWorkers!) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Spawn a new generic worker
|
|
756
|
+
const worker = this.spawn({
|
|
757
|
+
id: `auto-worker-${Date.now()}`,
|
|
758
|
+
type: 'generic',
|
|
759
|
+
capabilities: ['general'],
|
|
760
|
+
...this.config.defaultWorkerConfig,
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
this.emit('pool-scaled-up', {
|
|
764
|
+
poolId: this.id,
|
|
765
|
+
newWorkerId: worker.id,
|
|
766
|
+
workerCount: this.workers.size,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Scale down the pool
|
|
772
|
+
*/
|
|
773
|
+
private scaleDown(): void {
|
|
774
|
+
if (this.workers.size <= this.config.minWorkers!) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Find least utilized worker
|
|
779
|
+
const workers = Array.from(this.workers.values())
|
|
780
|
+
.filter((w) => w.status === 'idle')
|
|
781
|
+
.sort((a, b) => a.load - b.load);
|
|
782
|
+
|
|
783
|
+
if (workers.length > 0) {
|
|
784
|
+
const worker = workers[0];
|
|
785
|
+
this.terminate(worker.id);
|
|
786
|
+
|
|
787
|
+
this.emit('pool-scaled-down', {
|
|
788
|
+
poolId: this.id,
|
|
789
|
+
removedWorkerId: worker.id,
|
|
790
|
+
workerCount: this.workers.size,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Start health check timer
|
|
797
|
+
*/
|
|
798
|
+
private startHealthChecks(): void {
|
|
799
|
+
if (this.config.healthCheckInterval! <= 0) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
this.healthCheckTimer = setInterval(() => {
|
|
804
|
+
this.performHealthChecks();
|
|
805
|
+
}, this.config.healthCheckInterval!);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Stop health check timer
|
|
810
|
+
*/
|
|
811
|
+
private stopHealthChecks(): void {
|
|
812
|
+
if (this.healthCheckTimer) {
|
|
813
|
+
clearInterval(this.healthCheckTimer);
|
|
814
|
+
this.healthCheckTimer = null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Perform health checks on all workers
|
|
820
|
+
*/
|
|
821
|
+
private performHealthChecks(): void {
|
|
822
|
+
for (const worker of Array.from(this.workers.values())) {
|
|
823
|
+
const health = worker.getHealth();
|
|
824
|
+
|
|
825
|
+
if (health.status === 'unhealthy' && this.config.autoRecover) {
|
|
826
|
+
this.recoverWorker(worker);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
this.emit('worker-health-check', {
|
|
830
|
+
poolId: this.id,
|
|
831
|
+
workerId: worker.id,
|
|
832
|
+
health,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Balance load after health checks
|
|
837
|
+
this.balanceLoad();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Attempt to recover an unhealthy worker
|
|
842
|
+
*/
|
|
843
|
+
private async recoverWorker(worker: WorkerBase): Promise<void> {
|
|
844
|
+
this.emit('worker-recovering', {
|
|
845
|
+
poolId: this.id,
|
|
846
|
+
workerId: worker.id,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
try {
|
|
850
|
+
// Terminate and respawn
|
|
851
|
+
const config = { ...worker.config };
|
|
852
|
+
this.terminate(worker.id);
|
|
853
|
+
|
|
854
|
+
const newWorker = this.spawn(config, { initialize: true });
|
|
855
|
+
|
|
856
|
+
this.emit('worker-recovered', {
|
|
857
|
+
poolId: this.id,
|
|
858
|
+
oldWorkerId: worker.id,
|
|
859
|
+
newWorkerId: newWorker.id,
|
|
860
|
+
});
|
|
861
|
+
} catch (error) {
|
|
862
|
+
this.emit('worker-recovery-failed', {
|
|
863
|
+
poolId: this.id,
|
|
864
|
+
workerId: worker.id,
|
|
865
|
+
error: error as Error,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Forward worker events to pool
|
|
872
|
+
*/
|
|
873
|
+
private forwardWorkerEvents(worker: WorkerBase): void {
|
|
874
|
+
worker.on('task-started', (data) => {
|
|
875
|
+
this.emit('worker-task-started', { poolId: this.id, ...data });
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
worker.on('task-completed', (data) => {
|
|
879
|
+
this.emit('worker-task-completed', { poolId: this.id, ...data });
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
worker.on('task-failed', (data) => {
|
|
883
|
+
this.emit('worker-task-failed', { poolId: this.id, ...data });
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
worker.on('load-updated', (data) => {
|
|
887
|
+
this.emit('worker-load-updated', { poolId: this.id, ...data });
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Generic worker implementation for pool spawning
|
|
894
|
+
*/
|
|
895
|
+
class GenericWorker extends WorkerBase {
|
|
896
|
+
async execute(task: Task): Promise<import('./worker-base.js').AgentOutput> {
|
|
897
|
+
// Simple execution that returns processed task info
|
|
898
|
+
return {
|
|
899
|
+
content: {
|
|
900
|
+
taskId: task.id,
|
|
901
|
+
processed: true,
|
|
902
|
+
workerId: this.id,
|
|
903
|
+
timestamp: Date.now(),
|
|
904
|
+
},
|
|
905
|
+
success: true,
|
|
906
|
+
duration: 0,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Create a worker pool with the given configuration
|
|
913
|
+
*
|
|
914
|
+
* @param config - Pool configuration
|
|
915
|
+
* @returns Configured WorkerPool
|
|
916
|
+
*/
|
|
917
|
+
export function createWorkerPool(config: WorkerPoolConfig = {}): WorkerPool {
|
|
918
|
+
return new WorkerPool(config);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Create and initialize a worker pool
|
|
923
|
+
*
|
|
924
|
+
* @param config - Pool configuration
|
|
925
|
+
* @returns Initialized WorkerPool
|
|
926
|
+
*/
|
|
927
|
+
export async function createAndInitializeWorkerPool(
|
|
928
|
+
config: WorkerPoolConfig = {}
|
|
929
|
+
): Promise<WorkerPool> {
|
|
930
|
+
const pool = new WorkerPool(config);
|
|
931
|
+
await pool.initialize();
|
|
932
|
+
return pool;
|
|
933
|
+
}
|