elasticdash-test 0.1.17 → 0.1.18-alpha

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 (95) hide show
  1. package/dist/capture/event.d.ts +5 -1
  2. package/dist/capture/event.d.ts.map +1 -1
  3. package/dist/cli.js +100 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/evaluators/llm-judge.js +17 -14
  6. package/dist/evaluators/types.d.ts +1 -0
  7. package/dist/execution/tool-runner.d.ts +26 -0
  8. package/dist/execution/tool-runner.d.ts.map +1 -0
  9. package/dist/execution/tool-runner.js +270 -0
  10. package/dist/execution/tool-runner.js.map +1 -0
  11. package/dist/http.d.ts +2 -0
  12. package/dist/http.d.ts.map +1 -1
  13. package/dist/http.js +2 -0
  14. package/dist/http.js.map +1 -1
  15. package/dist/index.cjs +4310 -2672
  16. package/dist/index.d.ts +10 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +7 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/interceptors/ai-interceptor.d.ts.map +1 -1
  21. package/dist/interceptors/ai-interceptor.js +97 -4
  22. package/dist/interceptors/ai-interceptor.js.map +1 -1
  23. package/dist/interceptors/db-auto.d.ts.map +1 -1
  24. package/dist/interceptors/db-auto.js +116 -24
  25. package/dist/interceptors/db-auto.js.map +1 -1
  26. package/dist/interceptors/db.d.ts +5 -0
  27. package/dist/interceptors/db.d.ts.map +1 -1
  28. package/dist/interceptors/db.js +93 -15
  29. package/dist/interceptors/db.js.map +1 -1
  30. package/dist/interceptors/http.d.ts.map +1 -1
  31. package/dist/interceptors/http.js +125 -93
  32. package/dist/interceptors/http.js.map +1 -1
  33. package/dist/interceptors/telemetry-push.d.ts +15 -0
  34. package/dist/interceptors/telemetry-push.d.ts.map +1 -1
  35. package/dist/interceptors/telemetry-push.js +96 -13
  36. package/dist/interceptors/telemetry-push.js.map +1 -1
  37. package/dist/interceptors/tool.d.ts.map +1 -1
  38. package/dist/interceptors/tool.js +42 -5
  39. package/dist/interceptors/tool.js.map +1 -1
  40. package/dist/interceptors/workflow-ai.d.ts.map +1 -1
  41. package/dist/interceptors/workflow-ai.js +46 -2
  42. package/dist/interceptors/workflow-ai.js.map +1 -1
  43. package/dist/observability.d.ts +69 -0
  44. package/dist/observability.d.ts.map +1 -0
  45. package/dist/observability.js +242 -0
  46. package/dist/observability.js.map +1 -0
  47. package/dist/portal-executor.d.ts +30 -0
  48. package/dist/portal-executor.d.ts.map +1 -0
  49. package/dist/portal-executor.js +304 -0
  50. package/dist/portal-executor.js.map +1 -0
  51. package/dist/portal-server.d.ts +3 -0
  52. package/dist/portal-server.d.ts.map +1 -0
  53. package/dist/portal-server.js +265 -0
  54. package/dist/portal-server.js.map +1 -0
  55. package/dist/telemetry-batcher.d.ts +43 -0
  56. package/dist/telemetry-batcher.d.ts.map +1 -0
  57. package/dist/telemetry-batcher.js +111 -0
  58. package/dist/telemetry-batcher.js.map +1 -0
  59. package/dist/trigger-executor.d.ts +12 -0
  60. package/dist/trigger-executor.d.ts.map +1 -0
  61. package/dist/trigger-executor.js +83 -0
  62. package/dist/trigger-executor.js.map +1 -0
  63. package/dist/types/portal.d.ts +64 -0
  64. package/dist/types/portal.d.ts.map +1 -0
  65. package/dist/types/portal.js +2 -0
  66. package/dist/types/portal.js.map +1 -0
  67. package/dist/utils/debug.d.ts +3 -0
  68. package/dist/utils/debug.d.ts.map +1 -0
  69. package/dist/utils/debug.js +8 -0
  70. package/dist/utils/debug.js.map +1 -0
  71. package/dist/utils/redact.d.ts +7 -0
  72. package/dist/utils/redact.d.ts.map +1 -0
  73. package/dist/utils/redact.js +26 -0
  74. package/dist/utils/redact.js.map +1 -0
  75. package/package.json +9 -1
  76. package/src/capture/event.ts +5 -1
  77. package/src/cli.ts +109 -0
  78. package/src/execution/tool-runner.ts +304 -0
  79. package/src/http.ts +2 -0
  80. package/src/index.ts +14 -0
  81. package/src/interceptors/ai-interceptor.ts +110 -4
  82. package/src/interceptors/db-auto.ts +121 -25
  83. package/src/interceptors/db.ts +92 -17
  84. package/src/interceptors/http.ts +145 -107
  85. package/src/interceptors/telemetry-push.ts +113 -13
  86. package/src/interceptors/tool.ts +42 -5
  87. package/src/interceptors/workflow-ai.ts +49 -2
  88. package/src/observability.ts +281 -0
  89. package/src/portal-executor.ts +335 -0
  90. package/src/portal-server.ts +290 -0
  91. package/src/telemetry-batcher.ts +143 -0
  92. package/src/trigger-executor.ts +121 -0
  93. package/src/types/portal.ts +67 -0
  94. package/src/utils/debug.ts +8 -0
  95. package/src/utils/redact.ts +25 -0
