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