elasticdash-sdk 0.2.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 (349) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +775 -0
  3. package/dist/browser-ui.d.ts +43 -0
  4. package/dist/browser-ui.d.ts.map +1 -0
  5. package/dist/browser-ui.js +246 -0
  6. package/dist/browser-ui.js.map +1 -0
  7. package/dist/capture/event.d.ts +33 -0
  8. package/dist/capture/event.d.ts.map +1 -0
  9. package/dist/capture/event.js +2 -0
  10. package/dist/capture/event.js.map +1 -0
  11. package/dist/capture/index.d.ts +4 -0
  12. package/dist/capture/index.d.ts.map +1 -0
  13. package/dist/capture/index.js +4 -0
  14. package/dist/capture/index.js.map +1 -0
  15. package/dist/capture/recorder.d.ts +24 -0
  16. package/dist/capture/recorder.d.ts.map +1 -0
  17. package/dist/capture/recorder.js +46 -0
  18. package/dist/capture/recorder.js.map +1 -0
  19. package/dist/capture/replay.d.ts +20 -0
  20. package/dist/capture/replay.d.ts.map +1 -0
  21. package/dist/capture/replay.js +47 -0
  22. package/dist/capture/replay.js.map +1 -0
  23. package/dist/ci/api-client.d.ts +38 -0
  24. package/dist/ci/api-client.d.ts.map +1 -0
  25. package/dist/ci/api-client.js +96 -0
  26. package/dist/ci/api-client.js.map +1 -0
  27. package/dist/ci/benchmark.d.ts +33 -0
  28. package/dist/ci/benchmark.d.ts.map +1 -0
  29. package/dist/ci/benchmark.js +213 -0
  30. package/dist/ci/benchmark.js.map +1 -0
  31. package/dist/ci/ed-runner.d.ts +48 -0
  32. package/dist/ci/ed-runner.d.ts.map +1 -0
  33. package/dist/ci/ed-runner.js +260 -0
  34. package/dist/ci/ed-runner.js.map +1 -0
  35. package/dist/ci/executor.d.ts +13 -0
  36. package/dist/ci/executor.d.ts.map +1 -0
  37. package/dist/ci/executor.js +542 -0
  38. package/dist/ci/executor.js.map +1 -0
  39. package/dist/ci/git-info.d.ts +17 -0
  40. package/dist/ci/git-info.d.ts.map +1 -0
  41. package/dist/ci/git-info.js +102 -0
  42. package/dist/ci/git-info.js.map +1 -0
  43. package/dist/ci/index.d.ts +6 -0
  44. package/dist/ci/index.d.ts.map +1 -0
  45. package/dist/ci/index.js +4 -0
  46. package/dist/ci/index.js.map +1 -0
  47. package/dist/ci/measurement.d.ts +9 -0
  48. package/dist/ci/measurement.d.ts.map +1 -0
  49. package/dist/ci/measurement.js +15 -0
  50. package/dist/ci/measurement.js.map +1 -0
  51. package/dist/ci/replay.d.ts +31 -0
  52. package/dist/ci/replay.d.ts.map +1 -0
  53. package/dist/ci/replay.js +96 -0
  54. package/dist/ci/replay.js.map +1 -0
  55. package/dist/ci/reporters/default.d.ts +8 -0
  56. package/dist/ci/reporters/default.d.ts.map +1 -0
  57. package/dist/ci/reporters/default.js +46 -0
  58. package/dist/ci/reporters/default.js.map +1 -0
  59. package/dist/ci/reporters/index.d.ts +8 -0
  60. package/dist/ci/reporters/index.d.ts.map +1 -0
  61. package/dist/ci/reporters/index.js +14 -0
  62. package/dist/ci/reporters/index.js.map +1 -0
  63. package/dist/ci/reporters/json.d.ts +8 -0
  64. package/dist/ci/reporters/json.d.ts.map +1 -0
  65. package/dist/ci/reporters/json.js +14 -0
  66. package/dist/ci/reporters/json.js.map +1 -0
  67. package/dist/ci/reporters/junit.d.ts +8 -0
  68. package/dist/ci/reporters/junit.d.ts.map +1 -0
  69. package/dist/ci/reporters/junit.js +48 -0
  70. package/dist/ci/reporters/junit.js.map +1 -0
  71. package/dist/ci/runner.d.ts +3 -0
  72. package/dist/ci/runner.d.ts.map +1 -0
  73. package/dist/ci/runner.js +187 -0
  74. package/dist/ci/runner.js.map +1 -0
  75. package/dist/ci/test-discovery.d.ts +5 -0
  76. package/dist/ci/test-discovery.d.ts.map +1 -0
  77. package/dist/ci/test-discovery.js +11 -0
  78. package/dist/ci/test-discovery.js.map +1 -0
  79. package/dist/ci/test-loader.d.ts +19 -0
  80. package/dist/ci/test-loader.d.ts.map +1 -0
  81. package/dist/ci/test-loader.js +149 -0
  82. package/dist/ci/test-loader.js.map +1 -0
  83. package/dist/ci/test-registry.d.ts +42 -0
  84. package/dist/ci/test-registry.d.ts.map +1 -0
  85. package/dist/ci/test-registry.js +18 -0
  86. package/dist/ci/test-registry.js.map +1 -0
  87. package/dist/ci/trace-schema.d.ts +30 -0
  88. package/dist/ci/trace-schema.d.ts.map +1 -0
  89. package/dist/ci/trace-schema.js +66 -0
  90. package/dist/ci/trace-schema.js.map +1 -0
  91. package/dist/ci/trace-writer.d.ts +16 -0
  92. package/dist/ci/trace-writer.d.ts.map +1 -0
  93. package/dist/ci/trace-writer.js +108 -0
  94. package/dist/ci/trace-writer.js.map +1 -0
  95. package/dist/ci/types.d.ts +108 -0
  96. package/dist/ci/types.d.ts.map +1 -0
  97. package/dist/ci/types.js +3 -0
  98. package/dist/ci/types.js.map +1 -0
  99. package/dist/ci/upload-client.d.ts +74 -0
  100. package/dist/ci/upload-client.d.ts.map +1 -0
  101. package/dist/ci/upload-client.js +195 -0
  102. package/dist/ci/upload-client.js.map +1 -0
  103. package/dist/cli.d.ts +3 -0
  104. package/dist/cli.d.ts.map +1 -0
  105. package/dist/cli.js +716 -0
  106. package/dist/cli.js.map +1 -0
  107. package/dist/core/agent-state.d.ts +47 -0
  108. package/dist/core/agent-state.d.ts.map +1 -0
  109. package/dist/core/agent-state.js +137 -0
  110. package/dist/core/agent-state.js.map +1 -0
  111. package/dist/core/judge-utils.d.ts +22 -0
  112. package/dist/core/judge-utils.d.ts.map +1 -0
  113. package/dist/core/judge-utils.js +211 -0
  114. package/dist/core/judge-utils.js.map +1 -0
  115. package/dist/core/registry.d.ts +28 -0
  116. package/dist/core/registry.d.ts.map +1 -0
  117. package/dist/core/registry.js +52 -0
  118. package/dist/core/registry.js.map +1 -0
  119. package/dist/dashboard-server.d.ts +65 -0
  120. package/dist/dashboard-server.d.ts.map +1 -0
  121. package/dist/dashboard-server.js +3940 -0
  122. package/dist/dashboard-server.js.map +1 -0
  123. package/dist/execution/tool-runner.d.ts +26 -0
  124. package/dist/execution/tool-runner.d.ts.map +1 -0
  125. package/dist/execution/tool-runner.js +316 -0
  126. package/dist/execution/tool-runner.js.map +1 -0
  127. package/dist/html/dashboard.html +2218 -0
  128. package/dist/http.d.ts +14 -0
  129. package/dist/http.d.ts.map +1 -0
  130. package/dist/http.js +13 -0
  131. package/dist/http.js.map +1 -0
  132. package/dist/index.cjs +8102 -0
  133. package/dist/index.d.ts +61 -0
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +67 -0
  136. package/dist/index.js.map +1 -0
  137. package/dist/interceptors/ai-interceptor.d.ts +26 -0
  138. package/dist/interceptors/ai-interceptor.d.ts.map +1 -0
  139. package/dist/interceptors/ai-interceptor.js +756 -0
  140. package/dist/interceptors/ai-interceptor.js.map +1 -0
  141. package/dist/interceptors/db-auto.d.ts +8 -0
  142. package/dist/interceptors/db-auto.d.ts.map +1 -0
  143. package/dist/interceptors/db-auto.js +217 -0
  144. package/dist/interceptors/db-auto.js.map +1 -0
  145. package/dist/interceptors/db.d.ts +23 -0
  146. package/dist/interceptors/db.d.ts.map +1 -0
  147. package/dist/interceptors/db.js +137 -0
  148. package/dist/interceptors/db.js.map +1 -0
  149. package/dist/interceptors/http.d.ts +28 -0
  150. package/dist/interceptors/http.d.ts.map +1 -0
  151. package/dist/interceptors/http.js +356 -0
  152. package/dist/interceptors/http.js.map +1 -0
  153. package/dist/interceptors/side-effects.d.ts +7 -0
  154. package/dist/interceptors/side-effects.d.ts.map +1 -0
  155. package/dist/interceptors/side-effects.js +72 -0
  156. package/dist/interceptors/side-effects.js.map +1 -0
  157. package/dist/interceptors/telemetry-push.d.ts +142 -0
  158. package/dist/interceptors/telemetry-push.d.ts.map +1 -0
  159. package/dist/interceptors/telemetry-push.js +463 -0
  160. package/dist/interceptors/telemetry-push.js.map +1 -0
  161. package/dist/interceptors/tool.d.ts +2 -0
  162. package/dist/interceptors/tool.d.ts.map +1 -0
  163. package/dist/interceptors/tool.js +274 -0
  164. package/dist/interceptors/tool.js.map +1 -0
  165. package/dist/interceptors/workflow-ai.d.ts +5 -0
  166. package/dist/interceptors/workflow-ai.d.ts.map +1 -0
  167. package/dist/interceptors/workflow-ai.js +382 -0
  168. package/dist/interceptors/workflow-ai.js.map +1 -0
  169. package/dist/internals/conditional-recorder.d.ts +21 -0
  170. package/dist/internals/conditional-recorder.d.ts.map +1 -0
  171. package/dist/internals/conditional-recorder.js +54 -0
  172. package/dist/internals/conditional-recorder.js.map +1 -0
  173. package/dist/internals/mock-resolver.d.ts +146 -0
  174. package/dist/internals/mock-resolver.d.ts.map +1 -0
  175. package/dist/internals/mock-resolver.js +427 -0
  176. package/dist/internals/mock-resolver.js.map +1 -0
  177. package/dist/matchers/index.d.ts +96 -0
  178. package/dist/matchers/index.d.ts.map +1 -0
  179. package/dist/matchers/index.js +668 -0
  180. package/dist/matchers/index.js.map +1 -0
  181. package/dist/observability.d.ts +82 -0
  182. package/dist/observability.d.ts.map +1 -0
  183. package/dist/observability.js +471 -0
  184. package/dist/observability.js.map +1 -0
  185. package/dist/portal-executor.d.ts +30 -0
  186. package/dist/portal-executor.d.ts.map +1 -0
  187. package/dist/portal-executor.js +324 -0
  188. package/dist/portal-executor.js.map +1 -0
  189. package/dist/portal-server.d.ts +3 -0
  190. package/dist/portal-server.d.ts.map +1 -0
  191. package/dist/portal-server.js +279 -0
  192. package/dist/portal-server.js.map +1 -0
  193. package/dist/proxy/llm-capture.d.ts +14 -0
  194. package/dist/proxy/llm-capture.d.ts.map +1 -0
  195. package/dist/proxy/llm-capture.js +264 -0
  196. package/dist/proxy/llm-capture.js.map +1 -0
  197. package/dist/reporter.d.ts +3 -0
  198. package/dist/reporter.d.ts.map +1 -0
  199. package/dist/reporter.js +72 -0
  200. package/dist/reporter.js.map +1 -0
  201. package/dist/runWorkflowSubprocess.d.ts +14 -0
  202. package/dist/runWorkflowSubprocess.d.ts.map +1 -0
  203. package/dist/runWorkflowSubprocess.js +66 -0
  204. package/dist/runWorkflowSubprocess.js.map +1 -0
  205. package/dist/runner.d.ts +16 -0
  206. package/dist/runner.d.ts.map +1 -0
  207. package/dist/runner.js +138 -0
  208. package/dist/runner.js.map +1 -0
  209. package/dist/socket-connector.d.ts +22 -0
  210. package/dist/socket-connector.d.ts.map +1 -0
  211. package/dist/socket-connector.js +104 -0
  212. package/dist/socket-connector.js.map +1 -0
  213. package/dist/telemetry-batcher.d.ts +56 -0
  214. package/dist/telemetry-batcher.d.ts.map +1 -0
  215. package/dist/telemetry-batcher.js +143 -0
  216. package/dist/telemetry-batcher.js.map +1 -0
  217. package/dist/test-setup.d.ts +12 -0
  218. package/dist/test-setup.d.ts.map +1 -0
  219. package/dist/test-setup.js +13 -0
  220. package/dist/test-setup.js.map +1 -0
  221. package/dist/tool-registry.d.ts +31 -0
  222. package/dist/tool-registry.d.ts.map +1 -0
  223. package/dist/tool-registry.js +73 -0
  224. package/dist/tool-registry.js.map +1 -0
  225. package/dist/tool-runner-worker.d.ts +2 -0
  226. package/dist/tool-runner-worker.d.ts.map +1 -0
  227. package/dist/tool-runner-worker.js +215 -0
  228. package/dist/tool-runner-worker.js.map +1 -0
  229. package/dist/trace-adapter/context.d.ts +72 -0
  230. package/dist/trace-adapter/context.d.ts.map +1 -0
  231. package/dist/trace-adapter/context.js +80 -0
  232. package/dist/trace-adapter/context.js.map +1 -0
  233. package/dist/tracing.d.ts +2 -0
  234. package/dist/tracing.d.ts.map +1 -0
  235. package/dist/tracing.js +59 -0
  236. package/dist/tracing.js.map +1 -0
  237. package/dist/trigger-executor.d.ts +12 -0
  238. package/dist/trigger-executor.d.ts.map +1 -0
  239. package/dist/trigger-executor.js +130 -0
  240. package/dist/trigger-executor.js.map +1 -0
  241. package/dist/types/portal.d.ts +76 -0
  242. package/dist/types/portal.d.ts.map +1 -0
  243. package/dist/types/portal.js +2 -0
  244. package/dist/types/portal.js.map +1 -0
  245. package/dist/utils/debug.d.ts +3 -0
  246. package/dist/utils/debug.d.ts.map +1 -0
  247. package/dist/utils/debug.js +8 -0
  248. package/dist/utils/debug.js.map +1 -0
  249. package/dist/utils/license-error.d.ts +23 -0
  250. package/dist/utils/license-error.d.ts.map +1 -0
  251. package/dist/utils/license-error.js +42 -0
  252. package/dist/utils/license-error.js.map +1 -0
  253. package/dist/utils/redact.d.ts +7 -0
  254. package/dist/utils/redact.d.ts.map +1 -0
  255. package/dist/utils/redact.js +26 -0
  256. package/dist/utils/redact.js.map +1 -0
  257. package/dist/workflow-runner-worker.d.ts +2 -0
  258. package/dist/workflow-runner-worker.d.ts.map +1 -0
  259. package/dist/workflow-runner-worker.js +329 -0
  260. package/dist/workflow-runner-worker.js.map +1 -0
  261. package/dist/workflow-runner.d.ts +14 -0
  262. package/dist/workflow-runner.d.ts.map +1 -0
  263. package/dist/workflow-runner.js +34 -0
  264. package/dist/workflow-runner.js.map +1 -0
  265. package/docs/agent-coding-instructions.md +138 -0
  266. package/docs/agent-integration-guide.md +564 -0
  267. package/docs/agents.md +140 -0
  268. package/docs/dashboard.md +394 -0
  269. package/docs/deno.md +69 -0
  270. package/docs/instrumentation.md +424 -0
  271. package/docs/langfuse-trace-structure.md +145 -0
  272. package/docs/matchers.md +173 -0
  273. package/docs/observability_contract.md +192 -0
  274. package/docs/observability_mode.md +195 -0
  275. package/docs/quickstart.md +621 -0
  276. package/docs/security-compliance.md +566 -0
  277. package/docs/test-writing-guidelines.md +444 -0
  278. package/docs/tools.md +165 -0
  279. package/docs/workflow-modes.md +253 -0
  280. package/package.json +76 -0
  281. package/src/browser-ui.ts +281 -0
  282. package/src/capture/event.ts +30 -0
  283. package/src/capture/index.ts +3 -0
  284. package/src/capture/recorder.ts +62 -0
  285. package/src/capture/replay.ts +55 -0
  286. package/src/ci/api-client.ts +136 -0
  287. package/src/ci/benchmark.ts +257 -0
  288. package/src/ci/ed-runner.ts +351 -0
  289. package/src/ci/executor.ts +671 -0
  290. package/src/ci/git-info.ts +127 -0
  291. package/src/ci/index.ts +5 -0
  292. package/src/ci/measurement.ts +25 -0
  293. package/src/ci/replay.ts +127 -0
  294. package/src/ci/reporters/default.ts +50 -0
  295. package/src/ci/reporters/index.ts +21 -0
  296. package/src/ci/reporters/json.ts +18 -0
  297. package/src/ci/reporters/junit.ts +61 -0
  298. package/src/ci/runner.ts +208 -0
  299. package/src/ci/test-discovery.ts +16 -0
  300. package/src/ci/test-loader.ts +187 -0
  301. package/src/ci/test-registry.ts +62 -0
  302. package/src/ci/trace-schema.ts +96 -0
  303. package/src/ci/trace-writer.ts +107 -0
  304. package/src/ci/types.ts +115 -0
  305. package/src/ci/upload-client.ts +300 -0
  306. package/src/cli.ts +811 -0
  307. package/src/core/agent-state.ts +162 -0
  308. package/src/core/judge-utils.ts +232 -0
  309. package/src/core/registry.ts +92 -0
  310. package/src/dashboard-server.ts +2047 -0
  311. package/src/execution/tool-runner.ts +352 -0
  312. package/src/html/dashboard.html +2218 -0
  313. package/src/http.ts +13 -0
  314. package/src/index.ts +138 -0
  315. package/src/interceptors/ai-interceptor.ts +798 -0
  316. package/src/interceptors/db-auto.ts +243 -0
  317. package/src/interceptors/db.ts +156 -0
  318. package/src/interceptors/http.ts +393 -0
  319. package/src/interceptors/side-effects.ts +83 -0
  320. package/src/interceptors/telemetry-push.ts +537 -0
  321. package/src/interceptors/tool.ts +287 -0
  322. package/src/interceptors/workflow-ai.ts +419 -0
  323. package/src/internals/conditional-recorder.ts +63 -0
  324. package/src/internals/mock-resolver.ts +492 -0
  325. package/src/matchers/index.ts +824 -0
  326. package/src/observability.ts +501 -0
  327. package/src/portal-executor.ts +355 -0
  328. package/src/portal-server.ts +304 -0
  329. package/src/proxy/llm-capture.ts +301 -0
  330. package/src/reporter.ts +81 -0
  331. package/src/runWorkflowSubprocess.ts +74 -0
  332. package/src/runner.ts +178 -0
  333. package/src/socket-connector.ts +117 -0
  334. package/src/telemetry-batcher.ts +191 -0
  335. package/src/test-setup.ts +16 -0
  336. package/src/tool-registry.ts +94 -0
  337. package/src/tool-runner-worker.ts +244 -0
  338. package/src/trace-adapter/context.ts +156 -0
  339. package/src/tracing.ts +62 -0
  340. package/src/trigger-executor.ts +171 -0
  341. package/src/types/agent.d.ts +63 -0
  342. package/src/types/expect.d.ts +81 -0
  343. package/src/types/modules.d.ts +2 -0
  344. package/src/types/portal.ts +69 -0
  345. package/src/utils/debug.ts +8 -0
  346. package/src/utils/license-error.ts +43 -0
  347. package/src/utils/redact.ts +25 -0
  348. package/src/workflow-runner-worker.ts +386 -0
  349. package/src/workflow-runner.ts +58 -0
