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