digital-workers 2.1.3 → 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 (183) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +2 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +33 -21
  5. package/dist/actions.js.map +1 -1
  6. package/dist/agent-comms.d.ts.map +1 -1
  7. package/dist/agent-comms.js +36 -25
  8. package/dist/agent-comms.js.map +1 -1
  9. package/dist/approve.d.ts +40 -8
  10. package/dist/approve.d.ts.map +1 -1
  11. package/dist/approve.js +86 -20
  12. package/dist/approve.js.map +1 -1
  13. package/dist/ask.d.ts +38 -7
  14. package/dist/ask.d.ts.map +1 -1
  15. package/dist/ask.js +85 -25
  16. package/dist/ask.js.map +1 -1
  17. package/dist/browse.d.ts +223 -0
  18. package/dist/browse.d.ts.map +1 -0
  19. package/dist/browse.js +392 -0
  20. package/dist/browse.js.map +1 -0
  21. package/dist/capability-tiers.js +3 -3
  22. package/dist/capability-tiers.js.map +1 -1
  23. package/dist/cascade-context.d.ts +28 -28
  24. package/dist/client.d.ts +162 -0
  25. package/dist/client.d.ts.map +1 -0
  26. package/dist/client.js +64 -0
  27. package/dist/client.js.map +1 -0
  28. package/dist/decide.d.ts +42 -6
  29. package/dist/decide.d.ts.map +1 -1
  30. package/dist/decide.js +54 -11
  31. package/dist/decide.js.map +1 -1
  32. package/dist/do.d.ts +36 -7
  33. package/dist/do.d.ts.map +1 -1
  34. package/dist/do.js +82 -39
  35. package/dist/do.js.map +1 -1
  36. package/dist/error-escalation.d.ts.map +1 -1
  37. package/dist/error-escalation.js +38 -38
  38. package/dist/error-escalation.js.map +1 -1
  39. package/dist/generate.d.ts +48 -7
  40. package/dist/generate.d.ts.map +1 -1
  41. package/dist/generate.js +49 -8
  42. package/dist/generate.js.map +1 -1
  43. package/dist/goals.d.ts +10 -9
  44. package/dist/goals.d.ts.map +1 -1
  45. package/dist/goals.js +30 -24
  46. package/dist/goals.js.map +1 -1
  47. package/dist/image.d.ts +189 -0
  48. package/dist/image.d.ts.map +1 -0
  49. package/dist/image.js +528 -0
  50. package/dist/image.js.map +1 -0
  51. package/dist/index.d.ts +49 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +58 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/is.d.ts +45 -10
  56. package/dist/is.d.ts.map +1 -1
  57. package/dist/is.js +56 -21
  58. package/dist/is.js.map +1 -1
  59. package/dist/kpis.d.ts +24 -15
  60. package/dist/kpis.d.ts.map +1 -1
  61. package/dist/kpis.js +16 -14
  62. package/dist/kpis.js.map +1 -1
  63. package/dist/load-balancing.d.ts.map +1 -1
  64. package/dist/load-balancing.js +124 -38
  65. package/dist/load-balancing.js.map +1 -1
  66. package/dist/logger.d.ts +76 -0
  67. package/dist/logger.d.ts.map +1 -0
  68. package/dist/logger.js +39 -0
  69. package/dist/logger.js.map +1 -0
  70. package/dist/notify.d.ts +38 -9
  71. package/dist/notify.d.ts.map +1 -1
  72. package/dist/notify.js +72 -17
  73. package/dist/notify.js.map +1 -1
  74. package/dist/role.d.ts +5 -4
  75. package/dist/role.d.ts.map +1 -1
  76. package/dist/role.js +13 -10
  77. package/dist/role.js.map +1 -1
  78. package/dist/runtime.d.ts +310 -0
  79. package/dist/runtime.d.ts.map +1 -0
  80. package/dist/runtime.js +510 -0
  81. package/dist/runtime.js.map +1 -0
  82. package/dist/team.d.ts +11 -6
  83. package/dist/team.d.ts.map +1 -1
  84. package/dist/team.js +22 -15
  85. package/dist/team.js.map +1 -1
  86. package/dist/transports/email.d.ts +318 -0
  87. package/dist/transports/email.d.ts.map +1 -0
  88. package/dist/transports/email.js +779 -0
  89. package/dist/transports/email.js.map +1 -0
  90. package/dist/transports/slack.d.ts +515 -0
  91. package/dist/transports/slack.d.ts.map +1 -0
  92. package/dist/transports/slack.js +844 -0
  93. package/dist/transports/slack.js.map +1 -0
  94. package/dist/transports.d.ts.map +1 -1
  95. package/dist/transports.js +44 -25
  96. package/dist/transports.js.map +1 -1
  97. package/dist/types.d.ts +141 -19
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js +5 -0
  100. package/dist/types.js.map +1 -1
  101. package/dist/utils/id.d.ts +19 -0
  102. package/dist/utils/id.d.ts.map +1 -0
  103. package/dist/utils/id.js +21 -0
  104. package/dist/utils/id.js.map +1 -0
  105. package/dist/video.d.ts +203 -0
  106. package/dist/video.d.ts.map +1 -0
  107. package/dist/video.js +528 -0
  108. package/dist/video.js.map +1 -0
  109. package/dist/worker.d.ts +343 -0
  110. package/dist/worker.d.ts.map +1 -0
  111. package/dist/worker.js +698 -0
  112. package/dist/worker.js.map +1 -0
  113. package/package.json +32 -14
  114. package/src/actions.ts +39 -30
  115. package/src/agent-comms.ts +54 -92
  116. package/src/approve.ts +91 -20
  117. package/src/ask.ts +99 -25
  118. package/src/browse.ts +627 -0
  119. package/src/capability-tiers.ts +5 -5
  120. package/src/client.ts +221 -0
  121. package/src/decide.ts +81 -35
  122. package/src/do.ts +98 -52
  123. package/src/error-escalation.ts +55 -67
  124. package/src/generate.ts +52 -18
  125. package/src/goals.ts +36 -27
  126. package/src/image.ts +816 -0
  127. package/src/index.ts +187 -2
  128. package/src/is.ts +59 -25
  129. package/src/kpis.ts +41 -36
  130. package/src/load-balancing.ts +132 -46
  131. package/src/logger.ts +93 -0
  132. package/src/notify.ts +78 -17
  133. package/src/role.ts +30 -20
  134. package/src/runtime.ts +796 -0
  135. package/src/team.ts +24 -19
  136. package/src/transports/email.ts +1160 -0
  137. package/src/transports/slack.ts +1320 -0
  138. package/src/transports.ts +58 -43
  139. package/src/types.ts +174 -46
  140. package/src/utils/id.ts +21 -0
  141. package/src/video.ts +906 -0
  142. package/src/worker.ts +1007 -0
  143. package/test/approve.test.ts +305 -0
  144. package/test/ask.test.ts +274 -0
  145. package/test/browse.test.ts +361 -0
  146. package/test/decide.test.ts +252 -0
  147. package/test/do.test.ts +144 -0
  148. package/test/error-logging.test.ts +357 -0
  149. package/test/generate.test.ts +319 -0
  150. package/test/image.test.ts +398 -0
  151. package/test/is.test.ts +287 -0
  152. package/test/load-balancing-safety.test.ts +404 -0
  153. package/test/notify.test.ts +434 -0
  154. package/test/primitives.test.ts +320 -0
  155. package/test/runtime-integration.test.ts +892 -0
  156. package/test/transports/crypto.test.ts +230 -0
  157. package/test/transports/email.test.ts +866 -0
  158. package/test/transports/id-generation.test.ts +91 -0
  159. package/test/transports/slack.test.ts +760 -0
  160. package/test/type-safety.test.ts +834 -0
  161. package/test/types.test.ts +60 -2
  162. package/test/video.test.ts +530 -0
  163. package/test/worker.test.ts +1433 -0
  164. package/tsconfig.json +4 -1
  165. package/vitest.config.ts +42 -0
  166. package/wrangler.jsonc +36 -0
  167. package/.turbo/turbo-build.log +0 -4
  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