@@ -1,6 +1,8 @@
1
1
  import { getCurrentTrace } from '../trace-adapter/context.js'
2
2
  import { getCaptureContext } from '../capture/recorder.js'
3
3
  import { rawDateNow } from './side-effects.js'
4
+ import { getObservabilityContext, getHttpRunContext, getHttpFrozenEvent, pushTelemetryEvent, tryAutoInitHttpContext } from './telemetry-push.js'
5
+ import type { WorkflowEvent } from '../capture/event.js'
4
6
 
5
7
  type UsageInfo = { inputTokens?: number; outputTokens?: number; totalTokens?: number }
6
8
 
@@ -385,9 +387,12 @@ export function installAIInterceptor(): void {
385
387
 
386
388
  const provider = detectProvider(url)
387
389
  const traceAtCall = getCurrentTrace()
390
+ const obsCtx = getObservabilityContext()
388
391
 
389
- // No match or no active trace: pass through unchanged
390
- if (!provider || !traceAtCall) {
392
+ const httpCtx = getHttpRunContext()
393
+
394
+ // No match or no active context: pass through unchanged
395
+ if (!provider || (!traceAtCall && !obsCtx && !httpCtx)) {
391
396
  return originalFetch!(input, init)
392
397
  }
393
398
 
@@ -414,7 +419,106 @@ export function installAIInterceptor(): void {
414
419
 
415
420
  const ctx = getCaptureContext()
416
421
 
417
- if (ctx) {
422
+ // Observability-only mode: no trace handle, no capture context — record via pushTelemetryEvent
423
+ if (!traceAtCall && !ctx && obsCtx) {
424
+ const id = obsCtx.nextId()
425
+ const start = rawDateNow()
426
+ const eventInput = { url, provider, model, prompt, messages }
427
+
428
+ const response = await originalFetch!(input, init)
429
+
430
+ if (isStreaming && response.body) {
431
+ const [streamForCaller, streamForRecorder] = response.body.tee()
432
+ bufferSSEStream(provider, streamForRecorder).then((completion) => {
433
+ const durationMs = rawDateNow() - start
434
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { streamed: true, completion }, timestamp: start, durationMs })
435
+ }).catch(() => {
436
+ const durationMs = rawDateNow() - start
437
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs })
438
+ })
439
+ return new Response(streamForCaller, { status: response.status, statusText: response.statusText, headers: response.headers })
440
+ }
441
+
442
+ try {
443
+ const cloned = response.clone()
444
+ const responseBody = await cloned.json() as Record<string, unknown>
445
+ const completion = extractCompletion(provider, responseBody)
446
+ const usage = extractUsage(provider, responseBody)
447
+ const durationMs = rawDateNow() - start
448
+ const event: WorkflowEvent = {
449
+ id, type: 'ai', name: model, input: eventInput, output: { completion },
450
+ timestamp: start, durationMs,
451
+ ...(usage ? { usage } : {}),
452
+ }
453
+ pushTelemetryEvent(event)
454
+ } catch {
455
+ const durationMs = rawDateNow() - start
456
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, timestamp: start, durationMs })
457
+ }
458
+
459
+ return response
460
+ }
461
+
462
+ // HTTP mode (no capture context): replay frozen AI events or execute live + push telemetry
463
+ if (!ctx && httpCtx) {
464
+ const id = httpCtx.nextId()
465
+ const eventInput = { url, provider, model, prompt, messages }
466
+
467
+ // Replay frozen step
468
+ const frozen = getHttpFrozenEvent(id)
469
+ if (frozen && frozen.type === 'ai') {
470
+ pushTelemetryEvent(frozen)
471
+ const frozenOutput = frozen.output as Record<string, unknown> | null
472
+ const completion = frozenOutput ? extractCompletion(provider, frozenOutput) : '(replayed)'
473
+
474
+ if (isStreaming) {
475
+ return new Response(synthesizeSSEStream(provider, completion), {
476
+ status: 200,
477
+ headers: { 'Content-Type': provider === 'gemini' ? 'application/json' : 'text/event-stream' },
478
+ })
479
+ }
480
+
481
+ const body = frozenOutput?.streamed === true
482
+ ? synthesizeCompletionJSON(provider, completion)
483
+ : (frozenOutput ?? synthesizeCompletionJSON(provider, completion))
484
+ return new Response(JSON.stringify(body), {
485
+ status: 200,
486
+ headers: { 'Content-Type': 'application/json' },
487
+ })
488
+ }
489
+
490
+ // Not frozen → execute live, push telemetry
491
+ const start = rawDateNow()
492
+ const response = await originalFetch!(input, init)
493
+
494
+ if (isStreaming && response.body) {
495
+ const [streamForCaller, streamForRecorder] = response.body.tee()
496
+ bufferSSEStream(provider, streamForRecorder).then((completion) => {
497
+ const durationMs = rawDateNow() - start
498
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { streamed: true, completion }, timestamp: start, durationMs })
499
+ }).catch(() => {
500
+ const durationMs = rawDateNow() - start
501
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs })
502
+ })
503
+ return new Response(streamForCaller, { status: response.status, statusText: response.statusText, headers: response.headers })
504
+ }
505
+
506
+ try {
507
+ const cloned = response.clone()
508
+ const responseBody = await cloned.json() as Record<string, unknown>
509
+ const completion = extractCompletion(provider, responseBody)
510
+ const usage = extractUsage(provider, responseBody)
511
+ const durationMs = rawDateNow() - start
512
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: { completion }, timestamp: start, durationMs, ...(usage ? { usage } : {}) })
513
+ } catch {
514
+ const durationMs = rawDateNow() - start
515
+ pushTelemetryEvent({ id, type: 'ai', name: model, input: eventInput, output: null, timestamp: start, durationMs })
516
+ }
517
+
518
+ return response
519
+ }
520
+
521
+ if (ctx && traceAtCall) {
418
522
  const { recorder, replay } = ctx
419
523
  const id = recorder.nextId()
420
524
  const start = rawDateNow()
@@ -505,7 +609,9 @@ export function installAIInterceptor(): void {
505
609
  return response
506
610
  }
507
611
 
508
- // No capture context — original behaviour (outside of a workflow run)
612
+ // No capture context — original behaviour (trace handle only, outside of a workflow run)
613
+ if (!traceAtCall) return originalFetch!(input, init)
614
+
509
615
  const response = await originalFetch!(input, init)
510
616
 
511
617
  if (isStreaming && response.body) {
@@ -1,4 +1,7 @@
1
1
  import { getCaptureContext } from '../capture/recorder.js'
2
+ import { getCurrentTrace } from '../trace-adapter/context.js'
3
+ import { rawDateNow } from './side-effects.js'
4
+ import { getHttpRunContext, getHttpFrozenEvent, pushTelemetryEvent, tryAutoInitHttpContext, getObservabilityContext } from './telemetry-push.js'
2
5
 
3
6
  type AnyFn = (...args: unknown[]) => unknown
4
7
 
@@ -10,6 +13,14 @@ interface MethodPatch {
10
13
 
11
14
  const appliedPatches: MethodPatch[] = []
12
15
 
16
+ function toTraceArgs(input: unknown): Record<string, unknown> | undefined {
17
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
18
+ return input as Record<string, unknown>
19
+ }
20
+ if (input === undefined) return undefined
21
+ return { value: input }
22
+ }
23
+
13
24
  function wrapProtoMethod(proto: object, method: string, eventName: string): void {
14
25
  const p = proto as Record<string, unknown>
15
26
  if (typeof p[method] !== 'function') return
@@ -24,58 +35,143 @@ function wrapProtoMethod(proto: object, method: string, eventName: string): void
24
35
  }
25
36
 
26
37
  const ctx = getCaptureContext()
27
- if (!ctx) return original.apply(this, args)
38
+ const httpCtx = getHttpRunContext()
39
+ const obsCtx = getObservabilityContext()
40
+ const input = args.length === 1 ? args[0] : args
41
+
42
+ if (!ctx && !httpCtx && !obsCtx) return original.apply(this, args)
43
+
44
+ // Observability-only mode: record and push, no mocks/replay
45
+ if (!ctx && !httpCtx && obsCtx) {
46
+ const id = obsCtx.nextId()
47
+ const start = rawDateNow()
48
+
49
+ let result: unknown
50
+ try {
51
+ result = original.apply(this, args)
52
+ } catch (err) {
53
+ pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
54
+ throw err
55
+ }
56
+
57
+ if (result != null && typeof (result as Promise<unknown>).then === 'function') {
58
+ return (result as Promise<unknown>)
59
+ .then((output: unknown) => {
60
+ pushTelemetryEvent({ id, type: 'db', name: eventName, input, output, timestamp: start, durationMs: rawDateNow() - start })
61
+ return output
62
+ })
63
+ .catch((err: unknown) => {
64
+ pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
65
+ throw err
66
+ })
67
+ }
68
+
69
+ pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: result, timestamp: start, durationMs: rawDateNow() - start })
70
+ return result
71
+ }
28
72
 
