ai-workflows 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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +8 -1
  3. package/README.md +2 -0
  4. package/dist/barrier.d.ts +6 -0
  5. package/dist/barrier.d.ts.map +1 -1
  6. package/dist/barrier.js +45 -7
  7. package/dist/barrier.js.map +1 -1
  8. package/dist/cascade-context.d.ts.map +1 -1
  9. package/dist/cascade-context.js +25 -25
  10. package/dist/cascade-context.js.map +1 -1
  11. package/dist/cascade-executor.d.ts.map +1 -1
  12. package/dist/cascade-executor.js +1 -1
  13. package/dist/cascade-executor.js.map +1 -1
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/context.js +23 -7
  16. package/dist/context.js.map +1 -1
  17. package/dist/cron-parser.d.ts +65 -0
  18. package/dist/cron-parser.d.ts.map +1 -0
  19. package/dist/cron-parser.js +294 -0
  20. package/dist/cron-parser.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +117 -0
  22. package/dist/cron-scheduler.d.ts.map +1 -0
  23. package/dist/cron-scheduler.js +176 -0
  24. package/dist/cron-scheduler.js.map +1 -0
  25. package/dist/database-context.d.ts +184 -0
  26. package/dist/database-context.d.ts.map +1 -0
  27. package/dist/database-context.js +428 -0
  28. package/dist/database-context.js.map +1 -0
  29. package/dist/digital-objects-adapter.d.ts +159 -0
  30. package/dist/digital-objects-adapter.d.ts.map +1 -0
  31. package/dist/digital-objects-adapter.js +229 -0
  32. package/dist/digital-objects-adapter.js.map +1 -0
  33. package/dist/durable-execution-cloudflare.d.ts +427 -0
  34. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  35. package/dist/durable-execution-cloudflare.js +510 -0
  36. package/dist/durable-execution-cloudflare.js.map +1 -0
  37. package/dist/durable-execution.d.ts +482 -0
  38. package/dist/durable-execution.d.ts.map +1 -0
  39. package/dist/durable-execution.js +594 -0
  40. package/dist/durable-execution.js.map +1 -0
  41. package/dist/durable-workflow.d.ts +176 -0
  42. package/dist/durable-workflow.d.ts.map +1 -0
  43. package/dist/durable-workflow.js +552 -0
  44. package/dist/durable-workflow.js.map +1 -0
  45. package/dist/graph/topological-sort.d.ts.map +1 -1
  46. package/dist/graph/topological-sort.js +5 -5
  47. package/dist/graph/topological-sort.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +15 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/logger.d.ts +101 -0
  53. package/dist/logger.d.ts.map +1 -0
  54. package/dist/logger.js +115 -0
  55. package/dist/logger.js.map +1 -0
  56. package/dist/on.d.ts.map +1 -1
  57. package/dist/on.js +3 -3
  58. package/dist/on.js.map +1 -1
  59. package/dist/runtime.d.ts +169 -0
  60. package/dist/runtime.d.ts.map +1 -0
  61. package/dist/runtime.js +275 -0
  62. package/dist/runtime.js.map +1 -0
  63. package/dist/send.d.ts.map +1 -1
  64. package/dist/send.js +4 -3
  65. package/dist/send.js.map +1 -1
  66. package/dist/telemetry.d.ts +150 -0
  67. package/dist/telemetry.d.ts.map +1 -0
  68. package/dist/telemetry.js +388 -0
  69. package/dist/telemetry.js.map +1 -0
  70. package/dist/timer-registry.d.ts +25 -0
  71. package/dist/timer-registry.d.ts.map +1 -1
  72. package/dist/timer-registry.js +42 -8
  73. package/dist/timer-registry.js.map +1 -1
  74. package/dist/types.d.ts +17 -6
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +1 -1
  77. package/dist/types.js.map +1 -1
  78. package/dist/worker/durable-step.d.ts +481 -0
  79. package/dist/worker/durable-step.d.ts.map +1 -0
  80. package/dist/worker/durable-step.js +606 -0
  81. package/dist/worker/durable-step.js.map +1 -0
  82. package/dist/worker/index.d.ts +106 -0
  83. package/dist/worker/index.d.ts.map +1 -0
  84. package/dist/worker/index.js +124 -0
  85. package/dist/worker/index.js.map +1 -0
  86. package/dist/worker/state-adapter.d.ts +230 -0
  87. package/dist/worker/state-adapter.d.ts.map +1 -0
  88. package/dist/worker/state-adapter.js +409 -0
  89. package/dist/worker/state-adapter.js.map +1 -0
  90. package/dist/worker/topological-executor.d.ts +282 -0
  91. package/dist/worker/topological-executor.d.ts.map +1 -0
  92. package/dist/worker/topological-executor.js +396 -0
  93. package/dist/worker/topological-executor.js.map +1 -0
  94. package/dist/worker/workflow-builder.d.ts +286 -0
  95. package/dist/worker/workflow-builder.d.ts.map +1 -0
  96. package/dist/worker/workflow-builder.js +565 -0
  97. package/dist/worker/workflow-builder.js.map +1 -0
  98. package/dist/worker.d.ts +800 -0
  99. package/dist/worker.d.ts.map +1 -0
  100. package/dist/worker.js +2428 -0
  101. package/dist/worker.js.map +1 -0
  102. package/dist/workflow-builder.d.ts +287 -0
  103. package/dist/workflow-builder.d.ts.map +1 -0
  104. package/dist/workflow-builder.js +762 -0
  105. package/dist/workflow-builder.js.map +1 -0
  106. package/dist/workflow.d.ts +14 -30
  107. package/dist/workflow.d.ts.map +1 -1
  108. package/dist/workflow.js +132 -292
  109. package/dist/workflow.js.map +1 -1
  110. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  111. package/examples/02-content-moderation-cascade.ts +454 -0
  112. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  113. package/examples/04-database-persistence.ts +518 -0
  114. package/examples/README.md +173 -0
  115. package/package.json +30 -13
  116. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  117. package/src/__tests__/durable-workflow.test.ts +297 -0
  118. package/src/barrier.ts +48 -7
  119. package/src/cascade-context.ts +36 -29
  120. package/src/cascade-executor.ts +3 -2
  121. package/src/context.ts +41 -12
  122. package/src/cron-parser.ts +347 -0
  123. package/src/cron-scheduler.ts +239 -0
  124. package/src/database-context.ts +658 -0
  125. package/src/digital-objects-adapter.ts +351 -0
  126. package/src/durable-execution-cloudflare.ts +855 -0
  127. package/src/durable-execution.ts +1042 -0
  128. package/src/durable-workflow.ts +717 -0
  129. package/src/graph/topological-sort.ts +6 -8
  130. package/src/index.ts +69 -0
  131. package/src/logger.ts +148 -0
  132. package/src/on.ts +8 -9
  133. package/src/runtime.ts +436 -0
  134. package/src/send.ts +4 -5
  135. package/src/telemetry.ts +577 -0
  136. package/src/timer-registry.ts +44 -10
  137. package/src/types.ts +32 -17
  138. package/src/worker/durable-step.ts +976 -0
  139. package/src/worker/index.ts +216 -0
  140. package/src/worker/state-adapter.ts +589 -0
  141. package/src/worker/topological-executor.ts +625 -0
  142. package/src/worker/workflow-builder.ts +871 -0
  143. package/src/worker.ts +2906 -0
  144. package/src/workflow-builder.ts +1068 -0
  145. package/src/workflow.ts +188 -351
  146. package/test/barrier-join.test.ts +32 -24
  147. package/test/cascade-executor.test.ts +9 -16
  148. package/test/cron-parser.test.ts +314 -0
  149. package/test/cron-scheduler.test.ts +291 -0
  150. package/test/database-context.test.ts +770 -0
  151. package/test/db-provider-adapter.test.ts +862 -0
  152. package/test/durable-execution-cloudflare.test.ts +606 -0
  153. package/test/durable-execution-in-process.test.ts +286 -0
  154. package/test/durable-execution.test.ts +247 -0
  155. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  156. package/test/integration.test.ts +442 -0
  157. package/test/rpc-surface.test.ts +946 -0
  158. package/test/runtime.test.ts +262 -0
  159. package/test/schedule-timer-cleanup.test.ts +30 -21
  160. package/test/send-race-conditions.test.ts +30 -40
  161. package/test/worker/durable-cascade.test.ts +1117 -0
  162. package/test/worker/durable-step.test.ts +723 -0
  163. package/test/worker/topological-executor.test.ts +1240 -0
  164. package/test/worker/workflow-builder.test.ts +1067 -0
  165. package/test/worker.test.ts +608 -0
  166. package/test/workflow-builder.test.ts +1670 -0
  167. package/test/workflow-cron.test.ts +256 -0
  168. package/test/workflow-state-adapter.test.ts +923 -0
  169. package/test/workflow.test.ts +25 -22
  170. package/tsconfig.json +3 -1
  171. package/vitest.config.ts +38 -1
  172. package/vitest.workers.config.ts +44 -0
  173. package/wrangler.jsonc +22 -0
  174. package/.turbo/turbo-test.log +0 -169
  175. package/LICENSE +0 -21
  176. package/src/context.js +0 -83
  177. package/src/every.js +0 -267
  178. package/src/index.js +0 -71
  179. package/src/on.js +0 -79
  180. package/src/send.js +0 -111
  181. package/src/types.js +0 -4
  182. package/src/workflow.js +0 -455
  183. package/test/context.test.js +0 -116
  184. package/test/every.test.js +0 -282
  185. package/test/on.test.js +0 -80
  186. package/test/send.test.js +0 -89
  187. package/test/workflow.test.js +0 -224
  188. package/vitest.config.js +0 -7
