@softize/opus 9.0.9 → 9.1.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.
@@ -12,11 +12,13 @@
12
12
  */
13
13
 
14
14
  import { isReaction } from './reactions.ts'
15
+ import { isBackgroundAction } from './actions.ts'
15
16
  import { isSchedule } from './schedules.ts'
16
17
  import { collectDomainWarnings, flattenDomain, isDomainConfig } from './domain.ts'
17
18
  import type { DomainConfig } from './domain.ts'
18
19
  import { AuditEmitter } from './audit.ts'
19
20
  import { error, isActionError, normalizeError } from './errors.ts'
21
+ import { normalizeTraceContext } from './trace.ts'
20
22
  import { evalExpression } from '../dsl/eval.ts'
21
23
  import { parseExpression } from '../dsl/parser.ts'
22
24
  import { parseLoad, resolveLoadArgs } from '../dsl/loads.ts'
@@ -40,12 +42,16 @@ import type {
40
42
  EventBusAdapter,
41
43
  HealthStatus,
42
44
  I18nRef,
45
+ JobSpec,
43
46
  LoaderFn,
44
47
  LoaderResolver,
45
48
  LoadsSpec,
46
49
  Logger,
47
50
  LoggerAdapter,
51
+ ObservabilityAdapter,
52
+ ObservabilityOperation,
48
53
  Provenance,
54
+ ProgressReporter,
49
55
  QueueAdapter,
50
56
  ReactionContext,
51
57
  ReactionDef,
@@ -62,6 +68,7 @@ import type {
62
68
  AiTool,
63
69
  AIConfig,
64
70
  User,
71
+ TraceContext,
65
72
  } from './types.ts'
66
73
 
67
74
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -82,6 +89,7 @@ export interface RuntimeSetup {
82
89
  data?: DataAdapter
83
90
  auth?: AuthAdapter
84
91
  logger?: LoggerAdapter
92
+ observability?: ObservabilityAdapter
85
93
  audit?: AuditSink | AuditSink[]
86
94
  queue?: QueueAdapter
87
95
  eventBus?: EventBusAdapter
@@ -229,6 +237,7 @@ export class Runtime {
229
237
  private readonly server: ServerAdapter | undefined
230
238
  private readonly data: DataAdapter | undefined
231
239
  private readonly logger: LoggerAdapter | undefined
240
+ private readonly observability: ObservabilityAdapter | undefined
232
241
  private readonly queue: QueueAdapter | undefined
233
242
  private readonly eventBus: EventBusAdapter | undefined
234
243
  private readonly scheduler: SchedulerAdapter | undefined
@@ -267,6 +276,7 @@ export class Runtime {
267
276
  constructor(setup: RuntimeSetup) {
268
277
  this.config = applyConfigDefaults(setup.config)
269
278
  this.logger = setup.logger
279
+ this.observability = setup.observability
270
280
  this.log = setup.logger ?? new ConsoleLogger({ runtime: 'tbdlib' })
271
281
 
272
282
  this.audit = new AuditEmitter({ mode: this.config.auditMode, log: this.log })
@@ -488,10 +498,7 @@ export class Runtime {
488
498
  return { ok, adapters }
489
499
  }
490
500
 
491
- /**
492
- * Executa uma action de forma síncrona. Background actions usam
493
- * `executeBackground` (não implementado em v0; queue adapter dispara).
494
- */
501
+ /** Executa uma action; declarations background são validadas e enfileiradas. */
495
502
  async execute(
496
503
  actionName: string,
497
504
  input: unknown,
@@ -506,11 +513,81 @@ export class Runtime {
506
513
  }), 0, ctxBase.requestId)
507
514
  }
508
515
 
516
+ return this.executeAction(action, input, ctxBase, 'request')
517
+ }
518
+
519
+ /**
520
+ * Executa no worker um envelope criado por `execute()`. O consumer fornece
521
+ * auth/can reidratados; identidade, provenance e trace vêm do envelope.
522
+ */
523
+ async executeJob(
524
+ spec: JobSpec,
525
+ ctxBase: ContextBase,
526
+ progress?: ProgressReporter,
527
+ ): Promise<ActionResult<unknown>> {
528
+ const action = this.actions.get(spec.action)
529
+ if (action === undefined) {
530
+ return this.errorResult(spec.action, error({
531
+ code: 'runtime.action_not_found',
532
+ category: 'not_found',
533
+ message: `Action "${spec.action}" is not registered`,
534
+ }), 0, spec.ctx.requestId)
535
+ }
536
+ if (!isBackgroundAction(action)) {
537
+ return this.errorResult(spec.action, error({
538
+ code: 'runtime.action_not_background',
539
+ category: 'validation',
540
+ message: `Action "${spec.action}" is not declared as background`,
541
+ }), 0, spec.ctx.requestId)
542
+ }
543
+ if ((ctxBase.user?.id ?? null) !== spec.ctx.userId) {
544
+ return this.errorResult(spec.action, error({
545
+ code: 'runtime.job_actor_mismatch',
546
+ category: 'authentication',
547
+ message: 'Worker context actor does not match the job envelope',
548
+ }), 0, spec.ctx.requestId)
549
+ }
550
+
551
+ const trace = normalizeTraceContext(spec.ctx.trace)
552
+ return this.executeAction(action, spec.input, {
553
+ ...ctxBase,
554
+ tenantId: spec.ctx.tenantId,
555
+ ...(spec.ctx.requestId !== undefined ? { requestId: spec.ctx.requestId } : {}),
556
+ ...(trace !== undefined ? { trace } : {}),
557
+ provenance: {
558
+ kind: 'background',
559
+ queueName: spec.config.queue ?? 'default',
560
+ jobId: spec.jobId,
561
+ ...(spec.ctx.originalProvenance !== undefined
562
+ ? { originalProvenance: spec.ctx.originalProvenance }
563
+ : {}),
564
+ },
565
+ }, 'job', progress)
566
+ }
567
+
568
+ private async executeAction(
569
+ action: ActionDef,
570
+ input: unknown,
571
+ ctxBase: ContextBase,
572
+ execution: 'request' | 'job',
573
+ progress?: ProgressReporter,
574
+ ): Promise<ActionResult<unknown>> {
575
+
509
576
  const startedAt = performance.now()
510
577
  const actionId = generateId()
511
- const ctx = this.buildActionContext(action, actionId, ctxBase)
578
+ return this.runObserved(
579
+ {
580
+ name: action.name,
581
+ kind: 'action',
582
+ resultKind: 'action-result',
583
+ ...(ctxBase.trace !== undefined ? { parent: ctxBase.trace } : {}),
584
+ attributes: { 'opus.action.id': actionId, 'opus.action.kind': action.kind },
585
+ },
586
+ ctxBase.trace,
587
+ async (trace) => {
588
+ const ctx = this.buildActionContext(action, actionId, ctxBase, trace)
512
589
 
513
- try {
590
+ try {
514
591
  // — 1. Validate input ————————————————————————————————————————————————
515
592
  const validatedInput = await this.validate(action.input, input, 'input')
516
593
 
@@ -540,6 +617,60 @@ export class Runtime {
540
617
  if (decision !== true) throw decision
541
618
  }
542
619
 
620
+ if (execution === 'request' && isBackgroundAction(action)) {
621
+ if (this.queue === undefined) {
622
+ throw error({
623
+ code: 'runtime.queue_required',
624
+ category: 'internal',
625
+ message: `Background action "${action.name}" requires a QueueAdapter`,
626
+ })
627
+ }
628
+ const background = action.background
629
+ if (background === undefined) {
630
+ throw error({
631
+ code: 'runtime.invalid_background_config',
632
+ category: 'internal',
633
+ message: `Background action "${action.name}" has no background config`,
634
+ })
635
+ }
636
+ const spec: JobSpec = {
637
+ jobId: generateId(),
638
+ action: action.name,
639
+ input: validatedInput,
640
+ ctx: {
641
+ userId: ctx.user?.id ?? null,
642
+ tenantId: ctx.tenantId,
643
+ ...(ctxBase.requestId !== undefined ? { requestId: ctxBase.requestId } : {}),
644
+ ...(trace !== undefined ? { trace } : {}),
645
+ originalProvenance: ctx.provenance,
646
+ },
647
+ config: {
648
+ ...(background.queue !== undefined ? { queue: background.queue } : {}),
649
+ ...(background.priority !== undefined ? { priority: background.priority } : {}),
650
+ retry: background.retry ?? this.config.defaultRetry,
651
+ ...(background.timeout !== undefined ? { timeout: background.timeout } : {}),
652
+ },
653
+ }
654
+ const queuedHandle = await this.queue.enqueue(spec)
655
+ const handle = trace !== undefined ? { ...queuedHandle, trace } : queuedHandle
656
+ const durationMs = performance.now() - startedAt
657
+ await this.audit.emit(this.buildAuditRecord({
658
+ actionId,
659
+ action: action.name,
660
+ actionKind: action.kind,
661
+ outcome: 'success',
662
+ durationMs,
663
+ ctx,
664
+ input: validatedInput,
665
+ output: handle,
666
+ }), action.audit)
667
+ return {
668
+ ok: true,
669
+ data: handle,
670
+ meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId, trace),
671
+ }
672
+ }
673
+
543
674
  // — 5. Handler ————————————————————————————————————————————————————————
544
675
  // Modo design (`OPUS_MODE=design`): tenta mockHandler primeiro, cai
545
676
  // no handler real se não houver. Em qualquer outro modo, handler real
@@ -550,7 +681,14 @@ export class Runtime {
550
681
  isDesignMode && action.mockHandler !== undefined
551
682
  ? action.mockHandler
552
683
  : action.handler
553
- const output = await handlerToRun(ctx, validatedInput, loaded)
684
+ const output = await handlerToRun(
685
+ ctx,
686
+ validatedInput,
687
+ loaded,
688
+ execution === 'job' && isBackgroundAction(action) && action.background?.progress === true
689
+ ? progress
690
+ : undefined,
691
+ )
554
692
  const usedMock = handlerToRun === action.mockHandler
555
693
 
556
694
  // — 6. Validate output (dev only) —————————————————————————————————————
@@ -583,9 +721,9 @@ export class Runtime {
583
721
  return {
584
722
  ok: true,
585
723
  data: validatedOutput,
586
- meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId),
724
+ meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId, trace),
587
725
  }
588
- } catch (thrown) {
726
+ } catch (thrown) {
589
727
  const actionError = normalizeError(thrown)
590
728
  const durationMs = performance.now() - startedAt
591
729
  await this.audit.emit(
@@ -604,9 +742,11 @@ export class Runtime {
604
742
  return {
605
743
  ok: false,
606
744
  error: actionError,
607
- meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId),
745
+ meta: this.buildMeta(actionId, action.name, durationMs, ctxBase.requestId, trace),
608
746
  }
609
- }
747
+ }
748
+ },
749
+ )
610
750
  }
611
751
 
612
752
  // ===========================================================================
@@ -615,6 +755,7 @@ export class Runtime {
615
755
 
616
756
  private *allAdapters(): IterableIterator<Adapter> {
617
757
  if (this.logger !== undefined) yield this.logger
758
+ if (this.observability !== undefined) yield this.observability
618
759
  if (this.data !== undefined) yield this.data
619
760
  if (this.auth !== undefined) yield this.auth
620
761
  if (this.server !== undefined) yield this.server
@@ -698,6 +839,7 @@ export class Runtime {
698
839
  action: ActionDef,
699
840
  actionId: string,
700
841
  base: ContextBase,
842
+ trace: TraceContext | undefined,
701
843
  ): ActionContext {
702
844
  const dbExt = this.data?.contextExtension() ?? { db: undefined }
703
845
  const provenance = resolveProvenance(base)
@@ -716,10 +858,11 @@ export class Runtime {
716
858
  can: base.can,
717
859
  db: dbExt.db,
718
860
  log,
719
- emit: this.buildEmit(action, actionId, base),
861
+ emit: this.buildEmit(action, actionId, base, trace),
720
862
  storage: this.storage ?? null,
721
- ai: this.bindAi(base),
863
+ ai: this.bindAi({ ...base, ...(trace !== undefined ? { trace } : {}) }),
722
864
  provenance,
865
+ ...(trace !== undefined ? { trace } : {}),
723
866
  meta: base.meta ?? {},
724
867
  }
725
868
  }
@@ -728,6 +871,7 @@ export class Runtime {
728
871
  action: ActionDef,
729
872
  actionId: string,
730
873
  base: ContextBase,
874
+ trace: TraceContext | undefined,
731
875
  ): ActionContext['emit'] {
732
876
  return async (event, data, meta) => {
733
877
  if (
@@ -769,6 +913,7 @@ export class Runtime {
769
913
  actionId,
770
914
  ...(meta?.correlation !== undefined ? { correlation: meta.correlation } : {}),
771
915
  },
916
+ ...(trace !== undefined ? { trace } : {}),
772
917
  ...(meta !== undefined ? { meta: meta as Record<string, unknown> } : {}),
773
918
  }
774
919
 
@@ -933,6 +1078,7 @@ export class Runtime {
933
1078
  tenant: args.ctx.tenantId,
934
1079
  input: args.input,
935
1080
  severity: args.outcome === 'success' ? 'info' : severityForError(args.error),
1081
+ ...(args.ctx.trace !== undefined ? { traceContext: args.ctx.trace } : {}),
936
1082
  }
937
1083
  if (args.output !== undefined) record.output = args.output
938
1084
  if (args.error !== undefined) record.error = args.error
@@ -945,6 +1091,7 @@ export class Runtime {
945
1091
  actionName: string,
946
1092
  durationMs: number,
947
1093
  requestId?: string,
1094
+ trace?: TraceContext,
948
1095
  ): ResultMeta {
949
1096
  const meta: ResultMeta = {
950
1097
  actionId,
@@ -952,6 +1099,7 @@ export class Runtime {
952
1099
  durationMs,
953
1100
  }
954
1101
  if (requestId !== undefined) meta.requestId = requestId
1102
+ if (trace !== undefined) meta.trace = trace
955
1103
  return meta
956
1104
  }
957
1105
 
@@ -1021,15 +1169,27 @@ export class Runtime {
1021
1169
  sourceActionId: event.source.actionId,
1022
1170
  eventType: event.type,
1023
1171
  }
1024
- const ctx: ReactionContext = {
1172
+ const reactionLog = this.log.child({
1173
+ reaction: reaction.name,
1174
+ eventType: event.type,
1175
+ provenance: 'reaction',
1176
+ })
1177
+ try {
1178
+ await this.runObserved(
1179
+ {
1180
+ name: reaction.name,
1181
+ kind: 'reaction',
1182
+ resultKind: 'void',
1183
+ ...(event.trace !== undefined ? { parent: event.trace } : {}),
1184
+ attributes: { 'opus.event.type': event.type, 'opus.source.action_id': event.source.actionId },
1185
+ },
1186
+ event.trace,
1187
+ async (trace) => {
1188
+ const ctx: ReactionContext = {
1025
1189
  user: event.actor.id !== null ? ({ id: event.actor.id } as User) : null,
1026
1190
  tenantId: event.tenant ?? null,
1027
1191
  db: this.data?.contextExtension().db,
1028
- log: this.log.child({
1029
- reaction: reaction.name,
1030
- eventType: event.type,
1031
- provenance: 'reaction',
1032
- }),
1192
+ log: reactionLog,
1033
1193
  emit: noopEmit,
1034
1194
  storage: this.storage ?? null,
1035
1195
  // Reação = contexto de sistema; o agente só alcança actions públicas (can nega por
@@ -1039,31 +1199,75 @@ export class Runtime {
1039
1199
  tenantId: event.tenant ?? null,
1040
1200
  can: async () => false,
1041
1201
  meta: {},
1202
+ ...(trace !== undefined ? { trace } : {}),
1042
1203
  }),
1043
1204
  provenance,
1205
+ ...(trace !== undefined ? { trace } : {}),
1044
1206
  meta: {},
1045
1207
  }
1046
1208
 
1047
- try {
1048
- if (reaction.authorize !== undefined) {
1049
- const allowed = await reaction.authorize(ctx, event)
1050
- if (!allowed) return
1051
- }
1052
- await reaction.handler(ctx, event)
1209
+ if (reaction.authorize !== undefined) {
1210
+ const allowed = await reaction.authorize(ctx, event)
1211
+ if (!allowed) return
1212
+ }
1213
+ await reaction.handler(ctx, event)
1214
+ },
1215
+ )
1053
1216
  } catch (err) {
1054
1217
  const actionError = isActionError(err) ? err : normalizeError(err)
1055
- ctx.log.error('reaction failed', {
1218
+ reactionLog.error('reaction failed', {
1056
1219
  code: actionError.code,
1057
1220
  message: actionError.message,
1058
1221
  })
1059
1222
  }
1060
1223
  }
1224
+
1225
+ /** Observabilidade nunca repete nem substitui o resultado do callback. */
1226
+ private async runObserved<T>(
1227
+ operation: ObservabilityOperation,
1228
+ fallbackTrace: TraceContext | undefined,
1229
+ run: (trace: TraceContext | undefined) => Promise<T>,
1230
+ ): Promise<T> {
1231
+ if (this.observability === undefined) return run(fallbackTrace)
1232
+
1233
+ let execution: Promise<T> | undefined
1234
+ const executeOnce = (trace: TraceContext | undefined): Promise<T> => {
1235
+ if (execution !== undefined) return execution
1236
+ // Atribui a Promise antes de ceder controle: chamadas concorrentes, tardias ou
1237
+ // malcomportadas do adapter observam a mesma execução e nunca repetem efeitos.
1238
+ execution = Promise.resolve().then(() => run(trace))
1239
+ return execution
1240
+ }
1241
+
1242
+ try {
1243
+ await this.observability.runInSpan(operation, executeOnce)
1244
+ } catch (err) {
1245
+ // Rejeição da própria execução pertence à action/reaction e será devolvida abaixo;
1246
+ // só uma falha antes/depois do callback é falha de instrumentação.
1247
+ if (execution === undefined || await settledSuccessfully(execution)) {
1248
+ this.log.warn('observability adapter failed (lenient)', {
1249
+ operation: operation.name,
1250
+ error: String(err),
1251
+ })
1252
+ }
1253
+ }
1254
+ return executeOnce(fallbackTrace)
1255
+ }
1061
1256
  }
1062
1257
 
1063
1258
  // =============================================================================
1064
1259
  // Helpers
1065
1260
  // =============================================================================
1066
1261
 
1262
+ async function settledSuccessfully<T>(promise: Promise<T>): Promise<boolean> {
1263
+ try {
1264
+ await promise
1265
+ return true
1266
+ } catch {
1267
+ return false
1268
+ }
1269
+ }
1270
+
1067
1271
  // ContextBase moveu pra types.ts (compartilhado entre core e adapters).
1068
1272
 
1069
1273
  /**
@@ -0,0 +1,34 @@
1
+ import type { TraceContext } from './types.ts'
2
+
3
+ /** Valida carriers desserializados sem aceitar campos extras ou payload arbitrário. */
4
+ export function normalizeTraceContext(value: unknown): TraceContext | undefined {
5
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined
6
+ const record = value as Record<string, unknown>
7
+ const allowed = new Set(['traceId', 'spanId', 'traceFlags', 'traceState'])
8
+ if (Object.keys(record).some((key) => !allowed.has(key))) return undefined
9
+ if (
10
+ typeof record['traceId'] !== 'string' || record['traceId'].length === 0 ||
11
+ record['traceId'].length > 128
12
+ ) return undefined
13
+ if (
14
+ record['spanId'] !== undefined &&
15
+ (typeof record['spanId'] !== 'string' || record['spanId'].length === 0 ||
16
+ record['spanId'].length > 64)
17
+ ) return undefined
18
+ if (
19
+ record['traceFlags'] !== undefined &&
20
+ (!Number.isInteger(record['traceFlags']) || (record['traceFlags'] as number) < 0 ||
21
+ (record['traceFlags'] as number) > 255)
22
+ ) return undefined
23
+ if (
24
+ record['traceState'] !== undefined &&
25
+ (typeof record['traceState'] !== 'string' || record['traceState'].length > 512 ||
26
+ /[^\x20-\x7e]/.test(record['traceState']))
27
+ ) return undefined
28
+ return {
29
+ traceId: record['traceId'],
30
+ ...(record['spanId'] !== undefined ? { spanId: record['spanId'] as string } : {}),
31
+ ...(record['traceFlags'] !== undefined ? { traceFlags: record['traceFlags'] as number } : {}),
32
+ ...(record['traceState'] !== undefined ? { traceState: record['traceState'] as string } : {}),
33
+ }
34
+ }
package/src/core/types.ts CHANGED
@@ -38,6 +38,14 @@ export type I18nRef = string | { key: string; default: string }
38
38
  */
39
39
  export type Unsubscribe = () => void
40
40
 
41
+ /** Contexto de propagação distribuída, independente de SDK/vendor. */
42
+ export interface TraceContext {
43
+ traceId: string
44
+ spanId?: string
45
+ traceFlags?: number
46
+ traceState?: string
47
+ }
48
+
41
49
  /**
42
50
  * Logger genérico que mora no contexto. Implementado por `LoggerAdapter`.
43
51
  * Detalhes: §8.
@@ -169,6 +177,7 @@ export interface ResultMeta {
169
177
  action: string
170
178
  durationMs: number
171
179
  requestId?: string
180
+ trace?: TraceContext
172
181
  cached?: boolean
173
182
  }
174
183
 
@@ -270,6 +279,7 @@ export interface ActionContext {
270
279
  storage: StorageAdapter | null
271
280
  ai: BoundAi | null
272
281
  provenance: Provenance
282
+ trace?: TraceContext
273
283
  meta: Record<string, unknown>
274
284
  }
275
285
 
@@ -286,6 +296,7 @@ export interface ReactionContext {
286
296
  storage: StorageAdapter | null
287
297
  ai: BoundAi | null
288
298
  provenance: Provenance
299
+ trace?: TraceContext
289
300
  meta: Record<string, unknown>
290
301
  }
291
302
 
@@ -334,10 +345,13 @@ export interface AuditRecord {
334
345
  error?: ActionError
335
346
 
336
347
  severity: 'info' | 'warning' | 'error'
348
+ /** Campo legado preservado para compatibilidade com sinks/consumers existentes. */
337
349
  trace?: {
338
350
  requestId?: string
339
351
  parentActionId?: string
340
352
  }
353
+ /** Contexto de trace vendor-neutral; aditivo ao campo `trace` legado. */
354
+ traceContext?: TraceContext
341
355
  meta?: Record<string, unknown>
342
356
  }
343
357
 
@@ -373,6 +387,7 @@ export interface DomainEvent<T = unknown> {
373
387
  actionId: string
374
388
  correlation?: string
375
389
  }
390
+ trace?: TraceContext
376
391
  meta?: Record<string, unknown>
377
392
  }
378
393
 
@@ -397,6 +412,7 @@ export interface JobHandle<T = unknown> {
397
412
  startedAt?: string
398
413
  finishedAt?: string
399
414
  attempts: number
415
+ trace?: TraceContext
400
416
  }
401
417
 
402
418
  /**
@@ -411,6 +427,8 @@ export interface JobSpec {
411
427
  userId: string | null
412
428
  tenantId: string | null
413
429
  requestId?: string
430
+ trace?: TraceContext
431
+ originalProvenance?: Provenance
414
432
  }
415
433
  config: {
416
434
  queue?: string
@@ -926,6 +944,7 @@ export type AdapterKind =
926
944
  | 'ai'
927
945
  | 'ui'
928
946
  | 'schema'
947
+ | 'observability'
929
948
 
930
949
  /**
931
950
  * Resultado de health check de um adapter (ou do runtime agregado).
@@ -964,6 +983,8 @@ export interface ContextBase {
964
983
  provenance?: Provenance
965
984
  meta?: Record<string, unknown>
966
985
  requestId?: string
986
+ /** Trace pai recebido do transporte, evento ou job. */
987
+ trace?: TraceContext
967
988
  }
968
989
 
969
990
  /**
@@ -982,6 +1003,11 @@ export interface RuntimeRef {
982
1003
  input: unknown,
983
1004
  ctx: ContextBase,
984
1005
  ): Promise<ActionResult<unknown>>
1006
+ executeJob(
1007
+ spec: JobSpec,
1008
+ ctx: ContextBase,
1009
+ progress?: ProgressReporter,
1010
+ ): Promise<ActionResult<unknown>>
985
1011
  healthCheck(): Promise<{
986
1012
  ok: boolean
987
1013
  adapters: Record<string, HealthStatus>
@@ -1076,6 +1102,24 @@ export interface LoggerAdapter extends Adapter, Logger {
1076
1102
  kind: 'logger'
1077
1103
  }
1078
1104
 
1105
+ export interface ObservabilityOperation {
1106
+ name: string
1107
+ kind: 'action' | 'reaction'
1108
+ /** Como o driver determina outcome: action inspeciona `ActionResult.ok`; reaction usa reject. */
1109
+ resultKind: 'action-result' | 'void'
1110
+ parent?: TraceContext
1111
+ attributes?: Record<string, string | number | boolean>
1112
+ }
1113
+
1114
+ /** Porta de contexto ativo; drivers podem usar OTel, ALS ou outro mecanismo. */
1115
+ export interface ObservabilityAdapter extends Adapter {
1116
+ kind: 'observability'
1117
+ runInSpan<T>(
1118
+ operation: ObservabilityOperation,
1119
+ run: (trace: TraceContext) => Promise<T>,
1120
+ ): Promise<T>
1121
+ }
1122
+
1079
1123
  export interface QueueAdapter extends Adapter {
1080
1124
  kind: 'queue'
1081
1125
  enqueue(spec: JobSpec): Promise<JobHandle>