duron 0.3.0-beta.8 → 0.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 (91) hide show
  1. package/dist/action-job.d.ts +33 -2
  2. package/dist/action-job.d.ts.map +1 -1
  3. package/dist/action-job.js +93 -26
  4. package/dist/action-manager.d.ts +44 -2
  5. package/dist/action-manager.d.ts.map +1 -1
  6. package/dist/action-manager.js +64 -3
  7. package/dist/action.d.ts +388 -7
  8. package/dist/action.d.ts.map +1 -1
  9. package/dist/action.js +44 -23
  10. package/dist/adapters/adapter.d.ts +365 -8
  11. package/dist/adapters/adapter.d.ts.map +1 -1
  12. package/dist/adapters/adapter.js +221 -15
  13. package/dist/adapters/postgres/base.d.ts +184 -6
  14. package/dist/adapters/postgres/base.d.ts.map +1 -1
  15. package/dist/adapters/postgres/base.js +436 -75
  16. package/dist/adapters/postgres/pglite.d.ts +37 -0
  17. package/dist/adapters/postgres/pglite.d.ts.map +1 -1
  18. package/dist/adapters/postgres/pglite.js +38 -0
  19. package/dist/adapters/postgres/postgres.d.ts +35 -0
  20. package/dist/adapters/postgres/postgres.d.ts.map +1 -1
  21. package/dist/adapters/postgres/postgres.js +42 -0
  22. package/dist/adapters/postgres/schema.d.ts +150 -37
  23. package/dist/adapters/postgres/schema.d.ts.map +1 -1
  24. package/dist/adapters/postgres/schema.default.d.ts +151 -38
  25. package/dist/adapters/postgres/schema.default.d.ts.map +1 -1
  26. package/dist/adapters/postgres/schema.default.js +2 -2
  27. package/dist/adapters/postgres/schema.js +60 -23
  28. package/dist/adapters/schemas.d.ts +124 -80
  29. package/dist/adapters/schemas.d.ts.map +1 -1
  30. package/dist/adapters/schemas.js +139 -26
  31. package/dist/client.d.ts +426 -22
  32. package/dist/client.d.ts.map +1 -1
  33. package/dist/client.js +370 -20
  34. package/dist/constants.js +6 -0
  35. package/dist/errors.d.ts +166 -9
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +189 -19
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/server.d.ts +99 -37
  41. package/dist/server.d.ts.map +1 -1
  42. package/dist/server.js +84 -25
  43. package/dist/step-manager.d.ts +111 -4
  44. package/dist/step-manager.d.ts.map +1 -1
  45. package/dist/step-manager.js +411 -75
  46. package/dist/telemetry/index.d.ts +1 -4
  47. package/dist/telemetry/index.d.ts.map +1 -1
  48. package/dist/telemetry/index.js +2 -4
  49. package/dist/telemetry/local-span-exporter.d.ts +56 -0
  50. package/dist/telemetry/local-span-exporter.d.ts.map +1 -0
  51. package/dist/telemetry/local-span-exporter.js +118 -0
  52. package/dist/utils/p-retry.d.ts +5 -0
  53. package/dist/utils/p-retry.d.ts.map +1 -1
  54. package/dist/utils/p-retry.js +8 -0
  55. package/dist/utils/wait-for-abort.d.ts +1 -0
  56. package/dist/utils/wait-for-abort.d.ts.map +1 -1
  57. package/dist/utils/wait-for-abort.js +1 -0
  58. package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260121160012_normal_bloodstrike}/migration.sql +32 -20
  59. package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260121160012_normal_bloodstrike}/snapshot.json +241 -66
  60. package/package.json +42 -26
  61. package/src/action-job.ts +43 -32
  62. package/src/action-manager.ts +5 -5
  63. package/src/action.ts +317 -149
  64. package/src/adapters/adapter.ts +54 -54
  65. package/src/adapters/postgres/base.ts +266 -86
  66. package/src/adapters/postgres/schema.default.ts +2 -2
  67. package/src/adapters/postgres/schema.ts +52 -24
  68. package/src/adapters/schemas.ts +91 -36
  69. package/src/client.ts +322 -68
  70. package/src/errors.ts +141 -30
  71. package/src/index.ts +2 -0
  72. package/src/server.ts +39 -37
  73. package/src/step-manager.ts +254 -91
  74. package/src/telemetry/index.ts +2 -20
  75. package/src/telemetry/local-span-exporter.ts +148 -0
  76. package/dist/telemetry/adapter.d.ts +0 -107
  77. package/dist/telemetry/adapter.d.ts.map +0 -1
  78. package/dist/telemetry/adapter.js +0 -134
  79. package/dist/telemetry/local.d.ts +0 -22
  80. package/dist/telemetry/local.d.ts.map +0 -1
  81. package/dist/telemetry/local.js +0 -243
  82. package/dist/telemetry/noop.d.ts +0 -17
  83. package/dist/telemetry/noop.d.ts.map +0 -1
  84. package/dist/telemetry/noop.js +0 -66
  85. package/dist/telemetry/opentelemetry.d.ts +0 -25
  86. package/dist/telemetry/opentelemetry.d.ts.map +0 -1
  87. package/dist/telemetry/opentelemetry.js +0 -312
  88. package/src/telemetry/adapter.ts +0 -642
  89. package/src/telemetry/local.ts +0 -429
  90. package/src/telemetry/noop.ts +0 -141
  91. package/src/telemetry/opentelemetry.ts +0 -453
