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,253 @@
1
+ # Workflow Modes: Subprocess vs HTTP
2
+
3
+ ElasticDash supports two workflow execution modes. Choosing the right mode determines whether the dashboard can capture inner tool and AI events from your workflow.
4
+
5
+ ---
6
+
7
+ ## Quick Reference
8
+
9
+ | | Subprocess mode | HTTP mode |
10
+ |---|---|---|
11
+ | Defined in | `ed_workflows.ts` | `elasticdash.config.ts` |
12
+ | How it runs | Dashboard spawns a worker process, imports and calls your function | Dashboard sends an HTTP request directly to your running dev server |
13
+ | Context mechanism | `CaptureContext` (TraceRecorder) in the worker process | `HttpRunContext` (ALS + global fallback) in the dev server |
14
+ | Inner step visibility | Only if tools run in the same process as the workflow function | Yes — `wrapTool`/`wrapAI` push events back to the dashboard via HTTP |
15
+ | Best for | Pure functions that can run standalone (no framework dependencies) | Framework route handlers (Next.js, Remix, Fastify) where tools run inside the server |
16
+
17
+ ---
18
+
19
+ ## Subprocess Mode (`ed_workflows.ts`)
20
+
21
+ The dashboard spawns a child process, imports your function from `ed_workflows.ts`, and calls it. The SDK sets up a `CaptureContext` (TraceRecorder/ReplayController) in the worker process to record events.
22
+
23
+ ```ts
24
+ // ed_workflows.ts
25
+ export async function generateAIResponse(input: { message: string }) {
26
+ // Tools called HERE are captured — they run in the same process
27
+ const refined = await queryRefinement(input)
28
+ const result = await callLLM(refined)
29
+ return result
30
+ }
31
+ ```
32
+
33
+ **When it works well:** The workflow function and all its tool/AI calls run in the same Node.js process. The `CaptureContext` is available to `wrapTool`/`wrapAI` because they share the same `AsyncLocalStorage`.
34
+
35
+ **When it breaks:** The workflow function calls a remote server via `fetch`. The remote server runs in a different process and has no `CaptureContext`. Inner tool/AI calls on the server are invisible to the SDK.
36
+
37
+ ```ts
38
+ // ed_workflows.ts — THIS LOSES INNER STEPS
39
+ export async function chatHandler(input: { messages: Message[] }) {
40
+ // This fetch goes to a DIFFERENT process (Next.js dev server)
41
+ const response = await fetch('http://localhost:3001/api/chat', {
42
+ method: 'POST',
43
+ body: JSON.stringify(input),
44
+ })
45
+ // Only the top-level result is recorded
46
+ // Inner wrapTool/wrapAI calls on the server are NOT captured
47
+ const result = await response.json()
48
+ recordToolCall('chatHandler', input, result)
49
+ return result
50
+ }
51
+ ```
52
+
53
+ **Rule of thumb:** If your `ed_workflows.ts` function calls `fetch()` to your own server, you should use HTTP mode instead.
54
+
55
+ ---
56
+
57
+ ## HTTP Mode (`elasticdash.config.ts`)
58
+
59
+ The dashboard calls your dev server directly via HTTP. It injects special headers (`x-elasticdash-run-id`, `x-elasticdash-server`) that the SDK reads in your route handler to set up `HttpRunContext`. Inner `wrapTool`/`wrapAI` calls push telemetry events back to the dashboard.
60
+
61
+ **Important:** HTTP mode workflows require entries in **both** files:
62
+
63
+ 1. **`ed_workflows.ts`** — export a function so the workflow appears in the dashboard dropdown
64
+ 2. **`elasticdash.config.ts`** — define the same name with `mode: 'http'` so the dashboard uses HTTP mode when running it
65
+
66
+ The dashboard only discovers workflows from `ed_workflows.ts`. When it runs a workflow, it checks `elasticdash.config.ts` — if the name matches an HTTP config, it uses HTTP mode instead of subprocess. If you only define it in the config, it won't appear in the dropdown.
67
+
68
+ ```ts
69
+ // ed_workflows.ts — stub so the workflow appears in the dashboard
70
+ // The actual HTTP call is handled by elasticdash.config.ts, not this function.
71
+ // This function is only called if the config entry is missing or misconfigured.
72
+ export async function chatHandler(input: { messages: Array<{ role: string; content: string }> }) {
73
+ // This body is a fallback — the dashboard uses HTTP mode from the config instead
74
+ return { message: 'This should not run — check elasticdash.config.ts' }
75
+ }
76
+ ```
77
+
78
+ ```ts
79
+ // elasticdash.config.ts — defines how the dashboard calls the route
80
+ export default {
81
+ testMatch: ['**/*.ai.test.ts'],
82
+ workflows: {
83
+ chatHandler: { // ← same name as ed_workflows.ts export
84
+ mode: 'http' as const,
85
+ url: 'http://localhost:3001/api/chat',
86
+ method: 'POST' as const,
87
+ headers: { 'Content-Type': 'application/json' },
88
+ bodyTemplate: {
89
+ messages: '{{input.messages}}',
90
+ sessionId: '{{input.sessionId}}',
91
+ },
92
+ responseFormat: 'json' as const, // or 'vercel-ai-stream'
93
+ },
94
+ },
95
+ }
96
+ ```
97
+
98
+ **How it works:**
99
+
100
+ 1. Dashboard generates a unique `runId` and registers it
101
+ 2. Dashboard sends your request with `x-elasticdash-run-id` and `x-elasticdash-server` headers
102
+ 3. Your route handler reads these headers and calls `initHttpRunContext(runId, server)` or `runWithInitializedHttpContext(runId, server, callback)`
103
+ 4. Every `wrapTool`/`wrapAI` call in your route pushes a telemetry event back to the dashboard
104
+ 5. Dashboard collects all events and displays them as observations
105
+
106
+ **Route handler setup (Next.js example):**
107
+
108
+ ```ts
109
+ // app/api/chat/route.ts
110
+ import { NextRequest } from 'next/server'
111
+
112
+ export async function POST(request: NextRequest) {
113
+ const edRunId = request.headers.get('x-elasticdash-run-id')
114
+ const edServer = request.headers.get('x-elasticdash-server')
115
+
116
+ if (edRunId && edServer) {
117
+ // For non-streaming routes:
118
+ const { initHttpRunContext } = require('elasticdash-sdk')
119
+ await initHttpRunContext(edRunId, edServer)
120
+
121
+ // For streaming routes (preferred — context propagates through als.run):
122
+ const { runWithInitializedHttpContext } = require('elasticdash-sdk')
123
+ return runWithInitializedHttpContext(edRunId, edServer, async () => {
124
+ // All wrapTool/wrapAI calls inside here are captured
125
+ return doWork()
126
+ })
127
+ }
128
+
129
+ return doWork()
130
+ }
131
+ ```
132
+
133
+ **Response formats:**
134
+
135
+ | Format | Config value | Use when |
136
+ |---|---|---|
137
+ | JSON | `responseFormat: 'json'` | Route returns `NextResponse.json(...)` |
138
+ | Vercel AI stream | `responseFormat: 'vercel-ai-stream'` | Route returns `new Response(stream)` with Vercel AI SDK wire protocol |
139
+
140
+ ---
141
+
142
+ ## Common Mistake: Subprocess Wrapper Around an HTTP Call
143
+
144
+ A frequent anti-pattern is defining a subprocess workflow that calls your own server:
145
+
146
+ ```ts
147
+ // ed_workflows.ts — WRONG: subprocess calling your own server
148
+ export async function chatHandler(input: any) {
149
+ const response = await fetch('http://localhost:3001/api/chat', {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ body: JSON.stringify(input),
153
+ })
154
+ const result = await response.json()
155
+ recordToolCall('chatHandler', input, result)
156
+ return result
157
+ }
158
+ ```
159
+
160
+ This records the top-level result but loses all inner steps. The subprocess worker and the Next.js server are different processes — the `CaptureContext` in the worker doesn't reach `wrapTool`/`wrapAI` calls in the server.
161
+
162
+ **Fix:** Define the workflow in `elasticdash.config.ts` as HTTP mode, and simplify `ed_workflows.ts` to a stub:
163
+
164
+ ```ts
165
+ // elasticdash.config.ts — add HTTP mode config
166
+ export default {
167
+ workflows: {
168
+ chatHandler: {
169
+ mode: 'http' as const,
170
+ url: 'http://localhost:3001/api/chat',
171
+ method: 'POST' as const,
172
+ headers: { 'Content-Type': 'application/json' },
173
+ bodyTemplate: {
174
+ messages: '{{input.messages}}',
175
+ },
176
+ responseFormat: 'json' as const,
177
+ },
178
+ },
179
+ }
180
+ ```
181
+
182
+ ```ts
183
+ // ed_workflows.ts — keep the export so it appears in the dashboard dropdown,
184
+ // but remove the fetch logic (the config handles it now)
185
+ export async function chatHandler(input: { messages: Array<{ role: string; content: string }> }) {
186
+ return { message: 'Fallback — check elasticdash.config.ts' }
187
+ }
188
+ ```
189
+
190
+ The dashboard's HTTP mode injects ED headers automatically, your route sets up context, and inner steps flow back to the dashboard.
191
+
192
+ ---
193
+
194
+ ## When to Use Each Mode
195
+
196
+ | Scenario | Mode |
197
+ |---|---|
198
+ | Workflow function runs tools directly (same process) | Subprocess |
199
+ | Workflow calls your own server via HTTP | HTTP |
200
+ | Next.js / Remix / Fastify route handlers | HTTP |
201
+ | Streaming responses (Vercel AI SDK, SSE) | HTTP with `responseFormat: 'vercel-ai-stream'` |
202
+ | Pure AI pipeline with no server (script-based) | Subprocess |
203
+ | `.ai.test.ts` files (programmatic tests) | Subprocess (via test runner) |
204
+
205
+ ---
206
+
207
+ ## Streaming Routes and Context Propagation
208
+
209
+ For streaming routes that return a `ReadableStream`, use `runWithInitializedHttpContext` inside the stream's `start()` callback to ensure ALS context propagates through all async work:
210
+
211
+ ```ts
212
+ // Recommended pattern for streaming routes
213
+ const stream = new ReadableStream({
214
+ async start(controller) {
215
+ if (edRunId && edServer) {
216
+ const { runWithInitializedHttpContext } = require('elasticdash-sdk')
217
+ await runWithInitializedHttpContext(edRunId, edServer, async () => {
218
+ // All wrapTool/wrapAI calls here are captured
219
+ const result = await myWrappedTool(input)
220
+ controller.enqueue(encoder.encode(JSON.stringify(result)))
221
+ controller.close()
222
+ })
223
+ }
224
+ },
225
+ })
226
+ return new Response(stream, { headers: { ... } })
227
+ ```
228
+
229
+ If `runWithInitializedHttpContext` is called outside `start()` (e.g., in the handler scope), the SDK's global fallback ensures context is still available inside `start()`. However, wrapping inside `start()` is the more reliable pattern.
230
+
231
+ ---
232
+
233
+ ## Importing the SDK in Next.js / Turbopack
234
+
235
+ Turbopack statically analyzes `import()` and `require()` calls, which causes `Module not found` errors for `serverExternalPackages`. Use `eval('require')` or `createRequire` to bypass this:
236
+
237
+ ```ts
238
+ // Option 1: eval('require') — simple, works everywhere
239
+ const { wrapTool } = (eval('require') as (id: string) => any)('elasticdash-sdk')
240
+
241
+ // Option 2: createRequire — cleaner, no eval
242
+ import { createRequire } from 'node:module'
243
+ const nodeRequire = createRequire(process.cwd() + '/')
244
+ const { wrapTool } = nodeRequire('elasticdash-sdk')
245
+ ```
246
+
247
+ Also add the SDK to `serverExternalPackages` in `next.config.js`:
248
+
249
+ ```js
250
+ module.exports = {
251
+ serverExternalPackages: ['elasticdash-sdk'],
252
+ }
253
+ ```
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "elasticdash-sdk",
3
+ "version": "0.2.0",
4
+ "description": "AI-native SDK for ElasticDash workflow testing, tracing, and observability",
5
+ "type": "module",
6
+ "bin": {
7
+ "elasticdash": "./dist/cli.js",
8
+ "ed": "./dist/cli.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "require": "./dist/index.cjs",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./http": {
19
+ "types": "./dist/http.d.ts",
20
+ "default": "./dist/http.js"
21
+ },
22
+ "./observability": {
23
+ "types": "./dist/observability.d.ts",
24
+ "default": "./dist/observability.js"
25
+ },
26
+ "./portal": {
27
+ "types": "./dist/portal-server.d.ts",
28
+ "default": "./dist/portal-server.js"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "src",
34
+ "docs",
35
+ "package.json",
36
+ "README.md"
37
+ ],
38
+ "scripts": {
39
+ "build": "node scripts/inline-html.js && tsc && npm run restore:inline-html && node -e \"const{cpSync,mkdirSync}=require('fs');mkdirSync('dist/html',{recursive:true});cpSync('src/html/dashboard.html','dist/html/dashboard.html');\" && node_modules/.bin/esbuild src/index.ts --bundle --platform=node --format=cjs --outfile=dist/index.cjs --packages=external --target=node18 && npm pack",
40
+ "restore:inline-html": "node scripts/restore-inline-html.js",
41
+ "dev": "tsx src/cli.ts",
42
+ "start": "node dist/cli.js",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc --noEmit",
45
+ "dashboard": "node dist/cli.js dashboard",
46
+ "release": "npm run build && npm publish --tag beta",
47
+ "release:prod": "npm run build && npm publish"
48
+ },
49
+ "dependencies": {
50
+ "chalk": "^5.3.0",
51
+ "chokidar": "^5.0.0",
52
+ "commander": "^12.1.0",
53
+ "dotenv": "^17.3.1",
54
+ "esbuild": "^0.27.3",
55
+ "expect": "^29.7.0",
56
+ "express": "^5.2.1",
57
+ "fast-glob": "^3.3.2",
58
+ "socket.io-client": "^4.8.3",
59
+ "tsx": "^4.15.6"
60
+ },
61
+ "devDependencies": {
62
+ "@types/express": "^5.0.6",
63
+ "@types/node": "^20.14.0",
64
+ "typescript": "^5.9.3",
65
+ "vitest": "^4.1.4"
66
+ },
67
+ "engines": {
68
+ "node": ">=20.0.0"
69
+ },
70
+ "author": "ElasticDash <contact@elasticdash.com>",
71
+ "license": "MIT",
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "https://github.com/ElasticDash/elasticdash-test-js.git"
75
+ }
76
+ }
@@ -0,0 +1,281 @@
1
+ import http from 'node:http'
2
+ import path from 'node:path'
3
+ import os from 'node:os'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { spawn } from 'node:child_process'
6
+
7
+ export type UiEvent =
8
+ | { type: 'run-start'; payload: { files: string[] } }
9
+ | { type: 'test-start'; payload: { name: string } }
10
+ | { type: 'test-finish'; payload: { name: string; passed: boolean; durationMs: number; errorMessage?: string } }
11
+ | { type: 'run-summary'; payload: { passed: number; failed: number; total: number; durationMs: number; failures: Array<{ name: string; errorMessage?: string }> } }
12
+
13
+ export interface BrowserUiServer {
14
+ url: string
15
+ send(event: UiEvent): void
16
+ close(): void
17
+ }
18
+
19
+ interface BrowserUiOptions {
20
+ port?: number
21
+ autoOpen?: boolean
22
+ }
23
+
24
+ const defaultHtml = `<!doctype html>
25
+ <html lang="en">
26
+ <head>
27
+ <meta charset="UTF-8" />
28
+ <title>ElasticDash SDK Test Runner</title>
29
+ <style>
30
+ :root { font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0b1021; color: #e8ecf7; }
31
+ body { margin: 0; padding: 24px; }
32
+ h1 { margin: 0 0 16px; font-size: 20px; }
33
+ .summary { display: flex; gap: 12px; margin-bottom: 16px; }
34
+ .pill { padding: 8px 12px; border-radius: 999px; font-weight: 600; }
35
+ .pass { background: #12351b; color: #8de0a3; }
36
+ .fail { background: #351212; color: #f59b9b; }
37
+ .total { background: #1c2745; color: #cdd7ff; }
38
+ .tests { margin-top: 12px; }
39
+ .test { border: 1px solid #1f2a4f; border-radius: 8px; padding: 12px; margin-bottom: 8px; background: #0f1731; }
40
+ .name { font-weight: 600; }
41
+ .error { margin-top: 8px; white-space: pre-wrap; color: #f59b9b; font-family: ui-monospace, SFMono-Regular, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; }
42
+ .status { font-weight: 600; }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <h1>ElasticDash SDK Test Runner</h1>
47
+ <div class="summary">
48
+ <div class="pill total" id="total">Total: -</div>
49
+ <div class="pill pass" id="passed">Passed: -</div>
50
+ <div class="pill fail" id="failed">Failed: -</div>
51
+ </div>
52
+ <div id="progress">Waiting for test run...</div>
53
+ <div class="tests" id="tests"></div>
54
+ <script>
55
+ const totalEl = document.getElementById('total');
56
+ const passedEl = document.getElementById('passed');
57
+ const failedEl = document.getElementById('failed');
58
+ const progressEl = document.getElementById('progress');
59
+ const testsEl = document.getElementById('tests');
60
+
61
+ const tests = new Map();
62
+ let processed = 0;
63
+ let passedCount = 0;
64
+ let failedCount = 0;
65
+ let finalTotal = null;
66
+
67
+ function setError(el, message) {
68
+ const errEl = el.querySelector('.error');
69
+ let toggle = el.querySelector('.toggle');
70
+ if (!message) {
71
+ errEl.textContent = '';
72
+ errEl.style.display = 'none';
73
+ if (toggle) toggle.style.display = 'none';
74
+ return;
75
+ }
76
+ if (!toggle) {
77
+ toggle = document.createElement('button');
78
+ toggle.className = 'toggle';
79
+ toggle.textContent = 'Show details';
80
+ toggle.style.marginTop = '8px';
81
+ toggle.style.background = '#1c2745';
82
+ toggle.style.color = '#cdd7ff';
83
+ toggle.style.border = '1px solid #2a3866';
84
+ toggle.style.borderRadius = '6px';
85
+ toggle.style.padding = '6px 10px';
86
+ toggle.style.cursor = 'pointer';
87
+ toggle.style.fontWeight = '600';
88
+ el.appendChild(toggle);
89
+ toggle.addEventListener('click', () => {
90
+ const isHidden = errEl.style.display === 'none';
91
+ errEl.style.display = isHidden ? 'block' : 'none';
92
+ toggle.textContent = isHidden ? 'Hide details' : 'Show details';
93
+ });
94
+ }
95
+ errEl.textContent = message;
96
+ errEl.style.display = 'none';
97
+ toggle.style.display = 'inline-block';
98
+ toggle.textContent = 'Show details';
99
+ }
100
+
101
+ function renderTest(name) {
102
+ let el = tests.get(name);
103
+ if (!el) {
104
+ el = document.createElement('div');
105
+ el.className = 'test';
106
+ el.innerHTML = '<div class="name"></div><div class="status"></div><div class="error"></div>';
107
+ tests.set(name, el);
108
+ testsEl.appendChild(el);
109
+ }
110
+ return el;
111
+ }
112
+
113
+ function updatePills(passed, failed, total) {
114
+ totalEl.textContent = 'Total: ' + total;
115
+ passedEl.textContent = 'Passed: ' + passed;
116
+ failedEl.textContent = 'Failed: ' + failed;
117
+ }
118
+
119
+ const evtSource = new EventSource('/events');
120
+ evtSource.onmessage = (ev) => {
121
+ try {
122
+ const msg = JSON.parse(ev.data);
123
+ if (msg.type === 'test-start') {
124
+ const el = renderTest(msg.payload.name);
125
+ el.querySelector('.name').textContent = msg.payload.name;
126
+ el.querySelector('.status').textContent = 'Running...';
127
+ el.querySelector('.status').style.color = '#cdd7ff';
128
+ el.querySelector('.error').textContent = '';
129
+ progressEl.textContent = 'Running tests...';
130
+ }
131
+ if (msg.type === 'test-finish') {
132
+ const el = renderTest(msg.payload.name);
133
+ el.querySelector('.name').textContent = msg.payload.name;
134
+ el.querySelector('.status').textContent = msg.payload.passed ? 'Passed' : 'Failed';
135
+ el.querySelector('.status').style.color = msg.payload.passed ? '#8de0a3' : '#f59b9b';
136
+ setError(el, msg.payload.errorMessage || '');
137
+
138
+ // live tally
139
+ processed += 1;
140
+ if (msg.payload.passed) passedCount += 1;
141
+ else failedCount += 1;
142
+ const displayTotal = finalTotal !== null ? finalTotal : processed;
143
+ updatePills(passedCount, failedCount, displayTotal);
144
+ }
145
+ if (msg.type === 'run-summary') {
146
+ finalTotal = msg.payload.total;
147
+ passedCount = msg.payload.passed;
148
+ failedCount = msg.payload.failed;
149
+ processed = msg.payload.total;
150
+ updatePills(msg.payload.passed, msg.payload.failed, msg.payload.total);
151
+ progressEl.textContent = 'Finished';
152
+ msg.payload.failures.forEach(function (f) {
153
+ const el = renderTest(f.name);
154
+ el.querySelector('.name').textContent = f.name;
155
+ el.querySelector('.status').textContent = 'Failed';
156
+ el.querySelector('.status').style.color = '#f59b9b';
157
+ setError(el, f.errorMessage || '');
158
+ });
159
+ }
160
+ } catch (e) {
161
+ console.error('Bad event data', e);
162
+ }
163
+ };
164
+ </script>
165
+ </body>
166
+ </html>`
167
+
168
+ export async function startBrowserUiServer(opts: BrowserUiOptions = {}): Promise<BrowserUiServer | undefined> {
169
+ const autoOpen = opts.autoOpen !== false
170
+ let port = opts.port ?? 4571
171
+
172
+ // // Ensure base dir for potential static assets (none now)
173
+ // const __dirname = path.dirname(fileURLToPath(import.meta.url))
174
+
175
+ type FlushableResponse = http.ServerResponse & { flush?: () => void; flushHeaders?: () => void }
176
+ const clients: FlushableResponse[] = []
177
+ const eventBuffer: UiEvent[] = []
178
+
179
+ const handler: http.RequestListener = (req, res) => {
180
+ if (!req.url) return res.end()
181
+ if (req.url.startsWith('/events')) {
182
+ const sseRes = res as FlushableResponse
183
+ sseRes.writeHead(200, {
184
+ 'Content-Type': 'text/event-stream',
185
+ Connection: 'keep-alive',
186
+ 'Cache-Control': 'no-cache',
187
+ })
188
+ // Prime the connection so browsers render immediately
189
+ sseRes.flushHeaders?.()
190
+ sseRes.write(': connected\n\n')
191
+ sseRes.flush?.()
192
+ // Replay all previously sent events so late-connecting browsers get the full history
193
+ for (const e of eventBuffer) {
194
+ sseRes.write(`data: ${JSON.stringify(e)}\n\n`)
195
+ }
196
+ sseRes.flush?.()
197
+ clients.push(sseRes)
198
+ req.on('close', () => {
199
+ const idx = clients.indexOf(sseRes)
200
+ if (idx >= 0) clients.splice(idx, 1)
201
+ })
202
+ return
203
+ }
204
+
205
+ // Serve inline HTML
206
+ res.writeHead(200, { 'Content-Type': 'text/html' })
207
+ res.end(defaultHtml)
208
+ }
209
+
210
+ let server: http.Server | undefined
211
+ let heartbeat: ReturnType<typeof setInterval> | undefined
212
+ let started = false
213
+
214
+ while (!started) {
215
+ try {
216
+ server = http.createServer(handler)
217
+ await new Promise<void>((resolve, reject) => {
218
+ server!.once('error', reject)
219
+ server!.listen(port, resolve)
220
+ })
221
+ started = true
222
+ } catch (err) {
223
+ port += 1
224
+ if (port > (opts.port ?? 4571) + 10) {
225
+ console.error('[elasticdash] Browser UI server failed to start:', (err as Error).message)
226
+ return undefined
227
+ }
228
+ }
229
+ }
230
+
231
+ const url = `http://localhost:${port}`
232
+
233
+ function send(event: UiEvent): void {
234
+ eventBuffer.push(event)
235
+ const payload = `data: ${JSON.stringify(event)}\n\n`
236
+ for (const client of [...clients]) {
237
+ client.write(payload)
238
+ client.flush?.()
239
+ }
240
+ }
241
+
242
+ function close(): void {
243
+ if (heartbeat) {
244
+ clearInterval(heartbeat)
245
+ heartbeat = undefined
246
+ }
247
+ for (const client of clients) {
248
+ client.end()
249
+ }
250
+ clients.length = 0
251
+ server?.close()
252
+ }
253
+
254
+ if (autoOpen) {
255
+ openBrowser(url)
256
+ }
257
+
258
+ // Periodic keepalive comments to keep EventSource connections from timing out
259
+ heartbeat = setInterval(() => {
260
+ for (const client of [...clients]) {
261
+ client.write(': keepalive\n\n')
262
+ client.flush?.()
263
+ }
264
+ }, 5000)
265
+
266
+ return { url, send, close }
267
+ }
268
+
269
+ function openBrowser(url: string): void {
270
+ const platform = os.platform()
271
+ const command =
272
+ platform === 'darwin'
273
+ ? 'open'
274
+ : platform === 'win32'
275
+ ? 'cmd'
276
+ : 'xdg-open'
277
+
278
+ const args = platform === 'win32' ? ['/c', 'start', '""', url] : [url]
279
+ const child = spawn(command, args, { stdio: 'ignore', detached: true })
280
+ child.unref()
281
+ }
@@ -0,0 +1,30 @@
1
+ export type WorkflowEventType = 'ai' | 'tool' | 'http' | 'db' | 'side_effect' | 'workflow'
2
+
3
+ export interface WorkflowEvent {
4
+ id: number
5
+ type: WorkflowEventType
6
+ name: string
7
+ input: unknown
8
+ output: unknown
9
+ timestamp: number
10
+ durationMs: number
11
+ /** Token usage for LLM (ai) events */
12
+ usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number }
13
+ /** Optional: ID of the agent task that produced this event */
14
+ agentTaskId?: string
15
+ /** Optional: Zero-based index of the agent task that produced this event */
16
+ agentTaskIndex?: number
17
+ /** Set to true when the original response / output was a stream */
18
+ streamed?: boolean
19
+ /** Raw buffered text of a streamed response (used for replay) */
20
+ streamRaw?: string
21
+ /** Schema version for forward compatibility (default 1) */
22
+ schemaVersion?: number
23
+ /** Optional request-level trace ID for grouping events within a session */
24
+ traceId?: string
25
+ }
26
+
27
+ export interface WorkflowTrace {
28
+ traceId: string
29
+ events: WorkflowEvent[]
30
+ }
@@ -0,0 +1,3 @@
1
+ export * from './event.js'
2
+ export * from './recorder.js'
3
+ export * from './replay.js'