elasticdash-test 0.1.14 โ 0.1.17-alpha-1
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/README.md +36 -5
- package/dist/dashboard-server.d.ts +9 -0
- package/dist/dashboard-server.d.ts.map +1 -1
- package/dist/dashboard-server.js +213 -25
- package/dist/dashboard-server.js.map +1 -1
- package/dist/html/dashboard.html +159 -9
- package/dist/index.cjs +828 -108
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/interceptors/telemetry-push.d.ts +47 -0
- package/dist/interceptors/telemetry-push.d.ts.map +1 -1
- package/dist/interceptors/telemetry-push.js +139 -6
- package/dist/interceptors/telemetry-push.js.map +1 -1
- package/dist/interceptors/tool.d.ts.map +1 -1
- package/dist/interceptors/tool.js +2 -1
- package/dist/interceptors/tool.js.map +1 -1
- package/dist/interceptors/workflow-ai.d.ts.map +1 -1
- package/dist/interceptors/workflow-ai.js +28 -4
- package/dist/interceptors/workflow-ai.js.map +1 -1
- package/dist/internals/mock-resolver.d.ts +42 -5
- package/dist/internals/mock-resolver.d.ts.map +1 -1
- package/dist/internals/mock-resolver.js +124 -5
- package/dist/internals/mock-resolver.js.map +1 -1
- package/dist/workflow-runner-worker.js +8 -2
- package/dist/workflow-runner-worker.js.map +1 -1
- package/package.json +3 -2
- package/src/dashboard-server.ts +89 -19
- package/src/html/dashboard.html +159 -9
- package/src/index.ts +3 -2
- package/src/interceptors/telemetry-push.ts +158 -7
- package/src/interceptors/tool.ts +2 -1
- package/src/interceptors/workflow-ai.ts +30 -4
- package/src/internals/mock-resolver.ts +131 -5
- package/src/workflow-runner-worker.ts +23 -2
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ An AI-native test runner for ElasticDash workflow testing. Built for async AI pi
|
|
|
28
28
|
- ๐ฏ **Trace-first testing** โ every test gets a `trace` context to record and assert on LLM calls and tool invocations
|
|
29
29
|
- ๐ **Automatic AI interception** โ captures OpenAI, Gemini, and Grok calls without code changes
|
|
30
30
|
- ๐งช **AI-specific matchers** โ semantic output matching, LLM-judged evaluations, prompt assertions
|
|
31
|
-
- ๐ ๏ธ **Tool recording & replay** โ automatically trace tool calls with checkpoint-based replay
|
|
31
|
+
- ๐ ๏ธ **Tool & LLM recording & replay** โ automatically trace tool and AI calls with checkpoint-based replay and mock support
|
|
32
32
|
- ๐ **Interactive dashboard** โ browse workflows, debug traces, validate fixes visually
|
|
33
33
|
- ๐ค **Agent mid-trace replay** โ resume long-running agents from any task without re-execution
|
|
34
34
|
- ๐ **HTTP workflow mode** โ run workflows against your live dev server for framework-heavy apps (Next.js, Remix, etc.) with full AI and tool call observability
|
|
@@ -266,6 +266,8 @@ export const callClaude = wrapAI('claude-sonnet-4-5', async (messages: Anthropic
|
|
|
266
266
|
|
|
267
267
|
Use `wrapAI` when you have a custom AI wrapper or a provider not covered by automatic interception. For direct OpenAI/Anthropic/Gemini SDK calls inside a subprocess workflow, automatic interception via `installAIInterceptor` already handles recording without any code changes.
|
|
268
268
|
|
|
269
|
+
**AI mocking (subprocess / test runner mode):** `wrapAI` also checks `resolveAIMock` at call time, so the dashboard can mock LLM responses the same way it mocks tool calls โ without modifying your server code. Configure an `AIMockConfig` in the dashboard UI or pass it programmatically via the `aiMockConfig` option when running a workflow.
|
|
270
|
+
|
|
269
271
|
### HTTP Streaming Capture and Replay
|
|
270
272
|
|
|
271
273
|
ElasticDash also captures non-AI `fetch` responses that stream over HTTP (for example SSE and NDJSON endpoints) in the HTTP interceptor.
|
|
@@ -354,6 +356,25 @@ The dashboard injects `x-elasticdash-run-id` and `x-elasticdash-server` headers
|
|
|
354
356
|
|
|
355
357
|
> **Note:** Use `setHttpRunContext` (synchronous) if you only need observability and do not need step freezing. `initHttpRunContext` is required for the dashboard's breakpoint/replay functionality to work.
|
|
356
358
|
|
|
359
|
+
### Dashboard Auto-Detection (env var mode)
|
|
360
|
+
|
|
361
|
+
As an alternative to calling `initHttpRunContext` in your request handler, you can set two environment variables before starting your server or script. Every `wrapTool` and `wrapAI` call will then connect to the dashboard automatically โ no code changes needed:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
# Required: URL of the running ElasticDash dashboard
|
|
365
|
+
ELASTICDASH_SERVER=http://localhost:4573
|
|
366
|
+
|
|
367
|
+
# Optional: pre-registered run ID to fetch frozen steps for
|
|
368
|
+
ELASTICDASH_RUN_ID=<run-id-from-dashboard>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
- If only `ELASTICDASH_SERVER` is set, a fresh run ID is generated and all calls push live telemetry to the dashboard (observability only, no step freezing).
|
|
372
|
+
- If both variables are set, frozen steps are fetched from the dashboard at startup and replayed as configured.
|
|
373
|
+
- If the dashboard is unreachable the SDK falls through to live execution silently.
|
|
374
|
+
- The initialization runs **once per process** โ subsequent `wrapTool`/`wrapAI` calls reuse the cached context.
|
|
375
|
+
|
|
376
|
+
This mode is intended for local development and testing scenarios. For production HTTP servers with concurrent requests, continue using `initHttpRunContext` inside your request handler.
|
|
377
|
+
|
|
357
378
|
**Subprocess vs HTTP mode comparison:**
|
|
358
379
|
|
|
359
380
|
| | Subprocess (default) | HTTP mode |
|
|
@@ -361,9 +382,10 @@ The dashboard injects `x-elasticdash-run-id` and `x-elasticdash-server` headers
|
|
|
361
382
|
| Works with simple apps | Yes | Yes |
|
|
362
383
|
| Works with Next.js / Remix | No | Yes |
|
|
363
384
|
| Requires dev server running | No | Yes |
|
|
364
|
-
| App code changes needed | Extract handler to `ed_workflows.ts` | Add `initHttpRunContext` to request handler |
|
|
385
|
+
| App code changes needed | Extract handler to `ed_workflows.ts` | Add `initHttpRunContext` to request handler (or use env vars for auto-detect) |
|
|
365
386
|
| AI / tool call observability | Automatic via interceptors | Via `wrapAI` / `wrapTool` push |
|
|
366
|
-
| Step freezing / breakpoints | Yes | Yes (
|
|
387
|
+
| Step freezing / breakpoints | Yes | Yes (`initHttpRunContext`, or `ELASTICDASH_SERVER` + `ELASTICDASH_RUN_ID` env vars) |
|
|
388
|
+
| LLM response mocking | Yes (via `aiMockConfig`) | Yes (via frozen AI events) |
|
|
367
389
|
|
|
368
390
|
---
|
|
369
391
|
|
|
@@ -420,16 +442,25 @@ reportResults(results)
|
|
|
420
442
|
**HTTP mode context (call inside your request handler):**
|
|
421
443
|
|
|
422
444
|
```ts
|
|
423
|
-
import { initHttpRunContext } from 'elasticdash-test'
|
|
445
|
+
import { initHttpRunContext, setHttpRunContext } from 'elasticdash-test'
|
|
424
446
|
|
|
425
447
|
// Async โ fetches frozen steps from dashboard to enable step freezing/breakpoints
|
|
426
448
|
await initHttpRunContext(runId, dashboardUrl)
|
|
427
449
|
|
|
428
450
|
// Synchronous alternative โ observability only, no step freezing
|
|
429
|
-
import { setHttpRunContext } from 'elasticdash-test'
|
|
430
451
|
setHttpRunContext(runId, dashboardUrl)
|
|
431
452
|
```
|
|
432
453
|
|
|
454
|
+
**Dashboard auto-detection (env var mode โ no code changes needed):**
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
# Set before starting your server or script
|
|
458
|
+
ELASTICDASH_SERVER=http://localhost:4573 # required
|
|
459
|
+
ELASTICDASH_RUN_ID=<run-id-from-dashboard> # optional, enables step freezing
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`wrapTool` and `wrapAI` will auto-connect on their first call. See [Dashboard Auto-Detection](#dashboard-auto-detection-env-var-mode) for details.
|
|
463
|
+
|
|
433
464
|
---
|
|
434
465
|
|
|
435
466
|
## License
|
|
@@ -40,6 +40,15 @@ export interface ToolMockEntry {
|
|
|
40
40
|
export interface ToolMockConfig {
|
|
41
41
|
[toolName: string]: ToolMockEntry;
|
|
42
42
|
}
|
|
43
|
+
/** Per-model AI mock configuration sent from the dashboard UI */
|
|
44
|
+
export interface AIMockEntry {
|
|
45
|
+
mode: 'live' | 'mock-all' | 'mock-specific';
|
|
46
|
+
callIndices?: number[];
|
|
47
|
+
mockData?: Record<number, unknown>;
|
|
48
|
+
}
|
|
49
|
+
export interface AIMockConfig {
|
|
50
|
+
[modelName: string]: AIMockEntry;
|
|
51
|
+
}
|
|
43
52
|
export interface HttpWorkflowConfig {
|
|
44
53
|
mode: 'http';
|
|
45
54
|
url: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-server.d.ts","sourceRoot":"","sources":["../src/dashboard-server.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,YAAY,EAAE,CAAA;IACzB,KAAK,EAAE,QAAQ,EAAE,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AA2CD,6DAA6D;AAC7D,MAAM,WAAW,aAAa;IAC5B,oHAAoH;IACpH,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,eAAe,CAAA;IAC3C,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAA;CAClC;
|
|
1
|
+
{"version":3,"file":"dashboard-server.d.ts","sourceRoot":"","sources":["../src/dashboard-server.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,YAAY,EAAE,CAAA;IACzB,KAAK,EAAE,QAAQ,EAAE,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AA2CD,6DAA6D;AAC7D,MAAM,WAAW,aAAa;IAC5B,oHAAoH;IACpH,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,eAAe,CAAA;IAC3C,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAAA;CAClC;AAED,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,eAAe,CAAA;IAC3C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,YAAY;IAC3B,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAAA;CACjC;AAsgGD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACtC,cAAc,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAA;CAC7C;AA4ID;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,eAAe,CAAC,CA4b1B;AAiFD,eAAO,MAAM,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAa,CAAC"}
|
package/dist/dashboard-server.js
CHANGED
|
@@ -311,6 +311,8 @@ function runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowN
|
|
|
311
311
|
...(options?.history !== undefined ? { history: options.history } : {}),
|
|
312
312
|
...(options?.agentState !== undefined ? { agentState: options.agentState } : {}),
|
|
313
313
|
...(options?.toolMockConfig !== undefined ? { toolMockConfig: options.toolMockConfig } : {}),
|
|
314
|
+
...(options?.aiMockConfig !== undefined ? { aiMockConfig: options.aiMockConfig } : {}),
|
|
315
|
+
...(options?.promptMockConfig !== undefined ? { promptMockConfig: options.promptMockConfig } : {}),
|
|
314
316
|
});
|
|
315
317
|
child.stdin.write(payload);
|
|
316
318
|
child.stdin.end(); // Always close stdin to avoid subprocess hang
|
|
@@ -361,8 +363,9 @@ async function runGenerationObservation(observation) {
|
|
|
361
363
|
async function rerunObservation(cwd, observation, tools) {
|
|
362
364
|
const type = observation.type?.toUpperCase();
|
|
363
365
|
const name = observation.name ?? '(unknown)';
|
|
364
|
-
|
|
365
|
-
|
|
366
|
+
const isToolByName = name.startsWith('tool-') || name.startsWith('tool:');
|
|
367
|
+
if (type === 'TOOL' || isToolByName) {
|
|
368
|
+
observation.name = isToolByName ? name.slice(5) : name; // Support both explicit type and name prefix for tool observations
|
|
366
369
|
return runToolObservation(cwd, observation, tools);
|
|
367
370
|
}
|
|
368
371
|
if (type === 'GENERATION') {
|
|
@@ -633,6 +636,14 @@ async function validateWorkflowRuns(cwd, body) {
|
|
|
633
636
|
const toolMockConfig = body.toolMockConfig && typeof body.toolMockConfig === 'object' && !Array.isArray(body.toolMockConfig)
|
|
634
637
|
? body.toolMockConfig
|
|
635
638
|
: undefined;
|
|
639
|
+
// Parse AI mock config if provided
|
|
640
|
+
const aiMockConfig = body.aiMockConfig && typeof body.aiMockConfig === 'object' && !Array.isArray(body.aiMockConfig)
|
|
641
|
+
? body.aiMockConfig
|
|
642
|
+
: undefined;
|
|
643
|
+
// Parse prompt mock config if provided
|
|
644
|
+
const promptMockConfig = body.promptMockConfig && typeof body.promptMockConfig === 'object' && !Array.isArray(body.promptMockConfig)
|
|
645
|
+
? body.promptMockConfig
|
|
646
|
+
: undefined;
|
|
636
647
|
const workflowsModulePath = resolveWorkflowModule(cwd);
|
|
637
648
|
if (!workflowsModulePath) {
|
|
638
649
|
return {
|
|
@@ -648,7 +659,7 @@ async function validateWorkflowRuns(cwd, body) {
|
|
|
648
659
|
console.log(`[elasticdash] Running workflow "${workflowName}" ${runCount} time(s) in ${mode} mode via subprocess`);
|
|
649
660
|
async function runOne(runNumber) {
|
|
650
661
|
console.log(`[elasticdash] === Run ${runNumber}: Starting workflow "${workflowName}" ===`);
|
|
651
|
-
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, toolMockConfig ? { toolMockConfig } : undefined)
|
|
662
|
+
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, (toolMockConfig || aiMockConfig || promptMockConfig) ? { ...(toolMockConfig ? { toolMockConfig } : {}), ...(aiMockConfig ? { aiMockConfig } : {}), ...(promptMockConfig ? { promptMockConfig } : {}) } : undefined)
|
|
652
663
|
.catch(err => {
|
|
653
664
|
throw { ok: false, error: `Workflow subprocess failed: ${formatError(err)}` };
|
|
654
665
|
});
|
|
@@ -1267,12 +1278,16 @@ function getDashboardHtml() {
|
|
|
1267
1278
|
// ---- Tool Mock Config State ----
|
|
1268
1279
|
window._toolMockConfig = {}; // { toolName: { mode: 'live'|'mock-all'|'mock-specific', callIndices: [], mockData: {} } }
|
|
1269
1280
|
|
|
1281
|
+
// ---- Prompt Mock Config State ----
|
|
1282
|
+
// { [originalSystemPrompt]: newSystemPrompt } โ only keys where user enabled the override
|
|
1283
|
+
window._promptMockConfig = {};
|
|
1284
|
+
|
|
1270
1285
|
function getToolsFromTrace() {
|
|
1271
1286
|
// Extract unique tool names and their call details from the uploaded trace observations
|
|
1272
1287
|
const toolCalls = {};
|
|
1273
1288
|
currentObservations.forEach(function(obs, i) {
|
|
1274
1289
|
const isToolByType = obs.type === 'TOOL';
|
|
1275
|
-
const isToolByName = typeof obs.name === 'string' && obs.name.startsWith('tool-');
|
|
1290
|
+
const isToolByName = typeof obs.name === 'string' && (obs.name.startsWith('tool-') || obs.name.startsWith('tool:'));
|
|
1276
1291
|
if (!isToolByType && !isToolByName) return;
|
|
1277
1292
|
const name = isToolByName && obs.type !== 'TOOL'
|
|
1278
1293
|
? obs.name.slice(5)
|
|
@@ -1422,6 +1437,136 @@ function getDashboardHtml() {
|
|
|
1422
1437
|
return html;
|
|
1423
1438
|
}
|
|
1424
1439
|
|
|
1440
|
+
// ---- Prompt Mock Helpers ----
|
|
1441
|
+
|
|
1442
|
+
/** Extract the system prompt string from an LLM call input object or JSON string. */
|
|
1443
|
+
function extractSystemPromptFromInput(input) {
|
|
1444
|
+
// Input may arrive as a JSON-encoded string (e.g. from Langfuse traces)
|
|
1445
|
+
if (typeof input === 'string') {
|
|
1446
|
+
try { input = JSON.parse(input); } catch(e) { return null; }
|
|
1447
|
+
}
|
|
1448
|
+
if (!input || typeof input !== 'object') return null;
|
|
1449
|
+
// Anthropic style: { system: "...", messages: [...] }
|
|
1450
|
+
if (typeof input.system === 'string') return input.system;
|
|
1451
|
+
// Custom wrapAI callers: { systemPrompt: "...", messages: [...] }
|
|
1452
|
+
if (typeof input.systemPrompt === 'string' && input.systemPrompt.length > 0) return input.systemPrompt;
|
|
1453
|
+
// OpenAI / plain array: messages with role === "system"
|
|
1454
|
+
var msgs = Array.isArray(input.messages) ? input.messages : (Array.isArray(input) ? input : null);
|
|
1455
|
+
if (msgs) {
|
|
1456
|
+
for (var i = 0; i < msgs.length; i++) {
|
|
1457
|
+
var m = msgs[i];
|
|
1458
|
+
if (m && typeof m === 'object' && m.role === 'system' && typeof m.content === 'string') {
|
|
1459
|
+
return m.content;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* Returns an array of unique system prompts observed across all GENERATION observations.
|
|
1468
|
+
* Each entry: { systemPrompt, modelName, count, rowIndex }
|
|
1469
|
+
*/
|
|
1470
|
+
function getSystemPromptsFromTrace() {
|
|
1471
|
+
var seen = []; // [{ systemPrompt, modelName, count }]
|
|
1472
|
+
var seenMap = {}; // systemPrompt -> index in seen
|
|
1473
|
+
currentObservations.forEach(function(obs) {
|
|
1474
|
+
if (obs.type !== 'GENERATION') return;
|
|
1475
|
+
var sp = extractSystemPromptFromInput(obs.input);
|
|
1476
|
+
if (!sp) return;
|
|
1477
|
+
if (seenMap[sp] === undefined) {
|
|
1478
|
+
seenMap[sp] = seen.length;
|
|
1479
|
+
seen.push({ systemPrompt: sp, modelName: obs.model || obs.name || '(unknown)', count: 0 });
|
|
1480
|
+
}
|
|
1481
|
+
seen[seenMap[sp]].count++;
|
|
1482
|
+
});
|
|
1483
|
+
return seen.map(function(e, i) { return Object.assign({}, e, { rowIndex: i }); });
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function renderPromptMockSection() {
|
|
1487
|
+
const prompts = getSystemPromptsFromTrace();
|
|
1488
|
+
if (prompts.length === 0) {
|
|
1489
|
+
return '<div style="color:#999;font-size:13px;padding:6px 0;">No system prompts detected in trace. Only AI calls with a system prompt can be mocked here.</div>';
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
let html = '<div style="max-height:360px;overflow-y:auto;border:1px solid #e0e0e0;border-radius:6px;">';
|
|
1493
|
+
html += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
|
|
1494
|
+
html += '<thead><tr style="background:#f5f5f5;">';
|
|
1495
|
+
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;width:24px;"></th>';
|
|
1496
|
+
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Model</th>';
|
|
1497
|
+
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Uses</th>';
|
|
1498
|
+
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">System Prompt (override applies to all calls using this prompt)</th>';
|
|
1499
|
+
html += '</tr></thead><tbody>';
|
|
1500
|
+
|
|
1501
|
+
prompts.forEach(function(row) {
|
|
1502
|
+
const key = row.systemPrompt;
|
|
1503
|
+
const isEnabled = window._promptMockConfig[key] !== undefined;
|
|
1504
|
+
const currentVal = isEnabled ? window._promptMockConfig[key] : row.systemPrompt;
|
|
1505
|
+
const preview = key.length > 80 ? key.slice(0, 80) + 'โฆ' : key;
|
|
1506
|
+
|
|
1507
|
+
html += '<tr class="prompt-mock-row" data-row-index="' + row.rowIndex + '" style="border-bottom:1px solid #f0f0f0;vertical-align:top;">';
|
|
1508
|
+
|
|
1509
|
+
// Checkbox column
|
|
1510
|
+
html += '<td style="padding:8px 10px;white-space:nowrap;">';
|
|
1511
|
+
html += '<input type="checkbox" class="prompt-mock-enable" title="Override this system prompt"' + (isEnabled ? ' checked' : '') + ' onchange="window.onPromptMockToggle(' + row.rowIndex + ', this.checked)">';
|
|
1512
|
+
html += '</td>';
|
|
1513
|
+
|
|
1514
|
+
// Model column
|
|
1515
|
+
html += '<td style="padding:8px 10px;font-family:Monaco,monospace;font-size:12px;white-space:nowrap;">' + esc(row.modelName) + '</td>';
|
|
1516
|
+
|
|
1517
|
+
// Uses count column
|
|
1518
|
+
html += '<td style="padding:8px 10px;color:#555;white-space:nowrap;">' + row.count + 'x</td>';
|
|
1519
|
+
|
|
1520
|
+
// System prompt column
|
|
1521
|
+
html += '<td style="padding:8px 10px;width:100%;">';
|
|
1522
|
+
if (!isEnabled) {
|
|
1523
|
+
html += '<div style="font-size:11px;color:#888;font-style:italic;font-family:Monaco,monospace;max-width:380px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="' + esc(key) + '">' + esc(preview) + '</div>';
|
|
1524
|
+
} else {
|
|
1525
|
+
html += '<textarea class="prompt-mock-input" data-row-index="' + row.rowIndex + '" style="width:100%;box-sizing:border-box;font-size:11px;font-family:Monaco,monospace;padding:4px;border:1px solid #ddd;border-radius:4px;min-height:72px;resize:vertical;" oninput="window.onPromptMockInput(' + row.rowIndex + ', this.value)">' + esc(currentVal) + '</textarea>';
|
|
1526
|
+
}
|
|
1527
|
+
html += '</td>';
|
|
1528
|
+
|
|
1529
|
+
html += '</tr>';
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
html += '</tbody></table></div>';
|
|
1533
|
+
return html;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function buildPromptMockConfigFromUI() {
|
|
1537
|
+
// Return _promptMockConfig as-is (string key โ string value), filtering out blank values
|
|
1538
|
+
const config = {};
|
|
1539
|
+
Object.keys(window._promptMockConfig).forEach(function(key) {
|
|
1540
|
+
const val = window._promptMockConfig[key];
|
|
1541
|
+
if (typeof val === 'string' && val.trim()) config[key] = val;
|
|
1542
|
+
});
|
|
1543
|
+
return config;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
window.onPromptMockToggle = function(rowIndex, enabled) {
|
|
1547
|
+
const prompts = getSystemPromptsFromTrace();
|
|
1548
|
+
const row = prompts[rowIndex];
|
|
1549
|
+
if (!row) return;
|
|
1550
|
+
const key = row.systemPrompt;
|
|
1551
|
+
if (!enabled) {
|
|
1552
|
+
delete window._promptMockConfig[key];
|
|
1553
|
+
} else {
|
|
1554
|
+
// Pre-fill with the original system prompt so user can edit from there
|
|
1555
|
+
if (window._promptMockConfig[key] === undefined) {
|
|
1556
|
+
window._promptMockConfig[key] = row.systemPrompt;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const container = document.getElementById('promptMockContainer');
|
|
1560
|
+
if (container) container.innerHTML = renderPromptMockSection();
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
window.onPromptMockInput = function(rowIndex, value) {
|
|
1564
|
+
const prompts = getSystemPromptsFromTrace();
|
|
1565
|
+
const row = prompts[rowIndex];
|
|
1566
|
+
if (!row) return;
|
|
1567
|
+
window._promptMockConfig[row.systemPrompt] = value;
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1425
1570
|
window.onToolMockModeChange = function(toolName, mode) {
|
|
1426
1571
|
if (!window._toolMockConfig[toolName]) window._toolMockConfig[toolName] = { mode: 'live' };
|
|
1427
1572
|
// Save current mock data before switching
|
|
@@ -1452,7 +1597,8 @@ function getDashboardHtml() {
|
|
|
1452
1597
|
|
|
1453
1598
|
window.openLiveValidationDialog = function() {
|
|
1454
1599
|
if (window.liveValidationDialog) return;
|
|
1455
|
-
window._toolMockConfig = {};
|
|
1600
|
+
window._toolMockConfig = {}; // Reset mock configs each time dialog opens
|
|
1601
|
+
window._promptMockConfig = {};
|
|
1456
1602
|
|
|
1457
1603
|
const hasTraceTools = currentObservations.some(function(o) { return o.type === 'TOOL'; });
|
|
1458
1604
|
const hasRegisteredTools = codeIndex.tools && codeIndex.tools.length > 0;
|
|
@@ -1478,8 +1624,15 @@ function getDashboardHtml() {
|
|
|
1478
1624
|
Show all registered tools
|
|
1479
1625
|
</label>
|
|
1480
1626
|
</div>
|
|
1481
|
-
<div id="toolMockContainer"></div>
|
|
1627
|
+
<div id="toolMockContainer" style="max-height:160px;overflow-y:auto;"></div>
|
|
1482
1628
|
</div>\` : ''}
|
|
1629
|
+
<div style="border-top:1px solid #eee;padding-top:16px;margin-bottom:16px;">
|
|
1630
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
|
|
1631
|
+
<div style="font-size:15px;font-weight:600;">Prompt Mocking</div>
|
|
1632
|
+
<span style="font-size:12px;color:#888;">Check a row to replace the system prompt for all calls using it</span>
|
|
1633
|
+
</div>
|
|
1634
|
+
<div id="promptMockContainer" style="max-height:160px;overflow-y:auto;"></div>
|
|
1635
|
+
</div>
|
|
1483
1636
|
<div style="display:flex;gap:12px;justify-content:space-between;align-items:center;">
|
|
1484
1637
|
<span id="liveValidationProgress" style="font-size:14px;color:#555;"></span>
|
|
1485
1638
|
<div style="display:flex;gap:12px;">
|
|
@@ -1490,11 +1643,15 @@ function getDashboardHtml() {
|
|
|
1490
1643
|
</div>
|
|
1491
1644
|
\`;
|
|
1492
1645
|
document.body.appendChild(window.liveValidationDialog);
|
|
1493
|
-
// Render
|
|
1646
|
+
// Render mock sections after DOM insertion
|
|
1494
1647
|
const toolMockContainer = document.getElementById('toolMockContainer');
|
|
1495
1648
|
if (toolMockContainer) {
|
|
1496
1649
|
toolMockContainer.innerHTML = renderToolMockSection(false);
|
|
1497
1650
|
}
|
|
1651
|
+
const promptMockContainer = document.getElementById('promptMockContainer');
|
|
1652
|
+
if (promptMockContainer) {
|
|
1653
|
+
promptMockContainer.innerHTML = renderPromptMockSection();
|
|
1654
|
+
}
|
|
1498
1655
|
document.getElementById('cancelLiveValidation').onclick = function() {
|
|
1499
1656
|
window.liveValidationDialog.remove();
|
|
1500
1657
|
window.liveValidationDialog = null;
|
|
@@ -1503,9 +1660,11 @@ function getDashboardHtml() {
|
|
|
1503
1660
|
const count = parseInt(document.getElementById('liveValidationCount').value, 10);
|
|
1504
1661
|
const sequential = document.getElementById('liveValidationSequential').checked;
|
|
1505
1662
|
if (count >= 1) {
|
|
1506
|
-
// Build
|
|
1663
|
+
// Build mock configs from UI state and persist for "Run from here"
|
|
1507
1664
|
const toolMockConfig = buildToolMockConfigFromUI();
|
|
1508
1665
|
window._toolMockConfig = toolMockConfig;
|
|
1666
|
+
const promptMockConfig = buildPromptMockConfigFromUI();
|
|
1667
|
+
window._promptMockConfig = promptMockConfig;
|
|
1509
1668
|
const submitBtn = document.getElementById('submitLiveValidation');
|
|
1510
1669
|
submitBtn.disabled = true;
|
|
1511
1670
|
submitBtn.textContent = 'Validating...';
|
|
@@ -1542,7 +1701,7 @@ function getDashboardHtml() {
|
|
|
1542
1701
|
const collectedTraces = [];
|
|
1543
1702
|
let fatalError = null;
|
|
1544
1703
|
for (let i = 0; i < count; i++) {
|
|
1545
|
-
const singlePayload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig };
|
|
1704
|
+
const singlePayload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig, promptMockConfig };
|
|
1546
1705
|
try {
|
|
1547
1706
|
const response = await fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(singlePayload) });
|
|
1548
1707
|
const data = await response.json();
|
|
@@ -1561,7 +1720,7 @@ function getDashboardHtml() {
|
|
|
1561
1720
|
} else {
|
|
1562
1721
|
// Parallel mode: single bulk request
|
|
1563
1722
|
if (progressEl) progressEl.textContent = \`Running \${count} workflow run\${count !== 1 ? 's' : ''} in parallelโฆ\`;
|
|
1564
|
-
const payload = { workflowName: selectedWorkflow?.name, runCount: count, sequential: false, observations: currentObservations, toolMockConfig };
|
|
1723
|
+
const payload = { workflowName: selectedWorkflow?.name, runCount: count, sequential: false, observations: currentObservations, toolMockConfig, promptMockConfig };
|
|
1565
1724
|
try {
|
|
1566
1725
|
const response = await fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
|
1567
1726
|
const data = await response.json();
|
|
@@ -2096,7 +2255,7 @@ function getDashboardHtml() {
|
|
|
2096
2255
|
traceLayoutEl.classList.remove("step-4");
|
|
2097
2256
|
let headerHtml = '';
|
|
2098
2257
|
if (currentStep === 3) {
|
|
2099
|
-
headerHtml = '<tr><th style="width: 40px;">Check</th><th>Name</th><th>Type</th></tr>';
|
|
2258
|
+
headerHtml = '<tr><th style="width: 40px;">Check</th><th>Name</th><th>Type</th><th>Duration</th></tr>';
|
|
2100
2259
|
}
|
|
2101
2260
|
traceLayoutEl.innerHTML = \`
|
|
2102
2261
|
<div class="trace-left">
|
|
@@ -2265,6 +2424,7 @@ function getDashboardHtml() {
|
|
|
2265
2424
|
snapshotId: liveTrace.snapshotId,
|
|
2266
2425
|
observations: currentObservations,
|
|
2267
2426
|
toolMockConfig: window._toolMockConfig || {},
|
|
2427
|
+
promptMockConfig: window._promptMockConfig || {},
|
|
2268
2428
|
};
|
|
2269
2429
|
const response = await fetch('/api/run-from-breakpoint', {
|
|
2270
2430
|
method: 'POST',
|
|
@@ -2296,7 +2456,7 @@ function getDashboardHtml() {
|
|
|
2296
2456
|
step5RerunInFlight = true;
|
|
2297
2457
|
renderObservationTable();
|
|
2298
2458
|
try {
|
|
2299
|
-
const payload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations };
|
|
2459
|
+
const payload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig: window._toolMockConfig || {}, promptMockConfig: window._promptMockConfig || {} };
|
|
2300
2460
|
const response = await fetch('/api/validate-workflow', {
|
|
2301
2461
|
method: 'POST',
|
|
2302
2462
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -2347,6 +2507,7 @@ function getDashboardHtml() {
|
|
|
2347
2507
|
},
|
|
2348
2508
|
snapshotId: liveTrace.snapshotId,
|
|
2349
2509
|
toolMockConfig: window._toolMockConfig || {},
|
|
2510
|
+
promptMockConfig: window._promptMockConfig || {},
|
|
2350
2511
|
};
|
|
2351
2512
|
const response = await fetch('/api/resume-agent-from-task', {
|
|
2352
2513
|
method: 'POST',
|
|
@@ -2867,11 +3028,11 @@ function resolveTemplateValue(value, input) {
|
|
|
2867
3028
|
return value;
|
|
2868
3029
|
}
|
|
2869
3030
|
async function runHttpWorkflow(opts) {
|
|
2870
|
-
const { workflowName, workflowInput, frozenEvents = [], pushedEvents, runConfigs, config, dashboardPort } = opts;
|
|
3031
|
+
const { workflowName, workflowInput, frozenEvents = [], promptMocks = {}, pushedEvents, runConfigs, config, dashboardPort } = opts;
|
|
2871
3032
|
const runId = randomUUID();
|
|
2872
|
-
// Register run config so the user's server can fetch frozen events
|
|
3033
|
+
// Register run config so the user's server can fetch frozen events and prompt mocks
|
|
2873
3034
|
pushedEvents.set(runId, []);
|
|
2874
|
-
runConfigs.set(runId, { frozenEvents });
|
|
3035
|
+
runConfigs.set(runId, { frozenEvents, promptMocks });
|
|
2875
3036
|
try {
|
|
2876
3037
|
const parsedInput = parseObservationInput(workflowInput);
|
|
2877
3038
|
const inputObj = parsedInput && typeof parsedInput === 'object' && !Array.isArray(parsedInput) ? parsedInput : {};
|
|
@@ -2928,6 +3089,7 @@ async function runHttpWorkflow(opts) {
|
|
|
2928
3089
|
const drainMs = parseInt(process.env.ELASTICDASH_HTTP_DRAIN_MS ?? '300', 10);
|
|
2929
3090
|
await new Promise(resolve => setTimeout(resolve, drainMs));
|
|
2930
3091
|
const events = (pushedEvents.get(runId) ?? []).sort((a, b) => a.timestamp - b.timestamp);
|
|
3092
|
+
console.log(`[elasticdash] runHttpWorkflow drain complete: ${events.length} events collected for runId=${runId}`);
|
|
2931
3093
|
const workflowTrace = { traceId: runId, events };
|
|
2932
3094
|
return { ok: true, currentOutput, workflowTrace, steps: [], llmSteps: [], toolCalls: [], customSteps: [] };
|
|
2933
3095
|
}
|
|
@@ -2948,8 +3110,8 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
2948
3110
|
// In-memory store for telemetry events pushed from HTTP workflow mode runs.
|
|
2949
3111
|
// Maps runId -> accumulated WorkflowEvent[]
|
|
2950
3112
|
const pushedEvents = new Map();
|
|
2951
|
-
// Per-run config for HTTP workflow mode (frozen events for replay).
|
|
2952
|
-
// Maps runId -> { frozenEvents }
|
|
3113
|
+
// Per-run config for HTTP workflow mode (frozen events + prompt mocks for replay).
|
|
3114
|
+
// Maps runId -> { frozenEvents, promptMocks }
|
|
2953
3115
|
const runConfigs = new Map();
|
|
2954
3116
|
// Scan workflows, tools, and config once at startup
|
|
2955
3117
|
const workflows = scanWorkflows(cwd);
|
|
@@ -3015,10 +3177,14 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
3015
3177
|
const resolvedInput = resolveWorkflowArgsFromObservations(body, workflowName);
|
|
3016
3178
|
const workflowInput = resolvedInput.input ?? null;
|
|
3017
3179
|
const traces = [];
|
|
3180
|
+
const promptMocks = body.promptMockConfig && typeof body.promptMockConfig === 'object' && !Array.isArray(body.promptMockConfig)
|
|
3181
|
+
? body.promptMockConfig
|
|
3182
|
+
: {};
|
|
3018
3183
|
const runOne = async (runNumber) => {
|
|
3019
3184
|
const result = await runHttpWorkflow({
|
|
3020
3185
|
workflowName, workflowInput, pushedEvents, runConfigs,
|
|
3021
3186
|
config: httpConfig, dashboardPort: port,
|
|
3187
|
+
promptMocks,
|
|
3022
3188
|
});
|
|
3023
3189
|
const traceStub = { getSteps: () => [], getLLMSteps: () => [], getToolCalls: () => [], getCustomSteps: () => [], recordLLMStep: () => { }, recordToolCall: () => { }, recordCustomStep: () => { } };
|
|
3024
3190
|
return {
|
|
@@ -3085,12 +3251,15 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
3085
3251
|
const frozenEventIds = new Set(frozenEvents.map((e) => e.id));
|
|
3086
3252
|
const httpConfig = elasticdashConfig.workflows?.[workflowName];
|
|
3087
3253
|
if (httpConfig?.mode === 'http') {
|
|
3088
|
-
// HTTP workflow mode โ call user's dev server with frozen events for step replay
|
|
3254
|
+
// HTTP workflow mode โ call user's dev server with frozen events + prompt mocks for step replay
|
|
3255
|
+
const bpPromptMocks = body.promptMockConfig && typeof body.promptMockConfig === 'object' && !Array.isArray(body.promptMockConfig)
|
|
3256
|
+
? body.promptMockConfig
|
|
3257
|
+
: {};
|
|
3089
3258
|
console.log(`[elasticdash] Run from breakpoint (HTTP mode): workflow="${workflowName}" checkpoint=${checkpoint} frozen=${frozenEvents.length}`);
|
|
3090
3259
|
const result = await runHttpWorkflow({
|
|
3091
3260
|
workflowName, workflowInput, pushedEvents, runConfigs,
|
|
3092
3261
|
config: httpConfig, dashboardPort: port,
|
|
3093
|
-
frozenEvents,
|
|
3262
|
+
frozenEvents, promptMocks: bpPromptMocks,
|
|
3094
3263
|
});
|
|
3095
3264
|
const traceStub = { getSteps: () => [], getLLMSteps: () => [], getToolCalls: () => [], getCustomSteps: () => [], recordLLMStep: () => { }, recordToolCall: () => { }, recordCustomStep: () => { } };
|
|
3096
3265
|
const snapshotId = result.workflowTrace ? saveSnapshot(cwd, result.workflowTrace) : undefined;
|
|
@@ -3118,8 +3287,14 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
3118
3287
|
const toolMockConfig = body.toolMockConfig && typeof body.toolMockConfig === 'object' && !Array.isArray(body.toolMockConfig)
|
|
3119
3288
|
? body.toolMockConfig
|
|
3120
3289
|
: undefined;
|
|
3290
|
+
const aiMockConfig = body.aiMockConfig && typeof body.aiMockConfig === 'object' && !Array.isArray(body.aiMockConfig)
|
|
3291
|
+
? body.aiMockConfig
|
|
3292
|
+
: undefined;
|
|
3293
|
+
const promptMockConfig = body.promptMockConfig && typeof body.promptMockConfig === 'object' && !Array.isArray(body.promptMockConfig)
|
|
3294
|
+
? body.promptMockConfig
|
|
3295
|
+
: undefined;
|
|
3121
3296
|
console.log(`[elasticdash] Run from breakpoint: workflow="${workflowName}" checkpoint=${checkpoint} historyLen=${history.length}`);
|
|
3122
|
-
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, { replayMode: true, checkpoint, history, ...(toolMockConfig ? { toolMockConfig } : {}) });
|
|
3297
|
+
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, { replayMode: true, checkpoint, history, ...(toolMockConfig ? { toolMockConfig } : {}), ...(aiMockConfig ? { aiMockConfig } : {}), ...(promptMockConfig ? { promptMockConfig } : {}) });
|
|
3123
3298
|
const traceStub = {
|
|
3124
3299
|
getSteps: () => (result.steps ?? []),
|
|
3125
3300
|
getLLMSteps: () => (result.llmSteps ?? []),
|
|
@@ -3190,8 +3365,14 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
3190
3365
|
const toolMockConfig = body.toolMockConfig && typeof body.toolMockConfig === 'object' && !Array.isArray(body.toolMockConfig)
|
|
3191
3366
|
? body.toolMockConfig
|
|
3192
3367
|
: undefined;
|
|
3368
|
+
const aiMockConfig = body.aiMockConfig && typeof body.aiMockConfig === 'object' && !Array.isArray(body.aiMockConfig)
|
|
3369
|
+
? body.aiMockConfig
|
|
3370
|
+
: undefined;
|
|
3371
|
+
const promptMockConfig = body.promptMockConfig && typeof body.promptMockConfig === 'object' && !Array.isArray(body.promptMockConfig)
|
|
3372
|
+
? body.promptMockConfig
|
|
3373
|
+
: undefined;
|
|
3193
3374
|
console.log(`[elasticdash] Resume agent from task: workflow="${workflowName}" taskIndex=${taskIndex}`);
|
|
3194
|
-
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, [], null, { replayMode: history.length > 0, checkpoint: 0, history, agentState, ...(toolMockConfig ? { toolMockConfig } : {}) });
|
|
3375
|
+
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, [], null, { replayMode: history.length > 0, checkpoint: 0, history, agentState, ...(toolMockConfig ? { toolMockConfig } : {}), ...(aiMockConfig ? { aiMockConfig } : {}), ...(promptMockConfig ? { promptMockConfig } : {}) });
|
|
3195
3376
|
const traceStub = {
|
|
3196
3377
|
getSteps: () => (result.steps ?? []),
|
|
3197
3378
|
getLLMSteps: () => (result.llmSteps ?? []),
|
|
@@ -3242,7 +3423,7 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
3242
3423
|
const runId = url.pathname.slice('/api/run-configs/'.length);
|
|
3243
3424
|
const cfg = runConfigs.get(runId);
|
|
3244
3425
|
res.writeHead(cfg ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
3245
|
-
res.end(JSON.stringify({ frozenEvents: cfg?.frozenEvents ?? [] }));
|
|
3426
|
+
res.end(JSON.stringify({ frozenEvents: cfg?.frozenEvents ?? [], promptMocks: cfg?.promptMocks ?? {} }));
|
|
3246
3427
|
}
|
|
3247
3428
|
else if (url.pathname === '/api/trace-events' && req.method === 'POST') {
|
|
3248
3429
|
// Receive telemetry events pushed from wrapAI / wrapTool in HTTP workflow mode
|
|
@@ -3255,9 +3436,16 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
3255
3436
|
res.end(JSON.stringify({ ok: false, error: 'runId (string) and event (object) are required.' }));
|
|
3256
3437
|
return;
|
|
3257
3438
|
}
|
|
3258
|
-
const existing = pushedEvents.get(body.runId)
|
|
3259
|
-
existing
|
|
3260
|
-
|
|
3439
|
+
const existing = pushedEvents.get(body.runId);
|
|
3440
|
+
if (!existing) {
|
|
3441
|
+
console.log(`[elasticdash] /api/trace-events: unknown runId=${body.runId}, known runIds=[${[...pushedEvents.keys()].join(',')}]`);
|
|
3442
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
3443
|
+
res.end(JSON.stringify({ ok: false, error: 'unknown runId' }));
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
const evt = body.event;
|
|
3447
|
+
existing.push(evt);
|
|
3448
|
+
console.log(`[elasticdash] /api/trace-events: stored event type=${evt.type} name=${('name' in evt ? evt.name : '?')} runId=${body.runId} total=${existing.length}`);
|
|
3261
3449
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3262
3450
|
res.end(JSON.stringify({ ok: true }));
|
|
3263
3451
|
}
|