@@ -0,0 +1,577 @@
1
+ /**
2
+ * OpenTelemetry Integration for ai-workflows
3
+ *
4
+ * Provides instrumented wrappers and telemetry utilities for workflow execution.
5
+ * Integrates with cascade-context for distributed tracing support.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { Workflow } from 'ai-workflows'
10
+ * import { withWorkflowTelemetry, instrumentWorkflow } from 'ai-workflows/telemetry'
11
+ *
12
+ * // Enable telemetry globally
13
+ * withWorkflowTelemetry({ provider: createConsoleTelemetryProvider() }, async () => {
14
+ * const workflow = Workflow($ => {
15
+ * $.on.Customer.created(async (customer) => {
16
+ * // Traced automatically
17
+ * })
18
+ * })
19
+ * await workflow.start()
20
+ * })
21
+ * ```
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+
26
+ import {
27
+ getTracer,
28
+ getMeter,
29
+ getLogger,
30
+ setTelemetryProvider,
31
+ getTelemetryProvider,
32
+ createHandlerMetrics,
33
+ SemanticAttributes,
34
+ MetricNames,
35
+ createTraceparent,
36
+ parseTraceparent,
37
+ generateTraceId,
38
+ generateSpanId,
39
+ type Tracer,
40
+ type Meter,
41
+ type Logger,
42
+ type Span,
43
+ type SpanAttributes,
44
+ type TelemetryProvider,
45
+ type TraceContext,
46
+ type Counter,
47
+ type Histogram,
48
+ } from '@org.ai/types'
49
+
50
+ import type { CascadeContext, CascadeStep } from './cascade-context.js'
51
+
52
+ // Package info
53
+ const PACKAGE_NAME = 'ai-workflows'
54
+ const PACKAGE_VERSION = '2.1.4'
55
+
56
+ // ============================================================================
57
+ // Package-level Telemetry
58
+ // ============================================================================
59
+
60
+ let packageTracer: Tracer | undefined
61
+ let packageMeter: Meter | undefined
62
+ let packageLogger: Logger | undefined
63
+ let workflowMetrics: ReturnType<typeof createWorkflowMetrics> | undefined
64
+
65
+ /**
66
+ * Get the tracer for ai-workflows
67
+ */
68
+ export function getWorkflowTracer(): Tracer {
69
+ if (!packageTracer) {
70
+ packageTracer = getTracer(PACKAGE_NAME, PACKAGE_VERSION)
71
+ }
72
+ return packageTracer
73
+ }
74
+
75
+ /**
76
+ * Get the meter for ai-workflows
77
+ */
78
+ export function getWorkflowMeter(): Meter {
79
+ if (!packageMeter) {
80
+ packageMeter = getMeter(PACKAGE_NAME, PACKAGE_VERSION)
81
+ }
82
+ return packageMeter
83
+ }
84
+
85
+ /**
86
+ * Get the logger for ai-workflows
87
+ */
88
+ export function getWorkflowLogger(): Logger {
89
+ if (!packageLogger) {
90
+ packageLogger = getLogger(PACKAGE_NAME)
91
+ }
92
+ return packageLogger
93
+ }
94
+
95
+ /**
96
+ * Create workflow-specific metrics
97
+ */
98
+ export function createWorkflowMetrics(meter: Meter) {
99
+ return {
100
+ stepDuration: meter.createHistogram(
101
+ MetricNames.WORKFLOW_STEP_DURATION,
102
+ 'Duration of workflow steps',
103
+ 'ms'
104
+ ),
105
+ stepTotal: meter.createCounter(
106
+ MetricNames.WORKFLOW_STEP_TOTAL,
107
+ 'Total number of workflow steps executed'
108
+ ),
109
+ stepErrors: meter.createCounter(
110
+ MetricNames.WORKFLOW_STEP_ERRORS,
111
+ 'Number of failed workflow steps'
112
+ ),
113
+ eventHandlers: meter.createCounter('workflow.event.handlers', 'Event handler invocations'),
114
+ scheduleHandlers: meter.createCounter(
115
+ 'workflow.schedule.handlers',
116
+ 'Schedule handler invocations'
117
+ ),
118
+ cascadeDepth: meter.createHistogram('workflow.cascade.depth', 'Cascade context depth'),
119
+ cascadeDuration: meter.createHistogram(
120
+ 'workflow.cascade.duration',
121
+ 'Total cascade execution duration',
122
+ 'ms'
123
+ ),
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get workflow metrics for the package
129
+ */
130
+ export function getWorkflowMetrics() {
131
+ if (!workflowMetrics) {
132
+ workflowMetrics = createWorkflowMetrics(getWorkflowMeter())
133
+ }
134
+ return workflowMetrics
135
+ }
136
+
137
+ /**
138
+ * Reset cached telemetry instances
139
+ */
140
+ export function resetTelemetry(): void {
141
+ packageTracer = undefined
142
+ packageMeter = undefined
143
+ packageLogger = undefined
144
+ workflowMetrics = undefined
145
+ }
146
+
147
+ // ============================================================================
148
+ // Telemetry Context
149
+ // ============================================================================
150
+
151
+ /**
152
+ * Options for withWorkflowTelemetry
153
+ */
154
+ export interface WithWorkflowTelemetryOptions {
155
+ /** Telemetry provider to use */
156
+ provider?: TelemetryProvider
157
+ /** Workflow name for labeling */
158
+ workflowName?: string
159
+ }
160
+
161
+ /**
162
+ * Execute a function with workflow telemetry enabled
163
+ */
164
+ export async function withWorkflowTelemetry<T>(
165
+ options: WithWorkflowTelemetryOptions,
166
+ fn: () => Promise<T>
167
+ ): Promise<T> {
168
+ const previousProvider = getTelemetryProvider()
169
+
170
+ if (options.provider) {
171
+ setTelemetryProvider(options.provider)
172
+ resetTelemetry()
173
+ }
174
+
175
+ try {
176
+ return await fn()
177
+ } finally {
178
+ if (options.provider) {
179
+ setTelemetryProvider(previousProvider)
180
+ resetTelemetry()
181
+ }
182
+ }
183
+ }
184
+
185
+ // ============================================================================
186
+ // Cascade Context Integration
187
+ // ============================================================================
188
+
189
+ /**
190
+ * Create a span from a CascadeContext
191
+ *
192
+ * This bridges the cascade-context W3C Trace Context support with OpenTelemetry.
193
+ */
194
+ export function spanFromCascadeContext(ctx: CascadeContext, name?: string): Span {
195
+ const tracer = getWorkflowTracer()
196
+ const traceContext = ctx.toTraceContext()
197
+
198
+ return tracer.startSpan(name || ctx.name || 'cascade', {
199
+ kind: 'internal',
200
+ parent: traceContext,
201
+ attributes: {
202
+ [SemanticAttributes.WORKFLOW_NAME]: ctx.name || 'cascade',
203
+ 'cascade.correlationId': ctx.correlationId,
204
+ 'cascade.spanId': ctx.spanId,
205
+ 'cascade.depth': ctx.depth,
206
+ ...(ctx.parentId && { 'cascade.parentId': ctx.parentId }),
207
+ },
208
+ })
209
+ }
210
+
211
+ /**
212
+ * Create a TraceContext from a span
213
+ *
214
+ * Allows propagating span context to cascade-context.
215
+ */
216
+ export function traceContextFromSpan(span: Span): TraceContext {
217
+ return span.getTraceContext()
218
+ }
219
+
220
+ /**
221
+ * Record a CascadeStep as a span event
222
+ */
223
+ export function recordStepAsSpanEvent(span: Span, step: CascadeStep): void {
224
+ const attrs: SpanAttributes = {
225
+ 'step.status': step.status,
226
+ ...(step.duration !== undefined && { 'step.duration': step.duration }),
227
+ ...(step.metadata &&
228
+ Object.keys(step.metadata).length > 0 && { 'step.metadata': JSON.stringify(step.metadata) }),
229
+ }
230
+
231
+ span.addEvent(step.name, attrs)
232
+ }
233
+
234
+ /**
235
+ * Convert CascadeContext steps to span events
236
+ */
237
+ export function cascadeStepsToSpanEvents(span: Span, ctx: CascadeContext): void {
238
+ for (const step of ctx.steps) {
239
+ recordStepAsSpanEvent(span, step)
240
+ }
241
+ }
242
+
243
+ // ============================================================================
244
+ // Event Handler Instrumentation
245
+ // ============================================================================
246
+
247
+ /**
248
+ * Record an event handler invocation
249
+ */
250
+ export function recordEventHandler(params: {
251
+ event: string
252
+ durationMs: number
253
+ success: boolean
254
+ }): void {
255
+ const metrics = getWorkflowMetrics()
256
+ const labels = {
257
+ event: params.event,
258
+ status: params.success ? 'success' : 'error',
259
+ }
260
+
261
+ metrics.eventHandlers.add(1, labels)
262
+ metrics.stepDuration.record(params.durationMs, { ...labels, type: 'event' })
263
+ metrics.stepTotal.add(1, { ...labels, type: 'event' })
264
+
265
+ if (!params.success) {
266
+ metrics.stepErrors.add(1, { ...labels, type: 'event' })
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Record a schedule handler invocation
272
+ */
273
+ export function recordScheduleHandler(params: {
274
+ schedule: string
275
+ durationMs: number
276
+ success: boolean
277
+ }): void {
278
+ const metrics = getWorkflowMetrics()
279
+ const labels = {
280
+ schedule: params.schedule,
281
+ status: params.success ? 'success' : 'error',
282
+ }
283
+
284
+ metrics.scheduleHandlers.add(1, labels)
285
+ metrics.stepDuration.record(params.durationMs, { ...labels, type: 'schedule' })
286
+ metrics.stepTotal.add(1, { ...labels, type: 'schedule' })
287
+
288
+ if (!params.success) {
289
+ metrics.stepErrors.add(1, { ...labels, type: 'schedule' })
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Create a traced event handler
295
+ */
296
+ export function tracedEventHandler<TData, TResult>(
297
+ event: string,
298
+ handler: (data: TData, ctx: any) => Promise<TResult>
299
+ ): (data: TData, ctx: any) => Promise<TResult> {
300
+ return async (data: TData, ctx: any): Promise<TResult> => {
301
+ const tracer = getWorkflowTracer()
302
+ const logger = getWorkflowLogger()
303
+ const startTime = Date.now()
304
+
305
+ return tracer.withSpan(
306
+ `workflow.event.${event}`,
307
+ {
308
+ kind: 'consumer',
309
+ attributes: {
310
+ [SemanticAttributes.WORKFLOW_NAME]: event,
311
+ 'workflow.event': event,
312
+ },
313
+ },
314
+ async (span) => {
315
+ logger.info(`Event handler ${event} started`)
316
+
317
+ try {
318
+ const result = await handler(data, ctx)
319
+ const durationMs = Date.now() - startTime
320
+
321
+ recordEventHandler({
322
+ event,
323
+ durationMs,
324
+ success: true,
325
+ })
326
+
327
+ span.setStatus('ok')
328
+ logger.info(`Event handler ${event} completed`, { durationMs })
329
+
330
+ return result
331
+ } catch (error) {
332
+ const durationMs = Date.now() - startTime
333
+ const message = error instanceof Error ? error.message : String(error)
334
+
335
+ recordEventHandler({
336
+ event,
337
+ durationMs,
338
+ success: false,
339
+ })
340
+
341
+ span.setStatus('error', message)
342
+ logger.error(
343
+ `Event handler ${event} failed`,
344
+ error instanceof Error ? error : undefined,
345
+ { durationMs }
346
+ )
347
+
348
+ throw error
349
+ }
350
+ }
351
+ ) as Promise<TResult>
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Create a traced schedule handler
357
+ */
358
+ export function tracedScheduleHandler<TResult>(
359
+ schedule: string,
360
+ handler: (ctx: any) => Promise<TResult>
361
+ ): (ctx: any) => Promise<TResult> {
362
+ return async (ctx: any): Promise<TResult> => {
363
+ const tracer = getWorkflowTracer()
364
+ const logger = getWorkflowLogger()
365
+ const startTime = Date.now()
366
+
367
+ return tracer.withSpan(
368
+ `workflow.schedule.${schedule}`,
369
+ {
370
+ kind: 'internal',
371
+ attributes: {
372
+ [SemanticAttributes.WORKFLOW_NAME]: schedule,
373
+ 'workflow.schedule': schedule,
374
+ },
375
+ },
376
+ async (span) => {
377
+ logger.info(`Schedule handler ${schedule} started`)
378
+
379
+ try {
380
+ const result = await handler(ctx)
381
+ const durationMs = Date.now() - startTime
382
+
383
+ recordScheduleHandler({
384
+ schedule,
385
+ durationMs,
386
+ success: true,
387
+ })
388
+
389
+ span.setStatus('ok')
390
+ logger.info(`Schedule handler ${schedule} completed`, { durationMs })
391
+
392
+ return result
393
+ } catch (error) {
394
+ const durationMs = Date.now() - startTime
395
+ const message = error instanceof Error ? error.message : String(error)
396
+
397
+ recordScheduleHandler({
398
+ schedule,
399
+ durationMs,
400
+ success: false,
401
+ })
402
+
403
+ span.setStatus('error', message)
404
+ logger.error(
405
+ `Schedule handler ${schedule} failed`,
406
+ error instanceof Error ? error : undefined,
407
+ { durationMs }
408
+ )
409
+
410
+ throw error
411
+ }
412
+ }
413
+ ) as Promise<TResult>
414
+ }
415
+ }
416
+
417
+ // ============================================================================
418
+ // Workflow Step Instrumentation
419
+ // ============================================================================
420
+
421
+ /**
422
+ * Record a workflow step
423
+ */
424
+ export function recordWorkflowStep(params: {
425
+ step: string
426
+ workflow?: string | undefined
427
+ durationMs: number
428
+ success: boolean
429
+ tier?: string | undefined
430
+ }): void {
431
+ const metrics = getWorkflowMetrics()
432
+ const labels = {
433
+ step: params.step,
434
+ workflow: params.workflow || 'default',
435
+ status: params.success ? 'success' : 'error',
436
+ ...(params.tier && { tier: params.tier }),
437
+ }
438
+
439
+ metrics.stepTotal.add(1, labels)
440
+ metrics.stepDuration.record(params.durationMs, labels)
441
+
442
+ if (!params.success) {
443
+ metrics.stepErrors.add(1, labels)
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Create a traced workflow step
449
+ */
450
+ export function tracedStep<TArgs extends unknown[], TResult>(
451
+ stepName: string,
452
+ fn: (...args: TArgs) => Promise<TResult>,
453
+ options: {
454
+ workflowName?: string
455
+ tier?: string
456
+ } = {}
457
+ ): (...args: TArgs) => Promise<TResult> {
458
+ return async (...args: TArgs): Promise<TResult> => {
459
+ const tracer = getWorkflowTracer()
460
+ const logger = getWorkflowLogger()
461
+ const startTime = Date.now()
462
+
463
+ const attributes: SpanAttributes = {
464
+ [SemanticAttributes.WORKFLOW_STEP]: stepName,
465
+ ...(options.workflowName && { [SemanticAttributes.WORKFLOW_NAME]: options.workflowName }),
466
+ ...(options.tier && { [SemanticAttributes.AI_TIER]: options.tier }),
467
+ }
468
+
469
+ return tracer.withSpan(
470
+ `workflow.step.${stepName}`,
471
+ { kind: 'internal', attributes },
472
+ async (span) => {
473
+ logger.debug(`Step ${stepName} started`, {
474
+ workflow: options.workflowName,
475
+ tier: options.tier,
476
+ })
477
+
478
+ try {
479
+ const result = await fn(...args)
480
+ const durationMs = Date.now() - startTime
481
+
482
+ recordWorkflowStep({
483
+ step: stepName,
484
+ workflow: options.workflowName,
485
+ durationMs,
486
+ success: true,
487
+ tier: options.tier,
488
+ })
489
+
490
+ span.setStatus('ok')
491
+ return result
492
+ } catch (error) {
493
+ const durationMs = Date.now() - startTime
494
+ const message = error instanceof Error ? error.message : String(error)
495
+
496
+ recordWorkflowStep({
497
+ step: stepName,
498
+ workflow: options.workflowName,
499
+ durationMs,
500
+ success: false,
501
+ tier: options.tier,
502
+ })
503
+
504
+ span.setStatus('error', message)
505
+ throw error
506
+ }
507
+ }
508
+ ) as Promise<TResult>
509
+ }
510
+ }
511
+
512
+ // ============================================================================
513
+ // Span Helpers
514
+ // ============================================================================
515
+
516
+ /**
517
+ * Start a span for a workflow operation
518
+ */
519
+ export function startWorkflowSpan(name: string, attributes?: SpanAttributes): Span {
520
+ const tracer = getWorkflowTracer()
521
+ return tracer.startSpan(name, {
522
+ kind: 'internal',
523
+ attributes: {
524
+ [SemanticAttributes.SERVICE_NAME]: PACKAGE_NAME,
525
+ ...attributes,
526
+ },
527
+ })
528
+ }
529
+
530
+ /**
531
+ * Execute code within a workflow span
532
+ */
533
+ export async function withWorkflowSpan<T>(
534
+ name: string,
535
+ fn: (span: Span) => Promise<T>,
536
+ attributes?: SpanAttributes
537
+ ): Promise<T> {
538
+ const tracer = getWorkflowTracer()
539
+ return tracer.withSpan(name, { kind: 'internal', attributes }, fn) as Promise<T>
540
+ }
541
+
542
+ // ============================================================================
543
+ // Re-exports from @org.ai/types
544
+ // ============================================================================
545
+
546
+ export {
547
+ // Core types
548
+ type Tracer,
549
+ type Meter,
550
+ type Logger,
551
+ type Span,
552
+ type SpanAttributes,
553
+ type TelemetryProvider,
554
+ type TraceContext,
555
+ type Counter,
556
+ type Histogram,
557
+
558
+ // Global functions
559
+ getTracer,
560
+ getMeter,
561
+ getLogger,
562
+ setTelemetryProvider,
563
+ getTelemetryProvider,
564
+
565
+ // W3C Trace Context
566
+ createTraceparent,
567
+ parseTraceparent,
568
+ generateTraceId,
569
+ generateSpanId,
570
+
571
+ // Constants
572
+ SemanticAttributes,
573
+ MetricNames,
574
+
575
+ // Utilities
576
+ createHandlerMetrics,
577
+ } from '@org.ai/types'
@@ -110,23 +110,57 @@ export const timerRegistry = {
110
110
  getAll: () => Array.from(activeTimers.entries()),
111
111
  }
112
112
 
113
- // Register global cleanup functions immediately on module load
114
- // Register on both globalThis and global for maximum compatibility
113
+ // Global registration is opt-in to avoid namespace pollution
115
114
  declare const global: typeof globalThis
116
115
 
116
+ let globalRegistrationEnabled = false
117
+
117
118
  function registerGlobalFunctions(target: typeof globalThis) {
118
- (target as unknown as Record<string, unknown>).getActiveWorkflowTimerCount = getActiveTimerCount;
119
- (target as unknown as Record<string, unknown>).clearAllWorkflowTimers = clearAllTimers
119
+ ;(target as unknown as Record<string, unknown>)['getActiveWorkflowTimerCount'] =
120
+ getActiveTimerCount
121
+ ;(target as unknown as Record<string, unknown>)['clearAllWorkflowTimers'] = clearAllTimers
120
122
  }
121
123
 
122
- // Register on globalThis (standard)
123
- if (typeof globalThis !== 'undefined') {
124
- registerGlobalFunctions(globalThis)
124
+ /**
125
+ * Enable global timer registry functions.
126
+ *
127
+ * This opt-in function registers `getActiveWorkflowTimerCount` and `clearAllWorkflowTimers`
128
+ * on the global scope for debugging and cleanup purposes.
129
+ *
130
+ * Call this function explicitly if you need global access to timer management:
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * import { enableGlobalTimerRegistry } from 'ai-workflows'
135
+ *
136
+ * // Enable global registration
137
+ * enableGlobalTimerRegistry()
138
+ *
139
+ * // Now these are available globally:
140
+ * // globalThis.getActiveWorkflowTimerCount()
141
+ * // globalThis.clearAllWorkflowTimers()
142
+ * ```
143
+ */
144
+ export function enableGlobalTimerRegistry(): void {
145
+ if (globalRegistrationEnabled) return
146
+ globalRegistrationEnabled = true
147
+
148
+ // Register on globalThis (standard)
149
+ if (typeof globalThis !== 'undefined') {
150
+ registerGlobalFunctions(globalThis)
151
+ }
152
+
153
+ // Also register on global (Node.js specific, used in some test environments)
154
+ if (typeof global !== 'undefined' && global !== globalThis) {
155
+ registerGlobalFunctions(global)
156
+ }
125
157
  }
126
158
 
127
- // Also register on global (Node.js specific, used in some test environments)
128
- if (typeof global !== 'undefined' && global !== globalThis) {
129
- registerGlobalFunctions(global)
159
+ /**
160
+ * Check if global timer registry is enabled
161
+ */
162
+ export function isGlobalTimerRegistryEnabled(): boolean {
163
+ return globalRegistrationEnabled
130
164
  }
131
165
 
132
166
  // Register process exit handlers for cleanup