@@ -167,6 +167,38 @@ export interface MetricsCollector {
167
167
  reset(): void
168
168
  }
169
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
+
170
202
  // ============================================================================
171
203
  // MetricsCollector Implementation
172
204
  // ============================================================================
@@ -213,11 +245,7 @@ export function createMetricsCollector(): MetricsCollector {
213
245
  }
214
246
  let totalLatency = 0
215
247
 
216
- function record(
217
- result: RouteResult,
218
- latencyMs: number,
219
- strategy: BalancerStrategy
220
- ): void {
248
+ function record(result: RouteResult, latencyMs: number, strategy: BalancerStrategy): void {
221
249
  metricsState.totalRouted++
222
250
  totalLatency += latencyMs
223
251
  metricsState.averageLatencyMs = totalLatency / metricsState.totalRouted
@@ -326,7 +354,7 @@ export function createRoundRobinBalancer(
326
354
  const collector = options.metricsCollector ?? defaultMetricsCollector
327
355
 
328
356
  function getAvailableAgents(): AgentInfo[] {
329
- return agents.filter(a => a.status === 'available' || a.status === 'busy')
357
+ return agents.filter((a) => a.status === 'available' || a.status === 'busy')
330
358
  }
331
359
 
332
360
  function route(task: TaskRequest): RouteResult {
@@ -348,8 +376,12 @@ export function createRoundRobinBalancer(
348
376
  // Find the next available agent starting from current index
349
377
  let attempts = 0
350
378
  while (attempts < agents.length) {
351
- const agent = agents[currentIndex % agents.length]!
379
+ const agent = safeAt(agents, currentIndex % agents.length)
352
380
  currentIndex++
381
+ if (!agent) {
382
+ attempts++
383
+ continue
384
+ }
353
385
 
354
386
  if (agent.status === 'available' || agent.status === 'busy') {
355
387
  const result: RouteResult = {
@@ -380,7 +412,7 @@ export function createRoundRobinBalancer(
380
412
  }
381
413
 
382
414
  function removeAgent(agentId: string): void {
383
- agents = agents.filter(a => a.id !== agentId)
415
+ agents = agents.filter((a) => a.id !== agentId)
384
416
  }
385
417
 
386
418
  function getAgents(): AgentInfo[] {
@@ -430,10 +462,10 @@ export function createLeastBusyBalancer(
430
462
  const collector = options.metricsCollector ?? defaultMetricsCollector
431
463
 
432
464
  // Initialize load tracking
433
- agents.forEach(a => loadTracking.set(a.id, a.currentLoad))
465
+ agents.forEach((a) => loadTracking.set(a.id, a.currentLoad))
434
466
 
435
467
  function getAvailableAgents(): AgentInfo[] {
436
- return agents.filter(a => {
468
+ return agents.filter((a) => {
437
469
  if (a.status !== 'available' && a.status !== 'busy') return false
438
470
  const load = loadTracking.get(a.id) ?? a.currentLoad
439
471
  return load < a.maxLoad
@@ -471,7 +503,18 @@ export function createLeastBusyBalancer(
471
503
  return loadA - loadB
472
504
  })
473
505
 
474
- const selected = sorted[0]!
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
+ }
475
518
  lastRoutedIndex = agents.indexOf(selected)
476
519
 
477
520
  // Increment load
@@ -494,7 +537,7 @@ export function createLeastBusyBalancer(
494
537
  }
495
538
 
496
539
  function removeAgent(agentId: string): void {
497
- agents = agents.filter(a => a.id !== agentId)
540
+ agents = agents.filter((a) => a.id !== agentId)
498
541
  loadTracking.delete(agentId)
499
542
  }
500
543
 
@@ -504,7 +547,7 @@ export function createLeastBusyBalancer(
504
547
 
505
548
  function getLoadMetrics(): Record<string, number> {
506
549
  const metrics: Record<string, number> = {}
507
- agents.forEach(a => {
550
+ agents.forEach((a) => {
508
551
  const load = loadTracking.get(a.id) ?? a.currentLoad
509
552
  metrics[a.id] = load / a.maxLoad
510
553
  })
@@ -566,16 +609,16 @@ export function createCapabilityRouter(
566
609
  const collector = options.metricsCollector ?? defaultMetricsCollector
567
610
 
568
611
  function getAvailableAgents(): AgentInfo[] {
569
- return agents.filter(a => a.status === 'available' || a.status === 'busy')
612
+ return agents.filter((a) => a.status === 'available' || a.status === 'busy')
570
613
  }
571
614
 
572
615
  function hasAllSkills(agent: AgentInfo, requiredSkills: string[]): boolean {
573
- return requiredSkills.every(skill => agent.skills.includes(skill))
616
+ return requiredSkills.every((skill) => agent.skills.includes(skill))
574
617
  }
575
618
 
576
619
  function calculateMatchScore(agent: AgentInfo, requiredSkills: string[]): number {
577
620
  if (requiredSkills.length === 0) return 1
578
- const matchingSkills = requiredSkills.filter(s => agent.skills.includes(s))
621
+ const matchingSkills = requiredSkills.filter((s) => agent.skills.includes(s))
579
622
  return matchingSkills.length / requiredSkills.length
580
623
  }
581
624
 
@@ -584,7 +627,7 @@ export function createCapabilityRouter(
584
627
  const available = getAvailableAgents()
585
628
 
586
629
  // Find agents with all required skills
587
- let candidates = available.filter(a => hasAllSkills(a, task.requiredSkills))
630
+ let candidates = available.filter((a) => hasAllSkills(a, task.requiredSkills))
588
631
 
589
632
  if (candidates.length === 0) {
590
633
  const result: RouteResult = {
@@ -608,7 +651,18 @@ export function createCapabilityRouter(
608
651
  })
609
652
  }
610
653
 
611
- const selected = candidates[0]!
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
+ }
612
666
  const matchScore = calculateMatchScore(selected, task.requiredSkills)
613
667
 
614
668
  const result: RouteResult = {
@@ -627,7 +681,7 @@ export function createCapabilityRouter(
627
681
  }
628
682
 
629
683
  function removeAgent(agentId: string): void {
630
- agents = agents.filter(a => a.id !== agentId)
684
+ agents = agents.filter((a) => a.id !== agentId)
631
685
  }
632
686
 
633
687
  function getAgents(): AgentInfo[] {
@@ -635,13 +689,13 @@ export function createCapabilityRouter(
635
689
  }
636
690
 
637
691
  function findAgentsWithSkills(skills: string[]): AgentInfo[] {
638
- return agents.filter(a => hasAllSkills(a, skills))
692
+ return agents.filter((a) => hasAllSkills(a, skills))
639
693
  }
640
694
 
641
695
  function getSkillCoverage(): Record<string, number> {
642
696
  const coverage: Record<string, number> = {}
643
- agents.forEach(a => {
644
- a.skills.forEach(skill => {
697
+ agents.forEach((a) => {
698
+ a.skills.forEach((skill) => {
645
699
  coverage[skill] = (coverage[skill] || 0) + 1
646
700
  })
647
701
  })
@@ -706,11 +760,11 @@ export function createPriorityQueueBalancer(
706
760
  const collector = options.metricsCollector ?? defaultMetricsCollector
707
761
 
708
762
  function getAvailableAgents(): AgentInfo[] {
709
- return agents.filter(a => a.status === 'available' || a.status === 'busy')
763
+ return agents.filter((a) => a.status === 'available' || a.status === 'busy')
710
764
  }
711
765
 
712
766
  function getEffectivePriority(taskId: string): number {
713
- const task = queue.find(t => t.id === taskId)
767
+ const task = queue.find((t) => t.id === taskId)
714
768
  if (!task) return 0
715
769
 
716
770
  let priority = task.priority
@@ -758,7 +812,8 @@ export function createPriorityQueueBalancer(
758
812
  if (queue.length === 0) return null
759
813
 
760
814
  sortQueue()
761
- const task = queue.shift()!
815
+ const task = queue.shift()
816
+ if (!task) return null
762
817
  const start = performance.now()
763
818
  const available = getAvailableAgents()
764
819
 
@@ -775,7 +830,18 @@ export function createPriorityQueueBalancer(
775
830
  }
776
831
 
777
832
  // Simple round-robin among available for now
778
- const agent = available[0]!
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
+ }
779
845
 
780
846
  const result: RouteResult = {
781
847
  agent,
@@ -803,7 +869,18 @@ export function createPriorityQueueBalancer(
803
869
  return result
804
870
  }
805
871
 
806
- const agent = available[0]!
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
+ }
807
884
  const result: RouteResult = {
808
885
  agent,
809
886
  task,
@@ -819,7 +896,7 @@ export function createPriorityQueueBalancer(
819
896
  }
820
897
 
821
898
  function removeAgent(agentId: string): void {
822
- agents = agents.filter(a => a.id !== agentId)
899
+ agents = agents.filter((a) => a.id !== agentId)
823
900
  }
824
901
 
825
902
  function getAgents(): AgentInfo[] {
@@ -903,7 +980,7 @@ export function createAgentAvailabilityTracker(
903
980
  const { heartbeatTimeout = 30000 } = options
904
981
 
905
982
  // Initialize
906
- initialAgents.forEach(a => {
983
+ initialAgents.forEach((a) => {
907
984
  agents.set(a.id, a)
908
985
  availability.set(a.id, {
909
986
  status: a.status,
@@ -914,10 +991,12 @@ export function createAgentAvailabilityTracker(
914
991
  })
915
992
 
916
993
  function getAvailability(agentId: string): AgentAvailability {
917
- return availability.get(agentId) ?? {
918
- status: 'offline',
919
- lastSeen: new Date(0),
920
- }
994
+ return (
995
+ availability.get(agentId) ?? {
996
+ status: 'offline',
997
+ lastSeen: new Date(0),
998
+ }
999
+ )
921
1000
  }
922
1001
 
923
1002
  function updateStatus(agentId: string, status: WorkerStatus): void {
@@ -943,12 +1022,12 @@ export function createAgentAvailabilityTracker(
943
1022
  currentStatus: status,
944
1023
  timestamp: new Date(),
945
1024
  }
946
- handlers.forEach(h => h(event))
1025
+ handlers.forEach((h) => h(event))
947
1026
  }
948
1027
  }
949
1028
 
950
1029
  function getAvailableAgents(): AgentInfo[] {
951
- return Array.from(agents.values()).filter(a => {
1030
+ return Array.from(agents.values()).filter((a) => {
952
1031
  const avail = availability.get(a.id)
953
1032
  return avail?.status === 'available' || avail?.status === 'busy'
954
1033
  })
@@ -1137,7 +1216,7 @@ export function createRoutingRuleEngine(
1137
1216
 
1138
1217
  // Sort rules by priority (descending)
1139
1218
  const sortedRules = [...rules]
1140
- .filter(r => r.enabled !== false)
1219
+ .filter((r) => r.enabled !== false)
1141
1220
  .sort((a, b) => b.priority - a.priority)
1142
1221
 
1143
1222
  // Evaluate rules
@@ -1178,28 +1257,28 @@ export function createRoutingRuleEngine(
1178
1257
  }
1179
1258
 
1180
1259
  function removeRule(name: string): void {
1181
- const index = rules.findIndex(r => r.name === name)
1260
+ const index = rules.findIndex((r) => r.name === name)
1182
1261
  if (index !== -1) {
1183
1262
  rules.splice(index, 1)
1184
1263
  }
1185
1264
  }
1186
1265
 
1187
1266
  function updateRule(name: string, updates: Partial<RoutingRule>): void {
1188
- const rule = rules.find(r => r.name === name)
1267
+ const rule = rules.find((r) => r.name === name)
1189
1268
  if (rule) {
1190
1269
  Object.assign(rule, updates)
1191
1270
  }
1192
1271
  }
1193
1272
 
1194
1273
  function enableRule(name: string): void {
1195
- const rule = rules.find(r => r.name === name)
1274
+ const rule = rules.find((r) => r.name === name)
1196
1275
  if (rule) {
1197
1276
  rule.enabled = true
1198
1277
  }
1199
1278
  }
1200
1279
 
1201
1280
  function disableRule(name: string): void {
1202
- const rule = rules.find(r => r.name === name)
1281
+ const rule = rules.find((r) => r.name === name)
1203
1282
  if (rule) {
1204
1283
  rule.enabled = false
1205
1284
  }
@@ -1215,7 +1294,7 @@ export function createRoutingRuleEngine(
1215
1294
  }
1216
1295
 
1217
1296
  function removeAgent(agentId: string): void {
1218
- agents = agents.filter(a => a.id !== agentId)
1297
+ agents = agents.filter((a) => a.id !== agentId)
1219
1298
  defaultBalancer = undefined as any // Reset default balancer
1220
1299
  }
1221
1300
 
@@ -1283,7 +1362,14 @@ export function createCompositeBalancer(
1283
1362
  balancers.set(strategy, createRoundRobinBalancer(agents, balancerOptions))
1284
1363
  }
1285
1364
  }
1286
- return balancers.get(strategy)!
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
1287
1373
  }
1288
1374
 
1289
1375
  function route(task: TaskRequest): RouteResult {
@@ -1293,7 +1379,7 @@ export function createCompositeBalancer(
1293
1379
  let usedFallback = false
1294
1380
 
1295
1381
  // Handle weighted strategies
1296
- const weightedStrategies = config.strategies.map(s => {
1382
+ const weightedStrategies = config.strategies.map((s) => {
1297
1383
  if (typeof s === 'string') {
1298
1384
  return { strategy: s, weight: 1 }
1299
1385
  }
@@ -1365,12 +1451,12 @@ export function createCompositeBalancer(
1365
1451
 
1366
1452
  function addAgent(agent: AgentInfo): void {
1367
1453
  agents.push(agent)
1368
- balancers.forEach(b => b.addAgent(agent))
1454
+ balancers.forEach((b) => b.addAgent(agent))
1369
1455
  }
1370
1456
 
1371
1457
  function removeAgent(agentId: string): void {
1372
- agents = agents.filter(a => a.id !== agentId)
1373
- balancers.forEach(b => b.removeAgent(agentId))
1458
+ agents = agents.filter((a) => a.id !== agentId)
1459
+ balancers.forEach((b) => b.removeAgent(agentId))
1374
1460
  }
1375
1461
 
1376
1462
  function getAgents(): AgentInfo[] {
package/src/logger.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Logger Interface for Digital Workers
3
+ *
4
+ * Provides a simple, extensible logging interface that can be injected
5
+ * into components for error logging and debugging. Consumers can provide
6
+ * their own logger implementation (e.g., winston, pino, console).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ /**
12
+ * Logger interface for structured logging
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // Using with console
17
+ * const consoleLogger: Logger = {
18
+ * debug: (msg, meta) => console.debug(msg, meta),
19
+ * info: (msg, meta) => console.info(msg, meta),
20
+ * warn: (msg, meta) => console.warn(msg, meta),
21
+ * error: (msg, error, meta) => console.error(msg, error, meta),
22
+ * }
23
+ *
24
+ * // Using with a custom logger
25
+ * const customLogger: Logger = {
26
+ * debug: (msg, meta) => myLogger.debug({ message: msg, ...meta }),
27
+ * info: (msg, meta) => myLogger.info({ message: msg, ...meta }),
28
+ * warn: (msg, meta) => myLogger.warn({ message: msg, ...meta }),
29
+ * error: (msg, error, meta) => myLogger.error({ message: msg, error, ...meta }),
30
+ * }
31
+ * ```
32
+ */
33
+ export interface Logger {
34
+ /**
35
+ * Log debug-level messages
36
+ * @param msg - The log message
37
+ * @param meta - Optional metadata object
38
+ */
39
+ debug(msg: string, meta?: object): void
40
+
41
+ /**
42
+ * Log info-level messages
43
+ * @param msg - The log message
44
+ * @param meta - Optional metadata object
45
+ */
46
+ info(msg: string, meta?: object): void
47
+
48
+ /**
49
+ * Log warning-level messages
50
+ * @param msg - The log message
51
+ * @param meta - Optional metadata object
52
+ */
53
+ warn(msg: string, meta?: object): void
54
+
55
+ /**
56
+ * Log error-level messages
57
+ * @param msg - The log message
58
+ * @param error - Optional error object with stack trace
59
+ * @param meta - Optional metadata object
60
+ */
61
+ error(msg: string, error?: Error, meta?: object): void
62
+ }
63
+
64
+ /**
65
+ * A no-op logger that discards all log messages.
66
+ * Used as a default when no logger is provided.
67
+ */
68
+ export const noopLogger: Logger = {
69
+ debug: () => {},
70
+ info: () => {},
71
+ warn: () => {},
72
+ error: () => {},
73
+ }
74
+
75
+ /**
76
+ * Creates a console-based logger for debugging purposes.
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * const transport = createSlackTransport({
81
+ * ...config,
82
+ * logger: createConsoleLogger(),
83
+ * })
84
+ * ```
85
+ */
86
+ export function createConsoleLogger(): Logger {
87
+ return {
88
+ debug: (msg, meta) => console.debug(`[DEBUG] ${msg}`, meta ?? ''),
89
+ info: (msg, meta) => console.info(`[INFO] ${msg}`, meta ?? ''),
90
+ warn: (msg, meta) => console.warn(`[WARN] ${msg}`, meta ?? ''),
91
+ error: (msg, error, meta) => console.error(`[ERROR] ${msg}`, error ?? '', meta ?? ''),
92
+ }
93
+ }
package/src/notify.ts CHANGED
@@ -1,7 +1,28 @@
1
1
  /**
2
2
  * Notification functionality for digital workers
3
+ *
4
+ * IMPORTANT: Real Channel Delivery vs LLM Generation
5
+ * ---------------------------------------------------
6
+ * This module sends real notifications via communication channels,
7
+ * NOT LLM-generated notification content.
8
+ *
9
+ * - `digital-workers.notify()` - Sends actual notifications to Workers
10
+ * via real channels (Slack, email, SMS, etc.) with delivery tracking.
11
+ *
12
+ * - ai-functions does not have an equivalent `notify` primitive since
13
+ * it focuses on LLM operations, not communication channel delivery.
14
+ *
15
+ * Use digital-workers.notify() when you need:
16
+ * - Real notification delivery to people/agents
17
+ * - Multi-channel delivery (Slack + SMS for urgent)
18
+ * - Delivery tracking and confirmation
19
+ * - Priority-based channel selection
20
+ * - Scheduled notifications
21
+ *
22
+ * @module
3
23
  */
4
24
 
25
+ import { generateRequestId } from './utils/id.js'
5
26
  import type {
6
27
  Worker,
7
28
  Team,
@@ -14,30 +35,39 @@ import type {
14
35
  } from './types.js'
15
36
 
16
37
  /**
17
- * Send a notification to a worker or team
38
+ * Send a notification to a Worker (Human or AI Agent) via communication channels.
39
+ *
40
+ * **Unique to digital-workers:**
41
+ * This function sends real notifications via actual communication channels
42
+ * (Slack, email, SMS, phone, etc.). There is no equivalent in ai-functions
43
+ * since ai-functions focuses on LLM operations rather than external delivery.
18
44
  *
19
- * Routes notifications through the specified channel(s), falling back
20
- * to the target's preferred channel if not specified.
45
+ * This is a **communication delivery primitive** for worker coordination.
21
46
  *
22
- * @param target - The worker or team to notify
47
+ * @param target - The worker or team to notify (routes to their configured channels)
23
48
  * @param message - The notification message
24
- * @param options - Notification options
25
- * @returns Promise resolving to notification result
49
+ * @param options - Notification options (channel, priority, metadata)
50
+ * @returns Promise resolving to NotifyResult with delivery status and metadata
26
51
  *
27
52
  * @example
28
53
  * ```ts
29
54
  * // Notify a worker via their preferred channel
30
- * await notify(alice, 'Deployment completed successfully')
55
+ * const result = await notify(alice, 'Deployment completed successfully')
56
+ * console.log(result.sent) // true
57
+ * console.log(result.via) // ['slack']
31
58
  *
32
59
  * // Notify via specific channel
33
60
  * await notify(alice, 'Urgent: Server down!', { via: 'slack' })
34
61
  *
35
- * // Notify via multiple channels
62
+ * // Notify via multiple channels (for urgent messages)
36
63
  * await notify(alice, 'Critical alert', { via: ['slack', 'sms'] })
37
64
  *
38
- * // Notify a team
65
+ * // Notify a team (reaches all members via their channels)
39
66
  * await notify(engineering, 'Sprint planning tomorrow', { via: 'slack' })
40
67
  * ```
68
+ *
69
+ * @see {@link notify.alert} for high-priority urgent notifications
70
+ * @see {@link notify.schedule} for delayed/scheduled notifications
41
71
  */
42
72
  export async function notify(
43
73
  target: ActionTarget,
@@ -66,7 +96,10 @@ export async function notify(
66
96
  const delivery = await Promise.all(
67
97
  channels.map(async (channel) => {
68
98
  try {
69
- await sendToChannel(channel, message, contacts, { priority, metadata })
99
+ await sendToChannel(channel, message, contacts, {
100
+ priority,
101
+ ...(metadata !== undefined && { metadata }),
102
+ })
70
103
  return { channel, status: 'sent' as const }
71
104
  } catch (error) {
72
105
  return {
@@ -286,18 +319,46 @@ async function sendToChannel(
286
319
  throw new Error(`No ${channel} contact configured`)
287
320
  }
288
321
 
289
- // In a real implementation, this would:
290
- // 1. Format the message for the channel
291
- // 2. Send via the appropriate API (Slack, SendGrid, Twilio, etc.)
292
- // 3. Handle delivery confirmation
322
+ // Import transport functions dynamically to avoid circular dependencies
323
+ const { channelToTransport, sendViaTransport, hasTransport, resolveAddress } = await import(
324
+ './transports.js'
325
+ )
326
+
327
+ const transport = channelToTransport(channel)
328
+ const address = resolveAddress(contacts, channel)
329
+
330
+ // If transport is registered, use it for real delivery
331
+ if (hasTransport(transport) && address) {
332
+ const payload = {
333
+ to: address.value,
334
+ body: message,
335
+ type: 'notification' as const,
336
+ priority: (options.priority || 'normal') as 'low' | 'normal' | 'high' | 'urgent',
337
+ ...(options.metadata !== undefined && { metadata: options.metadata }),
338
+ }
339
+
340
+ const result = await sendViaTransport(transport, payload)
341
+
342
+ if (!result.success) {
343
+ throw new Error(`Failed to send notification via ${transport}: ${result.error}`)
344
+ }
345
+ return
346
+ }
293
347
 
294
- // For now, simulate success
295
- await new Promise((resolve) => setTimeout(resolve, 10))
348
+ // No transport registered - log to console for development/testing
349
+ // This provides visibility into what would be sent
350
+ console.log(`[digital-workers] Notification (${channel}):`, {
351
+ to: typeof contact === 'string' ? contact : JSON.stringify(contact),
352
+ message,
353
+ priority: options.priority || 'normal',
354
+ metadata: options.metadata,
355
+ note: `No transport registered for '${transport}'. Register a transport handler to enable real delivery.`,
356
+ })
296
357
  }
297
358
 
298
359
  /**
299
360
  * Generate a unique message ID
300
361
  */
301
362
  function generateMessageId(prefix = 'msg'): string {
302
- return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
363
+ return generateRequestId(prefix)
303
364
  }