digital-workers 2.1.1 → 2.3.0

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 (197) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +136 -180
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +34 -21
  5. package/dist/actions.js.map +1 -1
  6. package/dist/agent-comms.d.ts +438 -0
  7. package/dist/agent-comms.d.ts.map +1 -0
  8. package/dist/agent-comms.js +677 -0
  9. package/dist/agent-comms.js.map +1 -0
  10. package/dist/approve.d.ts +40 -8
  11. package/dist/approve.d.ts.map +1 -1
  12. package/dist/approve.js +86 -20
  13. package/dist/approve.js.map +1 -1
  14. package/dist/ask.d.ts +38 -7
  15. package/dist/ask.d.ts.map +1 -1
  16. package/dist/ask.js +85 -25
  17. package/dist/ask.js.map +1 -1
  18. package/dist/browse.d.ts +223 -0
  19. package/dist/browse.d.ts.map +1 -0
  20. package/dist/browse.js +392 -0
  21. package/dist/browse.js.map +1 -0
  22. package/dist/capability-tiers.d.ts +230 -0
  23. package/dist/capability-tiers.d.ts.map +1 -0
  24. package/dist/capability-tiers.js +388 -0
  25. package/dist/capability-tiers.js.map +1 -0
  26. package/dist/cascade-context.d.ts +523 -0
  27. package/dist/cascade-context.d.ts.map +1 -0
  28. package/dist/cascade-context.js +494 -0
  29. package/dist/cascade-context.js.map +1 -0
  30. package/dist/client.d.ts +162 -0
  31. package/dist/client.d.ts.map +1 -0
  32. package/dist/client.js +64 -0
  33. package/dist/client.js.map +1 -0
  34. package/dist/decide.d.ts +42 -6
  35. package/dist/decide.d.ts.map +1 -1
  36. package/dist/decide.js +54 -11
  37. package/dist/decide.js.map +1 -1
  38. package/dist/do.d.ts +36 -7
  39. package/dist/do.d.ts.map +1 -1
  40. package/dist/do.js +82 -39
  41. package/dist/do.js.map +1 -1
  42. package/dist/error-escalation.d.ts +416 -0
  43. package/dist/error-escalation.d.ts.map +1 -0
  44. package/dist/error-escalation.js +656 -0
  45. package/dist/error-escalation.js.map +1 -0
  46. package/dist/generate.d.ts +48 -7
  47. package/dist/generate.d.ts.map +1 -1
  48. package/dist/generate.js +49 -8
  49. package/dist/generate.js.map +1 -1
  50. package/dist/goals.d.ts +10 -9
  51. package/dist/goals.d.ts.map +1 -1
  52. package/dist/goals.js +30 -24
  53. package/dist/goals.js.map +1 -1
  54. package/dist/image.d.ts +189 -0
  55. package/dist/image.d.ts.map +1 -0
  56. package/dist/image.js +528 -0
  57. package/dist/image.js.map +1 -0
  58. package/dist/index.d.ts +59 -2
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +92 -2
  61. package/dist/index.js.map +1 -1
  62. package/dist/is.d.ts +45 -10
  63. package/dist/is.d.ts.map +1 -1
  64. package/dist/is.js +56 -21
  65. package/dist/is.js.map +1 -1
  66. package/dist/kpis.d.ts +24 -15
  67. package/dist/kpis.d.ts.map +1 -1
  68. package/dist/kpis.js +16 -14
  69. package/dist/kpis.js.map +1 -1
  70. package/dist/load-balancing.d.ts +395 -0
  71. package/dist/load-balancing.d.ts.map +1 -0
  72. package/dist/load-balancing.js +991 -0
  73. package/dist/load-balancing.js.map +1 -0
  74. package/dist/logger.d.ts +76 -0
  75. package/dist/logger.d.ts.map +1 -0
  76. package/dist/logger.js +39 -0
  77. package/dist/logger.js.map +1 -0
  78. package/dist/notify.d.ts +38 -9
  79. package/dist/notify.d.ts.map +1 -1
  80. package/dist/notify.js +72 -17
  81. package/dist/notify.js.map +1 -1
  82. package/dist/role.d.ts +5 -4
  83. package/dist/role.d.ts.map +1 -1
  84. package/dist/role.js +13 -10
  85. package/dist/role.js.map +1 -1
  86. package/dist/runtime.d.ts +310 -0
  87. package/dist/runtime.d.ts.map +1 -0
  88. package/dist/runtime.js +510 -0
  89. package/dist/runtime.js.map +1 -0
  90. package/dist/team.d.ts +11 -6
  91. package/dist/team.d.ts.map +1 -1
  92. package/dist/team.js +22 -15
  93. package/dist/team.js.map +1 -1
  94. package/dist/transports/email.d.ts +318 -0
  95. package/dist/transports/email.d.ts.map +1 -0
  96. package/dist/transports/email.js +779 -0
  97. package/dist/transports/email.js.map +1 -0
  98. package/dist/transports/slack.d.ts +515 -0
  99. package/dist/transports/slack.d.ts.map +1 -0
  100. package/dist/transports/slack.js +844 -0
  101. package/dist/transports/slack.js.map +1 -0
  102. package/dist/transports.d.ts.map +1 -1
  103. package/dist/transports.js +44 -25
  104. package/dist/transports.js.map +1 -1
  105. package/dist/types.d.ts +149 -19
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +6 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/id.d.ts +19 -0
  110. package/dist/utils/id.d.ts.map +1 -0
  111. package/dist/utils/id.js +21 -0
  112. package/dist/utils/id.js.map +1 -0
  113. package/dist/video.d.ts +203 -0
  114. package/dist/video.d.ts.map +1 -0
  115. package/dist/video.js +528 -0
  116. package/dist/video.js.map +1 -0
  117. package/dist/worker.d.ts +343 -0
  118. package/dist/worker.d.ts.map +1 -0
  119. package/dist/worker.js +698 -0
  120. package/dist/worker.js.map +1 -0
  121. package/package.json +24 -5
  122. package/src/actions.ts +48 -38
  123. package/src/agent-comms.ts +1200 -0
  124. package/src/approve.ts +91 -20
  125. package/src/ask.ts +99 -25
  126. package/src/browse.ts +627 -0
  127. package/src/capability-tiers.ts +545 -0
  128. package/src/cascade-context.ts +648 -0
  129. package/src/client.ts +221 -0
  130. package/src/decide.ts +81 -35
  131. package/src/do.ts +98 -52
  132. package/src/error-escalation.ts +1123 -0
  133. package/src/generate.ts +52 -18
  134. package/src/goals.ts +36 -27
  135. package/src/image.ts +816 -0
  136. package/src/index.ts +410 -2
  137. package/src/is.ts +59 -25
  138. package/src/kpis.ts +41 -36
  139. package/src/load-balancing.ts +1467 -0
  140. package/src/logger.ts +93 -0
  141. package/src/notify.ts +78 -17
  142. package/src/role.ts +30 -20
  143. package/src/runtime.ts +796 -0
  144. package/src/team.ts +24 -19
  145. package/src/transports/email.ts +1160 -0
  146. package/src/transports/slack.ts +1320 -0
  147. package/src/transports.ts +58 -43
  148. package/src/types.ts +182 -46
  149. package/src/utils/id.ts +21 -0
  150. package/src/video.ts +906 -0
  151. package/src/worker.ts +1007 -0
  152. package/test/agent-comms.test.ts +1397 -0
  153. package/test/approve.test.ts +305 -0
  154. package/test/ask.test.ts +274 -0
  155. package/test/browse.test.ts +361 -0
  156. package/test/capability-tiers.test.ts +631 -0
  157. package/test/cascade-context.test.ts +692 -0
  158. package/test/decide.test.ts +252 -0
  159. package/test/do.test.ts +144 -0
  160. package/test/error-escalation.test.ts +1205 -0
  161. package/test/error-logging.test.ts +357 -0
  162. package/test/generate.test.ts +319 -0
  163. package/test/image.test.ts +398 -0
  164. package/test/is.test.ts +287 -0
  165. package/test/load-balancing-safety.test.ts +404 -0
  166. package/test/load-balancing-thread-safety.test.ts +464 -0
  167. package/test/load-balancing.test.ts +1145 -0
  168. package/test/notify.test.ts +434 -0
  169. package/test/primitives.test.ts +320 -0
  170. package/test/runtime-integration.test.ts +892 -0
  171. package/test/transports/crypto.test.ts +230 -0
  172. package/test/transports/email.test.ts +866 -0
  173. package/test/transports/id-generation.test.ts +91 -0
  174. package/test/transports/slack.test.ts +760 -0
  175. package/test/type-safety.test.ts +834 -0
  176. package/test/types.test.ts +95 -2
  177. package/test/video.test.ts +530 -0
  178. package/test/worker.test.ts +1433 -0
  179. package/tsconfig.json +4 -1
  180. package/vitest.config.ts +42 -0
  181. package/wrangler.jsonc +36 -0
  182. package/.turbo/turbo-build.log +0 -5
  183. package/src/actions.js +0 -436
  184. package/src/approve.js +0 -234
  185. package/src/ask.js +0 -226
  186. package/src/decide.js +0 -244
  187. package/src/do.js +0 -227
  188. package/src/generate.js +0 -298
  189. package/src/goals.js +0 -205
  190. package/src/index.js +0 -68
  191. package/src/is.js +0 -317
  192. package/src/kpis.js +0 -270
  193. package/src/notify.js +0 -219
  194. package/src/role.js +0 -110
  195. package/src/team.js +0 -130
  196. package/src/transports.js +0 -357
  197. package/src/types.js +0 -71
