duron 0.3.0-beta.9 → 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 +88 -23
  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 +140 -3
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +152 -9
  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 +403 -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 +33 -29
  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 +84 -12
  71. package/src/index.ts +2 -0
  72. package/src/server.ts +39 -37
  73. package/src/step-manager.ts +246 -95
  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'
@@ -24,45 +34,118 @@ import {
24
34
  serializeError,
25
35
  UnhandledChildStepsError,
26
36
  } from './errors.js'
27
- import type { ObserveContext, Span, TelemetryAdapter, Tracer, TracerSpan } from './telemetry/adapter.js'
28
37
 
29
- // ============================================================================
30
- // Noop Tracer (for fallback observe context)
31
- // ============================================================================
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
32
47
 
33
- const noopTracerSpan: TracerSpan = {
34
- setAttribute() {
35
- // No-op
36
- },
37
- setAttributes() {
38
- // No-op
39
- },
40
- addEvent() {
41
- // No-op
42
- },
43
- recordException() {
44
- // No-op
45
- },
46
- setStatusOk() {
47
- // No-op
48
- },
49
- setStatusError() {
50
- // No-op
51
- },
52
- end() {
53
- // No-op
54
- },
55
- isRecording() {
56
- return false
57
- },
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
58
120
  }
59
121
 
60
- const createNoopTracer = (name: string): Tracer => ({
61
- name,
62
- startSpan(): TracerSpan {
63
- return noopTracerSpan
64
- },
65
- })
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
+ }
66
149
 
67
150
  import pRetry from './utils/p-retry.js'
68
151
  import waitForAbort from './utils/wait-for-abort.js'
@@ -172,7 +255,7 @@ export interface StepManagerOptions {
172
255
  jobId: string
173
256
  actionName: string
174
257
  adapter: Adapter
175
- telemetry: TelemetryAdapter
258
+ tracer: Tracer
176
259
  logger: Logger
177
260
  concurrencyLimit: number
178
261
  }
