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,537 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { randomUUID } from 'node:crypto'
3
+ import type { WorkflowEvent } from '../capture/event.js'
4
+ import { extractSystemPrompt, replaceSystemPrompt, extractUserPrompts, replaceUserPrompt, lookupMockEntry, normaliseMockResult } from '../internals/mock-resolver.js'
5
+ import type { AIMockEntry, UserPromptMockEntry } from '../internals/mock-resolver.js'
6
+ import { debugLog } from '../utils/debug.js'
7
+ import { notifyLicenseError } from '../utils/license-error.js'
8
+ import type { TelemetryBatcher } from '../telemetry-batcher.js'
9
+
10
+ interface ToolMockEntry {
11
+ mode: 'live' | 'mock-all' | 'mock-specific'
12
+ callIndices?: number[]
13
+ mockData?: Record<number, unknown>
14
+ }
15
+
16
+ type MockResult = { mocked: true; result: unknown } | { mocked: false }
17
+
18
+ export interface HttpRunContext {
19
+ runId: string
20
+ dashboardUrl: string
21
+ nextId: () => number
22
+ frozenEvents: Map<number, WorkflowEvent>
23
+ /** System-prompt-keyed overrides: original system prompt → replacement system prompt */
24
+ promptMocks: Map<string, string>
25
+ /** User prompt mock configuration (keyed by original user message text) */
26
+ userPromptMocks?: Record<string, UserPromptMockEntry>
27
+ /** Tool output mock configuration (keyed by tool name) */
28
+ toolMockConfig?: Record<string, ToolMockEntry>
29
+ /** AI output mock configuration (keyed by model name) */
30
+ aiMockConfig?: Record<string, AIMockEntry>
31
+ /** Per-tool call counters for mock resolution (scoped to this request) */
32
+ toolCallCounters: Record<string, number>
33
+ /** Per-model call counters for AI mock resolution (scoped to this request) */
34
+ aiCallCounters: Record<string, number>
35
+ /** Per-user-prompt-text call counters for user prompt mock resolution */
36
+ userPromptCallCounters: Record<string, number>
37
+ }
38
+
39
+ export interface ObservabilityContext {
40
+ sessionId: string
41
+ serverUrl: string
42
+ apiKey?: string
43
+ batcher: TelemetryBatcher
44
+ nextId: () => number
45
+ sampleRate: number
46
+ redactKeys: string[]
47
+ traceId: string
48
+ /** The default workflow name discovered from ed_workflows.ts, used as traceId prefix */
49
+ defaultWorkflowName: string
50
+ /** When true, events captured during trigger rerun execution are marked as isRerun */
51
+ isRerun?: boolean
52
+ /** When set, pushTelemetryEvent also collects events here (used by trace capture) */
53
+ eventCollector?: WorkflowEvent[]
54
+ }
55
+
56
+ const g = globalThis as Record<string, unknown>
57
+ const HTTP_RUN_ALS_KEY = '__elasticdash_http_run_als__'
58
+ const GLOBAL_CTX_KEY = '__elasticdash_global_http_ctx__'
59
+ const OBS_ALS_KEY = '__elasticdash_obs_als__'
60
+ const GLOBAL_OBS_KEY = '__elasticdash_global_obs_ctx__'
61
+
62
+ const httpRunAls: AsyncLocalStorage<HttpRunContext | undefined> =
63
+ (g[HTTP_RUN_ALS_KEY] as AsyncLocalStorage<HttpRunContext | undefined>) ??
64
+ new AsyncLocalStorage<HttpRunContext | undefined>()
65
+ if (!g[HTTP_RUN_ALS_KEY]) g[HTTP_RUN_ALS_KEY] = httpRunAls
66
+
67
+ const obsAls: AsyncLocalStorage<ObservabilityContext | undefined> =
68
+ (g[OBS_ALS_KEY] as AsyncLocalStorage<ObservabilityContext | undefined>) ??
69
+ new AsyncLocalStorage<ObservabilityContext | undefined>()
70
+ if (!g[OBS_ALS_KEY]) g[OBS_ALS_KEY] = obsAls
71
+
72
+ export function setObservabilityContext(ctx: ObservabilityContext): void {
73
+ obsAls.enterWith(ctx)
74
+ g[GLOBAL_OBS_KEY] = ctx
75
+ }
76
+
77
+ export function getObservabilityContext(): ObservabilityContext | undefined {
78
+ return obsAls.getStore() ?? (g[GLOBAL_OBS_KEY] as ObservabilityContext | undefined)
79
+ }
80
+
81
+ export function clearObservabilityContext(): void {
82
+ obsAls.enterWith(undefined)
83
+ g[GLOBAL_OBS_KEY] = undefined
84
+ }
85
+
86
+ function buildContext(
87
+ runId: string,
88
+ dashboardUrl: string,
89
+ frozenEvents: WorkflowEvent[],
90
+ promptMocksRecord: Record<string, string> = {},
91
+ toolMockConfig?: Record<string, ToolMockEntry>,
92
+ aiMockConfig?: Record<string, AIMockEntry>,
93
+ userPromptMocks?: Record<string, UserPromptMockEntry>,
94
+ ): HttpRunContext {
95
+ let counter = 0
96
+ const frozenMap = new Map<number, WorkflowEvent>()
97
+ for (const e of frozenEvents) frozenMap.set(e.id, e)
98
+ const promptMocksMap = new Map<string, string>(Object.entries(promptMocksRecord))
99
+ return {
100
+ runId, dashboardUrl, nextId: () => ++counter, frozenEvents: frozenMap, promptMocks: promptMocksMap,
101
+ userPromptMocks, toolMockConfig, aiMockConfig,
102
+ toolCallCounters: {}, aiCallCounters: {}, userPromptCallCounters: {},
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Sets a global (non-ALS) fallback context that `getHttpRunContext` checks when
108
+ * ALS returns `undefined`. Use this in streaming frameworks where ALS context is
109
+ * lost after the handler returns a ReadableStream.
110
+ */
111
+ export function setGlobalHttpContext(ctx: HttpRunContext): void {
112
+ g[GLOBAL_CTX_KEY] = ctx
113
+ }
114
+
115
+ /**
116
+ * Clears the global fallback context. Call this at the start of non-ElasticDash
117
+ * requests (or when a stream closes) to prevent stale context from leaking.
118
+ */
119
+ export function clearGlobalHttpContext(): void {
120
+ g[GLOBAL_CTX_KEY] = undefined
121
+ }
122
+
123
+ /** Synchronous setup — use when there are no frozen events (live run with no replay). */
124
+ export function setHttpRunContext(runId: string, dashboardUrl: string): void {
125
+ const ctx = buildContext(runId, dashboardUrl, [], {})
126
+ httpRunAls.enterWith(ctx)
127
+ g[GLOBAL_CTX_KEY] = ctx
128
+ ensureInterceptorsInstalled().catch(() => {})
129
+ }
130
+
131
+ /**
132
+ * Async setup — fetches frozen events from the dashboard then sets the ALS context.
133
+ * Use this in request handlers instead of setHttpRunContext when step freezing is needed.
134
+ * Falls back to a live (no-replay) context if the fetch fails or returns nothing.
135
+ */
136
+ export async function initHttpRunContext(runId: string, dashboardUrl: string): Promise<void> {
137
+ let frozenEvents: WorkflowEvent[] = []
138
+ let promptMocks: Record<string, string> = {}
139
+ let toolMockConfig: Record<string, ToolMockEntry> | undefined
140
+ let aiMockConfig: Record<string, AIMockEntry> | undefined
141
+ let userPromptMocks: Record<string, UserPromptMockEntry> | undefined
142
+ try {
143
+ if (!dashboardUrl) {
144
+ debugLog(`[elasticdash] initHttpRunContext: no dashboardUrl, skipping config fetch`)
145
+ } else {
146
+ const res = await fetch(`${dashboardUrl}/api/run-configs/${runId}`)
147
+ if (res.ok) {
148
+ const data = await res.json() as { frozenEvents?: WorkflowEvent[]; promptMocks?: Record<string, string>; toolMockConfig?: Record<string, ToolMockEntry>; aiMockConfig?: Record<string, AIMockEntry>; userPromptMocks?: Record<string, UserPromptMockEntry> }
149
+ frozenEvents = Array.isArray(data.frozenEvents) ? data.frozenEvents : []
150
+ promptMocks = (data.promptMocks && typeof data.promptMocks === 'object' && !Array.isArray(data.promptMocks))
151
+ ? data.promptMocks : {}
152
+ if (data.toolMockConfig && typeof data.toolMockConfig === 'object' && !Array.isArray(data.toolMockConfig)) {
153
+ toolMockConfig = data.toolMockConfig
154
+ }
155
+ if (data.aiMockConfig && typeof data.aiMockConfig === 'object' && !Array.isArray(data.aiMockConfig)) {
156
+ aiMockConfig = data.aiMockConfig
157
+ }
158
+ if (data.userPromptMocks && typeof data.userPromptMocks === 'object' && !Array.isArray(data.userPromptMocks)) {
159
+ userPromptMocks = data.userPromptMocks as Record<string, UserPromptMockEntry>
160
+ }
161
+ } else if (res.status === 402) {
162
+ notifyLicenseError(res.status, 'run-config')
163
+ }
164
+ } // end dashboardUrl guard
165
+ } catch {
166
+ // Dashboard unreachable or run config not registered — proceed with live execution
167
+ }
168
+ const ctx = buildContext(runId, dashboardUrl, frozenEvents, promptMocks, toolMockConfig, aiMockConfig, userPromptMocks)
169
+ httpRunAls.enterWith(ctx)
170
+ g[GLOBAL_CTX_KEY] = ctx
171
+ await ensureInterceptorsInstalled()
172
+ }
173
+
174
+ export function getHttpRunContext(): HttpRunContext | undefined {
175
+ return httpRunAls.getStore() ?? (g[GLOBAL_CTX_KEY] as HttpRunContext | undefined)
176
+ }
177
+
178
+ /** Returns the frozen WorkflowEvent for the given event id, or undefined if not frozen. */
179
+ export function getHttpFrozenEvent(id: number): WorkflowEvent | undefined {
180
+ return getHttpRunContext()?.frozenEvents.get(id)
181
+ }
182
+
183
+ /**
184
+ * If a prompt mock is configured for the system prompt found in `input`, returns
185
+ * a copy of `input` with the system prompt replaced. Otherwise returns `undefined`.
186
+ */
187
+ export function getHttpPromptMock(input: unknown): unknown | undefined {
188
+ const ctx = getHttpRunContext()
189
+ if (!ctx || ctx.promptMocks.size === 0) {
190
+ debugLog(`[elasticdash] getHttpPromptMock: skip — promptMocks.size=${ctx?.promptMocks.size ?? 'no ctx'}`)
191
+ return undefined
192
+ }
193
+ const systemPrompt = extractSystemPrompt(input)
194
+ if (systemPrompt === undefined) {
195
+ const inputKeys = (input && typeof input === 'object') ? Object.keys(input as object).join(',') : typeof input
196
+ debugLog(`[elasticdash] getHttpPromptMock: no system prompt found in input (keys: ${inputKeys})`)
197
+ return undefined
198
+ }
199
+ const newSystemPrompt = ctx.promptMocks.get(systemPrompt)
200
+ debugLog(`[elasticdash] getHttpPromptMock: extracted system prompt (len=${systemPrompt.length}, first50=${JSON.stringify(systemPrompt.slice(0,50))}) — mock found=${newSystemPrompt !== undefined}`)
201
+ if (newSystemPrompt !== undefined) {
202
+ debugLog(`[elasticdash] getHttpPromptMock: available mock keys=${JSON.stringify([...ctx.promptMocks.keys()].map(k => k.slice(0,50)))}`)
203
+ }
204
+ if (newSystemPrompt === undefined) {
205
+ debugLog(`[elasticdash] getHttpPromptMock: no mock for this prompt. Available mock keys (first 50 chars each): ${JSON.stringify([...ctx.promptMocks.keys()].map(k => k.slice(0,50)))}`)
206
+ return undefined
207
+ }
208
+ return replaceSystemPrompt(input, newSystemPrompt)
209
+ }
210
+
211
+ /**
212
+ * If user prompt mocks are configured, applies all matching replacements to `input`.
213
+ * Returns modified input or `undefined` if no replacements applied.
214
+ * Uses fuzzy matching (strip/add JSON quotes) to handle serialization mismatches.
215
+ */
216
+ export function getHttpUserPromptMock(input: unknown): unknown | undefined {
217
+ const ctx = getHttpRunContext()
218
+ if (!ctx?.userPromptMocks || Object.keys(ctx.userPromptMocks).length === 0) return undefined
219
+
220
+ const userPrompts = extractUserPrompts(input)
221
+ if (userPrompts.length === 0) return undefined
222
+
223
+ const mocks = ctx.userPromptMocks
224
+ const counters = ctx.userPromptCallCounters
225
+ const uniqueTexts = Array.from(new Set(userPrompts))
226
+ let modified = false
227
+ let result = input
228
+
229
+ for (const text of uniqueTexts) {
230
+ const match = lookupMockEntry(mocks, text)
231
+ if (!match || match.entry.mode === 'live') continue
232
+ const entry = match.entry
233
+
234
+ counters[match.key] = (counters[match.key] ?? 0) + 1
235
+ const callNumber = counters[match.key]
236
+
237
+ if (entry.mode === 'replace-all') {
238
+ result = replaceUserPrompt(result, text, entry.replacement)
239
+ modified = true
240
+ } else if (entry.mode === 'replace-specific') {
241
+ const indices = entry.callIndices ?? []
242
+ if (indices.includes(callNumber)) {
243
+ result = replaceUserPrompt(result, text, entry.replacement)
244
+ modified = true
245
+ }
246
+ }
247
+ }
248
+
249
+ return modified ? result : undefined
250
+ }
251
+
252
+ /**
253
+ * Resolves whether the current HTTP-mode call to `toolName` should be mocked.
254
+ * Mirrors `resolveMock()` but reads from the per-request HttpRunContext.
255
+ */
256
+ export function getHttpToolMock(toolName: string): MockResult {
257
+ const ctx = getHttpRunContext()
258
+ if (!ctx?.toolMockConfig) return { mocked: false }
259
+ const entry = ctx.toolMockConfig[toolName]
260
+ if (!entry || entry.mode === 'live') return { mocked: false }
261
+
262
+ ctx.toolCallCounters[toolName] = (ctx.toolCallCounters[toolName] ?? 0) + 1
263
+ const callNumber = ctx.toolCallCounters[toolName]
264
+
265
+ if (entry.mode === 'mock-all') {
266
+ const data = entry.mockData ?? {}
267
+ const raw = data[callNumber] !== undefined ? data[callNumber] : data[0]
268
+ return { mocked: true, result: normaliseMockResult(raw) }
269
+ }
270
+ if (entry.mode === 'mock-specific') {
271
+ const indices = entry.callIndices ?? []
272
+ if (indices.includes(callNumber)) {
273
+ const data = entry.mockData ?? {}
274
+ return { mocked: true, result: normaliseMockResult(data[callNumber]) }
275
+ }
276
+ return { mocked: false }
277
+ }
278
+ return { mocked: false }
279
+ }
280
+
281
+ /**
282
+ * Resolves whether the current HTTP-mode call to `modelName` should be mocked.
283
+ * Mirrors `resolveAIMock()` but reads from the per-request HttpRunContext.
284
+ */
285
+ export function getHttpAIMock(modelName: string): MockResult {
286
+ const ctx = getHttpRunContext()
287
+ if (!ctx?.aiMockConfig) return { mocked: false }
288
+ const entry = ctx.aiMockConfig[modelName]
289
+ if (!entry || entry.mode === 'live') return { mocked: false }
290
+
291
+ ctx.aiCallCounters[modelName] = (ctx.aiCallCounters[modelName] ?? 0) + 1
292
+ const callNumber = ctx.aiCallCounters[modelName]
293
+
294
+ if (entry.mode === 'mock-all') {
295
+ const data = entry.mockData ?? {}
296
+ const raw = data[callNumber] !== undefined ? data[callNumber] : data[0]
297
+ return { mocked: true, result: normaliseMockResult(raw) }
298
+ }
299
+ if (entry.mode === 'mock-specific') {
300
+ const indices = entry.callIndices ?? []
301
+ if (indices.includes(callNumber)) {
302
+ const data = entry.mockData ?? {}
303
+ return { mocked: true, result: normaliseMockResult(data[callNumber]) }
304
+ }
305
+ return { mocked: false }
306
+ }
307
+ return { mocked: false }
308
+ }
309
+
310
+ export function pushTelemetryEvent(event: WorkflowEvent, explicitCtx?: { runId: string; dashboardUrl: string }): void {
311
+ // Observability mode: route to batcher with sampling
312
+ const obsCtx = getObservabilityContext()
313
+ if (obsCtx && !explicitCtx) {
314
+ if (obsCtx.sampleRate < 1 && Math.random() >= obsCtx.sampleRate) return
315
+ if (!event.traceId && obsCtx.traceId) {
316
+ event.traceId = obsCtx.traceId
317
+ }
318
+ // Mark events captured during trigger rerun execution
319
+ if (obsCtx.isRerun) {
320
+ (event as WorkflowEvent & { isRerun?: boolean }).isRerun = true
321
+ }
322
+ if (obsCtx.eventCollector) {
323
+ obsCtx.eventCollector.push({ ...event })
324
+ debugLog(`[elasticdash] pushTelemetryEvent: collected event type=${event.type} name=${('name' in event ? event.name : '?')} (total: ${obsCtx.eventCollector.length})`)
325
+ }
326
+ obsCtx.batcher.enqueue(event)
327
+ return
328
+ }
329
+
330
+ const ctx = explicitCtx ?? getHttpRunContext()
331
+ if (!ctx) {
332
+ debugLog(`[elasticdash] pushTelemetryEvent: no HTTP context, dropping event type=${event.type} name=${('name' in event ? event.name : '?')}`)
333
+ return
334
+ }
335
+ const { runId, dashboardUrl } = ctx
336
+ if (!dashboardUrl) {
337
+ debugLog(`[elasticdash] pushTelemetryEvent: no dashboardUrl, dropping event type=${event.type} name=${('name' in event ? event.name : '?')}`)
338
+ return
339
+ }
340
+ debugLog(`[elasticdash] pushTelemetryEvent: posting event type=${event.type} name=${('name' in event ? event.name : '?')} runId=${runId} to ${dashboardUrl}`)
341
+ try {
342
+ fetch(`${dashboardUrl}/api/trace-events`, {
343
+ method: 'POST',
344
+ headers: { 'Content-Type': 'application/json' },
345
+ body: JSON.stringify({ runId, event }),
346
+ }).then(r => {
347
+ if (r.status === 402) notifyLicenseError(r.status, 'telemetry')
348
+ debugLog(`[elasticdash] pushTelemetryEvent: response status=${r.status} for type=${event.type} name=${('name' in event ? event.name : '?')}`)
349
+ }).catch(e => {
350
+ debugLog(`[elasticdash] pushTelemetryEvent: fetch failed: ${e instanceof Error ? e.message : String(e)}`)
351
+ })
352
+ } catch (e) {
353
+ debugLog(`[elasticdash] pushTelemetryEvent: fetch threw: ${e instanceof Error ? e.message : String(e)}`)
354
+ }
355
+ }
356
+
357
+ const INTERCEPTORS_KEY = '__elasticdash_interceptors_installed__'
358
+
359
+ /**
360
+ * Ensures fetch (HTTP + AI) and DB interceptors are installed globally.
361
+ * Uses dynamic imports to avoid circular dependencies. Safe to call multiple
362
+ * times — only the first call does actual work.
363
+ */
364
+ async function ensureInterceptorsInstalled(): Promise<void> {
365
+ if (g[INTERCEPTORS_KEY]) return
366
+ g[INTERCEPTORS_KEY] = true
367
+ try {
368
+ const [httpMod, aiMod, dbMod] = await Promise.all([
369
+ import('./http.js'),
370
+ import('./ai-interceptor.js'),
371
+ import('./db-auto.js'),
372
+ ])
373
+ httpMod.interceptFetch()
374
+ aiMod.installAIInterceptor()
375
+ await dbMod.installDBAutoInterceptor()
376
+ } catch {
377
+ // Non-fatal: interceptors may already be installed or deps missing
378
+ }
379
+ }
380
+
381
+ const AUTO_INIT_KEY = '__elasticdash_auto_init_promise__'
382
+
383
+ /**
384
+ * Lazily initialises an HTTP run context from environment variables when none
385
+ * has been set up explicitly. Only activates when `ELASTICDASH_SERVER` is set.
386
+ *
387
+ * - If `ELASTICDASH_RUN_ID` is also set, calls `initHttpRunContext` so that
388
+ * frozen steps are fetched from the dashboard (enables step freezing).
389
+ * - Otherwise calls `setHttpRunContext` with a fresh UUID (live/telemetry mode).
390
+ *
391
+ * The initialisation runs at most once per process — subsequent calls are
392
+ * no-ops once the context is established. Errors (e.g. dashboard unreachable)
393
+ * are swallowed so that live execution always continues unaffected.
394
+ *
395
+ * Typical usage: set `ELASTICDASH_SERVER=http://localhost:4573` and optionally
396
+ * `ELASTICDASH_RUN_ID=<id>` before starting your server or script. Every
397
+ * `wrapTool` / `wrapAI` call will then auto-connect to the dashboard without
398
+ * any explicit `initHttpRunContext` call in your code.
399
+ */
400
+ /**
401
+ * Runs `callback` inside a fresh HTTP run context scoped to `runId` / `dashboardUrl`.
402
+ * Uses `als.run()` which guarantees the store is inherited by all async descendants of
403
+ * `callback`, even when intermediate code (e.g. Langfuse / OTel) spawns its own async
404
+ * contexts via `als.run()`. Prefer this over `setHttpRunContext` when wrapping a long-lived
405
+ * async pipeline such as a streaming route handler.
406
+ */
407
+ export async function runInHttpContext<T>(
408
+ runId: string,
409
+ dashboardUrl: string,
410
+ callback: () => Promise<T>,
411
+ ): Promise<T> {
412
+ await ensureInterceptorsInstalled()
413
+ const ctx = buildContext(runId, dashboardUrl, [], {})
414
+ g[GLOBAL_CTX_KEY] = ctx
415
+ try {
416
+ return await httpRunAls.run(ctx, callback)
417
+ } finally {
418
+ g[GLOBAL_CTX_KEY] = undefined
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Fetches frozen events and prompt mocks from the dashboard, then runs `callback`
424
+ * inside an HTTP run context using `als.run()`. This is the preferred function for
425
+ * streaming route handlers that sit behind Langfuse / OTel instrumentation:
426
+ *
427
+ * - `als.run()` guarantees the elasticdash store is inherited through any nested
428
+ * `als.run()` calls made by third-party libraries (e.g. `startActiveObservation`).
429
+ * - Frozen events and prompt mocks are fetched before the callback so step replay
430
+ * and prompt mocking work correctly on reruns.
431
+ *
432
+ * Falls back to an empty context (live execution, no replay) if the dashboard is
433
+ * unreachable or the run config is not found.
434
+ */
435
+ export async function runWithInitializedHttpContext<T>(
436
+ runId: string,
437
+ dashboardUrl: string,
438
+ callback: () => Promise<T>,
439
+ ): Promise<T> {
440
+ let frozenEvents: WorkflowEvent[] = []
441
+ let promptMocks: Record<string, string> = {}
442
+ let toolMockConfig: Record<string, ToolMockEntry> | undefined
443
+ let aiMockConfig: Record<string, AIMockEntry> | undefined
444
+ let userPromptMocks: Record<string, UserPromptMockEntry> | undefined
445
+ try {
446
+ if (!dashboardUrl) {
447
+ debugLog(`[elasticdash] runWithInitializedHttpContext: no dashboardUrl, skipping config fetch`)
448
+ } else {
449
+ const res = await fetch(`${dashboardUrl}/api/run-configs/${runId}`)
450
+ if (res.ok) {
451
+ const data = await res.json() as { frozenEvents?: WorkflowEvent[]; promptMocks?: Record<string, string>; toolMockConfig?: Record<string, ToolMockEntry>; aiMockConfig?: Record<string, AIMockEntry>; userPromptMocks?: Record<string, UserPromptMockEntry> }
452
+ frozenEvents = Array.isArray(data.frozenEvents) ? data.frozenEvents : []
453
+ promptMocks = (data.promptMocks && typeof data.promptMocks === 'object' && !Array.isArray(data.promptMocks))
454
+ ? data.promptMocks : {}
455
+ if (data.toolMockConfig && typeof data.toolMockConfig === 'object' && !Array.isArray(data.toolMockConfig)) {
456
+ toolMockConfig = data.toolMockConfig
457
+ }
458
+ if (data.aiMockConfig && typeof data.aiMockConfig === 'object' && !Array.isArray(data.aiMockConfig)) {
459
+ aiMockConfig = data.aiMockConfig
460
+ }
461
+ if (data.userPromptMocks && typeof data.userPromptMocks === 'object' && !Array.isArray(data.userPromptMocks)) {
462
+ userPromptMocks = data.userPromptMocks as Record<string, UserPromptMockEntry>
463
+ }
464
+ const mockKeys = Object.keys(promptMocks)
465
+ const userMockKeys = Object.keys(userPromptMocks ?? {})
466
+ debugLog(`[elasticdash] runWithInitializedHttpContext: fetched ${mockKeys.length} prompt mocks, ${userMockKeys.length} user prompt mocks, ${frozenEvents.length} frozen events`)
467
+ if (mockKeys.length > 0) {
468
+ debugLog(`[elasticdash] runWithInitializedHttpContext: mock keys (first 80 chars each): ${JSON.stringify(mockKeys.map(k => k.slice(0,80)))}`)
469
+ }
470
+ } else {
471
+ if (res.status === 402) notifyLicenseError(res.status, 'run-config')
472
+ debugLog(`[elasticdash] runWithInitializedHttpContext: run-configs fetch returned ${res.status}`)
473
+ }
474
+ } // end dashboardUrl guard
475
+ } catch {
476
+ // Dashboard unreachable or run config not registered — proceed with live execution
477
+ }
478
+ await ensureInterceptorsInstalled()
479
+ const ctx = buildContext(runId, dashboardUrl, frozenEvents, promptMocks, toolMockConfig, aiMockConfig, userPromptMocks)
480
+ g[GLOBAL_CTX_KEY] = ctx
481
+ try {
482
+ return await httpRunAls.run(ctx, callback)
483
+ } finally {
484
+ g[GLOBAL_CTX_KEY] = undefined
485
+ }
486
+ }
487
+
488
+ export async function tryAutoInitHttpContext(): Promise<void> {
489
+ // Fast path: already initialised in this async context
490
+ if (getHttpRunContext()) return
491
+ if (getObservabilityContext()) return
492
+
493
+ // Check for observability mode first (ELASTICDASH_API_URL)
494
+ const apiUrl = (typeof process !== 'undefined' && process.env?.ELASTICDASH_API_URL) ?? ''
495
+ if (apiUrl) {
496
+ const obsInitKey = '__elasticdash_obs_auto_init__'
497
+ if (!g[obsInitKey]) {
498
+ g[obsInitKey] = (async () => {
499
+ try {
500
+ // Dynamic import to avoid circular dependency at module load time
501
+ const { initObservability } = await import('../observability.js')
502
+ initObservability({
503
+ serverUrl: apiUrl,
504
+ apiKey: process.env.ELASTICDASH_API_KEY,
505
+ sessionId: process.env.ELASTICDASH_SESSION_ID,
506
+ })
507
+ } catch (err) {
508
+ debugLog(`[elasticdash] Observability auto-init failed: ${err instanceof Error ? err.message : String(err)}`)
509
+ }
510
+ })()
511
+ }
512
+ await (g[obsInitKey] as Promise<void>)
513
+ return
514
+ }
515
+
516
+ // Fall back to HTTP run context (ELASTICDASH_SERVER — dashboard/test mode)
517
+ const serverUrl = (typeof process !== 'undefined' && process.env?.ELASTICDASH_SERVER) ?? ''
518
+ if (!serverUrl) return
519
+
520
+ // Deduplicate concurrent first calls within the same process
521
+ if (!g[AUTO_INIT_KEY]) {
522
+ g[AUTO_INIT_KEY] = (async () => {
523
+ try {
524
+ const runId = (typeof process !== 'undefined' && process.env?.ELASTICDASH_RUN_ID) ?? ''
525
+ if (runId) {
526
+ await initHttpRunContext(runId, serverUrl)
527
+ } else {
528
+ setHttpRunContext(randomUUID(), serverUrl)
529
+ }
530
+ } catch {
531
+ // Dashboard unreachable — fall through to live execution
532
+ }
533
+ })()
534
+ }
535
+
536
+ await (g[AUTO_INIT_KEY] as Promise<void>)
537
+ }