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,991 @@
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
+ // Safe Array Access Utilities
12
+ // ============================================================================
13
+ /**
14
+ * Safely get the first element of an array.
15
+ *
16
+ * Provides type-safe access to array elements without non-null assertions.
17
+ *
18
+ * @param arr - The array to access
19
+ * @returns The first element or undefined if array is empty
20
+ */
21
+ function safeFirst(arr) {
22
+ return arr.length > 0 ? arr[0] : undefined;
23
+ }
24
+ /**
25
+ * Safely get an element at a specific index.
26
+ *
27
+ * Provides type-safe access with bounds checking.
28
+ *
29
+ * @param arr - The array to access
30
+ * @param index - The index to access
31
+ * @returns The element at the index or undefined if out of bounds
32
+ */
33
+ function safeAt(arr, index) {
34
+ if (arr.length === 0 || index < 0 || index >= arr.length) {
35
+ return undefined;
36
+ }
37
+ return arr[index];
38
+ }
39
+ // ============================================================================
40
+ // MetricsCollector Implementation
41
+ // ============================================================================
42
+ /**
43
+ * Create a new MetricsCollector instance with isolated state.
44
+ *
45
+ * This factory function creates a thread-safe metrics collector that
46
+ * encapsulates all metrics state within the returned instance. Multiple
47
+ * collectors can be used independently without interference.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * // Create isolated collectors for different environments
52
+ * const prodCollector = createMetricsCollector()
53
+ * const testCollector = createMetricsCollector()
54
+ *
55
+ * // Balancers with separate metrics
56
+ * const prodBalancer = createRoundRobinBalancer(agents, { metricsCollector: prodCollector })
57
+ * const testBalancer = createRoundRobinBalancer(agents, { metricsCollector: testCollector })
58
+ * ```
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * // Shared collector for aggregated metrics
63
+ * const sharedCollector = createMetricsCollector()
64
+ * const balancer1 = createRoundRobinBalancer(agents, { metricsCollector: sharedCollector })
65
+ * const balancer2 = createLeastBusyBalancer(agents, { metricsCollector: sharedCollector })
66
+ *
67
+ * // Get combined metrics
68
+ * const metrics = sharedCollector.collect()
69
+ * ```
70
+ *
71
+ * @returns A new MetricsCollector instance
72
+ */
73
+ export function createMetricsCollector() {
74
+ // Instance-local state - each collector has its own isolated metrics
75
+ let metricsState = {
76
+ totalRouted: 0,
77
+ failedRoutes: 0,
78
+ averageLatencyMs: 0,
79
+ perAgent: {},
80
+ strategyUsage: {},
81
+ };
82
+ let totalLatency = 0;
83
+ function record(result, latencyMs, strategy) {
84
+ metricsState.totalRouted++;
85
+ totalLatency += latencyMs;
86
+ metricsState.averageLatencyMs = totalLatency / metricsState.totalRouted;
87
+ if (!result.agent) {
88
+ metricsState.failedRoutes++;
89
+ }
90
+ else {
91
+ const agentId = result.agent.id;
92
+ if (!metricsState.perAgent[agentId]) {
93
+ metricsState.perAgent[agentId] = { routedCount: 0 };
94
+ }
95
+ metricsState.perAgent[agentId].routedCount++;
96
+ metricsState.perAgent[agentId].lastRouted = new Date();
97
+ }
98
+ metricsState.strategyUsage[strategy] = (metricsState.strategyUsage[strategy] || 0) + 1;
99
+ }
100
+ function collect() {
101
+ // Return a deep copy to prevent external mutation
102
+ return {
103
+ ...metricsState,
104
+ perAgent: { ...metricsState.perAgent },
105
+ strategyUsage: { ...metricsState.strategyUsage },
106
+ };
107
+ }
108
+ function reset() {
109
+ metricsState = {
110
+ totalRouted: 0,
111
+ failedRoutes: 0,
112
+ averageLatencyMs: 0,
113
+ perAgent: {},
114
+ strategyUsage: {},
115
+ };
116
+ totalLatency = 0;
117
+ }
118
+ return { record, collect, reset };
119
+ }
120
+ // ============================================================================
121
+ // Default Global Metrics Collector (Backward Compatibility)
122
+ // ============================================================================
123
+ /**
124
+ * Default metrics collector singleton for backward compatibility.
125
+ * Used when no explicit collector is provided to balancer factories.
126
+ */
127
+ const defaultMetricsCollector = createMetricsCollector();
128
+ /**
129
+ * Collect current routing metrics from the default global collector.
130
+ *
131
+ * @remarks
132
+ * This function is provided for backward compatibility. For new code,
133
+ * consider using explicit MetricsCollector instances for better isolation.
134
+ *
135
+ * @returns Current routing metrics
136
+ */
137
+ export function collectRoutingMetrics() {
138
+ return defaultMetricsCollector.collect();
139
+ }
140
+ /**
141
+ * Reset all routing metrics in the default global collector.
142
+ *
143
+ * @remarks
144
+ * This function is provided for backward compatibility. For new code,
145
+ * consider using explicit MetricsCollector instances for better isolation.
146
+ */
147
+ export function resetRoutingMetrics() {
148
+ defaultMetricsCollector.reset();
149
+ }
150
+ /**
151
+ * Create a round-robin load balancer
152
+ *
153
+ * Distributes tasks evenly across all available agents in order.
154
+ *
155
+ * @param initialAgents - Initial set of agents to balance across
156
+ * @param options - Optional configuration including metricsCollector
157
+ * @returns A LoadBalancer instance
158
+ */
159
+ export function createRoundRobinBalancer(initialAgents, options = {}) {
160
+ let agents = [...initialAgents];
161
+ let currentIndex = 0;
162
+ const collector = options.metricsCollector ?? defaultMetricsCollector;
163
+ function getAvailableAgents() {
164
+ return agents.filter((a) => a.status === 'available' || a.status === 'busy');
165
+ }
166
+ function route(task) {
167
+ const start = performance.now();
168
+ const available = getAvailableAgents();
169
+ if (available.length === 0) {
170
+ const result = {
171
+ agent: null,
172
+ task,
173
+ strategy: 'round-robin',
174
+ timestamp: new Date(),
175
+ reason: 'no-available-agents',
176
+ };
177
+ collector.record(result, performance.now() - start, 'round-robin');
178
+ return result;
179
+ }
180
+ // Find the next available agent starting from current index
181
+ let attempts = 0;
182
+ while (attempts < agents.length) {
183
+ const agent = safeAt(agents, currentIndex % agents.length);
184
+ currentIndex++;
185
+ if (!agent) {
186
+ attempts++;
187
+ continue;
188
+ }
189
+ if (agent.status === 'available' || agent.status === 'busy') {
190
+ const result = {
191
+ agent,
192
+ task,
193
+ strategy: 'round-robin',
194
+ timestamp: new Date(),
195
+ };
196
+ collector.record(result, performance.now() - start, 'round-robin');
197
+ return result;
198
+ }
199
+ attempts++;
200
+ }
201
+ const result = {
202
+ agent: null,
203
+ task,
204
+ strategy: 'round-robin',
205
+ timestamp: new Date(),
206
+ reason: 'no-available-agents',
207
+ };
208
+ collector.record(result, performance.now() - start, 'round-robin');
209
+ return result;
210
+ }
211
+ function addAgent(agent) {
212
+ agents.push(agent);
213
+ }
214
+ function removeAgent(agentId) {
215
+ agents = agents.filter((a) => a.id !== agentId);
216
+ }
217
+ function getAgents() {
218
+ return [...agents];
219
+ }
220
+ return { route, addAgent, removeAgent, getAgents };
221
+ }
222
+ /**
223
+ * Create a least-busy load balancer
224
+ *
225
+ * Routes tasks to agents with the lowest current load.
226
+ *
227
+ * @param initialAgents - Initial set of agents to balance across
228
+ * @param options - Optional configuration including metricsCollector
229
+ * @returns A LeastBusyBalancer instance
230
+ */
231
+ export function createLeastBusyBalancer(initialAgents, options = {}) {
232
+ let agents = [...initialAgents];
233
+ const loadTracking = new Map();
234
+ let lastRoutedIndex = -1;
235
+ const collector = options.metricsCollector ?? defaultMetricsCollector;
236
+ // Initialize load tracking
237
+ agents.forEach((a) => loadTracking.set(a.id, a.currentLoad));
238
+ function getAvailableAgents() {
239
+ return agents.filter((a) => {
240
+ if (a.status !== 'available' && a.status !== 'busy')
241
+ return false;
242
+ const load = loadTracking.get(a.id) ?? a.currentLoad;
243
+ return load < a.maxLoad;
244
+ });
245
+ }
246
+ function route(task) {
247
+ const start = performance.now();
248
+ const available = getAvailableAgents();
249
+ if (available.length === 0) {
250
+ const result = {
251
+ agent: null,
252
+ task,
253
+ strategy: 'least-busy',
254
+ timestamp: new Date(),
255
+ reason: 'no-available-agents',
256
+ };
257
+ collector.record(result, performance.now() - start, 'least-busy');
258
+ return result;
259
+ }
260
+ // Sort by load percentage
261
+ const sorted = [...available].sort((a, b) => {
262
+ const loadA = (loadTracking.get(a.id) ?? a.currentLoad) / a.maxLoad;
263
+ const loadB = (loadTracking.get(b.id) ?? b.currentLoad) / b.maxLoad;
264
+ if (loadA === loadB) {
265
+ // Tie-breaking with round-robin behavior
266
+ const indexA = agents.indexOf(a);
267
+ const indexB = agents.indexOf(b);
268
+ const distA = (indexA - lastRoutedIndex + agents.length) % agents.length;
269
+ const distB = (indexB - lastRoutedIndex + agents.length) % agents.length;
270
+ return distA - distB;
271
+ }
272
+ return loadA - loadB;
273
+ });
274
+ const selected = safeFirst(sorted);
275
+ if (!selected) {
276
+ const result = {
277
+ agent: null,
278
+ task,
279
+ strategy: 'least-busy',
280
+ timestamp: new Date(),
281
+ reason: 'no-available-agents',
282
+ };
283
+ collector.record(result, performance.now() - start, 'least-busy');
284
+ return result;
285
+ }
286
+ lastRoutedIndex = agents.indexOf(selected);
287
+ // Increment load
288
+ const currentLoad = loadTracking.get(selected.id) ?? selected.currentLoad;
289
+ loadTracking.set(selected.id, currentLoad + 1);
290
+ const result = {
291
+ agent: selected,
292
+ task,
293
+ strategy: 'least-busy',
294
+ timestamp: new Date(),
295
+ };
296
+ collector.record(result, performance.now() - start, 'least-busy');
297
+ return result;
298
+ }
299
+ function addAgent(agent) {
300
+ agents.push(agent);
301
+ loadTracking.set(agent.id, agent.currentLoad);
302
+ }
303
+ function removeAgent(agentId) {
304
+ agents = agents.filter((a) => a.id !== agentId);
305
+ loadTracking.delete(agentId);
306
+ }
307
+ function getAgents() {
308
+ return [...agents];
309
+ }
310
+ function getLoadMetrics() {
311
+ const metrics = {};
312
+ agents.forEach((a) => {
313
+ const load = loadTracking.get(a.id) ?? a.currentLoad;
314
+ metrics[a.id] = load / a.maxLoad;
315
+ });
316
+ return metrics;
317
+ }
318
+ function releaseLoad(agentId) {
319
+ const current = loadTracking.get(agentId);
320
+ if (current !== undefined && current > 0) {
321
+ loadTracking.set(agentId, current - 1);
322
+ }
323
+ }
324
+ function setLoad(agentId, load) {
325
+ loadTracking.set(agentId, load);
326
+ }
327
+ return { route, addAgent, removeAgent, getAgents, getLoadMetrics, releaseLoad, setLoad };
328
+ }
329
+ /**
330
+ * Create a capability-based router
331
+ *
332
+ * Routes tasks to agents that have the required skills.
333
+ *
334
+ * @param initialAgents - Initial set of agents to route to
335
+ * @param options - Optional configuration including metricsCollector
336
+ * @returns A CapabilityRouter instance
337
+ */
338
+ export function createCapabilityRouter(initialAgents, options = {}) {
339
+ let agents = [...initialAgents];
340
+ const collector = options.metricsCollector ?? defaultMetricsCollector;
341
+ function getAvailableAgents() {
342
+ return agents.filter((a) => a.status === 'available' || a.status === 'busy');
343
+ }
344
+ function hasAllSkills(agent, requiredSkills) {
345
+ return requiredSkills.every((skill) => agent.skills.includes(skill));
346
+ }
347
+ function calculateMatchScore(agent, requiredSkills) {
348
+ if (requiredSkills.length === 0)
349
+ return 1;
350
+ const matchingSkills = requiredSkills.filter((s) => agent.skills.includes(s));
351
+ return matchingSkills.length / requiredSkills.length;
352
+ }
353
+ function route(task) {
354
+ const start = performance.now();
355
+ const available = getAvailableAgents();
356
+ // Find agents with all required skills
357
+ let candidates = available.filter((a) => hasAllSkills(a, task.requiredSkills));
358
+ if (candidates.length === 0) {
359
+ const result = {
360
+ agent: null,
361
+ task,
362
+ strategy: 'capability',
363
+ timestamp: new Date(),
364
+ reason: 'no-matching-capability',
365
+ };
366
+ collector.record(result, performance.now() - start, 'capability');
367
+ return result;
368
+ }
369
+ // Sort by match quality
370
+ if (options.preferExactMatch) {
371
+ // Prefer agents with closest skill count to requirements
372
+ candidates.sort((a, b) => {
373
+ const diffA = Math.abs(a.skills.length - task.requiredSkills.length);
374
+ const diffB = Math.abs(b.skills.length - task.requiredSkills.length);
375
+ return diffA - diffB;
376
+ });
377
+ }
378
+ const selected = safeFirst(candidates);
379
+ if (!selected) {
380
+ const result = {
381
+ agent: null,
382
+ task,
383
+ strategy: 'capability',
384
+ timestamp: new Date(),
385
+ reason: 'no-matching-capability',
386
+ };
387
+ collector.record(result, performance.now() - start, 'capability');
388
+ return result;
389
+ }
390
+ const matchScore = calculateMatchScore(selected, task.requiredSkills);
391
+ const result = {
392
+ agent: selected,
393
+ task,
394
+ strategy: 'capability',
395
+ timestamp: new Date(),
396
+ matchScore,
397
+ };
398
+ collector.record(result, performance.now() - start, 'capability');
399
+ return result;
400
+ }
401
+ function addAgent(agent) {
402
+ agents.push(agent);
403
+ }
404
+ function removeAgent(agentId) {
405
+ agents = agents.filter((a) => a.id !== agentId);
406
+ }
407
+ function getAgents() {
408
+ return [...agents];
409
+ }
410
+ function findAgentsWithSkills(skills) {
411
+ return agents.filter((a) => hasAllSkills(a, skills));
412
+ }
413
+ function getSkillCoverage() {
414
+ const coverage = {};
415
+ agents.forEach((a) => {
416
+ a.skills.forEach((skill) => {
417
+ coverage[skill] = (coverage[skill] || 0) + 1;
418
+ });
419
+ });
420
+ return coverage;
421
+ }
422
+ return { route, addAgent, removeAgent, getAgents, findAgentsWithSkills, getSkillCoverage };
423
+ }
424
+ /**
425
+ * Create a priority queue balancer
426
+ *
427
+ * Processes tasks in priority order with optional aging to prevent starvation.
428
+ *
429
+ * @param initialAgents - Initial set of agents to balance across
430
+ * @param options - Optional configuration including metricsCollector
431
+ * @returns A PriorityQueueBalancer instance
432
+ */
433
+ export function createPriorityQueueBalancer(initialAgents, options = {}) {
434
+ let agents = [...initialAgents];
435
+ const queue = [];
436
+ const { enableAging = false, agingBoostPerSecond = 1, maxWaitTime } = options;
437
+ const collector = options.metricsCollector ?? defaultMetricsCollector;
438
+ function getAvailableAgents() {
439
+ return agents.filter((a) => a.status === 'available' || a.status === 'busy');
440
+ }
441
+ function getEffectivePriority(taskId) {
442
+ const task = queue.find((t) => t.id === taskId);
443
+ if (!task)
444
+ return 0;
445
+ let priority = task.priority;
446
+ if (enableAging && task.enqueuedAt) {
447
+ const waitTimeMs = Date.now() - task.enqueuedAt.getTime();
448
+ const waitTimeSeconds = waitTimeMs / 1000;
449
+ priority += waitTimeSeconds * agingBoostPerSecond;
450
+ }
451
+ if (maxWaitTime && task.enqueuedAt) {
452
+ const waitTimeMs = Date.now() - task.enqueuedAt.getTime();
453
+ if (waitTimeMs >= maxWaitTime) {
454
+ priority = Infinity; // Promote to highest priority
455
+ }
456
+ }
457
+ return priority;
458
+ }
459
+ function sortQueue() {
460
+ queue.sort((a, b) => {
461
+ const priorityA = getEffectivePriority(a.id);
462
+ const priorityB = getEffectivePriority(b.id);
463
+ if (priorityB !== priorityA) {
464
+ return priorityB - priorityA; // Higher priority first
465
+ }
466
+ // FIFO for equal priority
467
+ const timeA = a.enqueuedAt?.getTime() ?? 0;
468
+ const timeB = b.enqueuedAt?.getTime() ?? 0;
469
+ return timeA - timeB;
470
+ });
471
+ }
472
+ function enqueue(task) {
473
+ if (task.priority < 1 || task.priority > 10) {
474
+ throw new Error('Priority must be between 1 and 10');
475
+ }
476
+ const taskWithTime = { ...task, enqueuedAt: new Date() };
477
+ queue.push(taskWithTime);
478
+ sortQueue();
479
+ }
480
+ async function routeNext() {
481
+ if (queue.length === 0)
482
+ return null;
483
+ sortQueue();
484
+ const task = queue.shift();
485
+ if (!task)
486
+ return null;
487
+ const start = performance.now();
488
+ const available = getAvailableAgents();
489
+ if (available.length === 0) {
490
+ const result = {
491
+ agent: null,
492
+ task,
493
+ strategy: 'priority-queue',
494
+ timestamp: new Date(),
495
+ reason: 'no-available-agents',
496
+ };
497
+ collector.record(result, performance.now() - start, 'priority-queue');
498
+ return result;
499
+ }
500
+ // Simple round-robin among available for now
501
+ const agent = safeFirst(available);
502
+ if (!agent) {
503
+ const result = {
504
+ agent: null,
505
+ task,
506
+ strategy: 'priority-queue',
507
+ timestamp: new Date(),
508
+ reason: 'no-available-agents',
509
+ };
510
+ collector.record(result, performance.now() - start, 'priority-queue');
511
+ return result;
512
+ }
513
+ const result = {
514
+ agent,
515
+ task,
516
+ strategy: 'priority-queue',
517
+ timestamp: new Date(),
518
+ };
519
+ collector.record(result, performance.now() - start, 'priority-queue');
520
+ return result;
521
+ }
522
+ function route(task) {
523
+ const start = performance.now();
524
+ const available = getAvailableAgents();
525
+ if (available.length === 0) {
526
+ const result = {
527
+ agent: null,
528
+ task,
529
+ strategy: 'priority-queue',
530
+ timestamp: new Date(),
531
+ reason: 'no-available-agents',
532
+ };
533
+ collector.record(result, performance.now() - start, 'priority-queue');
534
+ return result;
535
+ }
536
+ const agent = safeFirst(available);
537
+ if (!agent) {
538
+ const result = {
539
+ agent: null,
540
+ task,
541
+ strategy: 'priority-queue',
542
+ timestamp: new Date(),
543
+ reason: 'no-available-agents',
544
+ };
545
+ collector.record(result, performance.now() - start, 'priority-queue');
546
+ return result;
547
+ }
548
+ const result = {
549
+ agent,
550
+ task,
551
+ strategy: 'priority-queue',
552
+ timestamp: new Date(),
553
+ };
554
+ collector.record(result, performance.now() - start, 'priority-queue');
555
+ return result;
556
+ }
557
+ function addAgent(agent) {
558
+ agents.push(agent);
559
+ }
560
+ function removeAgent(agentId) {
561
+ agents = agents.filter((a) => a.id !== agentId);
562
+ }
563
+ function getAgents() {
564
+ return [...agents];
565
+ }
566
+ function queueSize() {
567
+ return queue.length;
568
+ }
569
+ function clear() {
570
+ queue.length = 0;
571
+ }
572
+ function peek() {
573
+ if (queue.length === 0)
574
+ return null;
575
+ sortQueue();
576
+ return queue[0] ?? null;
577
+ }
578
+ return {
579
+ route,
580
+ addAgent,
581
+ removeAgent,
582
+ getAgents,
583
+ enqueue,
584
+ routeNext,
585
+ queueSize,
586
+ clear,
587
+ peek,
588
+ getEffectivePriority,
589
+ };
590
+ }
591
+ /**
592
+ * Create an agent availability tracker
593
+ *
594
+ * Tracks agent status, heartbeats, and capacity.
595
+ */
596
+ export function createAgentAvailabilityTracker(initialAgents, options = {}) {
597
+ const agents = new Map();
598
+ const availability = new Map();
599
+ const handlers = [];
600
+ const { heartbeatTimeout = 30000 } = options;
601
+ // Initialize
602
+ initialAgents.forEach((a) => {
603
+ agents.set(a.id, a);
604
+ availability.set(a.id, {
605
+ status: a.status,
606
+ lastSeen: new Date(),
607
+ currentLoad: a.currentLoad,
608
+ maxLoad: a.maxLoad,
609
+ });
610
+ });
611
+ function getAvailability(agentId) {
612
+ return (availability.get(agentId) ?? {
613
+ status: 'offline',
614
+ lastSeen: new Date(0),
615
+ });
616
+ }
617
+ function updateStatus(agentId, status) {
618
+ const current = availability.get(agentId);
619
+ const previousStatus = current?.status ?? 'offline';
620
+ availability.set(agentId, {
621
+ ...current,
622
+ status,
623
+ lastSeen: new Date(),
624
+ });
625
+ const agent = agents.get(agentId);
626
+ if (agent) {
627
+ agent.status = status;
628
+ }
629
+ // Emit status change event
630
+ if (previousStatus !== status) {
631
+ const event = {
632
+ agentId,
633
+ previousStatus,
634
+ currentStatus: status,
635
+ timestamp: new Date(),
636
+ };
637
+ handlers.forEach((h) => h(event));
638
+ }
639
+ }
640
+ function getAvailableAgents() {
641
+ return Array.from(agents.values()).filter((a) => {
642
+ const avail = availability.get(a.id);
643
+ return avail?.status === 'available' || avail?.status === 'busy';
644
+ });
645
+ }
646
+ function heartbeat(agentId) {
647
+ const current = availability.get(agentId);
648
+ if (current) {
649
+ availability.set(agentId, {
650
+ ...current,
651
+ lastSeen: new Date(),
652
+ });
653
+ }
654
+ }
655
+ function checkTimeouts() {
656
+ const now = Date.now();
657
+ availability.forEach((avail, agentId) => {
658
+ if (avail.status !== 'offline') {
659
+ const timeSinceLastSeen = now - avail.lastSeen.getTime();
660
+ if (timeSinceLastSeen > heartbeatTimeout) {
661
+ updateStatus(agentId, 'offline');
662
+ }
663
+ }
664
+ });
665
+ }
666
+ function onStatusChange(handler) {
667
+ handlers.push(handler);
668
+ }
669
+ function updateLoad(agentId, current, max) {
670
+ const avail = availability.get(agentId);
671
+ if (avail) {
672
+ availability.set(agentId, {
673
+ ...avail,
674
+ currentLoad: current,
675
+ maxLoad: max,
676
+ });
677
+ }
678
+ }
679
+ function getCapacityUtilization() {
680
+ const result = {};
681
+ availability.forEach((avail, agentId) => {
682
+ if (avail.currentLoad !== undefined && avail.maxLoad !== undefined && avail.maxLoad > 0) {
683
+ result[agentId] = avail.currentLoad / avail.maxLoad;
684
+ }
685
+ });
686
+ return result;
687
+ }
688
+ function getOverallCapacity() {
689
+ let total = 0;
690
+ let used = 0;
691
+ availability.forEach((avail, agentId) => {
692
+ const agent = agents.get(agentId);
693
+ if (agent && (avail.status === 'available' || avail.status === 'busy')) {
694
+ total += avail.maxLoad ?? agent.maxLoad;
695
+ used += avail.currentLoad ?? agent.currentLoad;
696
+ }
697
+ });
698
+ return {
699
+ total,
700
+ used,
701
+ available: total - used,
702
+ utilization: total > 0 ? used / total : 0,
703
+ };
704
+ }
705
+ return {
706
+ getAvailability,
707
+ updateStatus,
708
+ getAvailableAgents,
709
+ heartbeat,
710
+ checkTimeouts,
711
+ onStatusChange,
712
+ updateLoad,
713
+ getCapacityUtilization,
714
+ getOverallCapacity,
715
+ };
716
+ }
717
+ /**
718
+ * Create a routing rule engine
719
+ *
720
+ * Evaluates routing rules in priority order to determine task routing.
721
+ *
722
+ * @param initialAgents - Initial set of agents to route to
723
+ * @param options - Optional configuration including metricsCollector
724
+ * @returns A RoutingRuleEngine instance
725
+ */
726
+ export function createRoutingRuleEngine(initialAgents, options = {}) {
727
+ let agents = [...initialAgents];
728
+ const rules = [];
729
+ const { defaultStrategy = 'round-robin' } = options;
730
+ const collector = options.metricsCollector ?? defaultMetricsCollector;
731
+ // Create default balancer for fallback
732
+ let defaultBalancer;
733
+ function getDefaultBalancer() {
734
+ if (!defaultBalancer) {
735
+ const balancerOptions = { metricsCollector: collector };
736
+ switch (defaultStrategy) {
737
+ case 'least-busy':
738
+ defaultBalancer = createLeastBusyBalancer(agents, balancerOptions);
739
+ break;
740
+ case 'capability':
741
+ defaultBalancer = createCapabilityRouter(agents, balancerOptions);
742
+ break;
743
+ default:
744
+ defaultBalancer = createRoundRobinBalancer(agents, balancerOptions);
745
+ }
746
+ }
747
+ return defaultBalancer;
748
+ }
749
+ function evaluateCondition(condition, task) {
750
+ if (typeof condition === 'function') {
751
+ return condition(task);
752
+ }
753
+ // Declarative condition evaluation
754
+ if (condition.requiredSkills?.contains) {
755
+ if (!task.requiredSkills.includes(condition.requiredSkills.contains)) {
756
+ return false;
757
+ }
758
+ }
759
+ if (condition.priority) {
760
+ if (condition.priority.gte !== undefined && task.priority < condition.priority.gte) {
761
+ return false;
762
+ }
763
+ if (condition.priority.lte !== undefined && task.priority > condition.priority.lte) {
764
+ return false;
765
+ }
766
+ }
767
+ if (condition.metadata) {
768
+ for (const [key, value] of Object.entries(condition.metadata)) {
769
+ if (task.metadata[key] !== value) {
770
+ return false;
771
+ }
772
+ }
773
+ }
774
+ return true;
775
+ }
776
+ function route(task) {
777
+ const start = performance.now();
778
+ // Sort rules by priority (descending)
779
+ const sortedRules = [...rules]
780
+ .filter((r) => r.enabled !== false)
781
+ .sort((a, b) => b.priority - a.priority);
782
+ // Evaluate rules
783
+ for (const rule of sortedRules) {
784
+ if (evaluateCondition(rule.condition, task)) {
785
+ const agent = rule.action(task, agents);
786
+ if (agent) {
787
+ const result = {
788
+ agent,
789
+ task,
790
+ strategy: 'custom',
791
+ timestamp: new Date(),
792
+ matchedRule: rule.name,
793
+ };
794
+ collector.record(result, performance.now() - start, 'custom');
795
+ return result;
796
+ }
797
+ }
798
+ }
799
+ // Use default strategy as fallback
800
+ const defaultResult = getDefaultBalancer().route(task);
801
+ return {
802
+ ...defaultResult,
803
+ matchedRule: null,
804
+ usedDefault: true,
805
+ };
806
+ }
807
+ function addRule(rule) {
808
+ if (!rule.name || rule.name.trim() === '') {
809
+ throw new Error('Rule name is required');
810
+ }
811
+ if (rule.priority < 0) {
812
+ throw new Error('Rule priority must be non-negative');
813
+ }
814
+ rules.push({ ...rule, enabled: rule.enabled ?? true });
815
+ }
816
+ function removeRule(name) {
817
+ const index = rules.findIndex((r) => r.name === name);
818
+ if (index !== -1) {
819
+ rules.splice(index, 1);
820
+ }
821
+ }
822
+ function updateRule(name, updates) {
823
+ const rule = rules.find((r) => r.name === name);
824
+ if (rule) {
825
+ Object.assign(rule, updates);
826
+ }
827
+ }
828
+ function enableRule(name) {
829
+ const rule = rules.find((r) => r.name === name);
830
+ if (rule) {
831
+ rule.enabled = true;
832
+ }
833
+ }
834
+ function disableRule(name) {
835
+ const rule = rules.find((r) => r.name === name);
836
+ if (rule) {
837
+ rule.enabled = false;
838
+ }
839
+ }
840
+ function getRules() {
841
+ return [...rules];
842
+ }
843
+ function addAgent(agent) {
844
+ agents.push(agent);
845
+ defaultBalancer = undefined; // Reset default balancer
846
+ }
847
+ function removeAgent(agentId) {
848
+ agents = agents.filter((a) => a.id !== agentId);
849
+ defaultBalancer = undefined; // Reset default balancer
850
+ }
851
+ function getAgents() {
852
+ return [...agents];
853
+ }
854
+ return {
855
+ route,
856
+ addAgent,
857
+ removeAgent,
858
+ getAgents,
859
+ addRule,
860
+ removeRule,
861
+ updateRule,
862
+ enableRule,
863
+ disableRule,
864
+ getRules,
865
+ };
866
+ }
867
+ /**
868
+ * Create a composite load balancer
869
+ *
870
+ * Combines multiple balancing strategies for sophisticated routing decisions.
871
+ *
872
+ * @param initialAgents - Initial set of agents to balance across
873
+ * @param config - Configuration including strategies and optional metricsCollector
874
+ * @returns A CompositeBalancer instance
875
+ */
876
+ export function createCompositeBalancer(initialAgents, config) {
877
+ let agents = [...initialAgents];
878
+ const balancers = new Map();
879
+ const collector = config.metricsCollector ?? defaultMetricsCollector;
880
+ // Initialize balancers
881
+ function getOrCreateBalancer(strategy) {
882
+ if (!balancers.has(strategy)) {
883
+ const balancerOptions = { metricsCollector: collector };
884
+ switch (strategy) {
885
+ case 'round-robin':
886
+ balancers.set(strategy, createRoundRobinBalancer(agents, balancerOptions));
887
+ break;
888
+ case 'least-busy':
889
+ balancers.set(strategy, createLeastBusyBalancer(agents, balancerOptions));
890
+ break;
891
+ case 'capability':
892
+ balancers.set(strategy, createCapabilityRouter(agents, balancerOptions));
893
+ break;
894
+ case 'custom':
895
+ // Custom strategies are handled separately
896
+ break;
897
+ default:
898
+ balancers.set(strategy, createRoundRobinBalancer(agents, balancerOptions));
899
+ }
900
+ }
901
+ const balancer = balancers.get(strategy);
902
+ if (!balancer) {
903
+ // Fallback to round-robin if strategy not found
904
+ const fallback = createRoundRobinBalancer(agents, { metricsCollector: collector });
905
+ balancers.set(strategy, fallback);
906
+ return fallback;
907
+ }
908
+ return balancer;
909
+ }
910
+ function route(task) {
911
+ const start = performance.now();
912
+ const strategies = [];
913
+ const strategyScores = {};
914
+ let usedFallback = false;
915
+ // Handle weighted strategies
916
+ const weightedStrategies = config.strategies.map((s) => {
917
+ if (typeof s === 'string') {
918
+ return { strategy: s, weight: 1 };
919
+ }
920
+ return s;
921
+ });
922
+ // Try each strategy in order
923
+ for (const { strategy, weight } of weightedStrategies) {
924
+ strategies.push(strategy);
925
+ // Handle custom strategies
926
+ if (strategy === 'custom' && config.customStrategies) {
927
+ for (const [name, fn] of Object.entries(config.customStrategies)) {
928
+ const agent = fn(task, agents);
929
+ if (agent) {
930
+ const result = {
931
+ agent,
932
+ task,
933
+ strategy: 'custom',
934
+ timestamp: new Date(),
935
+ strategies,
936
+ strategyScores,
937
+ };
938
+ collector.record(result, performance.now() - start, 'custom');
939
+ return result;
940
+ }
941
+ }
942
+ continue;
943
+ }
944
+ const balancer = getOrCreateBalancer(strategy);
945
+ const result = balancer.route(task);
946
+ if (result.agent) {
947
+ // Calculate score for weighted strategies
948
+ strategyScores[strategy] = weight;
949
+ const finalResult = {
950
+ ...result,
951
+ strategies,
952
+ strategyScores,
953
+ usedFallback,
954
+ };
955
+ collector.record(finalResult, performance.now() - start, strategy);
956
+ return finalResult;
957
+ }
958
+ // Handle fallback
959
+ if (config.fallbackBehavior === 'next-strategy') {
960
+ usedFallback = true;
961
+ continue;
962
+ }
963
+ }
964
+ // No strategy succeeded
965
+ const result = {
966
+ agent: null,
967
+ task,
968
+ strategy: 'custom',
969
+ timestamp: new Date(),
970
+ reason: 'no-strategy-succeeded',
971
+ strategies,
972
+ strategyScores,
973
+ usedFallback,
974
+ };
975
+ collector.record(result, performance.now() - start, 'custom');
976
+ return result;
977
+ }
978
+ function addAgent(agent) {
979
+ agents.push(agent);
980
+ balancers.forEach((b) => b.addAgent(agent));
981
+ }
982
+ function removeAgent(agentId) {
983
+ agents = agents.filter((a) => a.id !== agentId);
984
+ balancers.forEach((b) => b.removeAgent(agentId));
985
+ }
986
+ function getAgents() {
987
+ return [...agents];
988
+ }
989
+ return { route, addAgent, removeAgent, getAgents };
990
+ }
991
+ //# sourceMappingURL=load-balancing.js.map