@@ -185,7 +268,7 @@ export class StepManager {
185
268
  #jobId: string
186
269
  #actionName: string
187
270
  #stepStore: StepStore
188
- #telemetry: TelemetryAdapter
271
+ #tracer: Tracer
189
272
  #queue: fastq.queueAsPromised<TaskStep, any>
190
273
  #logger: Logger
191
274
  // each step name should be executed only once per parent (name + parentStepId)
@@ -193,7 +276,7 @@ export class StepManager {
193
276
  // Store step spans for nested step tracking
194
277
  #stepSpans = new Map<string, Span>()
195
278
  // Store the job span for creating step spans
196
- #jobSpan: Span | null = null
279
+ #jobSpan!: Span
197
280
  // Factory function to create run functions with the correct parent step ID and abort signal
198
281
  #runFnFactory: ((parentStepId: string | null, abortSignal: AbortSignal) => StepHandlerContext['run']) | null = null
199
282
 
@@ -210,7 +293,7 @@ export class StepManager {
210
293
  this.#jobId = options.jobId
211
294
  this.#actionName = options.actionName
212
295
  this.#logger = options.logger
213
- this.#telemetry = options.telemetry
296
+ this.#tracer = options.tracer
214
297
  this.#stepStore = new StepStore(options.adapter)
215
298
  this.#queue = fastq.promise(async (task: TaskStep) => {
216
299
  // Create composite key: name + parentStepId (allows same name under different parents)
@@ -254,7 +337,6 @@ export class StepManager {
254
337
  * @param variables - Variables available to the action
255
338
  * @param abortSignal - Abort signal for cancelling the action
256
339
  * @param logger - Pino child logger for this job
257
- * @param observeContext - Observability context for telemetry
258
340
  * @returns ActionHandlerContext instance
259
341
  */
260
342
  createActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVariables = Record<string, unknown>>(
@@ -263,38 +345,21 @@ export class StepManager {
263
345
  variables: TVariables,
264
346
  abortSignal: AbortSignal,
265
347
  logger: Logger,
266
- observeContext: ObserveContext,
267
348
  ): ActionHandlerContext<TInput, TVariables> {
268
- 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)
269
351
  }
270
352
 
271
353
  /**
272
- * Create an observe context for a step.
354
+ * Create a telemetry context for a step.
273
355
  */
274
- createStepObserveContext(stepId: string): ObserveContext {
356
+ createStepTelemetryContext(stepId: string): TelemetryContext {
275
357
  const stepSpan = this.#stepSpans.get(stepId)
276
358
  if (stepSpan) {
277
- return this.#telemetry.createObserveContext(this.#jobId, stepId, stepSpan)
359
+ return createTelemetryContext(stepSpan, this.#tracer)
278
360
  }
279
361
  // Fallback to job span if step span not found
280
- if (this.#jobSpan) {
281
- return this.#telemetry.createObserveContext(this.#jobId, stepId, this.#jobSpan)
282
- }
283
- // No-op observe context
284
- return {
285
- recordMetric: () => {
286
- // No-op
287
- },
288
- addSpanAttribute: () => {
289
- // No-op
290
- },
291
- addSpanEvent: () => {
292
- // No-op
293
- },
294
- getTracer: (name: string) => {
295
- return createNoopTracer(name)
296
- },
297
- }
362
+ return createTelemetryContext(this.#jobSpan, this.#tracer)
298
363
  }
299
364
 
300
365
  /**
@@ -381,15 +446,22 @@ export class StepManager {
381
446
 
382
447
  step = newStep
383
448
 
384
- // Start step telemetry span
449
+ // Start step span - uses no-op tracer if no SDK is configured
385
450
  const parentSpan = parentStepId ? this.#stepSpans.get(parentStepId) : this.#jobSpan
386
- const stepSpan = await this.#telemetry.startStepSpan({
387
- jobId: this.#jobId,
388
- stepId: step.id,
389
- stepName: name,
390
- parentSpan: parentSpan ?? undefined,
391
- parentStepId,
392
- })
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
+ )
393
465
  this.#stepSpans.set(step.id, stepSpan)
394
466
 
395
467
  if (abortSignal.aborted) {
@@ -427,7 +499,11 @@ export class StepManager {
427
499
  // Create abort controller for this step's timeout
428
500
  const stepAbortController = new AbortController()
429
501
  const timeoutId = setTimeout(() => {
430
- 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
+ })
431
507
  stepAbortController.abort(timeoutError)
432
508
  }, expire)
433
509
 
@@ -447,15 +523,15 @@ export class StepManager {
447
523
  const childAbortController = new AbortController()
448
524
  const childSignal = AbortSignal.any([stepSignal, childAbortController.signal])
449
525
 
450
- // Create observe context for this step
451
- const stepObserveContext = this.createStepObserveContext(step.id)
526
+ // Create telemetry context for this step
527
+ const stepTelemetryContext = this.createStepTelemetryContext(step.id)
452
528
 
453
529
  // Create StepHandlerContext with nested step support
454
530
  const stepContext: StepHandlerContext = {
455
531
  signal: stepSignal,
456
532
  stepId: step.id,
457
533
  parentStepId,
458
- observe: stepObserveContext,
534
+ telemetry: stepTelemetryContext,
459
535
  step: <TChildResult>(
460
536
  childName: string,
461
537
  childCb: (ctx: StepHandlerContext) => Promise<TChildResult>,
@@ -505,10 +581,15 @@ export class StepManager {
505
581
  try {
506
582
  // Race between abort signal and callback execution
507
583
  const abortPromise = waitForAbort(stepSignal)
508
- 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))
509
589
 
510
590
  let result: any = null
511
591
  let aborted = false
592
+ let callbackError: Error | null = null
512
593
 
513
594
  await Promise.race([
514
595
  abortPromise.promise.then(() => {
@@ -520,11 +601,25 @@ export class StepManager {
520
601
  result = res
521
602
  }
522
603
  })
604
+ .catch((err) => {
605
+ callbackError = err
606
+ })
523
607
  .finally(() => {
524
608
  abortPromise.release()
525
609
  }),
526
610
  ])
527
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
+
528
623
  // If aborted, wait for child steps to settle before propagating
529
624
  if (aborted) {
530
625
  // Wait for all child steps to settle (they'll be aborted via signal propagation)
@@ -550,7 +645,12 @@ export class StepManager {
550
645
  )
551
646
 
552
647
  // Abort all pending children
553
- 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
+ })
554
654
  childAbortController.abort(unhandledError)
555
655
 
556
656
  // Wait for all children to settle (they'll reject with cancellation)
@@ -566,10 +666,11 @@ export class StepManager {
566
666
  throw new Error(`Failed to complete step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`)
567
667
  }
568
668
 
569
- // End step telemetry span successfully
669
+ // End step span successfully
570
670
  const stepSpan = this.#stepSpans.get(step.id)
571
671
  if (stepSpan) {
572
- await this.#telemetry.endStepSpan(stepSpan, { status: 'ok' })
672
+ stepSpan.setStatus({ code: SpanStatusCode.OK })
673
+ stepSpan.end()
573
674
  this.#stepSpans.delete(step.id)
574
675
  }
575
676
 
@@ -599,12 +700,27 @@ export class StepManager {
599
700
  if (
600
701
  isNonRetriableError(error) ||
601
702
  (error.cause && isNonRetriableError(error.cause)) ||
602
- (error instanceof Error && error.name === 'AbortError' && isNonRetriableError(error.cause))
703
+ (error instanceof Error && error.name === 'AbortError')
603
704
  ) {
604
- 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
605
720
  }
606
721
 
607
722
  if (ctx.retriesLeft > 0 && step) {
723
+ this.#clearHistoryForStep(step.id)
608
724
  const delayed = await this.#stepStore.delay(step.id, ctx.finalDelay, serializeError(error))
609
725
  if (!delayed) {
610
726
  throw new Error(`Failed to delay step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`)
@@ -614,22 +730,35 @@ export class StepManager {
614
730
  ;(error as any).nonRetriable = true
615
731
  throw error
616
732
  }
617
- throw new NonRetriableError(
618
- `Failed to execute step="${name}", jobId="${this.#jobId}", action="${this.#actionName}", parentStepId="${parentStepId}"`,
619
- { cause: error },
620
- )
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
621
743
  }
622
744
  },
623
745
  }).catch(async (error) => {
624
746
  if (step) {
625
- // End step telemetry span with error/cancelled status
747
+ // End step span with error/cancelled status
626
748
  const stepSpan = this.#stepSpans.get(step.id)
627
749
  if (stepSpan) {
628
750
  if (isCancelError(error)) {
629
- await this.#telemetry.endStepSpan(stepSpan, { status: 'cancelled' })
751
+ stepSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'Step cancelled' })
630
752
  } else {
631
- 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
+ }
632
760
  }
761
+ stepSpan.end()
633
762
  this.#stepSpans.delete(step.id)
634
763
  }
635
764
 
@@ -642,6 +771,19 @@ export class StepManager {
642
771
  throw error
643
772
  })
644
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
+ }
645
787
  }
646
788
 
647
789
  // ============================================================================
@@ -663,7 +805,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
663
805
  #jobId: string
664
806
  #groupKey: string = '@default'
665
807
  #action: Action<TInput, TOutput, TVariables>
666
- #observeContext: ObserveContext
808
+ #telemetryContext: TelemetryContext
667
809
 
668
810
  // ============================================================================
669
811
  // Constructor
@@ -676,7 +818,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
676
818
  variables: TVariables,
677
819
  abortSignal: AbortSignal,
678
820
  logger: Logger,
679
- observeContext: ObserveContext,
821
+ telemetryContext: TelemetryContext,
680
822
  ) {
681
823
  this.#stepManager = stepManager
682
824
  this.#variables = variables
@@ -685,7 +827,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
685
827
  this.#action = action
686
828
  this.#jobId = job.id
687
829
  this.#groupKey = job.groupKey ?? '@default'
688
- this.#observeContext = observeContext
830
+ this.#telemetryContext = telemetryContext
689
831
  if (action.input) {
690
832
  this.#input = action.input.parse(job.input, {
691
833
  error: () => 'Error parsing action input',
@@ -745,10 +887,10 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
745
887
  }
746
888
 
747
889
  /**
748
- * Get the observability context for recording metrics and span data.
890
+ * Get the telemetry context for recording metrics and span data.
749
891
  */
750
- get observe(): ObserveContext {
751
- return this.#observeContext
892
+ get telemetry(): TelemetryContext {
893
+ return this.#telemetryContext
752
894
  }
753
895
 
754
896
  /**
@@ -818,7 +960,16 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
818
960
  : (input as z.infer<TStepInput>)
819
961
 
820
962
  // Resolve step name (static or dynamic)
821
- 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
822
973
 
823
974
  // Merge options: action defaults -> step definition -> call-time overrides
824
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'