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,492 @@
1
+ /**
2
+ * Runtime mock resolution for module-imported tools and AI calls.
3
+ *
4
+ * Tools/AI calls that are statically imported (not accessed via globalThis)
5
+ * cannot be intercepted by the worker's proxy-based mocking. Instead, each
6
+ * wrapped function calls `resolveMock` / `resolveAIMock` at its entry point.
7
+ * The worker writes the mock config to `__ELASTICDASH_TOOL_MOCKS__` /
8
+ * `__ELASTICDASH_AI_MOCKS__` before the workflow runs and clears it after.
9
+ */
10
+
11
+ interface ToolMockEntry {
12
+ mode: 'live' | 'mock-all' | 'mock-specific'
13
+ callIndices?: number[]
14
+ mockData?: Record<number, unknown>
15
+ }
16
+
17
+ /** Per-model AI mock configuration (mirrors ToolMockConfig) */
18
+ export interface AIMockEntry {
19
+ mode: 'live' | 'mock-all' | 'mock-specific'
20
+ callIndices?: number[]
21
+ mockData?: Record<number, unknown>
22
+ }
23
+
24
+ export interface AIMockConfig {
25
+ [modelName: string]: AIMockEntry
26
+ }
27
+
28
+ type MockResult =
29
+ | { mocked: true; result: unknown }
30
+ | { mocked: false }
31
+
32
+ /**
33
+ * Recursively normalises a mock result fetched from the trace.
34
+ *
35
+ * The trace recorder stores tool outputs as-is. When a tool returns a JSON
36
+ * string (rather than a parsed object), the stored value is that string, and
37
+ * inner string values may themselves be JSON-quoted. This function unwraps
38
+ * every layer so the calling code receives the same shape as the real output.
39
+ *
40
+ * - string → try JSON.parse, then recurse on the result
41
+ * - array → recurse on every element
42
+ * - object → recurse on every value
43
+ * - other → return unchanged
44
+ */
45
+ export function normaliseMockResult(value: unknown): unknown {
46
+ if (typeof value === 'string') {
47
+ try { return normaliseMockResult(JSON.parse(value)) } catch { return value }
48
+ }
49
+ if (Array.isArray(value)) {
50
+ return value.map(normaliseMockResult)
51
+ }
52
+ if (value !== null && typeof value === 'object') {
53
+ const out: Record<string, unknown> = {}
54
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
55
+ out[k] = normaliseMockResult(v)
56
+ }
57
+ return out
58
+ }
59
+ return value
60
+ }
61
+
62
+ /**
63
+ * Resolves whether the current call to `toolName` should be mocked.
64
+ *
65
+ * - Returns `{ mocked: false }` when no mock config is active or mode is `'live'`.
66
+ * - Increments the per-tool call counter on every non-live invocation so that
67
+ * `mock-specific` indices remain accurate even when some calls run live.
68
+ * - Safe to call in production: no-op when the mock globals are absent.
69
+ */
70
+ export function resolveMock(toolName: string): MockResult {
71
+ const g = globalThis as Record<string, unknown>
72
+
73
+ const mocks = g['__ELASTICDASH_TOOL_MOCKS__'] as Record<string, ToolMockEntry> | undefined
74
+ if (!mocks) return { mocked: false }
75
+
76
+ const entry = mocks[toolName]
77
+ if (!entry || entry.mode === 'live') return { mocked: false }
78
+
79
+ // Initialise counters map if not yet present
80
+ if (!g['__ELASTICDASH_TOOL_CALL_COUNTERS__']) {
81
+ g['__ELASTICDASH_TOOL_CALL_COUNTERS__'] = {} as Record<string, number>
82
+ }
83
+ const counters = g['__ELASTICDASH_TOOL_CALL_COUNTERS__'] as Record<string, number>
84
+ counters[toolName] = (counters[toolName] ?? 0) + 1
85
+ const callNumber = counters[toolName]
86
+
87
+ if (entry.mode === 'mock-all') {
88
+ const data = entry.mockData ?? {}
89
+ const raw = data[callNumber] !== undefined ? data[callNumber] : data[0]
90
+ return { mocked: true, result: normaliseMockResult(raw) }
91
+ }
92
+
93
+ if (entry.mode === 'mock-specific') {
94
+ const indices = entry.callIndices ?? []
95
+ if (indices.includes(callNumber)) {
96
+ const data = entry.mockData ?? {}
97
+ return { mocked: true, result: normaliseMockResult(data[callNumber]) }
98
+ }
99
+ // Counter already incremented; this specific call runs live
100
+ return { mocked: false }
101
+ }
102
+
103
+ return { mocked: false }
104
+ }
105
+
106
+ /**
107
+ * Extracts the system prompt string from an LLM call input.
108
+ * Handles:
109
+ * - Anthropic style: `{ system: "...", messages: [...] }`
110
+ * - OpenAI style: `{ messages: [{ role: "system", content: "..." }, ...] }`
111
+ * - Plain message array: `[{ role: "system", content: "..." }, ...]`
112
+ * - Separate field: `{ systemPrompt: "...", messages: [...] }` (custom wrapAI callers)
113
+ */
114
+ export function extractSystemPrompt(input: unknown): string | undefined {
115
+ if (!input || typeof input !== 'object') return undefined
116
+ const o = input as Record<string, unknown>
117
+ if (typeof o.system === 'string') return o.system
118
+ if (typeof o.systemPrompt === 'string' && o.systemPrompt.length > 0) return o.systemPrompt
119
+ const msgs = Array.isArray(o.messages) ? o.messages : (Array.isArray(input) ? input as unknown[] : null)
120
+ if (msgs) {
121
+ for (const m of msgs) {
122
+ if (m && typeof m === 'object') {
123
+ const msg = m as Record<string, unknown>
124
+ if (msg.role === 'system' && typeof msg.content === 'string') return msg.content
125
+ }
126
+ }
127
+ }
128
+ return undefined
129
+ }
130
+
131
+ /**
132
+ * Returns a shallow copy of `input` with the system prompt replaced by `newSystemPrompt`.
133
+ */
134
+ export function replaceSystemPrompt(input: unknown, newSystemPrompt: string): unknown {
135
+ if (!input || typeof input !== 'object') return input
136
+ const o = input as Record<string, unknown>
137
+ if (typeof o.system === 'string') return { ...o, system: newSystemPrompt }
138
+ if (typeof o.systemPrompt === 'string') return { ...o, systemPrompt: newSystemPrompt }
139
+ if (Array.isArray(input)) {
140
+ return (input as unknown[]).map(m => {
141
+ if (m && typeof m === 'object') {
142
+ const msg = m as Record<string, unknown>
143
+ if (msg.role === 'system' && typeof msg.content === 'string') return { ...msg, content: newSystemPrompt }
144
+ }
145
+ return m
146
+ })
147
+ }
148
+ if (Array.isArray(o.messages)) {
149
+ return {
150
+ ...o,
151
+ messages: (o.messages as unknown[]).map(m => {
152
+ if (m && typeof m === 'object') {
153
+ const msg = m as Record<string, unknown>
154
+ if (msg.role === 'system' && typeof msg.content === 'string') return { ...msg, content: newSystemPrompt }
155
+ }
156
+ return m
157
+ }),
158
+ }
159
+ }
160
+ return input
161
+ }
162
+
163
+ /**
164
+ * System prompt mock entry — mirrors user prompt mock entry.
165
+ */
166
+ export interface SystemPromptMockEntry {
167
+ mode: 'live' | 'replace-all' | 'replace-specific'
168
+ replacement: string
169
+ callIndices?: number[]
170
+ }
171
+
172
+ export interface SystemPromptMockConfig {
173
+ [originalText: string]: SystemPromptMockEntry
174
+ }
175
+
176
+ /**
177
+ * If a prompt mock is configured for the system prompt found in `input`, returns
178
+ * a copy of `input` with the system prompt replaced. Otherwise returns `undefined`.
179
+ *
180
+ * `__ELASTICDASH_PROMPT_MOCKS__` is `Record<string, SystemPromptMockEntry>` keyed by
181
+ * the original system prompt text. Supports replace-all and replace-specific modes
182
+ * with per-text call counting.
183
+ *
184
+ * For backward compatibility, also accepts the legacy `Record<string, string>` format
185
+ * (treated as replace-all).
186
+ */
187
+ export function resolvePromptMock(input: unknown): unknown | undefined {
188
+ const g = globalThis as Record<string, unknown>
189
+ const mocks = g['__ELASTICDASH_PROMPT_MOCKS__'] as Record<string, string | SystemPromptMockEntry> | undefined
190
+ if (!mocks || Object.keys(mocks).length === 0) return undefined
191
+ const systemPrompt = extractSystemPrompt(input)
192
+ if (systemPrompt === undefined) return undefined
193
+
194
+ const match = lookupMockEntry(mocks, systemPrompt)
195
+ if (!match) return undefined
196
+ const entry = match.entry
197
+
198
+ // Legacy format: plain string replacement (treat as replace-all)
199
+ if (typeof entry === 'string') {
200
+ return replaceSystemPrompt(input, entry)
201
+ }
202
+
203
+ if (entry.mode === 'live') return undefined
204
+
205
+ // Initialise per-text call counters
206
+ if (!g['__ELASTICDASH_PROMPT_CALL_COUNTERS__']) {
207
+ g['__ELASTICDASH_PROMPT_CALL_COUNTERS__'] = {} as Record<string, number>
208
+ }
209
+ const counters = g['__ELASTICDASH_PROMPT_CALL_COUNTERS__'] as Record<string, number>
210
+ counters[match.key] = (counters[match.key] ?? 0) + 1
211
+ const callNumber = counters[match.key]
212
+
213
+ if (entry.mode === 'replace-all') {
214
+ return replaceSystemPrompt(input, entry.replacement)
215
+ }
216
+
217
+ if (entry.mode === 'replace-specific') {
218
+ const indices = entry.callIndices ?? []
219
+ if (indices.includes(callNumber)) {
220
+ return replaceSystemPrompt(input, entry.replacement)
221
+ }
222
+ }
223
+
224
+ return undefined
225
+ }
226
+
227
+ /**
228
+ * Strips one layer of JSON string quoting if present.
229
+ * `'"hello"'` → `'hello'`, `'hello'` → `'hello'`
230
+ */
231
+ function stripJsonQuotes(s: string): string {
232
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
233
+ try {
234
+ const parsed = JSON.parse(s)
235
+ if (typeof parsed === 'string') return parsed
236
+ } catch { /* not valid JSON, return as-is */ }
237
+ }
238
+ return s
239
+ }
240
+
241
+ /**
242
+ * Look up a mock entry by key, falling back to stripped-quotes variants
243
+ * to handle mismatches between trace-recorded text and runtime text.
244
+ */
245
+ export function lookupMockEntry<T>(mocks: Record<string, T>, text: string): { key: string; entry: T } | undefined {
246
+ if (mocks[text]) return { key: text, entry: mocks[text] }
247
+ // Try stripping quotes from the runtime text
248
+ const stripped = stripJsonQuotes(text)
249
+ if (stripped !== text && mocks[stripped]) return { key: stripped, entry: mocks[stripped] }
250
+ // Try adding quotes to the runtime text (mock key might have quotes)
251
+ const quoted = JSON.stringify(text)
252
+ if (quoted !== text && mocks[quoted]) return { key: quoted, entry: mocks[quoted] }
253
+ return undefined
254
+ }
255
+
256
+ /**
257
+ * Extracts all user-role message content strings from an LLM call input.
258
+ * Handles OpenAI-style `{ messages: [...] }` and plain message arrays.
259
+ */
260
+ export function extractUserPrompts(input: unknown): string[] {
261
+ if (!input || typeof input !== 'object') return []
262
+ const o = input as Record<string, unknown>
263
+ const msgs = Array.isArray(o.messages) ? o.messages : (Array.isArray(input) ? input as unknown[] : null)
264
+ if (!msgs) return []
265
+ const results: string[] = []
266
+ for (const m of msgs) {
267
+ if (m && typeof m === 'object') {
268
+ const msg = m as Record<string, unknown>
269
+ if (msg.role === 'user' && typeof msg.content === 'string') results.push(msg.content)
270
+ }
271
+ }
272
+ return results
273
+ }
274
+
275
+ /**
276
+ * Returns a shallow copy of `input` with all user-role messages whose content
277
+ * matches `originalText` replaced with `newText`.
278
+ */
279
+ export function replaceUserPrompt(input: unknown, originalText: string, newText: string): unknown {
280
+ if (!input || typeof input !== 'object') return input
281
+ const o = input as Record<string, unknown>
282
+
283
+ const replaceInMsgs = (msgs: unknown[]): unknown[] =>
284
+ msgs.map(m => {
285
+ if (m && typeof m === 'object') {
286
+ const msg = m as Record<string, unknown>
287
+ if (msg.role === 'user' && typeof msg.content === 'string' && msg.content === originalText) {
288
+ return { ...msg, content: newText }
289
+ }
290
+ }
291
+ return m
292
+ })
293
+
294
+ if (Array.isArray(input)) return replaceInMsgs(input as unknown[])
295
+ if (Array.isArray(o.messages)) return { ...o, messages: replaceInMsgs(o.messages as unknown[]) }
296
+ return input
297
+ }
298
+
299
+ /**
300
+ * User prompt mock entry — mirrors tool mock but for user-role messages.
301
+ */
302
+ export interface UserPromptMockEntry {
303
+ mode: 'live' | 'replace-all' | 'replace-specific'
304
+ replacement: string
305
+ callIndices?: number[]
306
+ }
307
+
308
+ export interface UserPromptMockConfig {
309
+ [originalText: string]: UserPromptMockEntry
310
+ }
311
+
312
+ /**
313
+ * If user prompt mocks are configured, applies all matching replacements to `input`.
314
+ * Returns modified input or `undefined` if no replacements applied.
315
+ *
316
+ * `__ELASTICDASH_USER_PROMPT_MOCKS__` is `Record<string, UserPromptMockEntry>` keyed
317
+ * by original user message text.
318
+ *
319
+ * Call counting is per unique text — "the x-th LLM call where this text appears".
320
+ */
321
+ export function resolveUserPromptMock(input: unknown): unknown | undefined {
322
+ const g = globalThis as Record<string, unknown>
323
+ const mocks = g['__ELASTICDASH_USER_PROMPT_MOCKS__'] as UserPromptMockConfig | undefined
324
+ if (!mocks || Object.keys(mocks).length === 0) return undefined
325
+
326
+ const userPrompts = extractUserPrompts(input)
327
+ if (userPrompts.length === 0) return undefined
328
+
329
+ // Initialise per-text call counters
330
+ if (!g['__ELASTICDASH_USER_PROMPT_CALL_COUNTERS__']) {
331
+ g['__ELASTICDASH_USER_PROMPT_CALL_COUNTERS__'] = {} as Record<string, number>
332
+ }
333
+ const counters = g['__ELASTICDASH_USER_PROMPT_CALL_COUNTERS__'] as Record<string, number>
334
+
335
+ // Determine which texts in this call have active mock entries
336
+ const uniqueTexts = Array.from(new Set(userPrompts))
337
+ let modified = false
338
+ let result = input
339
+
340
+ for (const text of uniqueTexts) {
341
+ const match = lookupMockEntry(mocks, text)
342
+ if (!match || match.entry.mode === 'live') continue
343
+ const entry = match.entry
344
+
345
+ // Increment counter for this text (this is the x-th call where this text appears)
346
+ counters[match.key] = (counters[match.key] ?? 0) + 1
347
+ const callNumber = counters[match.key]
348
+
349
+ if (entry.mode === 'replace-all') {
350
+ result = replaceUserPrompt(result, text, entry.replacement)
351
+ modified = true
352
+ } else if (entry.mode === 'replace-specific') {
353
+ const indices = entry.callIndices ?? []
354
+ if (indices.includes(callNumber)) {
355
+ result = replaceUserPrompt(result, text, entry.replacement)
356
+ modified = true
357
+ }
358
+ }
359
+ }
360
+
361
+ return modified ? result : undefined
362
+ }
363
+
364
+ /**
365
+ * Resolves whether the current call to `modelName` should be mocked.
366
+ * Mirrors `resolveMock` but reads `__ELASTICDASH_AI_MOCKS__` and
367
+ * `__ELASTICDASH_AI_CALL_COUNTERS__`.
368
+ */
369
+ /**
370
+ * Standalone utility: applies all matching user prompt mock replacements to
371
+ * a messages array (or LLM input object). Works without ALS — takes the mock
372
+ * config and call counters as explicit parameters.
373
+ *
374
+ * Returns the modified input, or the original input unchanged if no mocks matched.
375
+ */
376
+ export function applyUserPromptMocks(
377
+ input: unknown,
378
+ userPromptMocks: Record<string, UserPromptMockEntry>,
379
+ callCounters?: Record<string, number>,
380
+ ): unknown {
381
+ if (!userPromptMocks || Object.keys(userPromptMocks).length === 0) return input
382
+
383
+ const userPrompts = extractUserPrompts(input)
384
+ if (userPrompts.length === 0) return input
385
+
386
+ const counters = callCounters ?? {}
387
+ const uniqueTexts = Array.from(new Set(userPrompts))
388
+ let modified = false
389
+ let result = input
390
+
391
+ for (const text of uniqueTexts) {
392
+ const match = lookupMockEntry(userPromptMocks, text)
393
+ if (!match || match.entry.mode === 'live') continue
394
+ const entry = match.entry
395
+
396
+ counters[match.key] = (counters[match.key] ?? 0) + 1
397
+ const callNumber = counters[match.key]
398
+
399
+ if (entry.mode === 'replace-all') {
400
+ result = replaceUserPrompt(result, text, entry.replacement)
401
+ modified = true
402
+ } else if (entry.mode === 'replace-specific') {
403
+ const indices = entry.callIndices ?? []
404
+ if (indices.includes(callNumber)) {
405
+ result = replaceUserPrompt(result, text, entry.replacement)
406
+ modified = true
407
+ }
408
+ }
409
+ }
410
+
411
+ return modified ? result : input
412
+ }
413
+
414
+ /**
415
+ * Standalone utility: applies system prompt mock replacement to an LLM input
416
+ * object. Works without ALS — takes the mock config and call counters as
417
+ * explicit parameters.
418
+ *
419
+ * Returns the modified input, or the original input unchanged if no mock matched.
420
+ */
421
+ export function applySystemPromptMocks(
422
+ input: unknown,
423
+ promptMocks: Record<string, string | SystemPromptMockEntry>,
424
+ callCounters?: Record<string, number>,
425
+ ): unknown {
426
+ if (!promptMocks || Object.keys(promptMocks).length === 0) return input
427
+
428
+ const systemPrompt = extractSystemPrompt(input)
429
+ if (systemPrompt === undefined) return input
430
+
431
+ const match = lookupMockEntry(promptMocks, systemPrompt)
432
+ if (!match) return input
433
+ const entry = match.entry
434
+
435
+ // Legacy format: plain string replacement (treat as replace-all)
436
+ if (typeof entry === 'string') {
437
+ return replaceSystemPrompt(input, entry)
438
+ }
439
+
440
+ if (entry.mode === 'live') return input
441
+
442
+ const counters = callCounters ?? {}
443
+ counters[match.key] = (counters[match.key] ?? 0) + 1
444
+ const callNumber = counters[match.key]
445
+
446
+ if (entry.mode === 'replace-all') {
447
+ return replaceSystemPrompt(input, entry.replacement)
448
+ }
449
+
450
+ if (entry.mode === 'replace-specific') {
451
+ const indices = entry.callIndices ?? []
452
+ if (indices.includes(callNumber)) {
453
+ return replaceSystemPrompt(input, entry.replacement)
454
+ }
455
+ }
456
+
457
+ return input
458
+ }
459
+
460
+ export function resolveAIMock(modelName: string): MockResult {
461
+ const g = globalThis as Record<string, unknown>
462
+
463
+ const mocks = g['__ELASTICDASH_AI_MOCKS__'] as Record<string, AIMockEntry> | undefined
464
+ if (!mocks) return { mocked: false }
465
+
466
+ const entry = mocks[modelName]
467
+ if (!entry || entry.mode === 'live') return { mocked: false }
468
+
469
+ if (!g['__ELASTICDASH_AI_CALL_COUNTERS__']) {
470
+ g['__ELASTICDASH_AI_CALL_COUNTERS__'] = {} as Record<string, number>
471
+ }
472
+ const counters = g['__ELASTICDASH_AI_CALL_COUNTERS__'] as Record<string, number>
473
+ counters[modelName] = (counters[modelName] ?? 0) + 1
474
+ const callNumber = counters[modelName]
475
+
476
+ if (entry.mode === 'mock-all') {
477
+ const data = entry.mockData ?? {}
478
+ const raw = data[callNumber] !== undefined ? data[callNumber] : data[0]
479
+ return { mocked: true, result: normaliseMockResult(raw) }
480
+ }
481
+
482
+ if (entry.mode === 'mock-specific') {
483
+ const indices = entry.callIndices ?? []
484
+ if (indices.includes(callNumber)) {
485
+ const data = entry.mockData ?? {}
486
+ return { mocked: true, result: normaliseMockResult(data[callNumber]) }
487
+ }
488
+ return { mocked: false }
489
+ }
490
+
491
+ return { mocked: false }
492
+ }