digital-workers 2.1.3 → 2.4.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 (183) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +2 -0
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +33 -21
  6. package/dist/actions.js.map +1 -1
  7. package/dist/agent-comms.d.ts.map +1 -1
  8. package/dist/agent-comms.js +36 -25
  9. package/dist/agent-comms.js.map +1 -1
  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.js +3 -3
  23. package/dist/capability-tiers.js.map +1 -1
  24. package/dist/cascade-context.d.ts +28 -28
  25. package/dist/client.d.ts +162 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/client.js +64 -0
  28. package/dist/client.js.map +1 -0
  29. package/dist/decide.d.ts +42 -6
  30. package/dist/decide.d.ts.map +1 -1
  31. package/dist/decide.js +54 -11
  32. package/dist/decide.js.map +1 -1
  33. package/dist/do.d.ts +36 -7
  34. package/dist/do.d.ts.map +1 -1
  35. package/dist/do.js +82 -39
  36. package/dist/do.js.map +1 -1
  37. package/dist/error-escalation.d.ts.map +1 -1
  38. package/dist/error-escalation.js +38 -38
  39. package/dist/error-escalation.js.map +1 -1
  40. package/dist/generate.d.ts +48 -7
  41. package/dist/generate.d.ts.map +1 -1
  42. package/dist/generate.js +49 -8
  43. package/dist/generate.js.map +1 -1
  44. package/dist/goals.d.ts +10 -9
  45. package/dist/goals.d.ts.map +1 -1
  46. package/dist/goals.js +30 -24
  47. package/dist/goals.js.map +1 -1
  48. package/dist/image.d.ts +189 -0
  49. package/dist/image.d.ts.map +1 -0
  50. package/dist/image.js +528 -0
  51. package/dist/image.js.map +1 -0
  52. package/dist/index.d.ts +49 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +58 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/is.d.ts +45 -10
  57. package/dist/is.d.ts.map +1 -1
  58. package/dist/is.js +56 -21
  59. package/dist/is.js.map +1 -1
  60. package/dist/kpis.d.ts +24 -15
  61. package/dist/kpis.d.ts.map +1 -1
  62. package/dist/kpis.js +16 -14
  63. package/dist/kpis.js.map +1 -1
  64. package/dist/load-balancing.d.ts.map +1 -1
  65. package/dist/load-balancing.js +124 -38
  66. package/dist/load-balancing.js.map +1 -1
  67. package/dist/logger.d.ts +76 -0
  68. package/dist/logger.d.ts.map +1 -0
  69. package/dist/logger.js +39 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/notify.d.ts +38 -9
  72. package/dist/notify.d.ts.map +1 -1
  73. package/dist/notify.js +72 -17
  74. package/dist/notify.js.map +1 -1
  75. package/dist/role.d.ts +5 -4
  76. package/dist/role.d.ts.map +1 -1
  77. package/dist/role.js +13 -10
  78. package/dist/role.js.map +1 -1
  79. package/dist/runtime.d.ts +310 -0
  80. package/dist/runtime.d.ts.map +1 -0
  81. package/dist/runtime.js +510 -0
  82. package/dist/runtime.js.map +1 -0
  83. package/dist/team.d.ts +11 -6
  84. package/dist/team.d.ts.map +1 -1
  85. package/dist/team.js +22 -15
  86. package/dist/team.js.map +1 -1
  87. package/dist/transports/email.d.ts +318 -0
  88. package/dist/transports/email.d.ts.map +1 -0
  89. package/dist/transports/email.js +779 -0
  90. package/dist/transports/email.js.map +1 -0
  91. package/dist/transports/slack.d.ts +515 -0
  92. package/dist/transports/slack.d.ts.map +1 -0
  93. package/dist/transports/slack.js +844 -0
  94. package/dist/transports/slack.js.map +1 -0
  95. package/dist/transports.d.ts.map +1 -1
  96. package/dist/transports.js +44 -25
  97. package/dist/transports.js.map +1 -1
  98. package/dist/types.d.ts +141 -19
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/types.js +5 -0
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/id.d.ts +19 -0
  103. package/dist/utils/id.d.ts.map +1 -0
  104. package/dist/utils/id.js +21 -0
  105. package/dist/utils/id.js.map +1 -0
  106. package/dist/video.d.ts +203 -0
  107. package/dist/video.d.ts.map +1 -0
  108. package/dist/video.js +528 -0
  109. package/dist/video.js.map +1 -0
  110. package/dist/worker.d.ts +343 -0
  111. package/dist/worker.d.ts.map +1 -0
  112. package/dist/worker.js +698 -0
  113. package/dist/worker.js.map +1 -0
  114. package/package.json +32 -14
  115. package/src/actions.ts +39 -30
  116. package/src/agent-comms.ts +54 -92
  117. package/src/approve.ts +91 -20
  118. package/src/ask.ts +99 -25
  119. package/src/browse.ts +627 -0
  120. package/src/capability-tiers.ts +5 -5
  121. package/src/client.ts +221 -0
  122. package/src/decide.ts +81 -35
  123. package/src/do.ts +98 -52
  124. package/src/error-escalation.ts +55 -67
  125. package/src/generate.ts +52 -18
  126. package/src/goals.ts +36 -27
  127. package/src/image.ts +816 -0
  128. package/src/index.ts +187 -2
  129. package/src/is.ts +59 -25
  130. package/src/kpis.ts +41 -36
  131. package/src/load-balancing.ts +132 -46
  132. package/src/logger.ts +93 -0
  133. package/src/notify.ts +78 -17
  134. package/src/role.ts +30 -20
  135. package/src/runtime.ts +796 -0
  136. package/src/team.ts +24 -19
  137. package/src/transports/email.ts +1160 -0
  138. package/src/transports/slack.ts +1320 -0
  139. package/src/transports.ts +58 -43
  140. package/src/types.ts +174 -46
  141. package/src/utils/id.ts +21 -0
  142. package/src/video.ts +906 -0
  143. package/src/worker.ts +1007 -0
  144. package/test/approve.test.ts +305 -0
  145. package/test/ask.test.ts +274 -0
  146. package/test/browse.test.ts +361 -0
  147. package/test/decide.test.ts +252 -0
  148. package/test/do.test.ts +144 -0
  149. package/test/error-logging.test.ts +357 -0
  150. package/test/generate.test.ts +319 -0
  151. package/test/image.test.ts +398 -0
  152. package/test/is.test.ts +287 -0
  153. package/test/load-balancing-safety.test.ts +404 -0
  154. package/test/notify.test.ts +434 -0
  155. package/test/primitives.test.ts +320 -0
  156. package/test/runtime-integration.test.ts +892 -0
  157. package/test/transports/crypto.test.ts +230 -0
  158. package/test/transports/email.test.ts +866 -0
  159. package/test/transports/id-generation.test.ts +91 -0
  160. package/test/transports/slack.test.ts +760 -0
  161. package/test/type-safety.test.ts +834 -0
  162. package/test/types.test.ts +60 -2
  163. package/test/video.test.ts +530 -0
  164. package/test/worker.test.ts +1433 -0
  165. package/tsconfig.json +4 -1
  166. package/vitest.config.ts +42 -0
  167. package/wrangler.jsonc +36 -0
  168. package/LICENSE +0 -21
  169. package/src/actions.js +0 -436
  170. package/src/approve.js +0 -234
  171. package/src/ask.js +0 -226
  172. package/src/decide.js +0 -244
  173. package/src/do.js +0 -227
  174. package/src/generate.js +0 -298
  175. package/src/goals.js +0 -205
  176. package/src/index.js +0 -68
  177. package/src/is.js +0 -317
  178. package/src/kpis.js +0 -270
  179. package/src/notify.js +0 -219
  180. package/src/role.js +0 -110
  181. package/src/team.js +0 -130
  182. package/src/transports.js +0 -357
  183. package/src/types.js +0 -71
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Load Balancing Safety Tests
3
+ *
4
+ * TDD tests for ensuring proper bounds checking and null safety
5
+ * in load balancing operations. Replaces non-null assertions with
6
+ * explicit bounds checks.
7
+ *
8
+ * Issue: aip-w8mm
9
+ */
10
+
11
+ import { describe, it, expect, beforeEach } from 'vitest'
12
+ import type { AgentInfo, TaskRequest } from '../src/load-balancing.js'
13
+ import {
14
+ createRoundRobinBalancer,
15
+ createLeastBusyBalancer,
16
+ createCapabilityRouter,
17
+ createPriorityQueueBalancer,
18
+ createMetricsCollector,
19
+ } from '../src/load-balancing.js'
20
+ import type { WorkerStatus } from '../src/types.js'
21
+
22
+ // ============================================================================
23
+ // Test Fixtures
24
+ // ============================================================================
25
+
26
+ const createAgent = (
27
+ id: string,
28
+ skills: string[] = [],
29
+ status: WorkerStatus = 'available',
30
+ currentLoad = 0
31
+ ): AgentInfo => ({
32
+ id,
33
+ name: `Agent ${id}`,
34
+ type: 'agent',
35
+ status,
36
+ skills,
37
+ currentLoad,
38
+ maxLoad: 10,
39
+ contacts: { api: `https://agent-${id}.example.com` },
40
+ metadata: {},
41
+ })
42
+
43
+ const createTask = (
44
+ id: string,
45
+ requiredSkills: string[] = [],
46
+ priority: number = 5
47
+ ): TaskRequest => ({
48
+ id,
49
+ name: `Task ${id}`,
50
+ requiredSkills,
51
+ priority,
52
+ metadata: {},
53
+ })
54
+
55
+ // ============================================================================
56
+ // Empty Agent Array Handling Tests
57
+ // ============================================================================
58
+
59
+ describe('Empty Agent Array Handling', () => {
60
+ describe('RoundRobinBalancer with empty agents', () => {
61
+ it('should handle empty agent array gracefully', () => {
62
+ const balancer = createRoundRobinBalancer([])
63
+ const task = createTask('task-1')
64
+
65
+ const result = balancer.route(task)
66
+
67
+ expect(result.agent).toBeNull()
68
+ expect(result.reason).toBe('no-available-agents')
69
+ expect(result.strategy).toBe('round-robin')
70
+ })
71
+
72
+ it('should not throw when routing with zero agents', () => {
73
+ const balancer = createRoundRobinBalancer([])
74
+
75
+ expect(() => balancer.route(createTask('task-1'))).not.toThrow()
76
+ expect(() => balancer.route(createTask('task-2'))).not.toThrow()
77
+ expect(() => balancer.route(createTask('task-3'))).not.toThrow()
78
+ })
79
+
80
+ it('should handle agents being removed until empty', () => {
81
+ const agents = [createAgent('agent-1')]
82
+ const balancer = createRoundRobinBalancer(agents)
83
+
84
+ // First route works
85
+ const result1 = balancer.route(createTask('task-1'))
86
+ expect(result1.agent?.id).toBe('agent-1')
87
+
88
+ // Remove the only agent
89
+ balancer.removeAgent('agent-1')
90
+
91
+ // Now routing should handle empty gracefully
92
+ const result2 = balancer.route(createTask('task-2'))
93
+ expect(result2.agent).toBeNull()
94
+ expect(result2.reason).toBe('no-available-agents')
95
+ })
96
+ })
97
+
98
+ describe('LeastBusyBalancer with empty agents', () => {
99
+ it('should handle empty agent array gracefully', () => {
100
+ const balancer = createLeastBusyBalancer([])
101
+ const task = createTask('task-1')
102
+
103
+ const result = balancer.route(task)
104
+
105
+ expect(result.agent).toBeNull()
106
+ expect(result.reason).toBe('no-available-agents')
107
+ expect(result.strategy).toBe('least-busy')
108
+ })
109
+
110
+ it('should not throw when sorting empty available agents', () => {
111
+ // All agents are at max capacity
112
+ const agents = [
113
+ createAgent('agent-1', [], 'available', 10), // maxLoad is 10
114
+ createAgent('agent-2', [], 'available', 10),
115
+ ]
116
+ const balancer = createLeastBusyBalancer(agents)
117
+
118
+ // Should not throw when sorting an empty array after filtering
119
+ expect(() => balancer.route(createTask('task-1'))).not.toThrow()
120
+ })
121
+
122
+ it('should handle all agents being removed', () => {
123
+ const agents = [createAgent('agent-1')]
124
+ const balancer = createLeastBusyBalancer(agents)
125
+
126
+ balancer.removeAgent('agent-1')
127
+
128
+ const result = balancer.route(createTask('task-1'))
129
+ expect(result.agent).toBeNull()
130
+ })
131
+ })
132
+
133
+ describe('CapabilityRouter with empty agents', () => {
134
+ it('should handle empty agent array gracefully', () => {
135
+ const router = createCapabilityRouter([])
136
+ const task = createTask('task-1', ['code'])
137
+
138
+ const result = router.route(task)
139
+
140
+ expect(result.agent).toBeNull()
141
+ expect(result.reason).toBe('no-matching-capability')
142
+ })
143
+
144
+ it('should handle empty candidates after skill filtering', () => {
145
+ const agents = [createAgent('agent-1', ['deploy']), createAgent('agent-2', ['monitor'])]
146
+ const router = createCapabilityRouter(agents)
147
+
148
+ // No agent has 'code' skill
149
+ const result = router.route(createTask('code-task', ['code']))
150
+
151
+ expect(result.agent).toBeNull()
152
+ expect(result.reason).toBe('no-matching-capability')
153
+ })
154
+ })
155
+
156
+ describe('PriorityQueueBalancer with empty agents', () => {
157
+ it('should handle empty agent array gracefully', () => {
158
+ const balancer = createPriorityQueueBalancer([])
159
+ const task = createTask('task-1')
160
+
161
+ const result = balancer.route(task)
162
+
163
+ expect(result.agent).toBeNull()
164
+ expect(result.reason).toBe('no-available-agents')
165
+ })
166
+
167
+ it('should handle routeNext with empty agents', async () => {
168
+ const balancer = createPriorityQueueBalancer([])
169
+ balancer.enqueue(createTask('task-1', [], 5))
170
+
171
+ const result = await balancer.routeNext()
172
+
173
+ expect(result).not.toBeNull()
174
+ expect(result?.agent).toBeNull()
175
+ expect(result?.reason).toBe('no-available-agents')
176
+ })
177
+ })
178
+ })
179
+
180
+ // ============================================================================
181
+ // Single Agent Selection Tests
182
+ // ============================================================================
183
+
184
+ describe('Single Agent Selection', () => {
185
+ describe('RoundRobinBalancer with single agent', () => {
186
+ it('should always route to the single available agent', () => {
187
+ const agents = [createAgent('only-agent')]
188
+ const balancer = createRoundRobinBalancer(agents)
189
+
190
+ for (let i = 0; i < 5; i++) {
191
+ const result = balancer.route(createTask(`task-${i}`))
192
+ expect(result.agent?.id).toBe('only-agent')
193
+ }
194
+ })
195
+
196
+ it('should handle single offline agent', () => {
197
+ const agents = [createAgent('offline-agent', [], 'offline')]
198
+ const balancer = createRoundRobinBalancer(agents)
199
+
200
+ const result = balancer.route(createTask('task-1'))
201
+
202
+ expect(result.agent).toBeNull()
203
+ expect(result.reason).toBe('no-available-agents')
204
+ })
205
+ })
206
+
207
+ describe('LeastBusyBalancer with single agent', () => {
208
+ it('should route to single agent regardless of load', () => {
209
+ const agents = [createAgent('only-agent', [], 'available', 5)]
210
+ const balancer = createLeastBusyBalancer(agents)
211
+
212
+ const result = balancer.route(createTask('task-1'))
213
+
214
+ expect(result.agent?.id).toBe('only-agent')
215
+ })
216
+
217
+ it('should not route when single agent is at max capacity', () => {
218
+ const agents = [createAgent('full-agent', [], 'available', 10)]
219
+ const balancer = createLeastBusyBalancer(agents)
220
+
221
+ const result = balancer.route(createTask('task-1'))
222
+
223
+ expect(result.agent).toBeNull()
224
+ })
225
+ })
226
+
227
+ describe('CapabilityRouter with single agent', () => {
228
+ it('should route when single agent has required skills', () => {
229
+ const agents = [createAgent('skilled-agent', ['code', 'review'])]
230
+ const router = createCapabilityRouter(agents)
231
+
232
+ const result = router.route(createTask('code-task', ['code']))
233
+
234
+ expect(result.agent?.id).toBe('skilled-agent')
235
+ })
236
+
237
+ it('should not route when single agent lacks skills', () => {
238
+ const agents = [createAgent('wrong-skills', ['deploy'])]
239
+ const router = createCapabilityRouter(agents)
240
+
241
+ const result = router.route(createTask('code-task', ['code']))
242
+
243
+ expect(result.agent).toBeNull()
244
+ })
245
+ })
246
+ })
247
+
248
+ // ============================================================================
249
+ // Empty Queue Handling Tests
250
+ // ============================================================================
251
+
252
+ describe('Empty Queue Handling', () => {
253
+ describe('PriorityQueueBalancer empty queue', () => {
254
+ it('should return null for routeNext on empty queue', async () => {
255
+ const agents = [createAgent('agent-1')]
256
+ const balancer = createPriorityQueueBalancer(agents)
257
+
258
+ const result = await balancer.routeNext()
259
+
260
+ expect(result).toBeNull()
261
+ })
262
+
263
+ it('should return null for peek on empty queue', () => {
264
+ const agents = [createAgent('agent-1')]
265
+ const balancer = createPriorityQueueBalancer(agents)
266
+
267
+ const peeked = balancer.peek()
268
+
269
+ expect(peeked).toBeNull()
270
+ })
271
+
272
+ it('should handle queue being emptied', async () => {
273
+ const agents = [createAgent('agent-1')]
274
+ const balancer = createPriorityQueueBalancer(agents)
275
+
276
+ balancer.enqueue(createTask('task-1', [], 5))
277
+
278
+ // Dequeue the only task
279
+ const result1 = await balancer.routeNext()
280
+ expect(result1?.task.id).toBe('task-1')
281
+
282
+ // Queue is now empty
283
+ const result2 = await balancer.routeNext()
284
+ expect(result2).toBeNull()
285
+ })
286
+
287
+ it('should handle clear then routeNext', async () => {
288
+ const agents = [createAgent('agent-1')]
289
+ const balancer = createPriorityQueueBalancer(agents)
290
+
291
+ balancer.enqueue(createTask('task-1', [], 5))
292
+ balancer.enqueue(createTask('task-2', [], 5))
293
+
294
+ balancer.clear()
295
+
296
+ const result = await balancer.routeNext()
297
+ expect(result).toBeNull()
298
+ })
299
+ })
300
+ })
301
+
302
+ // ============================================================================
303
+ // Edge Cases with Zero-Length Arrays
304
+ // ============================================================================
305
+
306
+ describe('Edge Cases with Zero-Length Arrays', () => {
307
+ describe('Index out of bounds prevention', () => {
308
+ it('should handle modulo on empty array length safely', () => {
309
+ // This tests the fix for: const agent = agents[currentIndex % agents.length]!
310
+ // When agents.length is 0, this would cause division by zero
311
+ const balancer = createRoundRobinBalancer([])
312
+
313
+ // Route multiple times to test index incrementing
314
+ for (let i = 0; i < 10; i++) {
315
+ expect(() => balancer.route(createTask(`task-${i}`))).not.toThrow()
316
+ }
317
+ })
318
+
319
+ it('should handle sorted array access on empty result safely', () => {
320
+ // This tests the fix for: const selected = sorted[0]!
321
+ // When sorted is empty, this would throw
322
+ const balancer = createLeastBusyBalancer([])
323
+
324
+ expect(() => balancer.route(createTask('task-1'))).not.toThrow()
325
+ })
326
+
327
+ it('should handle shift on empty queue safely', async () => {
328
+ // This tests the fix for: const task = queue.shift()!
329
+ const balancer = createPriorityQueueBalancer([createAgent('agent-1')])
330
+
331
+ // Don't enqueue anything, just try to route
332
+ const result = await balancer.routeNext()
333
+ expect(result).toBeNull()
334
+ })
335
+ })
336
+
337
+ describe('Concurrent agent modifications', () => {
338
+ it('should handle agent removal during routing iteration', () => {
339
+ const agents = [createAgent('agent-1'), createAgent('agent-2'), createAgent('agent-3')]
340
+ const balancer = createRoundRobinBalancer(agents)
341
+
342
+ // Route once
343
+ balancer.route(createTask('task-1'))
344
+
345
+ // Remove an agent
346
+ balancer.removeAgent('agent-2')
347
+
348
+ // Should still work
349
+ expect(() => balancer.route(createTask('task-2'))).not.toThrow()
350
+ })
351
+
352
+ it('should handle all agents being removed during operation', () => {
353
+ const agents = [createAgent('agent-1'), createAgent('agent-2')]
354
+ const balancer = createRoundRobinBalancer(agents)
355
+
356
+ balancer.removeAgent('agent-1')
357
+ balancer.removeAgent('agent-2')
358
+
359
+ const result = balancer.route(createTask('task-1'))
360
+ expect(result.agent).toBeNull()
361
+ })
362
+ })
363
+
364
+ describe('Metrics collection with empty results', () => {
365
+ it('should collect metrics even with empty agent arrays', () => {
366
+ const collector = createMetricsCollector()
367
+ const balancer = createRoundRobinBalancer([], { metricsCollector: collector })
368
+
369
+ balancer.route(createTask('task-1'))
370
+ balancer.route(createTask('task-2'))
371
+
372
+ const metrics = collector.collect()
373
+
374
+ expect(metrics.totalRouted).toBe(2)
375
+ expect(metrics.failedRoutes).toBe(2)
376
+ })
377
+ })
378
+
379
+ describe('Return type consistency', () => {
380
+ it('should always return RouteResult even when no agent found', () => {
381
+ const balancer = createRoundRobinBalancer([])
382
+ const task = createTask('task-1')
383
+
384
+ const result = balancer.route(task)
385
+
386
+ // Verify all required properties exist
387
+ expect(result).toHaveProperty('agent')
388
+ expect(result).toHaveProperty('task')
389
+ expect(result).toHaveProperty('strategy')
390
+ expect(result).toHaveProperty('timestamp')
391
+ expect(result.task).toBe(task)
392
+ expect(result.timestamp).toBeInstanceOf(Date)
393
+ })
394
+
395
+ it('should return null agent, not undefined', () => {
396
+ const balancer = createRoundRobinBalancer([])
397
+
398
+ const result = balancer.route(createTask('task-1'))
399
+
400
+ expect(result.agent).toBeNull()
401
+ expect(result.agent).not.toBeUndefined()
402
+ })
403
+ })
404
+ })