29
- const { recorder, replay } = ctx
73
+ // HTTP mode (no capture context): replay frozen events or execute live
74
+ if (!ctx && httpCtx) {
75
+ const id = httpCtx.nextId()
76
+
77
+ // Replay frozen step
78
+ const frozen = getHttpFrozenEvent(id)
79
+ if (frozen && frozen.type === 'db') {
80
+ pushTelemetryEvent(frozen)
81
+ return Promise.resolve(frozen.output)
82
+ }
83
+
84
+ // Not frozen → execute live, push telemetry
85
+ const start = rawDateNow()
86
+
87
+ let result: unknown
88
+ try {
89
+ result = original.apply(this, args)
90
+ } catch (err) {
91
+ pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
92
+ throw err
93
+ }
94
+
95
+ if (result != null && typeof (result as Promise<unknown>).then === 'function') {
96
+ return (result as Promise<unknown>)
97
+ .then((output: unknown) => {
98
+ pushTelemetryEvent({ id, type: 'db', name: eventName, input, output, timestamp: start, durationMs: rawDateNow() - start })
99
+ return output
100
+ })
101
+ .catch((err: unknown) => {
102
+ pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs: rawDateNow() - start })
103
+ throw err
104
+ })
105
+ }
106
+
107
+ pushTelemetryEvent({ id, type: 'db', name: eventName, input, output: result, timestamp: start, durationMs: rawDateNow() - start })
108
+ return result
109
+ }
110
+
111
+ // Capture mode (enhanced with telemetry + TraceHandle)
112
+ const trace = getCurrentTrace()
113
+ const { recorder, replay } = ctx!
30
114
  const id = recorder.nextId()