@@ -0,0 +1,1467 @@
1
+ /**
2
+ * Load Balancing and Routing for Agent Coordination
3
+ *
4
+ * Provides intelligent task distribution and priority-based handling for
5
+ * coordinating work across multiple agents. Includes multiple balancing
6
+ * strategies, capability-based routing, and comprehensive metrics.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ import type { WorkerStatus, Contacts } from './types.js'
12
+
13
+ // ============================================================================
14
+ // Type Definitions
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Balancer strategy identifier
19
+ */
20
+ export type BalancerStrategy =
21
+ | 'round-robin'
22
+ | 'least-busy'
23
+ | 'capability'
24
+ | 'priority-queue'
25
+ | 'custom'
26
+
27
+ /**
28
+ * Extended agent information for load balancing
29
+ */
30
+ export interface AgentInfo {
31
+ id: string
32
+ name: string
33
+ type: 'agent'
34
+ status: WorkerStatus
35
+ skills: string[]
36
+ currentLoad: number
37
+ maxLoad: number
38
+ contacts: Contacts
39
+ metadata: Record<string, unknown>
40
+ }
41
+
42
+ /**
43
+ * Task request for routing
44
+ */
45
+ export interface TaskRequest {
46
+ id: string
47
+ name: string
48
+ requiredSkills: string[]
49
+ priority: number
50
+ metadata: Record<string, unknown>
51
+ enqueuedAt?: Date
52
+ }
53
+
54
+ /**
55
+ * Result of a routing decision
56
+ */
57
+ export interface RouteResult {
58
+ agent: AgentInfo | null
59
+ task: TaskRequest
60
+ strategy: BalancerStrategy
61
+ timestamp: Date
62
+ reason?: string
63
+ matchScore?: number
64
+ matchedRule?: string | null
65
+ usedDefault?: boolean
66
+ usedFallback?: boolean
67
+ strategies?: BalancerStrategy[]
68
+ strategyScores?: Record<string, number>
69
+ }
70
+
71
+ /**
72
+ * Agent availability information
73
+ */
74
+ export interface AgentAvailability {
75
+ status: WorkerStatus
76
+ lastSeen: Date
77
+ currentLoad?: number
78
+ maxLoad?: number
79
+ }
80
+
81
+ /**
82
+ * Routing rule definition
83
+ */
84
+ export interface RoutingRule {
85
+ name: string
86
+ priority: number
87
+ condition: RoutingRuleCondition
88
+ action: (task: TaskRequest, agents: AgentInfo[]) => AgentInfo | null
89
+ enabled?: boolean
90
+ }
91
+
92
+ /**
93
+ * Routing rule condition - function or declarative
94
+ */
95
+ export type RoutingRuleCondition =
96
+ | ((task: TaskRequest) => boolean)
97
+ | {
98
+ requiredSkills?: { contains: string }
99
+ priority?: { gte?: number; lte?: number }
100
+ metadata?: Record<string, unknown>
101
+ }
102
+
103
+ /**
104
+ * Routing metrics
105
+ */
106
+ export interface RoutingMetrics {
107
+ totalRouted: number
108
+ failedRoutes: number
109
+ averageLatencyMs: number
110
+ perAgent: Record<string, { routedCount: number; lastRouted?: Date }>
111
+ strategyUsage: Record<string, number>
112
+ }
113
+
114
+ /**
115
+ * Composite balancer configuration
116
+ */
117
+ export interface CompositeBalancerConfig {
118
+ strategies: Array<BalancerStrategy | { strategy: BalancerStrategy; weight: number }>
119
+ fallbackBehavior?: 'next-strategy' | 'none'
120
+ customStrategies?: Record<string, (task: TaskRequest, agents: AgentInfo[]) => AgentInfo | null>
121
+ /**
122
+ * Optional metrics collector for isolated metrics tracking.
123
+ * If not provided, uses the default global collector.
124
+ */
125
+ metricsCollector?: MetricsCollector
126
+ }
127
+
128
+ /**
129
+ * Load balancer interface
130
+ */
131
+ export interface LoadBalancer {
132
+ route(task: TaskRequest): RouteResult
133
+ addAgent(agent: AgentInfo): void
134
+ removeAgent(agentId: string): void
135
+ getAgents(): AgentInfo[]
136
+ }
137
+
138
+ /**
139
+ * Metrics collector interface for thread-safe metrics collection.
140
+ *
141
+ * Each MetricsCollector instance maintains its own isolated state,
142
+ * allowing multiple balancers to either share a collector for
143
+ * aggregated metrics or use separate collectors for isolated tracking.
144
+ *
145
+ * @remarks
146
+ * Thread-safety is achieved through instance isolation. Each collector
147
+ * maintains its own metrics state, eliminating race conditions between
148
+ * different balancer instances. For shared metrics across multiple
149
+ * balancers, pass the same collector instance to each balancer.
150
+ */
151
+ export interface MetricsCollector {
152
+ /**
153
+ * Record a routing event.
154
+ * @internal This method is called by balancers during routing.
155
+ */
156
+ record(result: RouteResult, latencyMs: number, strategy: BalancerStrategy): void
157
+
158
+ /**
159
+ * Collect current routing metrics.
160
+ * @returns A copy of the current metrics state
161
+ */
162
+ collect(): RoutingMetrics
163
+
164
+ /**
165
+ * Reset all metrics to initial state.
166
+ */
167
+ reset(): void
168
+ }
169
+
170
+ // ============================================================================
171
+ // Safe Array Access Utilities
172
+ // ============================================================================
173
+
174
+ /**
175
+ * Safely get the first element of an array.
176
+ *
177
+ * Provides type-safe access to array elements without non-null assertions.
178
+ *
179
+ * @param arr - The array to access
180
+ * @returns The first element or undefined if array is empty
181
+ */
182
+ function safeFirst<T>(arr: T[]): T | undefined {
183
+ return arr.length > 0 ? arr[0] : undefined
184
+ }
185
+
186
+ /**
187
+ * Safely get an element at a specific index.
188
+ *
189
+ * Provides type-safe access with bounds checking.
190
+ *
191
+ * @param arr - The array to access
192
+ * @param index - The index to access
193
+ * @returns The element at the index or undefined if out of bounds
194
+ */
195
+ function safeAt<T>(arr: T[], index: number): T | undefined {
196
+ if (arr.length === 0 || index < 0 || index >= arr.length) {
197
+ return undefined
198
+ }
199
+ return arr[index]
200
+ }
201
+
202
+ // ============================================================================
203
+ // MetricsCollector Implementation
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Create a new MetricsCollector instance with isolated state.
208
+ *
209
+ * This factory function creates a thread-safe metrics collector that
210
+ * encapsulates all metrics state within the returned instance. Multiple
211
+ * collectors can be used independently without interference.
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * // Create isolated collectors for different environments
216
+ * const prodCollector = createMetricsCollector()
217
+ * const testCollector = createMetricsCollector()
218
+ *
219
+ * // Balancers with separate metrics
220
+ * const prodBalancer = createRoundRobinBalancer(agents, { metricsCollector: prodCollector })
221
+ * const testBalancer = createRoundRobinBalancer(agents, { metricsCollector: testCollector })
222
+ * ```
223
+ *
224
+ * @example
225
+ * ```typescript
226
+ * // Shared collector for aggregated metrics
227
+ * const sharedCollector = createMetricsCollector()
228
+ * const balancer1 = createRoundRobinBalancer(agents, { metricsCollector: sharedCollector })
229
+ * const balancer2 = createLeastBusyBalancer(agents, { metricsCollector: sharedCollector })
230
+ *
231
+ * // Get combined metrics
232
+ * const metrics = sharedCollector.collect()
233
+ * ```
234
+ *
235
+ * @returns A new MetricsCollector instance
236
+ */
237
+ export function createMetricsCollector(): MetricsCollector {
238
+ // Instance-local state - each collector has its own isolated metrics
239
+ let metricsState: RoutingMetrics = {
240
+ totalRouted: 0,
241
+ failedRoutes: 0,
242
+ averageLatencyMs: 0,
243
+ perAgent: {},
244
+ strategyUsage: {},
245
+ }
246
+ let totalLatency = 0
247
+
248
+ function record(result: RouteResult, latencyMs: number, strategy: BalancerStrategy): void {
249
+ metricsState.totalRouted++
250
+ totalLatency += latencyMs
251
+ metricsState.averageLatencyMs = totalLatency / metricsState.totalRouted
252
+
253
+ if (!result.agent) {
254
+ metricsState.failedRoutes++
255
+ } else {
256
+ const agentId = result.agent.id
257
+ if (!metricsState.perAgent[agentId]) {
258
+ metricsState.perAgent[agentId] = { routedCount: 0 }
259
+ }
260
+ metricsState.perAgent[agentId].routedCount++
261
+ metricsState.perAgent[agentId].lastRouted = new Date()
262
+ }
263
+
264
+ metricsState.strategyUsage[strategy] = (metricsState.strategyUsage[strategy] || 0) + 1
265
+ }
266
+
267
+ function collect(): RoutingMetrics {
268
+ // Return a deep copy to prevent external mutation
269
+ return {
270
+ ...metricsState,
271
+ perAgent: { ...metricsState.perAgent },
272
+ strategyUsage: { ...metricsState.strategyUsage },
273
+ }
274
+ }
275
+
276
+ function reset(): void {
277
+ metricsState = {
278
+ totalRouted: 0,
279
+ failedRoutes: 0,
280
+ averageLatencyMs: 0,
281
+ perAgent: {},
282
+ strategyUsage: {},
283
+ }
284
+ totalLatency = 0
285
+ }
286
+
287
+ return { record, collect, reset }
288
+ }
289
+
290
+ // ============================================================================
291
+ // Default Global Metrics Collector (Backward Compatibility)
292
+ // ============================================================================
293
+
294
+ /**
295
+ * Default metrics collector singleton for backward compatibility.
296
+ * Used when no explicit collector is provided to balancer factories.
297
+ */
298
+ const defaultMetricsCollector = createMetricsCollector()
299
+
300
+ /**
301
+ * Collect current routing metrics from the default global collector.
302
+ *
303
+ * @remarks
304
+ * This function is provided for backward compatibility. For new code,
305
+ * consider using explicit MetricsCollector instances for better isolation.
306
+ *
307
+ * @returns Current routing metrics
308
+ */
309
+ export function collectRoutingMetrics(): RoutingMetrics {
310
+ return defaultMetricsCollector.collect()
311
+ }
312
+
313
+ /**
314
+ * Reset all routing metrics in the default global collector.
315
+ *
316
+ * @remarks
317
+ * This function is provided for backward compatibility. For new code,
318
+ * consider using explicit MetricsCollector instances for better isolation.
319
+ */
320
+ export function resetRoutingMetrics(): void {
321
+ defaultMetricsCollector.reset()
322
+ }
323
+
324
+ // ============================================================================
325
+ // Round-Robin Balancer
326
+ // ============================================================================
327
+
328
+ /**
329
+ * Options for round-robin load balancer
330
+ */
331
+ interface RoundRobinBalancerOptions {
332
+ /**
333
+ * Optional metrics collector for isolated metrics tracking.
334
+ * If not provided, uses the default global collector.
335
+ */
336
+ metricsCollector?: MetricsCollector
337
+ }
338
+
339
+ /**
340
+ * Create a round-robin load balancer
341
+ *
342
+ * Distributes tasks evenly across all available agents in order.
343
+ *
344
+ * @param initialAgents - Initial set of agents to balance across
345
+ * @param options - Optional configuration including metricsCollector
346
+ * @returns A LoadBalancer instance
347
+ */
348
+ export function createRoundRobinBalancer(
349
+ initialAgents: AgentInfo[],
350
+ options: RoundRobinBalancerOptions = {}
351
+ ): LoadBalancer {
352
+ let agents = [...initialAgents]
353
+ let currentIndex = 0
354
+ const collector = options.metricsCollector ?? defaultMetricsCollector
355
+
356
+ function getAvailableAgents(): AgentInfo[] {
357
+ return agents.filter((a) => a.status === 'available' || a.status === 'busy')
358
+ }
359
+
360
+ function route(task: TaskRequest): RouteResult {
361
+ const start = performance.now()
362
+ const available = getAvailableAgents()
363
+
364
+ if (available.length === 0) {
365
+ const result: RouteResult = {
366
+ agent: null,
367
+ task,
368
+ strategy: 'round-robin',
369
+ timestamp: new Date(),
370
+ reason: 'no-available-agents',
371
+ }
372
+ collector.record(result, performance.now() - start, 'round-robin')
373
+ return result
374
+ }
375
+
376
+ // Find the next available agent starting from current index
377
+ let attempts = 0
378
+ while (attempts < agents.length) {
379
+ const agent = safeAt(agents, currentIndex % agents.length)
380
+ currentIndex++
381
+ if (!agent) {
382
+ attempts++
383
+ continue
384
+ }
385
+
386
+ if (agent.status === 'available' || agent.status === 'busy') {
387
+ const result: RouteResult = {
388
+ agent,
389
+ task,
390
+ strategy: 'round-robin',
391
+ timestamp: new Date(),
392
+ }
393
+ collector.record(result, performance.now() - start, 'round-robin')
394
+ return result
395
+ }
396
+ attempts++
397
+ }
398
+
399
+ const result: RouteResult = {
400
+ agent: null,
401
+ task,
402
+ strategy: 'round-robin',
403
+ timestamp: new Date(),
404
+ reason: 'no-available-agents',
405
+ }
406
+ collector.record(result, performance.now() - start, 'round-robin')
407
+ return result
408
+ }
409
+
410
+ function addAgent(agent: AgentInfo): void {
411
+ agents.push(agent)
412
+ }
413
+
414
+ function removeAgent(agentId: string): void {
415
+ agents = agents.filter((a) => a.id !== agentId)
416
+ }
417
+
418
+ function getAgents(): AgentInfo[] {
419
+ return [...agents]
420
+ }
421
+
422
+ return { route, addAgent, removeAgent, getAgents }
423
+ }
424
+
425
+ // ============================================================================
426
+ // Least-Busy Balancer
427
+ // ============================================================================
428
+
429
+ interface LeastBusyBalancer extends LoadBalancer {
430
+ getLoadMetrics(): Record<string, number>
431
+ releaseLoad(agentId: string): void
432
+ setLoad(agentId: string, load: number): void
433
+ }
434
+
435
+ /**
436
+ * Options for least-busy load balancer
437
+ */
438
+ interface LeastBusyBalancerOptions {
439
+ /**
440
+ * Optional metrics collector for isolated metrics tracking.
441
+ * If not provided, uses the default global collector.
442
+ */
443
+ metricsCollector?: MetricsCollector
444
+ }
445
+
446
+ /**
447
+ * Create a least-busy load balancer
448
+ *
449
+ * Routes tasks to agents with the lowest current load.
450
+ *
451
+ * @param initialAgents - Initial set of agents to balance across
452
+ * @param options - Optional configuration including metricsCollector
453
+ * @returns A LeastBusyBalancer instance
454
+ */
455
+ export function createLeastBusyBalancer(
456
+ initialAgents: AgentInfo[],
457
+ options: LeastBusyBalancerOptions = {}
458
+ ): LeastBusyBalancer {
459
+ let agents = [...initialAgents]
460
+ const loadTracking = new Map<string, number>()
461
+ let lastRoutedIndex = -1
462
+ const collector = options.metricsCollector ?? defaultMetricsCollector
463
+
464
+ // Initialize load tracking
465
+ agents.forEach((a) => loadTracking.set(a.id, a.currentLoad))
466
+
467
+ function getAvailableAgents(): AgentInfo[] {
468
+ return agents.filter((a) => {
469
+ if (a.status !== 'available' && a.status !== 'busy') return false
470
+ const load = loadTracking.get(a.id) ?? a.currentLoad
471
+ return load < a.maxLoad
472
+ })
473
+ }
474
+
475
+ function route(task: TaskRequest): RouteResult {
476
+ const start = performance.now()
477
+ const available = getAvailableAgents()
478
+
479
+ if (available.length === 0) {
480
+ const result: RouteResult = {
481
+ agent: null,
482
+ task,
483
+ strategy: 'least-busy',
484
+ timestamp: new Date(),
485
+ reason: 'no-available-agents',
486
+ }
487
+ collector.record(result, performance.now() - start, 'least-busy')
488
+ return result
489
+ }
490
+
491
+ // Sort by load percentage
492
+ const sorted = [...available].sort((a, b) => {
493
+ const loadA = (loadTracking.get(a.id) ?? a.currentLoad) / a.maxLoad
494
+ const loadB = (loadTracking.get(b.id) ?? b.currentLoad) / b.maxLoad
495
+ if (loadA === loadB) {
496
+ // Tie-breaking with round-robin behavior
497
+ const indexA = agents.indexOf(a)
498
+ const indexB = agents.indexOf(b)
499
+ const distA = (indexA - lastRoutedIndex + agents.length) % agents.length
500
+ const distB = (indexB - lastRoutedIndex + agents.length) % agents.length
501
+ return distA - distB
502
+ }
503
+ return loadA - loadB
504
+ })
505
+
506
+ const selected = safeFirst(sorted)
507
+ if (!selected) {
508
+ const result: RouteResult = {
509
+ agent: null,
510
+ task,
511
+ strategy: 'least-busy',
512
+ timestamp: new Date(),
513
+ reason: 'no-available-agents',
514
+ }
515
+ collector.record(result, performance.now() - start, 'least-busy')
516
+ return result
517
+ }
518
+ lastRoutedIndex = agents.indexOf(selected)
519
+
520
+ // Increment load
521
+ const currentLoad = loadTracking.get(selected.id) ?? selected.currentLoad
522
+ loadTracking.set(selected.id, currentLoad + 1)
523
+
524
+ const result: RouteResult = {
525
+ agent: selected,
526
+ task,
527
+ strategy: 'least-busy',
528
+ timestamp: new Date(),
529
+ }
530
+ collector.record(result, performance.now() - start, 'least-busy')
531
+ return result
532
+ }
533
+
534
+ function addAgent(agent: AgentInfo): void {
535
+ agents.push(agent)
536
+ loadTracking.set(agent.id, agent.currentLoad)
537
+ }
538
+
539
+ function removeAgent(agentId: string): void {
540
+ agents = agents.filter((a) => a.id !== agentId)
541
+ loadTracking.delete(agentId)
542
+ }
543
+
544
+ function getAgents(): AgentInfo[] {
545
+ return [...agents]
546
+ }
547
+
548
+ function getLoadMetrics(): Record<string, number> {
549
+ const metrics: Record<string, number> = {}
550
+ agents.forEach((a) => {
551
+ const load = loadTracking.get(a.id) ?? a.currentLoad
552
+ metrics[a.id] = load / a.maxLoad
553
+ })
554
+ return metrics
555
+ }
556
+
557
+ function releaseLoad(agentId: string): void {
558
+ const current = loadTracking.get(agentId)
559
+ if (current !== undefined && current > 0) {
560
+ loadTracking.set(agentId, current - 1)
561
+ }
562
+ }
563
+
564
+ function setLoad(agentId: string, load: number): void {
565
+ loadTracking.set(agentId, load)
566
+ }
567
+
568
+ return { route, addAgent, removeAgent, getAgents, getLoadMetrics, releaseLoad, setLoad }
569
+ }
570
+
571
+ // ============================================================================
572
+ // Capability-Based Router
573
+ // ============================================================================
574
+
575
+ /**
576
+ * Options for capability-based router
577
+ */
578
+ interface CapabilityRouterOptions {
579
+ /**
580
+ * Prefer agents with exact skill match over agents with additional skills
581
+ */
582
+ preferExactMatch?: boolean
583
+ /**
584
+ * Optional metrics collector for isolated metrics tracking.
585
+ * If not provided, uses the default global collector.
586
+ */
587
+ metricsCollector?: MetricsCollector
588
+ }
589
+
590
+ interface CapabilityRouter extends LoadBalancer {
591
+ findAgentsWithSkills(skills: string[]): AgentInfo[]
592
+ getSkillCoverage(): Record<string, number>
593
+ }
594
+
595
+ /**
596
+ * Create a capability-based router
597
+ *
598
+ * Routes tasks to agents that have the required skills.
599
+ *
600
+ * @param initialAgents - Initial set of agents to route to
601
+ * @param options - Optional configuration including metricsCollector
602
+ * @returns A CapabilityRouter instance
603
+ */
604
+ export function createCapabilityRouter(
605
+ initialAgents: AgentInfo[],
606
+ options: CapabilityRouterOptions = {}
607
+ ): CapabilityRouter {
608
+ let agents = [...initialAgents]
609
+ const collector = options.metricsCollector ?? defaultMetricsCollector
610
+
611
+ function getAvailableAgents(): AgentInfo[] {
612
+ return agents.filter((a) => a.status === 'available' || a.status === 'busy')
613
+ }
614
+
615
+ function hasAllSkills(agent: AgentInfo, requiredSkills: string[]): boolean {
616
+ return requiredSkills.every((skill) => agent.skills.includes(skill))
617
+ }
618
+
619
+ function calculateMatchScore(agent: AgentInfo, requiredSkills: string[]): number {
620
+ if (requiredSkills.length === 0) return 1
621
+ const matchingSkills = requiredSkills.filter((s) => agent.skills.includes(s))
622
+ return matchingSkills.length / requiredSkills.length
623
+ }
624
+
625
+ function route(task: TaskRequest): RouteResult {
626
+ const start = performance.now()
627
+ const available = getAvailableAgents()
628
+
629
+ // Find agents with all required skills
630
+ let candidates = available.filter((a) => hasAllSkills(a, task.requiredSkills))
631
+
632
+ if (candidates.length === 0) {
633
+ const result: RouteResult = {
634
+ agent: null,
635
+ task,
636
+ strategy: 'capability',
637
+ timestamp: new Date(),
638
+ reason: 'no-matching-capability',
639
+ }
640
+ collector.record(result, performance.now() - start, 'capability')
641
+ return result
642
+ }
643
+
644
+ // Sort by match quality
645
+ if (options.preferExactMatch) {
646
+ // Prefer agents with closest skill count to requirements
647
+ candidates.sort((a, b) => {
648
+ const diffA = Math.abs(a.skills.length - task.requiredSkills.length)
649
+ const diffB = Math.abs(b.skills.length - task.requiredSkills.length)
650
+ return diffA - diffB
651
+ })
652
+ }
653
+
654
+ const selected = safeFirst(candidates)
655
+ if (!selected) {
656
+ const result: RouteResult = {
657
+ agent: null,
658
+ task,
659
+ strategy: 'capability',
660
+ timestamp: new Date(),
661
+ reason: 'no-matching-capability',
662
+ }
663
+ collector.record(result, performance.now() - start, 'capability')
664
+ return result
665
+ }
666
+ const matchScore = calculateMatchScore(selected, task.requiredSkills)
667
+
668
+ const result: RouteResult = {
669
+ agent: selected,
670
+ task,
671
+ strategy: 'capability',
672
+ timestamp: new Date(),
673
+ matchScore,
674
+ }
675
+ collector.record(result, performance.now() - start, 'capability')
676
+ return result
677
+ }
678
+
679
+ function addAgent(agent: AgentInfo): void {
680
+ agents.push(agent)
681
+ }
682
+
683
+ function removeAgent(agentId: string): void {
684
+ agents = agents.filter((a) => a.id !== agentId)
685
+ }
686
+
687
+ function getAgents(): AgentInfo[] {
688
+ return [...agents]
689
+ }
690
+
691
+ function findAgentsWithSkills(skills: string[]): AgentInfo[] {
692
+ return agents.filter((a) => hasAllSkills(a, skills))
693
+ }
694
+
695
+ function getSkillCoverage(): Record<string, number> {
696
+ const coverage: Record<string, number> = {}
697
+ agents.forEach((a) => {
698
+ a.skills.forEach((skill) => {
699
+ coverage[skill] = (coverage[skill] || 0) + 1
700
+ })
701
+ })
702
+ return coverage
703
+ }
704
+
705
+ return { route, addAgent, removeAgent, getAgents, findAgentsWithSkills, getSkillCoverage }
706
+ }
707
+
708
+ // ============================================================================
709
+ // Priority Queue Balancer
710
+ // ============================================================================
711
+
712
+ /**
713
+ * Options for priority queue balancer
714
+ */
715
+ interface PriorityQueueOptions {
716
+ /**
717
+ * Enable priority aging to prevent task starvation
718
+ */
719
+ enableAging?: boolean
720
+ /**
721
+ * Priority boost per second when aging is enabled
722
+ */
723
+ agingBoostPerSecond?: number
724
+ /**
725
+ * Maximum wait time before task is promoted to highest priority
726
+ */
727
+ maxWaitTime?: number
728
+ /**
729
+ * Optional metrics collector for isolated metrics tracking.
730
+ * If not provided, uses the default global collector.
731
+ */
732
+ metricsCollector?: MetricsCollector
733
+ }
734
+
735
+ interface PriorityQueueBalancer extends LoadBalancer {
736
+ enqueue(task: TaskRequest): void
737
+ routeNext(): Promise<RouteResult | null>
738
+ queueSize(): number
739
+ clear(): void
740
+ peek(): TaskRequest | null
741
+ getEffectivePriority(taskId: string): number
742
+ }
743
+
744
+ /**
745
+ * Create a priority queue balancer
746
+ *
747
+ * Processes tasks in priority order with optional aging to prevent starvation.
748
+ *
749
+ * @param initialAgents - Initial set of agents to balance across
750
+ * @param options - Optional configuration including metricsCollector
751
+ * @returns A PriorityQueueBalancer instance
752
+ */
753
+ export function createPriorityQueueBalancer(
754
+ initialAgents: AgentInfo[],
755
+ options: PriorityQueueOptions = {}
756
+ ): PriorityQueueBalancer {
757
+ let agents = [...initialAgents]
758
+ const queue: TaskRequest[] = []
759
+ const { enableAging = false, agingBoostPerSecond = 1, maxWaitTime } = options
760
+ const collector = options.metricsCollector ?? defaultMetricsCollector
761
+
762
+ function getAvailableAgents(): AgentInfo[] {
763
+ return agents.filter((a) => a.status === 'available' || a.status === 'busy')
764
+ }
765
+
766
+ function getEffectivePriority(taskId: string): number {
767
+ const task = queue.find((t) => t.id === taskId)
768
+ if (!task) return 0
769
+
770
+ let priority = task.priority
771
+
772
+ if (enableAging && task.enqueuedAt) {
773
+ const waitTimeMs = Date.now() - task.enqueuedAt.getTime()
774
+ const waitTimeSeconds = waitTimeMs / 1000
775
+ priority += waitTimeSeconds * agingBoostPerSecond
776
+ }
777
+
778
+ if (maxWaitTime && task.enqueuedAt) {
779
+ const waitTimeMs = Date.now() - task.enqueuedAt.getTime()
780
+ if (waitTimeMs >= maxWaitTime) {
781
+ priority = Infinity // Promote to highest priority
782
+ }
783
+ }
784
+
785
+ return priority
786
+ }
787
+
788
+ function sortQueue(): void {
789
+ queue.sort((a, b) => {
790
+ const priorityA = getEffectivePriority(a.id)
791
+ const priorityB = getEffectivePriority(b.id)
792
+ if (priorityB !== priorityA) {
793
+ return priorityB - priorityA // Higher priority first
794
+ }
795
+ // FIFO for equal priority
796
+ const timeA = a.enqueuedAt?.getTime() ?? 0
797
+ const timeB = b.enqueuedAt?.getTime() ?? 0
798
+ return timeA - timeB
799
+ })
800
+ }
801
+
802
+ function enqueue(task: TaskRequest): void {
803
+ if (task.priority < 1 || task.priority > 10) {
804
+ throw new Error('Priority must be between 1 and 10')
805
+ }
806
+ const taskWithTime = { ...task, enqueuedAt: new Date() }
807
+ queue.push(taskWithTime)
808
+ sortQueue()
809
+ }
810
+
811
+ async function routeNext(): Promise<RouteResult | null> {
812
+ if (queue.length === 0) return null
813
+
814
+ sortQueue()
815
+ const task = queue.shift()
816
+ if (!task) return null
817
+ const start = performance.now()
818
+ const available = getAvailableAgents()
819
+
820
+ if (available.length === 0) {
821
+ const result: RouteResult = {
822
+ agent: null,
823
+ task,
824
+ strategy: 'priority-queue',
825
+ timestamp: new Date(),
826
+ reason: 'no-available-agents',
827
+ }
828
+ collector.record(result, performance.now() - start, 'priority-queue')
829
+ return result
830
+ }
831
+
832
+ // Simple round-robin among available for now
833
+ const agent = safeFirst(available)
834
+ if (!agent) {
835
+ const result: RouteResult = {
836
+ agent: null,
837
+ task,
838
+ strategy: 'priority-queue',
839
+ timestamp: new Date(),
840
+ reason: 'no-available-agents',
841
+ }
842
+ collector.record(result, performance.now() - start, 'priority-queue')
843
+ return result
844
+ }
845
+
846
+ const result: RouteResult = {
847
+ agent,
848
+ task,
849
+ strategy: 'priority-queue',
850
+ timestamp: new Date(),
851
+ }
852
+ collector.record(result, performance.now() - start, 'priority-queue')
853
+ return result
854
+ }
855
+
856
+ function route(task: TaskRequest): RouteResult {
857
+ const start = performance.now()
858
+ const available = getAvailableAgents()
859
+
860
+ if (available.length === 0) {
861
+ const result: RouteResult = {
862
+ agent: null,
863
+ task,
864
+ strategy: 'priority-queue',
865
+ timestamp: new Date(),
866
+ reason: 'no-available-agents',
867
+ }
868
+ collector.record(result, performance.now() - start, 'priority-queue')
869
+ return result
870
+ }
871
+
872
+ const agent = safeFirst(available)
873
+ if (!agent) {
874
+ const result: RouteResult = {
875
+ agent: null,
876
+ task,
877
+ strategy: 'priority-queue',
878
+ timestamp: new Date(),
879
+ reason: 'no-available-agents',
880
+ }
881
+ collector.record(result, performance.now() - start, 'priority-queue')
882
+ return result
883
+ }
884
+ const result: RouteResult = {
885
+ agent,
886
+ task,
887
+ strategy: 'priority-queue',
888
+ timestamp: new Date(),
889
+ }
890
+ collector.record(result, performance.now() - start, 'priority-queue')
891
+ return result
892
+ }
893
+
894
+ function addAgent(agent: AgentInfo): void {
895
+ agents.push(agent)
896
+ }
897
+
898
+ function removeAgent(agentId: string): void {
899
+ agents = agents.filter((a) => a.id !== agentId)
900
+ }
901
+
902
+ function getAgents(): AgentInfo[] {
903
+ return [...agents]
904
+ }
905
+
906
+ function queueSize(): number {
907
+ return queue.length
908
+ }
909
+
910
+ function clear(): void {
911
+ queue.length = 0
912
+ }
913
+
914
+ function peek(): TaskRequest | null {
915
+ if (queue.length === 0) return null
916
+ sortQueue()
917
+ return queue[0] ?? null
918
+ }
919
+
920
+ return {
921
+ route,
922
+ addAgent,
923
+ removeAgent,
924
+ getAgents,
925
+ enqueue,
926
+ routeNext,
927
+ queueSize,
928
+ clear,
929
+ peek,
930
+ getEffectivePriority,
931
+ }
932
+ }
933
+
934
+ // ============================================================================
935
+ // Agent Availability Tracker
936
+ // ============================================================================
937
+
938
+ interface AvailabilityTrackerOptions {
939
+ heartbeatTimeout?: number
940
+ }
941
+
942
+ interface StatusChangeEvent {
943
+ agentId: string
944
+ previousStatus: WorkerStatus
945
+ currentStatus: WorkerStatus
946
+ timestamp: Date
947
+ }
948
+
949
+ interface CapacityInfo {
950
+ total: number
951
+ used: number
952
+ available: number
953
+ utilization: number
954
+ }
955
+
956
+ interface AgentAvailabilityTracker {
957
+ getAvailability(agentId: string): AgentAvailability
958
+ updateStatus(agentId: string, status: WorkerStatus): void
959
+ getAvailableAgents(): AgentInfo[]
960
+ heartbeat(agentId: string): void
961
+ checkTimeouts(): void
962
+ onStatusChange(handler: (event: StatusChangeEvent) => void): void
963
+ updateLoad(agentId: string, current: number, max: number): void
964
+ getCapacityUtilization(): Record<string, number>
965
+ getOverallCapacity(): CapacityInfo
966
+ }
967
+
968
+ /**
969
+ * Create an agent availability tracker
970
+ *
971
+ * Tracks agent status, heartbeats, and capacity.
972
+ */
973
+ export function createAgentAvailabilityTracker(
974
+ initialAgents: AgentInfo[],
975
+ options: AvailabilityTrackerOptions = {}
976
+ ): AgentAvailabilityTracker {
977
+ const agents = new Map<string, AgentInfo>()
978
+ const availability = new Map<string, AgentAvailability>()
979
+ const handlers: Array<(event: StatusChangeEvent) => void> = []
980
+ const { heartbeatTimeout = 30000 } = options
981
+
982
+ // Initialize
983
+ initialAgents.forEach((a) => {
984
+ agents.set(a.id, a)
985
+ availability.set(a.id, {
986
+ status: a.status,
987
+ lastSeen: new Date(),
988
+ currentLoad: a.currentLoad,
989
+ maxLoad: a.maxLoad,
990
+ })
991
+ })
992
+
993
+ function getAvailability(agentId: string): AgentAvailability {
994
+ return (
995
+ availability.get(agentId) ?? {
996
+ status: 'offline',
997
+ lastSeen: new Date(0),
998
+ }
999
+ )
1000
+ }
1001
+
1002
+ function updateStatus(agentId: string, status: WorkerStatus): void {
1003
+ const current = availability.get(agentId)
1004
+ const previousStatus = current?.status ?? 'offline'
1005
+
1006
+ availability.set(agentId, {
1007
+ ...current,
1008
+ status,
1009
+ lastSeen: new Date(),
1010
+ })
1011
+
1012
+ const agent = agents.get(agentId)
1013
+ if (agent) {
1014
+ agent.status = status
1015
+ }
1016
+
1017
+ // Emit status change event
1018
+ if (previousStatus !== status) {
1019
+ const event: StatusChangeEvent = {
1020
+ agentId,
1021
+ previousStatus,
1022
+ currentStatus: status,
1023
+ timestamp: new Date(),
1024
+ }
1025
+ handlers.forEach((h) => h(event))
1026
+ }
1027
+ }
1028
+
1029
+ function getAvailableAgents(): AgentInfo[] {
1030
+ return Array.from(agents.values()).filter((a) => {
1031
+ const avail = availability.get(a.id)
1032
+ return avail?.status === 'available' || avail?.status === 'busy'
1033
+ })
1034
+ }
1035
+
1036
+ function heartbeat(agentId: string): void {
1037
+ const current = availability.get(agentId)
1038
+ if (current) {
1039
+ availability.set(agentId, {
1040
+ ...current,
1041
+ lastSeen: new Date(),
1042
+ })
1043
+ }
1044
+ }
1045
+
1046
+ function checkTimeouts(): void {
1047
+ const now = Date.now()
1048
+ availability.forEach((avail, agentId) => {
1049
+ if (avail.status !== 'offline') {
1050
+ const timeSinceLastSeen = now - avail.lastSeen.getTime()
1051
+ if (timeSinceLastSeen > heartbeatTimeout) {
1052
+ updateStatus(agentId, 'offline')
1053
+ }
1054
+ }
1055
+ })
1056
+ }
1057
+
1058
+ function onStatusChange(handler: (event: StatusChangeEvent) => void): void {
1059
+ handlers.push(handler)
1060
+ }
1061
+
1062
+ function updateLoad(agentId: string, current: number, max: number): void {
1063
+ const avail = availability.get(agentId)
1064
+ if (avail) {
1065
+ availability.set(agentId, {
1066
+ ...avail,
1067
+ currentLoad: current,
1068
+ maxLoad: max,
1069
+ })
1070
+ }
1071
+ }
1072
+
1073
+ function getCapacityUtilization(): Record<string, number> {
1074
+ const result: Record<string, number> = {}
1075
+ availability.forEach((avail, agentId) => {
1076
+ if (avail.currentLoad !== undefined && avail.maxLoad !== undefined && avail.maxLoad > 0) {
1077
+ result[agentId] = avail.currentLoad / avail.maxLoad
1078
+ }
1079
+ })
1080
+ return result
1081
+ }
1082
+
1083
+ function getOverallCapacity(): CapacityInfo {
1084
+ let total = 0
1085
+ let used = 0
1086
+
1087
+ availability.forEach((avail, agentId) => {
1088
+ const agent = agents.get(agentId)
1089
+ if (agent && (avail.status === 'available' || avail.status === 'busy')) {
1090
+ total += avail.maxLoad ?? agent.maxLoad
1091
+ used += avail.currentLoad ?? agent.currentLoad
1092
+ }
1093
+ })
1094
+
1095
+ return {
1096
+ total,
1097
+ used,
1098
+ available: total - used,
1099
+ utilization: total > 0 ? used / total : 0,
1100
+ }
1101
+ }
1102
+
1103
+ return {
1104
+ getAvailability,
1105
+ updateStatus,
1106
+ getAvailableAgents,
1107
+ heartbeat,
1108
+ checkTimeouts,
1109
+ onStatusChange,
1110
+ updateLoad,
1111
+ getCapacityUtilization,
1112
+ getOverallCapacity,
1113
+ }
1114
+ }
1115
+
1116
+ // ============================================================================
1117
+ // Routing Rule Engine
1118
+ // ============================================================================
1119
+
1120
+ /**
1121
+ * Options for routing rule engine
1122
+ */
1123
+ interface RoutingRuleEngineOptions {
1124
+ /**
1125
+ * Default strategy to use when no rules match
1126
+ */
1127
+ defaultStrategy?: 'round-robin' | 'least-busy' | 'capability'
1128
+ /**
1129
+ * Optional metrics collector for isolated metrics tracking.
1130
+ * If not provided, uses the default global collector.
1131
+ */
1132
+ metricsCollector?: MetricsCollector
1133
+ }
1134
+
1135
+ interface RoutingRuleEngine extends LoadBalancer {
1136
+ addRule(rule: RoutingRule): void
1137
+ removeRule(name: string): void
1138
+ updateRule(name: string, updates: Partial<RoutingRule>): void
1139
+ enableRule(name: string): void
1140
+ disableRule(name: string): void
1141
+ getRules(): RoutingRule[]
1142
+ }
1143
+
1144
+ /**
1145
+ * Create a routing rule engine
1146
+ *
1147
+ * Evaluates routing rules in priority order to determine task routing.
1148
+ *
1149
+ * @param initialAgents - Initial set of agents to route to
1150
+ * @param options - Optional configuration including metricsCollector
1151
+ * @returns A RoutingRuleEngine instance
1152
+ */
1153
+ export function createRoutingRuleEngine(
1154
+ initialAgents: AgentInfo[],
1155
+ options: RoutingRuleEngineOptions = {}
1156
+ ): RoutingRuleEngine {
1157
+ let agents = [...initialAgents]
1158
+ const rules: RoutingRule[] = []
1159
+ const { defaultStrategy = 'round-robin' } = options
1160
+ const collector = options.metricsCollector ?? defaultMetricsCollector
1161
+
1162
+ // Create default balancer for fallback
1163
+ let defaultBalancer: LoadBalancer
1164
+
1165
+ function getDefaultBalancer(): LoadBalancer {
1166
+ if (!defaultBalancer) {
1167
+ const balancerOptions = { metricsCollector: collector }
1168
+ switch (defaultStrategy) {
1169
+ case 'least-busy':
1170
+ defaultBalancer = createLeastBusyBalancer(agents, balancerOptions)
1171
+ break
1172
+ case 'capability':
1173
+ defaultBalancer = createCapabilityRouter(agents, balancerOptions)
1174
+ break
1175
+ default:
1176
+ defaultBalancer = createRoundRobinBalancer(agents, balancerOptions)
1177
+ }
1178
+ }
1179
+ return defaultBalancer
1180
+ }
1181
+
1182
+ function evaluateCondition(condition: RoutingRuleCondition, task: TaskRequest): boolean {
1183
+ if (typeof condition === 'function') {
1184
+ return condition(task)
1185
+ }
1186
+
1187
+ // Declarative condition evaluation
1188
+ if (condition.requiredSkills?.contains) {
1189
+ if (!task.requiredSkills.includes(condition.requiredSkills.contains)) {
1190
+ return false
1191
+ }
1192
+ }
1193
+
1194
+ if (condition.priority) {
1195
+ if (condition.priority.gte !== undefined && task.priority < condition.priority.gte) {
1196
+ return false
1197
+ }
1198
+ if (condition.priority.lte !== undefined && task.priority > condition.priority.lte) {
1199
+ return false
1200
+ }
1201
+ }
1202
+
1203
+ if (condition.metadata) {
1204
+ for (const [key, value] of Object.entries(condition.metadata)) {
1205
+ if (task.metadata[key] !== value) {
1206
+ return false
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ return true
1212
+ }
1213
+
1214
+ function route(task: TaskRequest): RouteResult {
1215
+ const start = performance.now()
1216
+
1217
+ // Sort rules by priority (descending)
1218
+ const sortedRules = [...rules]
1219
+ .filter((r) => r.enabled !== false)
1220
+ .sort((a, b) => b.priority - a.priority)
1221
+
1222
+ // Evaluate rules
1223
+ for (const rule of sortedRules) {
1224
+ if (evaluateCondition(rule.condition, task)) {
1225
+ const agent = rule.action(task, agents)
1226
+ if (agent) {
1227
+ const result: RouteResult = {
1228
+ agent,
1229
+ task,
1230
+ strategy: 'custom',
1231
+ timestamp: new Date(),
1232
+ matchedRule: rule.name,
1233
+ }
1234
+ collector.record(result, performance.now() - start, 'custom')
1235
+ return result
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ // Use default strategy as fallback
1241
+ const defaultResult = getDefaultBalancer().route(task)
1242
+ return {
1243
+ ...defaultResult,
1244
+ matchedRule: null,
1245
+ usedDefault: true,
1246
+ }
1247
+ }
1248
+
1249
+ function addRule(rule: RoutingRule): void {
1250
+ if (!rule.name || rule.name.trim() === '') {
1251
+ throw new Error('Rule name is required')
1252
+ }
1253
+ if (rule.priority < 0) {
1254
+ throw new Error('Rule priority must be non-negative')
1255
+ }
1256
+ rules.push({ ...rule, enabled: rule.enabled ?? true })
1257
+ }
1258
+
1259
+ function removeRule(name: string): void {
1260
+ const index = rules.findIndex((r) => r.name === name)
1261
+ if (index !== -1) {
1262
+ rules.splice(index, 1)
1263
+ }
1264
+ }
1265
+
1266
+ function updateRule(name: string, updates: Partial<RoutingRule>): void {
1267
+ const rule = rules.find((r) => r.name === name)
1268
+ if (rule) {
1269
+ Object.assign(rule, updates)
1270
+ }
1271
+ }
1272
+
1273
+ function enableRule(name: string): void {
1274
+ const rule = rules.find((r) => r.name === name)
1275
+ if (rule) {
1276
+ rule.enabled = true
1277
+ }
1278
+ }
1279
+
1280
+ function disableRule(name: string): void {
1281
+ const rule = rules.find((r) => r.name === name)
1282
+ if (rule) {
1283
+ rule.enabled = false
1284
+ }
1285
+ }
1286
+
1287
+ function getRules(): RoutingRule[] {
1288
+ return [...rules]
1289
+ }
1290
+
1291
+ function addAgent(agent: AgentInfo): void {
1292
+ agents.push(agent)
1293
+ defaultBalancer = undefined as any // Reset default balancer
1294
+ }
1295
+
1296
+ function removeAgent(agentId: string): void {
1297
+ agents = agents.filter((a) => a.id !== agentId)
1298
+ defaultBalancer = undefined as any // Reset default balancer
1299
+ }
1300
+
1301
+ function getAgents(): AgentInfo[] {
1302
+ return [...agents]
1303
+ }
1304
+
1305
+ return {
1306
+ route,
1307
+ addAgent,
1308
+ removeAgent,
1309
+ getAgents,
1310
+ addRule,
1311
+ removeRule,
1312
+ updateRule,
1313
+ enableRule,
1314
+ disableRule,
1315
+ getRules,
1316
+ }
1317
+ }
1318
+
1319
+ // ============================================================================
1320
+ // Composite Balancer
1321
+ // ============================================================================
1322
+
1323
+ interface CompositeBalancer extends LoadBalancer {
1324
+ // No additional methods for now
1325
+ }
1326
+
1327
+ /**
1328
+ * Create a composite load balancer
1329
+ *
1330
+ * Combines multiple balancing strategies for sophisticated routing decisions.
1331
+ *
1332
+ * @param initialAgents - Initial set of agents to balance across
1333
+ * @param config - Configuration including strategies and optional metricsCollector
1334
+ * @returns A CompositeBalancer instance
1335
+ */
1336
+ export function createCompositeBalancer(
1337
+ initialAgents: AgentInfo[],
1338
+ config: CompositeBalancerConfig
1339
+ ): CompositeBalancer {
1340
+ let agents = [...initialAgents]
1341
+ const balancers = new Map<BalancerStrategy, LoadBalancer>()
1342
+ const collector = config.metricsCollector ?? defaultMetricsCollector
1343
+
1344
+ // Initialize balancers
1345
+ function getOrCreateBalancer(strategy: BalancerStrategy): LoadBalancer {
1346
+ if (!balancers.has(strategy)) {
1347
+ const balancerOptions = { metricsCollector: collector }
1348
+ switch (strategy) {
1349
+ case 'round-robin':
1350
+ balancers.set(strategy, createRoundRobinBalancer(agents, balancerOptions))
1351
+ break
1352
+ case 'least-busy':
1353
+ balancers.set(strategy, createLeastBusyBalancer(agents, balancerOptions))
1354
+ break
1355
+ case 'capability':
1356
+ balancers.set(strategy, createCapabilityRouter(agents, balancerOptions))
1357
+ break
1358
+ case 'custom':
1359
+ // Custom strategies are handled separately
1360
+ break
1361
+ default:
1362
+ balancers.set(strategy, createRoundRobinBalancer(agents, balancerOptions))
1363
+ }
1364
+ }
1365
+ const balancer = balancers.get(strategy)
1366
+ if (!balancer) {
1367
+ // Fallback to round-robin if strategy not found
1368
+ const fallback = createRoundRobinBalancer(agents, { metricsCollector: collector })
1369
+ balancers.set(strategy, fallback)
1370
+ return fallback
1371
+ }
1372
+ return balancer
1373
+ }
1374
+
1375
+ function route(task: TaskRequest): RouteResult {
1376
+ const start = performance.now()
1377
+ const strategies: BalancerStrategy[] = []
1378
+ const strategyScores: Record<string, number> = {}
1379
+ let usedFallback = false
1380
+
1381
+ // Handle weighted strategies
1382
+ const weightedStrategies = config.strategies.map((s) => {
1383
+ if (typeof s === 'string') {
1384
+ return { strategy: s, weight: 1 }
1385
+ }
1386
+ return s
1387
+ })
1388
+
1389
+ // Try each strategy in order
1390
+ for (const { strategy, weight } of weightedStrategies) {
1391
+ strategies.push(strategy)
1392
+
1393
+ // Handle custom strategies
1394
+ if (strategy === 'custom' && config.customStrategies) {
1395
+ for (const [name, fn] of Object.entries(config.customStrategies)) {
1396
+ const agent = fn(task, agents)
1397
+ if (agent) {
1398
+ const result: RouteResult = {
1399
+ agent,
1400
+ task,
1401
+ strategy: 'custom',
1402
+ timestamp: new Date(),
1403
+ strategies,
1404
+ strategyScores,
1405
+ }
1406
+ collector.record(result, performance.now() - start, 'custom')
1407
+ return result
1408
+ }
1409
+ }
1410
+ continue
1411
+ }
1412
+
1413
+ const balancer = getOrCreateBalancer(strategy)
1414
+ const result = balancer.route(task)
1415
+
1416
+ if (result.agent) {
1417
+ // Calculate score for weighted strategies
1418
+ strategyScores[strategy] = weight
1419
+
1420
+ const finalResult: RouteResult = {
1421
+ ...result,
1422
+ strategies,
1423
+ strategyScores,
1424
+ usedFallback,
1425
+ }
1426
+ collector.record(finalResult, performance.now() - start, strategy)
1427
+ return finalResult
1428
+ }
1429
+
1430
+ // Handle fallback
1431
+ if (config.fallbackBehavior === 'next-strategy') {
1432
+ usedFallback = true
1433
+ continue
1434
+ }
1435
+ }
1436
+
1437
+ // No strategy succeeded
1438
+ const result: RouteResult = {
1439
+ agent: null,
1440
+ task,
1441
+ strategy: 'custom',
1442
+ timestamp: new Date(),
1443
+ reason: 'no-strategy-succeeded',
1444
+ strategies,
1445
+ strategyScores,
1446
+ usedFallback,
1447
+ }
1448
+ collector.record(result, performance.now() - start, 'custom')
1449
+ return result
1450
+ }
1451
+
1452
+ function addAgent(agent: AgentInfo): void {
1453
+ agents.push(agent)
1454
+ balancers.forEach((b) => b.addAgent(agent))
1455
+ }
1456
+
1457
+ function removeAgent(agentId: string): void {
1458
+ agents = agents.filter((a) => a.id !== agentId)
1459
+ balancers.forEach((b) => b.removeAgent(agentId))
1460
+ }
1461
+
1462
+ function getAgents(): AgentInfo[] {
1463
+ return [...agents]
1464
+ }
1465
+
1466
+ return { route, addAgent, removeAgent, getAgents }
1467
+ }