@@ -0,0 +1,419 @@
1
+ import { getCaptureContext } from '../capture/recorder.js'
2
+ import { rawDateNow } from './side-effects.js'
3
+ import { getHttpRunContext, getHttpFrozenEvent, getHttpPromptMock, getHttpUserPromptMock, getHttpAIMock, pushTelemetryEvent, tryAutoInitHttpContext, getObservabilityContext } from './telemetry-push.js'
4
+ import { resolveAIMock, resolvePromptMock, resolveUserPromptMock } from '../internals/mock-resolver.js'
5
+ import { consumeCapturedLLMRequest } from './ai-interceptor.js'
6
+ import { getEdReplayContext, replayCall } from '../ci/replay.js'
7
+ import type { WorkflowEvent } from '../capture/event.js'
8
+
9
+ /**
10
+ * Tracks whether we are currently inside a `wrapAI` execution.
11
+ * When active, `ai-interceptor` should skip recording to avoid duplicates.
12
+ * Uses a counter (not boolean) to handle nested wrapAI calls correctly.
13
+ */
14
+ const AI_WRAPPER_KEY = '__elasticdash_ai_wrapper_depth__'
15
+ const g = globalThis as Record<string, unknown>
16
+ if (g[AI_WRAPPER_KEY] == null) g[AI_WRAPPER_KEY] = 0
17
+
18
+ function enterAIWrapper(): void {
19
+ g[AI_WRAPPER_KEY] = ((g[AI_WRAPPER_KEY] as number) ?? 0) + 1
20
+ }
21
+
22
+ function leaveAIWrapper(): void {
23
+ g[AI_WRAPPER_KEY] = Math.max(0, ((g[AI_WRAPPER_KEY] as number) ?? 0) - 1)
24
+ }
25
+
26
+ type UsageInfo = { inputTokens?: number; outputTokens?: number; totalTokens?: number }
27
+
28
+ function extractUsage(output: unknown): UsageInfo | undefined {
29
+ if (!output || typeof output !== 'object') return undefined
30
+ const o = output as Record<string, unknown>
31
+ if (o.usage && typeof o.usage === 'object') {
32
+ const u = o.usage as Record<string, number>
33
+ // Anthropic SDK: input_tokens / output_tokens
34
+ if (u.input_tokens != null || u.output_tokens != null) {
35
+ return {
36
+ inputTokens: u.input_tokens,
37
+ outputTokens: u.output_tokens,
38
+ totalTokens: (u.input_tokens ?? 0) + (u.output_tokens ?? 0),
39
+ }
40
+ }
41
+ // OpenAI SDK: prompt_tokens / completion_tokens
42
+ if (u.prompt_tokens != null || u.completion_tokens != null) {
43
+ return {
44
+ inputTokens: u.prompt_tokens,
45
+ outputTokens: u.completion_tokens,
46
+ totalTokens: u.total_tokens,
47
+ }
48
+ }
49
+ // Vercel AI SDK: { inputTokens, outputTokens }
50
+ if (u.inputTokens != null || u.outputTokens != null) {
51
+ return {
52
+ inputTokens: u.inputTokens,
53
+ outputTokens: u.outputTokens,
54
+ totalTokens: (u.inputTokens ?? 0) + (u.outputTokens ?? 0),
55
+ }
56
+ }
57
+ }
58
+ // Gemini SDK: usageMetadata
59
+ if (o.usageMetadata && typeof o.usageMetadata === 'object') {
60
+ const u = o.usageMetadata as Record<string, number>
61
+ return {
62
+ inputTokens: u.promptTokenCount,
63
+ outputTokens: u.candidatesTokenCount,
64
+ totalTokens: u.totalTokenCount,
65
+ }
66
+ }
67
+ // Flat token fields (e.g. { tokens: N } or { outputTokens: N, inputTokens: N })
68
+ if (typeof o.tokens === 'number' || typeof o.outputTokens === 'number') {
69
+ return {
70
+ inputTokens: typeof o.inputTokens === 'number' ? o.inputTokens : undefined,
71
+ outputTokens: typeof o.outputTokens === 'number' ? o.outputTokens : (typeof o.tokens === 'number' ? o.tokens : undefined),
72
+ totalTokens: (typeof o.inputTokens === 'number' ? o.inputTokens : 0) + (typeof o.outputTokens === 'number' ? o.outputTokens : (typeof o.tokens === 'number' ? o.tokens : 0)),
73
+ }
74
+ }
75
+ return undefined
76
+ }
77
+
78
+ /**
79
+ * After callFn returns, consume any captured LLM request from ai-interceptor
80
+ * and merge it into the event input so the recorded event contains the actual
81
+ * HTTP payload sent to the LLM (messages, model, parameters).
82
+ * Also returns captured usage when the app's output doesn't include it.
83
+ */
84
+ function enrichFromLLMCapture(
85
+ input: unknown,
86
+ appUsage: UsageInfo | undefined,
87
+ fallbackModel?: string,
88
+ fallbackProvider?: string,
89
+ ): { input: unknown; usage: UsageInfo | undefined } {
90
+ const captured = consumeCapturedLLMRequest()
91
+ if (captured) {
92
+ const enrichedInput = (input && typeof input === 'object')
93
+ ? { ...(input as Record<string, unknown>), llmRequest: captured.body, promptSnippet: captured.promptSnippet }
94
+ : { originalInput: input, llmRequest: captured.body, promptSnippet: captured.promptSnippet }
95
+ const usage = appUsage ?? captured.usage
96
+ return { input: enrichedInput, usage }
97
+ }
98
+
99
+ // Interceptor capture failed (e.g. OpenAI SDK uses native fetch not globalThis.fetch).
100
+ // Fall back to model/provider from wrapAI options or function arguments.
101
+ const resolvedModel = fallbackModel
102
+ || (input && typeof input === 'object' ? (input as Record<string, unknown>).model as string | undefined : undefined)
103
+ const resolvedProvider = fallbackProvider
104
+ || (input && typeof input === 'object' ? (input as Record<string, unknown>).provider as string | undefined : undefined)
105
+
106
+ if (resolvedModel || resolvedProvider) {
107
+ const enrichedInput = (input && typeof input === 'object')
108
+ ? { ...(input as Record<string, unknown>), llmRequest: { model: resolvedModel, provider: resolvedProvider } }
109
+ : { originalInput: input, llmRequest: { model: resolvedModel, provider: resolvedProvider } }
110
+ return { input: enrichedInput, usage: appUsage }
111
+ }
112
+
113
+ return { input, usage: appUsage }
114
+ }
115
+
116
+ function isReadableStream(v: unknown): v is ReadableStream<Uint8Array> {
117
+ return (
118
+ typeof v === 'object' &&
119
+ v !== null &&
120
+ typeof (v as ReadableStream).getReader === 'function' &&
121
+ typeof (v as ReadableStream).tee === 'function'
122
+ )
123
+ }
124
+
125
+ function isAsyncIterable(v: unknown): v is AsyncIterable<unknown> {
126
+ return typeof v === 'object' && v !== null && Symbol.asyncIterator in (v as object)
127
+ }
128
+
129
+ async function bufferReadableStream(stream: ReadableStream<Uint8Array>): Promise<string> {
130
+ const decoder = new TextDecoder()
131
+ const reader = stream.getReader()
132
+ let raw = ''
133
+ try {
134
+ for (;;) {
135
+ const { done, value } = await reader.read()
136
+ if (done) break
137
+ raw += decoder.decode(value, { stream: true })
138
+ }
139
+ } finally {
140
+ reader.releaseLock()
141
+ }
142
+ return raw
143
+ }
144
+
145
+ function reconstructStream(raw: string): ReadableStream<Uint8Array> {
146
+ const encoder = new TextEncoder()
147
+ return new ReadableStream<Uint8Array>({
148
+ start(ctrl) {
149
+ ctrl.enqueue(encoder.encode(raw))
150
+ ctrl.close()
151
+ },
152
+ })
153
+ }
154
+
155
+ /** Wraps an AsyncIterable so chunks are collected while the caller iterates */
156
+ function wrapAsyncIterable<T>(
157
+ source: AsyncIterable<T>,
158
+ onComplete: (chunks: T[]) => void,
159
+ ): AsyncIterable<T> {
160
+ return {
161
+ [Symbol.asyncIterator]() {
162
+ const iter = source[Symbol.asyncIterator]()
163
+ const collected: T[] = []
164
+ return {
165
+ async next() {
166
+ const result = await iter.next()
167
+ if (!result.done) {
168
+ collected.push(result.value)
169
+ } else {
170
+ onComplete(collected)
171
+ }
172
+ return result
173
+ },
174
+ async return(value?: unknown) {
175
+ onComplete(collected)
176
+ return iter.return ? iter.return(value) : { done: true as const, value: undefined }
177
+ },
178
+ }
179
+ },
180
+ }
181
+ }
182
+
183
+ export function wrapAI<Args extends unknown[], R>(
184
+ modelName: string,
185
+ callFn: (...args: Args) => Promise<R>,
186
+ _options?: { model?: string; provider?: string },
187
+ ): (...args: Args) => Promise<R> {
188
+ return async (...args: Args): Promise<R> => {
189
+ // Phase 3 fixture replay: short-circuit if ed-runner replay is active
190
+ const edReplay = getEdReplayContext()
191
+ if (edReplay) {
192
+ const input = args.length === 1 ? args[0] : args
193
+ const { output } = replayCall(edReplay, 'ai_call', modelName, input)
194
+ return output as R
195
+ }
196
+
197
+ await tryAutoInitHttpContext()
198
+ const ctx = getCaptureContext()
199
+ const httpCtx = getHttpRunContext()
200
+ const obsCtx = getObservabilityContext()
201
+
202
+ if (!ctx && !httpCtx && !obsCtx) return callFn(...args)
203
+
204
+ enterAIWrapper()
205
+ try {
206
+ const start = rawDateNow()
207
+
208
+ // Observability-only mode: record and push, no mocks/replay
209
+ if (!ctx && !httpCtx && obsCtx) {
210
+ const id = obsCtx.nextId()
211
+ const input = args.length === 1 ? args[0] : args
212
+ try {
213
+ const output = await callFn(...args)
214
+ const enriched = enrichFromLLMCapture(input, extractUsage(output), _options?.model, _options?.provider)
215
+
216
+ if (isReadableStream(output)) {
217
+ const [streamForCaller, streamForRecorder] = output.tee()
218
+ bufferReadableStream(streamForRecorder).then((rawText) => {
219
+ const durationMs = rawDateNow() - start
220
+ pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
221
+ }).catch(() => {
222
+ const durationMs = rawDateNow() - start
223
+ pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
224
+ })
225
+ return streamForCaller as unknown as R
226
+ }
227
+
228
+ if (isAsyncIterable(output)) {
229
+ return wrapAsyncIterable(output, (chunks) => {
230
+ const durationMs = rawDateNow() - start
231
+ const rawText = chunks.map((c) => (typeof c === 'string' ? c : JSON.stringify(c))).join('')
232
+ pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
233
+ }) as unknown as R
234
+ }
235
+
236
+ const durationMs = rawDateNow() - start
237
+ const event: WorkflowEvent = {
238
+ id, type: 'ai', name: modelName, input: enriched.input, output,
239
+ timestamp: start, durationMs,
240
+ ...(enriched.usage ? { usage: enriched.usage } : {}),
241
+ }
242
+ pushTelemetryEvent(event)
243
+ return output
244
+ } catch (e) {
245
+ const enriched = enrichFromLLMCapture(input, undefined, _options?.model, _options?.provider)
246
+ const durationMs = rawDateNow() - start
247
+ pushTelemetryEvent({
248
+ id, type: 'ai', name: modelName, input: enriched.input,
249
+ output: { error: String(e) }, timestamp: start, durationMs,
250
+ })
251
+ throw e
252
+ }
253
+ }
254
+
255
+ if (ctx) {
256
+ const { recorder, replay } = ctx
257
+ const id = recorder.nextId()
258
+
259
+ if (replay.shouldReplay(id)) {
260
+ const historical = replay.getRecordedEvent(id)
261
+ if (historical?.streamed === true) {
262
+ const raw = typeof historical.streamRaw === 'string' ? historical.streamRaw : ''
263
+ return reconstructStream(raw) as unknown as R
264
+ }
265
+ return replay.getRecordedResult(id) as R
266
+ }
267
+
268
+ // Check AI mock (output mock — skip real call, return recorded result)
269
+ const aiMock = resolveAIMock(modelName)
270
+ if (aiMock.mocked) {
271
+ const input = args.length === 1 ? args[0] : args
272
+ const event: WorkflowEvent = {
273
+ id, type: 'ai', name: modelName, input,
274
+ output: aiMock.result, timestamp: start, durationMs: 0,
275
+ }
276
+ recorder.record(event)
277
+ if (httpCtx) pushTelemetryEvent(event)
278
+ return aiMock.result as R
279
+ }
280
+
281
+ // Check prompt mocks (system + user prompt replacement — call real LLM with modified prompts)
282
+ const rawInput = args.length === 1 ? args[0] : args
283
+ const promptModifiedInput = resolvePromptMock(rawInput)
284
+ const userPromptModifiedInput = resolveUserPromptMock(promptModifiedInput !== undefined ? promptModifiedInput : rawInput)
285
+ const modifiedInput = userPromptModifiedInput !== undefined ? userPromptModifiedInput : promptModifiedInput
286
+ const effectiveArgs: Args = modifiedInput !== undefined ? [modifiedInput] as unknown as Args : args
287
+ const input = modifiedInput !== undefined ? modifiedInput : rawInput
288
+
289
+ try {
290
+ const output = await callFn(...effectiveArgs)
291
+
292
+ if (isReadableStream(output)) {
293
+ const [streamForCaller, streamForRecorder] = output.tee()
294
+ bufferReadableStream(streamForRecorder).then((rawText) => {
295
+ const durationMs = rawDateNow() - start
296
+ const event: WorkflowEvent = { id, type: 'ai', name: modelName, input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs }
297
+ recorder.record(event)
298
+ if (httpCtx) pushTelemetryEvent(event)
299
+ }).catch(() => {
300
+ const durationMs = rawDateNow() - start
301
+ const event: WorkflowEvent = { id, type: 'ai', name: modelName, input, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs }
302
+ recorder.record(event)
303
+ if (httpCtx) pushTelemetryEvent(event)
304
+ })
305
+ return streamForCaller as unknown as R
306
+ }
307
+
308
+ if (isAsyncIterable(output)) {
309
+ return wrapAsyncIterable(output, (chunks) => {
310
+ const durationMs = rawDateNow() - start
311
+ const rawText = chunks.map((c) => (typeof c === 'string' ? c : JSON.stringify(c))).join('')
312
+ const event: WorkflowEvent = { id, type: 'ai', name: modelName, input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs }
313
+ recorder.record(event)
314
+ if (httpCtx) pushTelemetryEvent(event)
315
+ }) as unknown as R
316
+ }
317
+
318
+ const durationMs = rawDateNow() - start
319
+ const usage = extractUsage(output)
320
+ const event: WorkflowEvent = {
321
+ id, type: 'ai', name: modelName, input, output,
322
+ timestamp: start, durationMs,
323
+ ...(usage ? { usage } : {}),
324
+ }
325
+ recorder.record(event)
326
+ if (httpCtx) pushTelemetryEvent(event)
327
+ return output
328
+ } catch (e) {
329
+ const durationMs = rawDateNow() - start
330
+ const event: WorkflowEvent = {
331
+ id, type: 'ai', name: modelName, input,
332
+ output: { error: String(e) }, timestamp: start, durationMs,
333
+ }
334
+ recorder.record(event)
335
+ if (httpCtx) pushTelemetryEvent(event)
336
+ throw e
337
+ }
338
+ }
339
+
340
+ // HTTP mode only (no capture context)
341
+ const id = httpCtx!.nextId()
342
+
343
+ // Replay frozen step: push historical event so dashboard trace stays complete
344
+ const frozen = getHttpFrozenEvent(id)
345
+ if (frozen) {
346
+ pushTelemetryEvent(frozen)
347
+ if (frozen.streamed === true) {
348
+ const raw = typeof frozen.streamRaw === 'string' ? frozen.streamRaw : ''
349
+ return reconstructStream(raw) as unknown as R
350
+ }
351
+ return frozen.output as R
352
+ }
353
+
354
+ // Check AI output mock (skip real call, return mocked result)
355
+ const aiMock = getHttpAIMock(modelName)
356
+ if (aiMock.mocked) {
357
+ const input = args.length === 1 ? args[0] : args
358
+ const event: WorkflowEvent = {
359
+ id, type: 'ai', name: modelName, input,
360
+ output: aiMock.result, timestamp: start, durationMs: 0,
361
+ }
362
+ pushTelemetryEvent(event)
363
+ return aiMock.result as R
364
+ }
365
+
366
+ // Check prompt mocks (system + user prompt replacement in HTTP mode)
367
+ const rawHttpInput = args.length === 1 ? args[0] : args
368
+ const httpSystemModified = getHttpPromptMock(rawHttpInput)
369
+ const httpUserModified = getHttpUserPromptMock(httpSystemModified !== undefined ? httpSystemModified : rawHttpInput)
370
+ const httpModifiedInput = httpUserModified !== undefined ? httpUserModified : httpSystemModified
371
+ const httpEffectiveArgs: Args = httpModifiedInput !== undefined ? [httpModifiedInput] as unknown as Args : args
372
+ const input = httpModifiedInput !== undefined ? httpModifiedInput : rawHttpInput
373
+
374
+ try {
375
+ const output = await callFn(...httpEffectiveArgs)
376
+ const enriched = enrichFromLLMCapture(input, extractUsage(output), _options?.model, _options?.provider)
377
+
378
+ if (isReadableStream(output)) {
379
+ const [streamForCaller, streamForRecorder] = output.tee()
380
+ bufferReadableStream(streamForRecorder).then((rawText) => {
381
+ const durationMs = rawDateNow() - start
382
+ pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
383
+ }).catch(() => {
384
+ const durationMs = rawDateNow() - start
385
+ pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: '', timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
386
+ })
387
+ return streamForCaller as unknown as R
388
+ }
389
+
390
+ if (isAsyncIterable(output)) {
391
+ return wrapAsyncIterable(output, (chunks) => {
392
+ const durationMs = rawDateNow() - start
393
+ const rawText = chunks.map((c) => (typeof c === 'string' ? c : JSON.stringify(c))).join('')
394
+ pushTelemetryEvent({ id, type: 'ai', name: modelName, input: enriched.input, output: null, streamed: true, streamRaw: rawText, timestamp: start, durationMs, ...(enriched.usage ? { usage: enriched.usage } : {}) })
395
+ }) as unknown as R
396
+ }
397
+
398
+ const durationMs = rawDateNow() - start
399
+ const event: WorkflowEvent = {
400
+ id, type: 'ai', name: modelName, input: enriched.input, output,
401
+ timestamp: start, durationMs,
402
+ ...(enriched.usage ? { usage: enriched.usage } : {}),
403
+ }
404
+ pushTelemetryEvent(event)
405
+ return output
406
+ } catch (e) {
407
+ const enriched = enrichFromLLMCapture(input, undefined, _options?.model, _options?.provider)
408
+ const durationMs = rawDateNow() - start
409
+ pushTelemetryEvent({
410
+ id, type: 'ai', name: modelName, input: enriched.input,
411
+ output: { error: String(e) }, timestamp: start, durationMs,
412
+ })
413
+ throw e
414
+ }
415
+ } finally {
416
+ leaveAIWrapper()
417
+ }
418
+ }
419
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Conditional recorder utility for worker contexts
3
+ *
4
+ * This module provides a safe wrapper around recordToolCall that only executes
5
+ * in worker environments. In non-worker contexts, it's a no-op to avoid unnecessary
6
+ * imports and function calls.
7
+ */
8
+
9
+ // Check if running in an Elasticdash worker context
10
+ const isWorkerContext = (): boolean => {
11
+ if (typeof globalThis === 'undefined') {
12
+ return false;
13
+ }
14
+ return (globalThis as any).__ELASTICDASH_WORKER__ === true;
15
+ };
16
+
17
+ let recordToolCallFn: ((name: string, input: any, output: any) => void) | null = null;
18
+
19
+ // Lazy load recordToolCall only if in worker context
20
+ const getRecordToolCall = async (): Promise<((name: string, input: any, output: any) => void) | null> => {
21
+ if (!isWorkerContext()) {
22
+ return null;
23
+ }
24
+
25
+ if (recordToolCallFn !== null) {
26
+ return recordToolCallFn;
27
+ }
28
+
29
+ try {
30
+ const { recordToolCall } = await import('../tracing.js');
31
+ recordToolCallFn = recordToolCall;
32
+ return recordToolCallFn;
33
+ } catch (err) {
34
+ console.warn('Failed to load recordToolCall from tracing module:', err);
35
+ return null;
36
+ }
37
+ };
38
+
39
+ /**
40
+ * Safely record a tool call only if in worker context
41
+ *
42
+ * @param name - Name of the tool/function
43
+ * @param input - Input parameters to the tool
44
+ * @param output - Output/result from the tool
45
+ */
46
+ export const safeRecordToolCall = async (
47
+ name: string,
48
+ input: any,
49
+ output: any
50
+ ): Promise<void> => {
51
+ const recorder = await getRecordToolCall();
52
+ if (recorder) {
53
+ recorder(name, input, output);
54
+ }
55
+ };
56
+
57
+ /**
58
+ * Synchronous version - checks if we're in worker context without importing
59
+ * Use this if you want to avoid async/await in your tool functions
60
+ */
61
+ export const isWorker = (): boolean => {
62
+ return isWorkerContext();
63
+ };