31
115
 
32
116
  if (replay.shouldReplay(id)) {
33
117
  const historicalEvent = replay.getRecordedEvent(id)
34
118
  if (historicalEvent) recorder.record(historicalEvent)
35
- return Promise.resolve(replay.getRecordedResult(id))
119
+ if (httpCtx) pushTelemetryEvent(historicalEvent ?? { id, type: 'db', name: eventName, input, output: null, timestamp: rawDateNow(), durationMs: 0 })
120
+ const replayed = replay.getRecordedResult(id)
121
+ if (trace && typeof trace.recordToolCall === 'function') {
122
+ trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: replayed, workflowEventId: id })
123
+ }
124
+ return Promise.resolve(replayed)
36
125
  }
37
126
 
38
- const start = Date.now()
39
- const input = args.length === 1 ? args[0] : args
127
+ const start = rawDateNow()
40
128
 
41
129
  let result: unknown
42
130
  try {
43
131
  result = original.apply(this, args)
44
132
  } catch (err) {
45
- recorder.record({
46
- id, type: 'db', name: eventName,
47
- input, output: { error: String(err) },
48
- timestamp: start, durationMs: Date.now() - start,
49
- })
133
+ const durationMs = rawDateNow() - start
134
+ const event = { id, type: 'db' as const, name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs }
135
+ recorder.record(event)
136
+ if (httpCtx) pushTelemetryEvent(event)
137
+ if (trace && typeof trace.recordToolCall === 'function') {
138
+ trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: { error: String(err) }, workflowEventId: id, durationMs })
139
+ }
50
140
  throw err