@@ -1,3 +1,13 @@
1
+ import {
2
+ type Context,
3
+ context,
4
+ type Span,
5
+ SpanKind,
6
+ type SpanOptions,
7
+ SpanStatusCode,
8
+ type Tracer,
9
+ trace,
10
+ } from '@opentelemetry/api'
1
11
  import fastq from 'fastq'
2
12
  import type { Logger } from 'pino'
3
13
  import type { z } from 'zod'
@@ -17,51 +27,125 @@ import {
17
27
  ActionCancelError,
18
28
  isCancelError,
19
29
  isNonRetriableError,
30
+ isTimeoutError,
20
31
  NonRetriableError,
21
32
  StepAlreadyExecutedError,
22
33
  StepTimeoutError,
23
34
  serializeError,
24
35
  UnhandledChildStepsError,
25
36
  } from './errors.js'
26
- import type { ObserveContext, Span, TelemetryAdapter, Tracer, TracerSpan } from './telemetry/adapter.js'
27
37
 
28
- // ============================================================================
29
- // Noop Tracer (for fallback observe context)
30
- // ============================================================================
38
+ /**
39
+ * Telemetry context provided to action and step handlers.
40
+ * Provides access to OpenTelemetry APIs for recording traces and metrics.
41
+ */
42
+ export interface TelemetryContext {
43
+ /**
44
+ * Get the active OpenTelemetry span for the current job/step.
45
+ */
46
+ getActiveSpan(): Span | undefined
31
47
 
32
- const noopTracerSpan: TracerSpan = {
33
- setAttribute() {
34
- // No-op
35
- },
36
- setAttributes() {
37
- // No-op
38
- },
39
- addEvent() {
40
- // No-op
41
- },
42
- recordException() {
43
- // No-op
44
- },
45
- setStatusOk() {
46
- // No-op
47
- },
48
- setStatusError() {
49
- // No-op
50
- },
51
- end() {
52
- // No-op
53
- },
54
- isRecording() {
55
- return false
56
- },
48
+ /**
49
+ * Get an OpenTelemetry tracer for creating custom spans.
50
+ */
51
+ getTracer(name: string): Tracer
52
+
53
+ /**
54
+ * Start a new span as a child of the current job/step span.
55
+ * This is a convenience method that properly links the span to the trace hierarchy.
56
+ */
57
+ startSpan(name: string, options?: { attributes?: Record<string, any> }): Span
58
+
59
+ /**
60
+ * Record a custom metric as a span event.
61
+ */
62
+ recordMetric(name: string, value: number, attributes?: Record<string, any>): void
63
+ }
64
+
65
+ /**
66
+ * Inject parent span into a context if we have one.
67
+ */
68
+ function injectParentSpan(ctx: Context, parentSpan: Span | null): Context {
69
+ return parentSpan ? trace.setSpan(ctx, parentSpan) : ctx
70
+ }
71
+
72
+ /**
73
+ * Create a context-aware tracer wrapper that automatically injects the parent span.
74
+ * This ensures spans created by external libraries (like AI SDK) are properly linked
75
+ * to the current job/step trace hierarchy.
76
+ */
77
+ function createContextAwareTracer(tracer: Tracer, parentSpan: Span | null): Tracer {
78
+ return {
79
+ startSpan(name: string, options?: SpanOptions, ctx?: Context): Span {
80
+ // Always inject our parent span into the context, regardless of what context is passed.
81
+ // This is necessary because without global registration, context.active() returns
82
+ // ROOT_CONTEXT, so external libraries (like AI SDK) that pass context.active()
83
+ // would otherwise create orphan spans.
84
+ const baseContext = ctx ?? context.active()
85
+ const effectiveContext = injectParentSpan(baseContext, parentSpan)
86
+ return tracer.startSpan(name, options, effectiveContext)
87
+ },
88
+ // startActiveSpan has multiple overloads, we need to handle them all
89
+ startActiveSpan<F extends (span: Span) => unknown>(
90
+ name: string,
91
+ optionsOrFn: SpanOptions | F,
92
+ ctxOrFn?: Context | F,
93
+ fn?: F,
94
+ ): ReturnType<F> {
95
+ // Parse the overloaded arguments
96
+ let options: SpanOptions | undefined
97
+ let ctx: Context | undefined
98
+ let callback: F
99
+
100
+ if (typeof optionsOrFn === 'function') {
101
+ // startActiveSpan(name, fn)
102
+ callback = optionsOrFn
103
+ } else if (typeof ctxOrFn === 'function') {
104
+ // startActiveSpan(name, options, fn)
105
+ options = optionsOrFn
106
+ callback = ctxOrFn
107
+ } else {
108
+ // startActiveSpan(name, options, context, fn)
109
+ options = optionsOrFn
110
+ ctx = ctxOrFn
111
+ callback = fn!
112
+ }
113
+
114
+ const baseContext = ctx ?? context.active()
115
+ const effectiveContext = injectParentSpan(baseContext, parentSpan)
116
+
117
+ return tracer.startActiveSpan(name, options ?? {}, effectiveContext, callback)
118
+ },
119
+ } as Tracer
57
120
  }
58
121
 
59
- const createNoopTracer = (name: string): Tracer => ({
60
- name,
61
- startSpan(): TracerSpan {
62
- return noopTracerSpan
63
- },
64
- })
122
+ /**
123
+ * Create a TelemetryContext that wraps an OTel span.
124
+ */
125
+ function createTelemetryContext(span: Span | null, tracer: Tracer): TelemetryContext {
126
+ return {
127
+ getActiveSpan(): Span | undefined {
128
+ return span ?? undefined
129
+ },
130
+ getTracer(_name: string): Tracer {
131
+ // Return a context-aware tracer that automatically links spans to the current trace
132
+ return createContextAwareTracer(tracer, span)
133
+ },
134
+ startSpan(name: string, options?: { attributes?: Record<string, any> }): Span {
135
+ // Create a child span linked to the current span (job or step)
136
+ const parentContext = span ? trace.setSpan(context.active(), span) : context.active()
137
+ return tracer.startSpan(name, { attributes: options?.attributes }, parentContext)
138
+ },
139
+ recordMetric(name: string, value: number, attributes?: Record<string, any>): void {
140
+ if (span) {
141
+ span.addEvent(`metric:${name}`, {
142
+ 'metric.value': value,
143
+ ...attributes,
144
+ })
145
+ }
146
+ },
147
+ }
148
+ }
65
149
 
66
150
  import pRetry from './utils/p-retry.js'
67
151
  import waitForAbort from './utils/wait-for-abort.js'
@@ -171,7 +255,7 @@ export interface StepManagerOptions {
171
255
  jobId: string
172
256
  actionName: string
173
257
  adapter: Adapter
174
- telemetry: TelemetryAdapter
258
+ tracer: Tracer
175
259
  logger: Logger
176
260
  concurrencyLimit: number
177
261
  }
@@ -184,7 +268,7 @@ export class StepManager {
184
268
  #jobId: string
185
269
  #actionName: string
186
270
  #stepStore: StepStore
187
- #telemetry: TelemetryAdapter
271
+ #tracer: Tracer
188
272
  #queue: fastq.queueAsPromised<TaskStep, any>
189
273
  #logger: Logger
190
274
  // each step name should be executed only once per parent (name + parentStepId)
@@ -192,7 +276,7 @@ export class StepManager {
192
276
  // Store step spans for nested step tracking
193
277
  #stepSpans = new Map<string, Span>()
194
278
  // Store the job span for creating step spans
195
- #jobSpan: Span | null = null
279
+ #jobSpan!: Span
196
280
  // Factory function to create run functions with the correct parent step ID and abort signal
197
281
  #runFnFactory: ((parentStepId: string | null, abortSignal: AbortSignal) => StepHandlerContext['run']) | null = null
198
282
 
@@ -209,7 +293,7 @@ export class StepManager {
209
293
  this.#jobId = options.jobId
210
294
  this.#actionName = options.actionName
211
295
  this.#logger = options.logger
212
- this.#telemetry = options.telemetry
296
+ this.#tracer = options.tracer
213
297
  this.#stepStore = new StepStore(options.adapter)
214
298
  this.#queue = fastq.promise(async (task: TaskStep) => {
215
299
  // Create composite key: name + parentStepId (allows same name under different parents)
@@ -253,7 +337,6 @@ export class StepManager {
253
337
  * @param variables - Variables available to the action
254
338
  * @param abortSignal - Abort signal for cancelling the action
255
339
  * @param logger - Pino child logger for this job
256
- * @param observeContext - Observability context for telemetry
257
340
  * @returns ActionHandlerContext instance
258
341
  */
259
342
  createActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVariables = Record<string, unknown>>(
@@ -262,38 +345,21 @@ export class StepManager {
262
345
  variables: TVariables,
263
346
  abortSignal: AbortSignal,
264
347
  logger: Logger,
265
- observeContext: ObserveContext,
266
348
  ): ActionHandlerContext<TInput, TVariables> {
267
- return new ActionContext(this, job, action, variables, abortSignal, logger, observeContext)
349
+ const telemetryContext = createTelemetryContext(this.#jobSpan, this.#tracer)
350
+ return new ActionContext(this, job, action, variables, abortSignal, logger, telemetryContext)
268
351
  }
269
352
 
270
353
  /**
271
- * Create an observe context for a step.
354
+ * Create a telemetry context for a step.
272
355
  */
273
- createStepObserveContext(stepId: string): ObserveContext {
356
+ createStepTelemetryContext(stepId: string): TelemetryContext {
274
357
  const stepSpan = this.#stepSpans.get(stepId)
275
358
  if (stepSpan) {
276
- return this.#telemetry.createObserveContext(this.#jobId, stepId, stepSpan)
359
+ return createTelemetryContext(stepSpan, this.#tracer)
277
360
  }
278
361
  // Fallback to job span if step span not found
279
- if (this.#jobSpan) {
280
- return this.#telemetry.createObserveContext(this.#jobId, stepId, this.#jobSpan)
281
- }
282
- // No-op observe context
283
- return {
284
- recordMetric: () => {
285
- // No-op
286
- },
287
- addSpanAttribute: () => {
288
- // No-op
289
- },
290
- addSpanEvent: () => {
291
- // No-op
292
- },
293
- getTracer: (name: string) => {
294
- return createNoopTracer(name)
295
- },
296
- }
362
+ return createTelemetryContext(this.#jobSpan, this.#tracer)
297
363
  }
298
364
 
299
365
  /**
@@ -380,15 +446,22 @@ export class StepManager {
380
446
 
381
447
  step = newStep
382
448
 
383
- // Start step telemetry span
449
+ // Start step span - uses no-op tracer if no SDK is configured
384
450
  const parentSpan = parentStepId ? this.#stepSpans.get(parentStepId) : this.#jobSpan
385
- const stepSpan = await this.#telemetry.startStepSpan({
386
- jobId: this.#jobId,
387
- stepId: step.id,
388
- stepName: name,
389
- parentSpan: parentSpan ?? undefined,
390
- parentStepId,
391
- })
451
+ const parentContext = parentSpan ? trace.setSpan(context.active(), parentSpan) : context.active()
452
+ const stepSpan = this.#tracer.startSpan(
453
+ `step:${name}`,
454
+ {
455
+ kind: SpanKind.INTERNAL,
456
+ attributes: {
457
+ 'duron.job.id': this.#jobId,
458
+ 'duron.step.id': step.id,
459
+ 'duron.step.name': name,
460
+ 'duron.step.parent_id': parentStepId ?? undefined,
461
+ },
462
+ },
463
+ parentContext,
464
+ )
392
465
  this.#stepSpans.set(step.id, stepSpan)
393
466
 
394
467
  if (abortSignal.aborted) {
@@ -426,7 +499,11 @@ export class StepManager {
426
499
  // Create abort controller for this step's timeout
427
500
  const stepAbortController = new AbortController()
428
501
  const timeoutId = setTimeout(() => {
429
- const timeoutError = new StepTimeoutError(name, this.#jobId, expire)
502
+ const timeoutError = new StepTimeoutError(name, this.#jobId, expire, {
503
+ stepId: step?.id,
504
+ parentStepId,
505
+ actionName: this.#actionName,
506
+ })
430
507
  stepAbortController.abort(timeoutError)
431
508
  }, expire)
432
509
 
@@ -446,15 +523,15 @@ export class StepManager {
446
523
  const childAbortController = new AbortController()
447
524
  const childSignal = AbortSignal.any([stepSignal, childAbortController.signal])
448
525
 
449
- // Create observe context for this step
450
- const stepObserveContext = this.createStepObserveContext(step.id)
526
+ // Create telemetry context for this step
527
+ const stepTelemetryContext = this.createStepTelemetryContext(step.id)
451
528
 
452
529
  // Create StepHandlerContext with nested step support
453
530
  const stepContext: StepHandlerContext = {
454
531
  signal: stepSignal,
455
532
  stepId: step.id,
456
533
  parentStepId,
457
- observe: stepObserveContext,
534
+ telemetry: stepTelemetryContext,
458
535
  step: <TChildResult>(
459
536
  childName: string,
460
537
  childCb: (ctx: StepHandlerContext) => Promise<TChildResult>,
@@ -493,6 +570,7 @@ export class StepManager {
493
570
  .catch(() => {
494
571
  trackedChild.settled = true
495
572
  // Swallow the error here - it will be re-thrown to the caller via the returned promise
573
+ // Note: sibling steps will be aborted when the error propagates to the action level
496
574
  })
497
575
 
498
576
  return childPromise
@@ -503,10 +581,15 @@ export class StepManager {
503
581
  try {
504
582
  // Race between abort signal and callback execution
505
583
  const abortPromise = waitForAbort(stepSignal)
506
- const callbackPromise = cb(stepContext)
584
+
585
+ // Execute callback within the span context so that child spans inherit the trace
586
+ const currentStepSpan = step?.id ? this.#stepSpans.get(step.id) : undefined
587
+ const spanContext = currentStepSpan ? trace.setSpan(context.active(), currentStepSpan) : context.active()
588
+ const callbackPromise = context.with(spanContext, () => cb(stepContext))
507
589
 
508
590
  let result: any = null
509
591
  let aborted = false
592
+ let callbackError: Error | null = null
510
593
 
511
594
  await Promise.race([
512
595
  abortPromise.promise.then(() => {
@@ -518,11 +601,25 @@ export class StepManager {
518
601
  result = res
519
602
  }
520
603
  })
604
+ .catch((err) => {
605
+ callbackError = err
606
+ })
521
607
  .finally(() => {
522
608
  abortPromise.release()
523
609
  }),
524
610
  ])
525
611
 
612
+ // If callback threw an error, abort children and wait for them before re-throwing
613
+ if (callbackError) {
614
+ if (childSteps.length > 0) {
615
+ // Abort all children with the callback error as reason
616
+ childAbortController.abort(callbackError)
617
+ // Wait for all children to settle
618
+ await Promise.allSettled(childSteps.map((c) => c.promise))
619
+ }
620
+ throw callbackError
621
+ }
622
+
526
623
  // If aborted, wait for child steps to settle before propagating
527
624
  if (aborted) {
528
625
  // Wait for all child steps to settle (they'll be aborted via signal propagation)
@@ -548,7 +645,12 @@ export class StepManager {
548
645
  )
549
646
 
550
647
  // Abort all pending children
551
- const unhandledError = new UnhandledChildStepsError(name, unsettledChildren.length)
648
+ const unhandledError = new UnhandledChildStepsError(name, unsettledChildren.length, {
649
+ stepId: step.id,
650
+ parentStepId,
651
+ jobId: this.#jobId,
652
+ actionName: this.#actionName,
653
+ })
552
654
  childAbortController.abort(unhandledError)
553
655
 
554
656
  // Wait for all children to settle (they'll reject with cancellation)
@@ -564,10 +666,11 @@ export class StepManager {
564
666
  throw new Error(`Failed to complete step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`)
565
667
  }
566
668
 
567
- // End step telemetry span successfully
669
+ // End step span successfully
568
670
  const stepSpan = this.#stepSpans.get(step.id)
569
671
  if (stepSpan) {
570
- await this.#telemetry.endStepSpan(stepSpan, { status: 'ok' })
672
+ stepSpan.setStatus({ code: SpanStatusCode.OK })
673
+ stepSpan.end()
571
674
  this.#stepSpans.delete(step.id)
572
675
  }
573
676
 
@@ -597,28 +700,65 @@ export class StepManager {
597
700
  if (
598
701
  isNonRetriableError(error) ||
599
702
  (error.cause && isNonRetriableError(error.cause)) ||
600
- (error instanceof Error && error.name === 'AbortError' && isNonRetriableError(error.cause))
703
+ (error instanceof Error && error.name === 'AbortError')
601
704
  ) {
602
- throw error
705
+ const err = isNonRetriableError(error)
706
+ ? error
707
+ : error instanceof Error && error.name === 'AbortError'
708
+ ? new NonRetriableError(error.message, { cause: error.cause })
709
+ : (error.cause as NonRetriableError)
710
+
711
+ if (Object.keys(err.metadata).length === 0) {
712
+ err.setMetadata({
713
+ stepId: step?.id,
714
+ parentStepId,
715
+ jobId: this.#jobId,
716
+ actionName: this.#actionName,
717
+ })
718
+ }
719
+ throw err
603
720
  }
604
721
 
605
722
  if (ctx.retriesLeft > 0 && step) {
723
+ this.#clearHistoryForStep(step.id)
606
724
  const delayed = await this.#stepStore.delay(step.id, ctx.finalDelay, serializeError(error))
607
725
  if (!delayed) {
608
726
  throw new Error(`Failed to delay step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`)
609
727
  }
728
+ } else {
729
+ if (isTimeoutError(error)) {
730
+ ;(error as any).nonRetriable = true
731
+ throw error
732
+ }
733
+
734
+ const errorMessage = error instanceof Error ? error.message : String(error)
735
+ const err = new NonRetriableError(errorMessage, { cause: error })
736
+ err.setMetadata({
737
+ stepId: step?.id,
738
+ parentStepId,
739
+ jobId: this.#jobId,
740
+ actionName: this.#actionName,
741
+ })
742
+ throw err
610
743
  }
611
744
  },
612
745
  }).catch(async (error) => {
613
746
  if (step) {
614
- // End step telemetry span with error/cancelled status
747
+ // End step span with error/cancelled status
615
748
  const stepSpan = this.#stepSpans.get(step.id)
616
749
  if (stepSpan) {
617
750
  if (isCancelError(error)) {
618
- await this.#telemetry.endStepSpan(stepSpan, { status: 'cancelled' })
751
+ stepSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'Step cancelled' })
619
752
  } else {
620
- await this.#telemetry.endStepSpan(stepSpan, { status: 'error', error })
753
+ stepSpan.setStatus({
754
+ code: SpanStatusCode.ERROR,
755
+ message: error instanceof Error ? error.message : String(error),
756
+ })
757
+ if (error instanceof Error) {
758
+ stepSpan.recordException(error)
759
+ }
621
760
  }
761
+ stepSpan.end()
622
762
  this.#stepSpans.delete(step.id)
623
763
  }
624
764
 
@@ -631,6 +771,19 @@ export class StepManager {
631
771
  throw error
632
772
  })
633
773
  }
774
+
775
+ /**
776
+ * Clear the history of nested steps for a given step.
777
+ * We do't need to clear the history for the root step because it's not a parent step, it's the action itself.
778
+ * @param stepId - The ID of the step to clear the history for
779
+ */
780
+ #clearHistoryForStep(stepId: string): void {
781
+ this.#historySteps.forEach((stepKey) => {
782
+ if (stepKey.startsWith(stepId)) {
783
+ this.#historySteps.delete(stepKey)
784
+ }
785
+ })
786
+ }
634
787
  }
635
788
 
636
789
  // ============================================================================
@@ -652,7 +805,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
652
805
  #jobId: string
653
806
  #groupKey: string = '@default'
654
807
  #action: Action<TInput, TOutput, TVariables>
655
- #observeContext: ObserveContext
808
+ #telemetryContext: TelemetryContext
656
809
 
657
810
  // ============================================================================
658
811
  // Constructor
@@ -665,7 +818,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
665
818
  variables: TVariables,
666
819
  abortSignal: AbortSignal,
667
820
  logger: Logger,
668
- observeContext: ObserveContext,
821
+ telemetryContext: TelemetryContext,
669
822
  ) {
670
823
  this.#stepManager = stepManager
671
824
  this.#variables = variables
@@ -674,7 +827,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
674
827
  this.#action = action
675
828
  this.#jobId = job.id
676
829
  this.#groupKey = job.groupKey ?? '@default'
677
- this.#observeContext = observeContext
830
+ this.#telemetryContext = telemetryContext
678
831
  if (action.input) {
679
832
  this.#input = action.input.parse(job.input, {
680
833
  error: () => 'Error parsing action input',
@@ -734,10 +887,10 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
734
887
  }
735
888
 
736
889
  /**
737
- * Get the observability context for recording metrics and span data.
890
+ * Get the telemetry context for recording metrics and span data.
738
891
  */
739
- get observe(): ObserveContext {
740
- return this.#observeContext
892
+ get telemetry(): TelemetryContext {
893
+ return this.#telemetryContext
741
894
  }
742
895
 
743
896
  /**
@@ -758,6 +911,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
758
911
  ...this.#action.steps,
759
912
  ...options,
760
913
  })
914
+
761
915
  return this.#stepManager.push({
762
916
  name,
763
917
  cb,
@@ -806,7 +960,16 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
806
960
  : (input as z.infer<TStepInput>)
807
961
 
808
962
  // Resolve step name (static or dynamic)
809
- const stepName = typeof stepDef.name === 'function' ? stepDef.name({ input: validatedInput }) : stepDef.name
963
+ // If it's a function, pass the full context including input, variables, jobId, and parentStepId
964
+ const stepName =
965
+ typeof stepDef.name === 'function'
966
+ ? stepDef.name({
967
+ input: validatedInput,
968
+ var: this.#variables,
969
+ jobId: this.#jobId,
970
+ parentStepId,
971
+ })
972
+ : stepDef.name
810
973
 
811
974
  // Merge options: action defaults -> step definition -> call-time overrides
812
975
  const mergedOptions: z.input<typeof StepOptionsSchema> = {
@@ -1,20 +1,2 @@
1
- // Re-export telemetry adapters and types
2
-
3
- export {
4
- type AddSpanAttributeOptions,
5
- type AddSpanEventOptions,
6
- type EndSpanOptions,
7
- type ObserveContext,
8
- type RecordMetricOptions,
9
- type Span,
10
- type StartDatabaseSpanOptions,
11
- type StartJobSpanOptions,
12
- type StartSpanOptions,
13
- type StartStepSpanOptions,
14
- TelemetryAdapter,
15
- type Tracer,
16
- type TracerSpan,
17
- } from './adapter.js'
18
- export { LocalTelemetryAdapter, type LocalTelemetryAdapterOptions, localTelemetryAdapter } from './local.js'
19
- export { NoopTelemetryAdapter, noopTelemetryAdapter } from './noop.js'
20
- export { OpenTelemetryAdapter, type OpenTelemetryAdapterOptions, openTelemetryAdapter } from './opentelemetry.js'
1
+ // OpenTelemetry-based span exporter for local database persistence
2
+ export { LocalSpanExporter, type LocalSpanExporterOptions } from './local-span-exporter.js'