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,301 @@
1
+ import http from 'node:http'
2
+ import { URL } from 'node:url'
3
+ import { Readable } from 'node:stream'
4
+ import type { LLMStep } from '../trace-adapter/context.js'
5
+
6
+ const DEFAULT_PORT = 8787
7
+ const HEADER_TRACE_ID = 'x-trace-id'
8
+
9
+ type Provider = 'openai' | 'gemini' | 'grok' | 'anthropic'
10
+
11
+ const DEFAULT_UPSTREAM: Record<Provider, string> = {
12
+ openai: 'https://api.openai.com',
13
+ gemini: 'https://generativelanguage.googleapis.com',
14
+ grok: 'https://api.x.ai',
15
+ anthropic: 'https://api.anthropic.com',
16
+ }
17
+
18
+ const AI_PATTERNS: Record<Provider, RegExp> = {
19
+ openai: /\/v1\/(chat\/)?completions/, // also covers legacy /v1/completions
20
+ gemini: /\/v1beta\/models\/[^/:]+:(generateContent|streamGenerateContent)/,
21
+ grok: /\/v1\/(chat\/)?completions/,
22
+ anthropic: /\/v1\/messages/,
23
+ }
24
+
25
+ function detectProvider(pathname: string): Provider | null {
26
+ for (const [provider, pattern] of Object.entries(AI_PATTERNS) as Array<[Provider, RegExp]>) {
27
+ if (pattern.test(pathname)) return provider
28
+ }
29
+ return null
30
+ }
31
+
32
+ function extractModel(provider: Provider, body: Record<string, unknown>, url: string): string {
33
+ if (provider === 'gemini') {
34
+ const match = /\/models\/([^/:]+):/.exec(url)
35
+ return match ? match[1] : 'unknown'
36
+ }
37
+ if (provider === 'anthropic') {
38
+ return typeof body.model === 'string' ? body.model : 'unknown'
39
+ }
40
+ return typeof body.model === 'string' ? body.model : 'unknown'
41
+ }
42
+
43
+ function extractPrompt(provider: Provider, body: Record<string, unknown>): string {
44
+ if (provider === 'openai' || provider === 'grok') {
45
+ const messages = body.messages
46
+ if (Array.isArray(messages)) {
47
+ return messages
48
+ .map((m: unknown) => {
49
+ if (m && typeof m === 'object') {
50
+ const msg = m as Record<string, unknown>
51
+ return `${msg.role}: ${msg.content}`
52
+ }
53
+ return String(m)
54
+ })
55
+ .join('\n')
56
+ }
57
+ return typeof body.prompt === 'string' ? body.prompt : ''
58
+ }
59
+
60
+ if (provider === 'gemini') {
61
+ const contents = body.contents
62
+ if (Array.isArray(contents)) {
63
+ return contents
64
+ .flatMap((c: unknown) => {
65
+ if (c && typeof c === 'object') {
66
+ const parts = (c as Record<string, unknown>).parts
67
+ if (Array.isArray(parts)) {
68
+ return parts.map((p: unknown) => {
69
+ if (p && typeof p === 'object') {
70
+ return String((p as Record<string, unknown>).text ?? '')
71
+ }
72
+ return ''
73
+ })
74
+ }
75
+ }
76
+ return []
77
+ })
78
+ .join('\n')
79
+ }
80
+ }
81
+
82
+ if (provider === 'anthropic') {
83
+ const messages = body.messages
84
+ if (Array.isArray(messages)) {
85
+ return messages
86
+ .map((m: unknown) => {
87
+ if (m && typeof m === 'object') {
88
+ const msg = m as Record<string, unknown>
89
+ return `${msg.role ?? 'user'}: ${msg.content ?? ''}`
90
+ }
91
+ return String(m)
92
+ })
93
+ .join('\n')
94
+ }
95
+ }
96
+
97
+ return ''
98
+ }
99
+
100
+ function extractCompletion(provider: Provider, responseBody: Record<string, unknown>): string {
101
+ if (provider === 'openai' || provider === 'grok') {
102
+ const choices = responseBody.choices
103
+ if (Array.isArray(choices) && choices.length > 0) {
104
+ const first = choices[0] as Record<string, unknown>
105
+ if (first.message && typeof first.message === 'object') {
106
+ return String((first.message as Record<string, unknown>).content ?? '')
107
+ }
108
+ if (typeof first.text === 'string') return first.text
109
+ }
110
+ }
111
+
112
+ if (provider === 'gemini') {
113
+ const candidates = responseBody.candidates
114
+ if (Array.isArray(candidates) && candidates.length > 0) {
115
+ const first = candidates[0] as Record<string, unknown>
116
+ if (first.content && typeof first.content === 'object') {
117
+ const parts = (first.content as Record<string, unknown>).parts
118
+ if (Array.isArray(parts) && parts.length > 0) {
119
+ return String((parts[0] as Record<string, unknown>).text ?? '')
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ if (provider === 'anthropic') {
126
+ const content = responseBody.content
127
+ if (Array.isArray(content) && content.length > 0) {
128
+ const first = content[0] as Record<string, unknown>
129
+ if (typeof first.text === 'string') return first.text
130
+ if (first.type === 'text' && typeof first.text === 'string') return first.text
131
+ }
132
+ }
133
+
134
+ return ''
135
+ }
136
+
137
+ function cloneHeaders(headers: http.IncomingHttpHeaders): Record<string, string> {
138
+ const result: Record<string, string> = {}
139
+ for (const [key, value] of Object.entries(headers)) {
140
+ if (Array.isArray(value)) {
141
+ result[key] = value.join(', ')
142
+ } else if (typeof value === 'string') {
143
+ result[key] = value
144
+ }
145
+ }
146
+ return result
147
+ }
148
+
149
+ function normalizeUpstream(provider: Provider, userBase?: string): string {
150
+ const base = userBase || DEFAULT_UPSTREAM[provider]
151
+ return base.endsWith('/') ? base.slice(0, -1) : base
152
+ }
153
+
154
+ type Store = Map<string, LLMStep[]>
155
+
156
+ function recordStep(store: Store, traceId: string, step: LLMStep): void {
157
+ if (!store.has(traceId)) {
158
+ store.set(traceId, [])
159
+ }
160
+ store.get(traceId)!.push(step)
161
+ }
162
+
163
+ async function readBody(req: http.IncomingMessage): Promise<Buffer> {
164
+ const chunks: Buffer[] = []
165
+ for await (const chunk of req) {
166
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
167
+ }
168
+ return Buffer.concat(chunks)
169
+ }
170
+
171
+ function sendUpstreamResponse(upstreamRes: Response, res: http.ServerResponse): void {
172
+ res.statusCode = upstreamRes.status
173
+ for (const [key, value] of upstreamRes.headers.entries()) {
174
+ res.setHeader(key, value)
175
+ }
176
+ const body = upstreamRes.body
177
+ if (!body) {
178
+ res.end()
179
+ return
180
+ }
181
+ const nodeStream = Readable.fromWeb(body as unknown as ReadableStream)
182
+ nodeStream.pipe(res)
183
+ }
184
+
185
+ export interface StartedProxy {
186
+ url: string
187
+ stop(): Promise<void>
188
+ }
189
+
190
+ export interface ProxyOptions {
191
+ port?: number
192
+ upstream?: Partial<Record<Provider, string>>
193
+ }
194
+
195
+ export async function startLLMProxy(options: ProxyOptions = {}): Promise<StartedProxy> {
196
+ const port = options.port ?? DEFAULT_PORT
197
+ const store: Store = new Map()
198
+ const upstreamOverride = options.upstream || {}
199
+
200
+ const server = http.createServer(async (req, res) => {
201
+ try {
202
+ if (!req.url || !req.method) {
203
+ res.statusCode = 400
204
+ res.end('Bad request')
205
+ return
206
+ }
207
+
208
+ const parsed = new URL(req.url, `http://localhost:${port}`)
209
+
210
+ if (req.method === 'GET' && parsed.pathname.startsWith('/traces/')) {
211
+ const traceId = decodeURIComponent(parsed.pathname.replace('/traces/', ''))
212
+ const steps = store.get(traceId) ?? []
213
+ store.delete(traceId)
214
+ res.setHeader('content-type', 'application/json')
215
+ res.end(JSON.stringify({ steps }))
216
+ return
217
+ }
218
+
219
+ if (req.method === 'GET' && parsed.pathname === '/health') {
220
+ res.statusCode = 200
221
+ res.end('ok')
222
+ return
223
+ }
224
+
225
+ const bodyBuf = await readBody(req)
226
+ const bodyText = bodyBuf.toString() || '{}'
227
+ const requestBody = (() => {
228
+ try {
229
+ return JSON.parse(bodyText) as Record<string, unknown>
230
+ } catch {
231
+ return {} as Record<string, unknown>
232
+ }
233
+ })()
234
+
235
+ const provider = detectProvider(parsed.pathname)
236
+ const traceId = (req.headers[HEADER_TRACE_ID] as string | undefined)?.toString()
237
+ const isStreaming = requestBody && typeof requestBody === 'object' ? (requestBody as any).stream === true : false
238
+ const headers = cloneHeaders(req.headers)
239
+
240
+ const upstreamBase = provider ? normalizeUpstream(provider, upstreamOverride[provider]) : undefined
241
+ if (!provider || !upstreamBase) {
242
+ // Fallback passthrough without capture
243
+ const passthrough = await fetch(parsed.toString(), {
244
+ method: req.method,
245
+ headers,
246
+ body: bodyBuf.length > 0 ? bodyBuf : undefined,
247
+ })
248
+ sendUpstreamResponse(passthrough, res)
249
+ return
250
+ }
251
+
252
+ const targetUrl = `${upstreamBase}${parsed.pathname}${parsed.search}`
253
+ const upstreamRes = await fetch(targetUrl, {
254
+ method: req.method,
255
+ headers,
256
+ body: bodyBuf.length > 0 ? bodyBuf : undefined,
257
+ })
258
+
259
+ if (traceId) {
260
+ const model = extractModel(provider, requestBody, targetUrl)
261
+ const prompt = extractPrompt(provider, requestBody)
262
+ if (isStreaming) {
263
+ recordStep(store, traceId, { model, provider, prompt, completion: '(streamed)' })
264
+ } else {
265
+ try {
266
+ const clone = upstreamRes.clone()
267
+ const responseBody = (await clone.json()) as Record<string, unknown>
268
+ const completion = extractCompletion(provider, responseBody)
269
+ recordStep(store, traceId, { model, provider, prompt, completion })
270
+ } catch {
271
+ recordStep(store, traceId, { model, provider, prompt, completion: '' })
272
+ }
273
+ }
274
+ }
275
+
276
+ sendUpstreamResponse(upstreamRes, res)
277
+ } catch (err) {
278
+ res.statusCode = 500
279
+ res.end(`proxy error: ${(err as Error).message}`)
280
+ }
281
+ })
282
+
283
+ await new Promise<void>((resolve) => server.listen(port, resolve))
284
+
285
+ return {
286
+ url: `http://localhost:${port}`,
287
+ async stop() {
288
+ await new Promise<void>((resolve) => server.close(() => resolve()))
289
+ },
290
+ }
291
+ }
292
+
293
+ export async function fetchCapturedTrace(proxyUrl: string, traceId: string): Promise<LLMStep[]> {
294
+ const url = `${proxyUrl.replace(/\/$/, '')}/traces/${encodeURIComponent(traceId)}`
295
+ const res = await fetch(url)
296
+ if (!res.ok) {
297
+ throw new Error(`failed to fetch trace ${traceId} from proxy: ${res.status}`)
298
+ }
299
+ const data = (await res.json()) as { steps?: LLMStep[] }
300
+ return data.steps || []
301
+ }
@@ -0,0 +1,81 @@
1
+ import chalk from 'chalk'
2
+ import type { TestResult, FileResult } from './runner.js'
3
+
4
+ export function reportResults(fileResults: FileResult[]): void {
5
+ let totalPassed = 0
6
+ let totalFailed = 0
7
+ let totalDurationMs = 0
8
+
9
+ for (const fileResult of fileResults) {
10
+ if (fileResults.length > 1) {
11
+ console.log(chalk.dim(`\n${fileResult.file}`))
12
+ }
13
+
14
+ for (const result of fileResult.results) {
15
+ printTestResult(result)
16
+ totalDurationMs += result.durationMs
17
+ if (result.passed) {
18
+ totalPassed++
19
+ } else {
20
+ totalFailed++
21
+ }
22
+ }
23
+ }
24
+
25
+ printSummary(totalPassed, totalFailed, totalDurationMs)
26
+ }
27
+
28
+ function printTestResult(result: TestResult): void {
29
+ const duration = chalk.dim(`(${formatDuration(result.durationMs)})`)
30
+
31
+ if (result.passed) {
32
+ console.log(` ${chalk.green('✓')} ${result.name} ${duration}`)
33
+ } else {
34
+ console.log(` ${chalk.red('✗')} ${result.name} ${duration}`)
35
+ if (result.error) {
36
+ const errorLines = formatError(result.error)
37
+ for (const line of errorLines) {
38
+ console.log(` ${chalk.red('→')} ${line}`)
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ function printSummary(passed: number, failed: number, totalMs: number): void {
45
+ const total = passed + failed
46
+ console.log('')
47
+
48
+ if (passed > 0) {
49
+ console.log(chalk.green(`${passed} passed`))
50
+ }
51
+ if (failed > 0) {
52
+ console.log(chalk.red(`${failed} failed`))
53
+ }
54
+
55
+ console.log(chalk.dim(`Total: ${total}`))
56
+ console.log(chalk.dim(`Duration: ${formatDuration(totalMs)}`))
57
+ }
58
+
59
+ function formatDuration(ms: number): string {
60
+ if (ms >= 1000) {
61
+ return `${(ms / 1000).toFixed(1)}s`
62
+ }
63
+ return `${ms}ms`
64
+ }
65
+
66
+ function formatError(error: Error): string[] {
67
+ const lines: string[] = []
68
+ if (error.message) {
69
+ lines.push(error.message)
70
+ }
71
+ if (error.stack) {
72
+ const stackLines = error.stack
73
+ .split('\n')
74
+ .slice(1)
75
+ .map((l) => l.trim())
76
+ .filter((l) => l.startsWith('at '))
77
+ .slice(0, 3)
78
+ lines.push(...stackLines)
79
+ }
80
+ return lines
81
+ }
@@ -0,0 +1,74 @@
1
+ import { spawn } from 'child_process';
2
+ import { join } from 'path';
3
+ import { Readable } from 'stream';
4
+
5
+ /**
6
+ * Run workflow logic in a subprocess using the workflow-runner-worker (fd3 IPC).
7
+ * This utility is designed for use in Next.js API routes or any parent process.
8
+ *
9
+ * @param {object} input - The workflow input (requestBody, userToken, testCaseId, testCaseRunRecordId, etc)
10
+ * @returns {Promise<any>} - Resolves with the result from the worker, or throws on error.
11
+ *
12
+ * Usage (in your Next.js API route):
13
+ * import { runWorkflowInSubprocess } from './src/runWorkflowSubprocess';
14
+ * const result = await runWorkflowInSubprocess({ ... });
15
+ * return NextResponse.json(result);
16
+ */
17
+ export async function runWorkflowInSubprocess(input: Record<string, any>): Promise<any> {
18
+ return new Promise((resolve, reject) => {
19
+ // Path to the workflow worker entry point
20
+ const workerPath = join(process.cwd(), 'src', 'workflow-runner-worker.ts');
21
+
22
+ // Spawn the worker as a subprocess with fd3 pipe
23
+ const child = spawn(
24
+ process.execPath,
25
+ [workerPath],
26
+ {
27
+ stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
28
+ env: { ...process.env },
29
+ }
30
+ );
31
+
32
+ // Write the input as JSON to the worker's stdin
33
+ child.stdin.write(JSON.stringify(input));
34
+ child.stdin.end();
35
+
36
+ let resultData = '';
37
+ let errorData = '';
38
+
39
+ // Read result from fd3 (child.stdio[3])
40
+ if (child.stdio[3] && child.stdio[3] instanceof Readable) {
41
+ child.stdio[3].setEncoding('utf8');
42
+ child.stdio[3].on('data', (chunk) => {
43
+ resultData += chunk;
44
+ });
45
+ }
46
+
47
+ // Optionally, collect logs from stdout/stderr for debugging
48
+ child.stdout?.on('data', (chunk) => {
49
+ // Optionally log or buffer
50
+ });
51
+ child.stderr?.on('data', (chunk) => {
52
+ errorData += chunk;
53
+ });
54
+
55
+ child.on('error', (err) => {
56
+ reject(err);
57
+ });
58
+
59
+ child.on('close', (code) => {
60
+ if (resultData) {
61
+ try {
62
+ const parsed = JSON.parse(resultData);
63
+ resolve(parsed);
64
+ } catch (err) {
65
+ reject(new Error('Failed to parse worker result: ' + err));
66
+ }
67
+ } else if (errorData) {
68
+ reject(new Error('Worker error: ' + errorData));
69
+ } else {
70
+ reject(new Error('Worker exited with code ' + code + ' and no result.'));
71
+ }
72
+ });
73
+ });
74
+ }
package/src/runner.ts ADDED
@@ -0,0 +1,178 @@
1
+ import { clearRegistry, getRegistry } from './core/registry.js'
2
+ import { startTraceSession, setCurrentTrace } from './trace-adapter/context.js'
3
+ import { startLLMProxy, fetchCapturedTrace } from './proxy/llm-capture.js'
4
+ import type { RunnerHooks } from './trace-adapter/context.js'
5
+ import { pathToFileURL } from 'node:url'
6
+ import { randomUUID } from 'node:crypto'
7
+ import path from 'node:path'
8
+
9
+ export interface TestResult {
10
+ name: string
11
+ passed: boolean
12
+ durationMs: number
13
+ error?: Error
14
+ }
15
+
16
+ export interface FileResult {
17
+ file: string
18
+ results: TestResult[]
19
+ }
20
+
21
+ export interface RunnerOptions {
22
+ hooks?: RunnerHooks
23
+ }
24
+
25
+ export async function runFiles(files: string[], options: RunnerOptions = {}): Promise<FileResult[]> {
26
+ // Optional local LLM capture proxy (opt-in via env). Default behavior stays unchanged when disabled.
27
+ const proxyOptIn = process.env.ELASTICDASH_LLM_PROXY === '1' || Boolean(process.env.ELASTICDASH_LLM_PROXY_URL)
28
+ const proxyPort = Number.parseInt(process.env.ELASTICDASH_LLM_PROXY_PORT || '8787', 10)
29
+ let proxyUrl = process.env.ELASTICDASH_LLM_PROXY_URL
30
+ const proxyHandle = proxyOptIn && !proxyUrl ? await startLLMProxy({ port: proxyPort }) : null
31
+ if (proxyHandle) {
32
+ proxyUrl = proxyHandle.url
33
+ }
34
+
35
+ const fileResults: FileResult[] = []
36
+
37
+ for (const file of files) {
38
+ const result = await runFile(file, options, { proxyOptIn, proxyUrl })
39
+ fileResults.push(result)
40
+ }
41
+
42
+ if (proxyHandle) {
43
+ await proxyHandle.stop()
44
+ }
45
+
46
+ return fileResults
47
+ }
48
+
49
+ async function runFile(file: string, options: RunnerOptions, proxyCtx: { proxyOptIn: boolean; proxyUrl: string | null | undefined }): Promise<FileResult> {
50
+ const { hooks = {} } = options
51
+
52
+ // 1. Clear the global registry before loading the file
53
+ clearRegistry()
54
+
55
+ // 2. Dynamically import the test file (triggers aiTest() registrations)
56
+ const resolvedPath = file.startsWith('file://')
57
+ ? file
58
+ : pathToFileURL(path.resolve(file)).href
59
+
60
+ if (resolvedPath.endsWith('.ts') && typeof (globalThis as any).Deno === 'undefined') {
61
+ await import('tsx/esm')
62
+ await import('tsx/cjs')
63
+ }
64
+
65
+ await import(resolvedPath)
66
+
67
+ const registry = getRegistry()
68
+ const results: TestResult[] = []
69
+
70
+ // Shared unhandled error trap for this file's test run
71
+ let currentTestName: string | null = null
72
+ let pendingUnhandled: Error | undefined
73
+ const onUnhandled = (reason: unknown) => {
74
+ if (!pendingUnhandled) pendingUnhandled = reason instanceof Error ? reason : new Error(String(reason))
75
+ }
76
+ process.on('unhandledRejection', onUnhandled)
77
+ process.on('uncaughtException', onUnhandled)
78
+
79
+ // 3. Run beforeAll hooks
80
+ for (const hook of registry.beforeAllHooks) {
81
+ await hook()
82
+ }
83
+
84
+ // 4. Execute each test sequentially
85
+ for (const entry of registry.tests) {
86
+ const { context, finalise } = startTraceSession()
87
+ const traceId = proxyCtx.proxyOptIn ? randomUUID() : null
88
+ setCurrentTrace(context.trace)
89
+ if (traceId) {
90
+ process.env.ELASTICDASH_TRACE_ID = traceId
91
+ }
92
+
93
+ if (hooks.onTestStart) {
94
+ await hooks.onTestStart(entry.name)
95
+ }
96
+
97
+ const startTime = Date.now()
98
+ let passed = false
99
+ let error: Error | undefined
100
+
101
+ // Reset per-test unhandled capture and mark current test name
102
+ pendingUnhandled = undefined
103
+ currentTestName = entry.name
104
+
105
+ setCurrentTrace(context.trace)
106
+ try {
107
+ for (const hook of registry.beforeEachHooks) {
108
+ await hook()
109
+ }
110
+
111
+ await entry.fn(context)
112
+ passed = true
113
+ } catch (err) {
114
+ error = err instanceof Error ? err : new Error(String(err))
115
+ } finally {
116
+ try {
117
+ for (const hook of registry.afterEachHooks) {
118
+ await hook()
119
+ }
120
+ } catch (afterErr) {
121
+ if (!error) {
122
+ error = afterErr instanceof Error ? afterErr : new Error(String(afterErr))
123
+ passed = false
124
+ }
125
+ }
126
+
127
+ setCurrentTrace(undefined)
128
+ if (!error && pendingUnhandled) {
129
+ error = pendingUnhandled
130
+ passed = false
131
+ }
132
+ currentTestName = null
133
+ }
134
+
135
+ const durationMs = Date.now() - startTime
136
+
137
+ if (hooks.onTestFinish) {
138
+ await hooks.onTestFinish(entry.name, passed, durationMs, error)
139
+ }
140
+
141
+ if (hooks.onTraceComplete) {
142
+ await hooks.onTraceComplete(entry.name, context.trace)
143
+ }
144
+
145
+ // If proxy mode is enabled, pull captured LLM steps and fold into the trace
146
+ if (traceId && proxyCtx.proxyUrl) {
147
+ try {
148
+ const captured = await fetchCapturedTrace(proxyCtx.proxyUrl, traceId)
149
+ for (const step of captured) {
150
+ context.trace.recordLLMStep(step)
151
+ }
152
+ } catch (proxyErr) {
153
+ // Non-fatal: keep test result as-is
154
+ // eslint-disable-next-line no-console
155
+ console.warn('[elasticdash] Failed to fetch proxy-captured steps:', proxyErr)
156
+ }
157
+ }
158
+
159
+ finalise()
160
+ setCurrentTrace(undefined)
161
+ if (traceId) {
162
+ delete process.env.ELASTICDASH_TRACE_ID
163
+ }
164
+
165
+ results.push({ name: entry.name, passed, durationMs, error })
166
+ }
167
+
168
+ // 5. Run afterAll hooks
169
+ for (const hook of registry.afterAllHooks) {
170
+ await hook()
171
+ }
172
+
173
+ // Cleanup shared handlers
174
+ process.off('unhandledRejection', onUnhandled)
175
+ process.off('uncaughtException', onUnhandled)
176
+
177
+ return { file, results }
178
+ }