51
141
  }
52
142
 
53
143
  if (result != null && typeof (result as Promise<unknown>).then === 'function') {
54
144
  return (result as Promise<unknown>)
55
145
  .then((output: unknown) => {
56
- recorder.record({
57
- id, type: 'db', name: eventName,
58
- input, output,
59
- timestamp: start, durationMs: Date.now() - start,
60
- })
146
+ const durationMs = rawDateNow() - start
147
+ const event = { id, type: 'db' as const, name: eventName, input, output, timestamp: start, durationMs }
148
+ recorder.record(event)
149
+ if (httpCtx) pushTelemetryEvent(event)
150
+ if (trace && typeof trace.recordToolCall === 'function') {
151
+ trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: output, workflowEventId: id, durationMs })
152
+ }
61
153
  return output
62
154
  })
63
155
  .catch((err: unknown) => {
64
- recorder.record({
65
- id, type: 'db', name: eventName,
66
- input, output: { error: String(err) },
67
- timestamp: start, durationMs: Date.now() - start,
68
- })
156
+ const durationMs = rawDateNow() - start
157
+ const event = { id, type: 'db' as const, name: eventName, input, output: { error: String(err) }, timestamp: start, durationMs }
158
+ recorder.record(event)
159
+ if (httpCtx) pushTelemetryEvent(event)
160
+ if (trace && typeof trace.recordToolCall === 'function') {
161
+ trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result: { error: String(err) }, workflowEventId: id, durationMs })
162
+ }
69
163
  throw err
70
164
  })
71
165
  }
72
166
 
73
167
  // Sync return (rare for DB calls)
74
- recorder.record({
75
- id, type: 'db', name: eventName,
76
- input, output: result,
77
- timestamp: start, durationMs: Date.now() - start,
78
- })
168
+ const durationMs = rawDateNow() - start
169
+ const event = { id, type: 'db' as const, name: eventName, input, output: result, timestamp: start, durationMs }
170
+ recorder.record(event)
171
+ if (httpCtx) pushTelemetryEvent(event)
172
+ if (trace && typeof trace.recordToolCall === 'function') {
173
+ trace.recordToolCall({ name: eventName, args: toTraceArgs(input), result, workflowEventId: id, durationMs })
174
+ }
79
175
  return result
80
176
  }
81
177
  }
@@ -1,9 +1,25 @@
1
1
  import { getCaptureContext } from '../capture/recorder.js'
2
+ import { getCurrentTrace } from '../trace-adapter/context.js'
3
+ import { rawDateNow } from './side-effects.js'
4
+ import { getHttpRunContext, getHttpFrozenEvent, pushTelemetryEvent, tryAutoInitHttpContext, getObservabilityContext } from './telemetry-push.js'
5
+
6
+ function toTraceArgs(input: unknown): Record<string, unknown> | undefined {
7
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
8
+ return input as Record<string, unknown>
9
+ }
10
+ if (input === undefined) return undefined
11
+ return { value: input }
12
+ }
2
13
 
3
14
  /**
4
15
  * Wraps named methods on a DB client instance in-place so their calls are
5
16
  * recorded as "db" events. Returns the same client object.
6
17
  *
18
+ * Supports all three execution modes:
19
+ * - Capture mode: replay from TraceRecorder / ReplayController
20
+ * - HTTP mode: replay frozen events from dashboard, push telemetry
21
+ * - Observability mode: record and push telemetry
22
+ *
7
23
  * @param client Any DB client (pg.Client, redis client, mongoose Model, etc.)
8
24
  * @param methodNames Method names to wrap
9
25
  * @param label Optional label prefix for event names (defaults to constructor name)
@@ -20,30 +36,89 @@ export function wrapDB<T extends object>(
20
36
  if (typeof original !== 'function') continue
21
37
 
22
38
  ;(client as Record<string, unknown>)[method] = async (...args: unknown[]) => {
39
+ await tryAutoInitHttpContext()
23
40
  const ctx = getCaptureContext()
24
- if (!ctx) return (original as (...a: unknown[]) => unknown).apply(client, args)
41
+ const httpCtx = getHttpRunContext()
42
+ const obsCtx = getObservabilityContext()
43
+ const name = `${prefix}.${method}`
44
+ const input = args.length === 1 ? args[0] : args
45
+
46
+ if (!ctx && !httpCtx && !obsCtx) return (original as (...a: unknown[]) => unknown).apply(client, args)
47
+
48
+ // Observability-only mode: record and push, no mocks/replay
49
+ if (!ctx && !httpCtx && obsCtx) {
50
+ const id = obsCtx.nextId()
51
+ const start = rawDateNow()
52
+ try {
53
+ const output = await (original as (...a: unknown[]) => unknown).apply(client, args)
54
+ pushTelemetryEvent({ id, type: 'db', name, input, output, timestamp: start, durationMs: rawDateNow() - start })
55
+ return output
56
+ } catch (e) {
57
+ pushTelemetryEvent({ id, type: 'db', name, input, output: { error: String(e) }, timestamp: start, durationMs: rawDateNow() - start })
58
+ throw e
59
+ }
60
+ }
61
+
62
+ // HTTP mode (no capture context): replay frozen events or execute live
63
+ if (!ctx && httpCtx) {
64
+ const id = httpCtx.nextId()
25
65
 
26
- const { recorder, replay } = ctx
66
+ // Replay frozen step
67
+ const frozen = getHttpFrozenEvent(id)
68
+ if (frozen && frozen.type === 'db') {
69
+ pushTelemetryEvent(frozen)
70
+ return frozen.output
71
+ }
72
+
73
+ // Not frozen → execute live, push telemetry
74
+ const start = rawDateNow()
75
+ try {
76
+ const output = await (original as (...a: unknown[]) => unknown).apply(client, args)
77
+ pushTelemetryEvent({ id, type: 'db', name, input, output, timestamp: start, durationMs: rawDateNow() - start })
78
+ return output
79
+ } catch (e) {
80
+ pushTelemetryEvent({ id, type: 'db', name, input, output: { error: String(e) }, timestamp: start, durationMs: rawDateNow() - start })
81
+ throw e
82
+ }
83
+ }
84
+
85
+ // Capture mode
86
+ const trace = getCurrentTrace()
87
+ const { recorder, replay } = ctx!
27
88
  const id = recorder.nextId()
28
- const name = `${prefix}.${method}`
29
89
 
30
90
  if (replay.shouldReplay(id)) {
31
- return replay.getRecordedResult(id)
91
+ const historical = replay.getRecordedEvent(id)
92
+ if (historical) recorder.record(historical)
93
+ if (httpCtx) pushTelemetryEvent(historical ?? { id, type: 'db', name, input, output: null, timestamp: rawDateNow(), durationMs: 0 })
94
+ const replayed = replay.getRecordedResult(id)
95
+ if (trace && typeof trace.recordToolCall === 'function') {
96
+ trace.recordToolCall({ name, args: toTraceArgs(input), result: replayed, workflowEventId: id })
97
+ }
98
+ return replayed
32
99
  }
33
100
 
34
- const start = Date.now()
35
- const output = await (original as (...a: unknown[]) => unknown).apply(client, args)
36
- recorder.record({
37
- id,
38
- type: 'db',
39
- name,
40
- input: args.length === 1 ? args[0] : args,
41
- output,
42
- timestamp: start,
43
- durationMs: Date.now() - start,
44
- })
45
-
46
- return output
101
+ const start = rawDateNow()
102
+ try {
103
+ const output = await (original as (...a: unknown[]) => unknown).apply(client, args)
104
+ const durationMs = rawDateNow() - start
105
+ const event = { id, type: 'db' as const, name, input, output, timestamp: start, durationMs }
106
+ recorder.record(event)
107
+ if (httpCtx) pushTelemetryEvent(event)
108
+ if (trace && typeof trace.recordToolCall === 'function') {
109
+ trace.recordToolCall({ name, args: toTraceArgs(input), result: output, workflowEventId: id, durationMs })
110
+ }
111
+ return output
112
+ } catch (e) {
113
+ const durationMs = rawDateNow() - start
114
+ const event = { id, type: 'db' as const, name, input, output: { error: String(e) }, timestamp: start, durationMs }
115
+ recorder.record(event)
116
+ if (httpCtx) pushTelemetryEvent(event)
117
+ if (trace && typeof trace.recordToolCall === 'function') {
118
+ trace.recordToolCall({ name, args: toTraceArgs(input), result: { error: String(e) }, workflowEventId: id, durationMs })
119
+ }
120
+ throw e
121
+ }
47
122
  }
48
123
  }
49
124