elasticdash-test 0.1.11 → 0.1.12
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 +51 -0
- package/dist/capture/event.d.ts +4 -0
- package/dist/capture/event.d.ts.map +1 -1
- package/dist/capture/recorder.d.ts +5 -0
- package/dist/capture/recorder.d.ts.map +1 -1
- package/dist/capture/recorder.js +10 -0
- package/dist/capture/recorder.js.map +1 -1
- package/dist/dashboard-server.d.ts +12 -0
- package/dist/dashboard-server.d.ts.map +1 -1
- package/dist/dashboard-server.js +269 -46
- package/dist/dashboard-server.js.map +1 -1
- package/dist/index.cjs +2526 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/interceptors/ai-interceptor.d.ts.map +1 -1
- package/dist/interceptors/ai-interceptor.js +101 -7
- package/dist/interceptors/ai-interceptor.js.map +1 -1
- package/dist/interceptors/http.d.ts +20 -0
- package/dist/interceptors/http.d.ts.map +1 -1
- package/dist/interceptors/http.js +184 -17
- package/dist/interceptors/http.js.map +1 -1
- package/dist/interceptors/tool.d.ts.map +1 -1
- package/dist/interceptors/tool.js +91 -0
- package/dist/interceptors/tool.js.map +1 -1
- package/dist/internals/mock-resolver.d.ts +25 -0
- package/dist/internals/mock-resolver.d.ts.map +1 -0
- package/dist/internals/mock-resolver.js +82 -0
- package/dist/internals/mock-resolver.js.map +1 -0
- package/dist/workflow-runner-worker.js +50 -3
- package/dist/workflow-runner-worker.js.map +1 -1
- package/dist/workflow-runner.d.ts.map +1 -1
- package/dist/workflow-runner.js +1 -0
- package/dist/workflow-runner.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,12 @@ npm install elasticdash-test
|
|
|
38
38
|
|
|
39
39
|
**Requirements:** Node 20+. For Deno projects, see [Using elasticdash-test in Deno](docs/deno.md).
|
|
40
40
|
|
|
41
|
+
**Git ignore:** ElasticDash writes temporary runtime artifacts under `.temp/`. Add this to your `.gitignore`:
|
|
42
|
+
|
|
43
|
+
```gitignore
|
|
44
|
+
.temp/
|
|
45
|
+
```
|
|
46
|
+
|
|
41
47
|
**Running CLI commands:** Use `npx` to run commands with your locally installed version (recommended to avoid version drift):
|
|
42
48
|
|
|
43
49
|
```bash
|
|
@@ -92,6 +98,12 @@ Total: 3
|
|
|
92
98
|
Duration: 3.4s
|
|
93
99
|
```
|
|
94
100
|
|
|
101
|
+
**Workflow export requirements:**
|
|
102
|
+
|
|
103
|
+
- Export plain callable functions from `ed_workflows.ts/js`.
|
|
104
|
+
- Use JSON-serializable inputs/outputs (object or array) so dashboard replay can pass args and read results.
|
|
105
|
+
- Do not export framework-bound handlers directly (for example Next.js `NextRequest`/`NextResponse` route handlers).
|
|
106
|
+
|
|
95
107
|
---
|
|
96
108
|
|
|
97
109
|
## Documentation
|
|
@@ -213,6 +225,45 @@ In manual mode, always isolate tracing in a separate `try/catch` so trace loggin
|
|
|
213
225
|
|
|
214
226
|
**→ See [Tool Recording & Replay](docs/tools.md) for checkpoint-based replay and freezing**
|
|
215
227
|
|
|
228
|
+
### HTTP Streaming Capture and Replay
|
|
229
|
+
|
|
230
|
+
ElasticDash also captures non-AI `fetch` responses that stream over HTTP (for example SSE and NDJSON endpoints) in the HTTP interceptor.
|
|
231
|
+
|
|
232
|
+
Currently detected as streaming when response `content-type` includes:
|
|
233
|
+
- `text/event-stream`
|
|
234
|
+
- `application/x-ndjson`
|
|
235
|
+
- `application/stream+json`
|
|
236
|
+
- `application/jsonl`
|
|
237
|
+
|
|
238
|
+
How it behaves today:
|
|
239
|
+
- During live execution, ElasticDash tees the response stream and returns a real stream to your app code.
|
|
240
|
+
- In parallel, ElasticDash buffers the recorder side of the stream as raw text for trace replay.
|
|
241
|
+
- During replay, ElasticDash reconstructs a stream from that captured raw payload and restores status, status text, and response headers.
|
|
242
|
+
|
|
243
|
+
Replay fidelity note:
|
|
244
|
+
- Replay preserves stream payload content, but not original chunk boundaries or timing cadence.
|
|
245
|
+
|
|
246
|
+
Minimal stream consumption example:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
const res = await fetch('https://example.com/events')
|
|
250
|
+
if (!res.body) throw new Error('Expected a streaming response body')
|
|
251
|
+
|
|
252
|
+
const reader = res.body.getReader()
|
|
253
|
+
const decoder = new TextDecoder()
|
|
254
|
+
let buffer = ''
|
|
255
|
+
|
|
256
|
+
for (;;) {
|
|
257
|
+
const { done, value } = await reader.read()
|
|
258
|
+
if (done) break
|
|
259
|
+
buffer += decoder.decode(value, { stream: true })
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
buffer += decoder.decode()
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**→ See [Quick Start Guide](docs/quickstart.md#capture-streaming-flows) for end-to-end setup guidance**
|
|
266
|
+
|
|
216
267
|
---
|
|
217
268
|
|
|
218
269
|
## Configuration
|
package/dist/capture/event.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export interface WorkflowEvent {
|
|
|
11
11
|
agentTaskId?: string;
|
|
12
12
|
/** Optional: Zero-based index of the agent task that produced this event */
|
|
13
13
|
agentTaskIndex?: number;
|
|
14
|
+
/** Set to true when the original response / output was a stream */
|
|
15
|
+
streamed?: boolean;
|
|
16
|
+
/** Raw buffered text of a streamed response (used for replay) */
|
|
17
|
+
streamRaw?: string;
|
|
14
18
|
}
|
|
15
19
|
export interface WorkflowTrace {
|
|
16
20
|
traceId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/capture/event.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,aAAa,CAAA;AAE7E,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,iBAAiB,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"event.d.ts","sourceRoot":"","sources":["../../src/capture/event.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,aAAa,CAAA;AAE7E,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,iBAAiB,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,MAAM,EAAE,OAAO,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,aAAa,EAAE,CAAA;CACxB"}
|
|
@@ -4,7 +4,12 @@ export declare class TraceRecorder {
|
|
|
4
4
|
events: WorkflowEvent[];
|
|
5
5
|
private _counter;
|
|
6
6
|
private _sideEffectCounter;
|
|
7
|
+
private _pending;
|
|
7
8
|
record(event: WorkflowEvent): void;
|
|
9
|
+
/** Register an in-flight async recording promise so flush() can await it. */
|
|
10
|
+
trackAsync(promise: Promise<void>): void;
|
|
11
|
+
/** Await all in-flight async recordings. No-op when none are pending. */
|
|
12
|
+
flush(): Promise<void>;
|
|
8
13
|
nextId(): number;
|
|
9
14
|
/** Separate counter for Date.now / Math.random — never shares IDs with main events. */
|
|
10
15
|
nextSideEffectId(): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/capture/recorder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,qBAAa,aAAa;IACxB,MAAM,EAAE,aAAa,EAAE,CAAK;IAC5B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,kBAAkB,CAAI;
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/capture/recorder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,qBAAa,aAAa;IACxB,MAAM,EAAE,aAAa,EAAE,CAAK;IAC5B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,kBAAkB,CAAI;IAC9B,OAAO,CAAC,QAAQ,CAAgC;IAEhD,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAIlC,6EAA6E;IAC7E,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAKxC,yEAAyE;IACnE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,MAAM,IAAI,MAAM;IAIhB,uFAAuF;IACvF,gBAAgB,IAAI,MAAM;IAI1B,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,aAAa;CAMzC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,aAAa,CAAA;IACvB,MAAM,EAAE,gBAAgB,CAAA;CACzB;AASD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAEvE;AAED,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,SAAS,CAE9D"}
|
package/dist/capture/recorder.js
CHANGED
|
@@ -4,9 +4,19 @@ export class TraceRecorder {
|
|
|
4
4
|
events = [];
|
|
5
5
|
_counter = 0;
|
|
6
6
|
_sideEffectCounter = 0;
|
|
7
|
+
_pending = new Set();
|
|
7
8
|
record(event) {
|
|
8
9
|
this.events.push(event);
|
|
9
10
|
}
|
|
11
|
+
/** Register an in-flight async recording promise so flush() can await it. */
|
|
12
|
+
trackAsync(promise) {
|
|
13
|
+
this._pending.add(promise);
|
|
14
|
+
promise.finally(() => { this._pending.delete(promise); });
|
|
15
|
+
}
|
|
16
|
+
/** Await all in-flight async recordings. No-op when none are pending. */
|
|
17
|
+
async flush() {
|
|
18
|
+
await Promise.allSettled([...this._pending]);
|
|
19
|
+
}
|
|
10
20
|
nextId() {
|
|
11
21
|
return ++this._counter;
|
|
12
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../src/capture/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAIxC,MAAM,OAAO,aAAa;IACxB,MAAM,GAAoB,EAAE,CAAA;IACpB,QAAQ,GAAG,CAAC,CAAA;IACZ,kBAAkB,GAAG,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../src/capture/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAIxC,MAAM,OAAO,aAAa;IACxB,MAAM,GAAoB,EAAE,CAAA;IACpB,QAAQ,GAAG,CAAC,CAAA;IACZ,kBAAkB,GAAG,CAAC,CAAA;IACtB,QAAQ,GAAuB,IAAI,GAAG,EAAE,CAAA;IAEhD,MAAM,CAAC,KAAoB;QACzB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACzB,CAAC;IAED,6EAA6E;IAC7E,UAAU,CAAC,OAAsB;QAC/B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAC1B,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;IAC1D,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,KAAK;QACT,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC9C,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAA;IACxB,CAAC;IAED,uFAAuF;IACvF,gBAAgB;QACd,OAAO,EAAE,IAAI,CAAC,kBAAkB,CAAA;IAClC,CAAC;IAED,OAAO,CAAC,OAAgB;QACtB,OAAO;YACL,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE;YAChC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;SACzB,CAAA;IACH,CAAC;CACF;AAOD,MAAM,CAAC,GAAG,UAAqC,CAAA;AAC/C,MAAM,eAAe,GAAG,6BAA6B,CAAA;AACrD,MAAM,UAAU,GACb,CAAC,CAAC,eAAe,CAAmD;IACrE,IAAI,iBAAiB,EAA8B,CAAA;AACrD,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC;IAAE,CAAC,CAAC,eAAe,CAAC,GAAG,UAAU,CAAA;AAExD,MAAM,UAAU,iBAAiB,CAAC,GAA+B;IAC/D,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;AAC3B,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,UAAU,CAAC,QAAQ,EAAE,CAAA;AAC9B,CAAC"}
|
|
@@ -28,6 +28,18 @@ export interface DashboardServer {
|
|
|
28
28
|
url: string;
|
|
29
29
|
close(): Promise<void>;
|
|
30
30
|
}
|
|
31
|
+
/** Per-tool mock configuration sent from the dashboard UI */
|
|
32
|
+
export interface ToolMockEntry {
|
|
33
|
+
/** 'live' = always call real tool, 'mock-all' = mock every call, 'mock-specific' = mock only listed call indices */
|
|
34
|
+
mode: 'live' | 'mock-all' | 'mock-specific';
|
|
35
|
+
/** When mode is 'mock-specific', which 1-based call indices to mock */
|
|
36
|
+
callIndices?: number[];
|
|
37
|
+
/** Mock data keyed by 1-based call index (or 0 for mock-all default) */
|
|
38
|
+
mockData?: Record<number, unknown>;
|
|
39
|
+
}
|
|
40
|
+
export interface ToolMockConfig {
|
|
41
|
+
[toolName: string]: ToolMockEntry;
|
|
42
|
+
}
|
|
31
43
|
/**
|
|
32
44
|
* Start the dashboard server
|
|
33
45
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-server.d.ts","sourceRoot":"","sources":["../src/dashboard-server.ts"],"names":[],"mappings":"AAcA,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;
|
|
1
|
+
{"version":3,"file":"dashboard-server.d.ts","sourceRoot":"","sources":["../src/dashboard-server.ts"],"names":[],"mappings":"AAcA,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;AAuCD,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;AAqpFD;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,eAAe,CAAC,CA+S1B;AAiFD,eAAO,MAAM,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAa,CAAC"}
|
package/dist/dashboard-server.js
CHANGED
|
@@ -260,6 +260,7 @@ function runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowN
|
|
|
260
260
|
...(options?.checkpoint !== undefined ? { checkpoint: options.checkpoint } : {}),
|
|
261
261
|
...(options?.history !== undefined ? { history: options.history } : {}),
|
|
262
262
|
...(options?.agentState !== undefined ? { agentState: options.agentState } : {}),
|
|
263
|
+
...(options?.toolMockConfig !== undefined ? { toolMockConfig: options.toolMockConfig } : {}),
|
|
263
264
|
});
|
|
264
265
|
child.stdin.write(payload);
|
|
265
266
|
child.stdin.end(); // Always close stdin to avoid subprocess hang
|
|
@@ -309,7 +310,9 @@ async function runGenerationObservation(observation) {
|
|
|
309
310
|
}
|
|
310
311
|
async function rerunObservation(cwd, observation, tools) {
|
|
311
312
|
const type = observation.type?.toUpperCase();
|
|
312
|
-
|
|
313
|
+
const name = observation.name ?? '(unknown)';
|
|
314
|
+
if (type === 'TOOL' || name.startsWith('tool-')) {
|
|
315
|
+
observation.name = name.startsWith('tool-') ? name.slice(5) : name; // Support both explicit type and name prefix for tool observations
|
|
313
316
|
return runToolObservation(cwd, observation, tools);
|
|
314
317
|
}
|
|
315
318
|
if (type === 'GENERATION') {
|
|
@@ -362,7 +365,9 @@ function resolveWorkflowArgsFromObservations(body, workflowName) {
|
|
|
362
365
|
return typeof item.name === 'string' && (item.name ?? '').trim() === workflowName;
|
|
363
366
|
});
|
|
364
367
|
if (!matched) {
|
|
365
|
-
|
|
368
|
+
// No workflow-level observation found (e.g. trace was loaded from an external format that
|
|
369
|
+
// only contains child observations). Fall back to running the workflow with no arguments.
|
|
370
|
+
return { args: [], input: null };
|
|
366
371
|
}
|
|
367
372
|
return { args: normalizeWorkflowArgs(matched.input), input: matched.input };
|
|
368
373
|
}
|
|
@@ -597,6 +602,10 @@ async function validateWorkflowRuns(cwd, body) {
|
|
|
597
602
|
}
|
|
598
603
|
const workflowArgs = resolvedInput.args ?? [];
|
|
599
604
|
const workflowInput = resolvedInput.input ?? null;
|
|
605
|
+
// Parse tool mock config if provided
|
|
606
|
+
const toolMockConfig = body.toolMockConfig && typeof body.toolMockConfig === 'object' && !Array.isArray(body.toolMockConfig)
|
|
607
|
+
? body.toolMockConfig
|
|
608
|
+
: undefined;
|
|
600
609
|
const workflowsModulePath = resolveWorkflowModule(cwd);
|
|
601
610
|
if (!workflowsModulePath) {
|
|
602
611
|
return {
|
|
@@ -612,7 +621,7 @@ async function validateWorkflowRuns(cwd, body) {
|
|
|
612
621
|
console.log(`[elasticdash] Running workflow "${workflowName}" ${runCount} time(s) in ${mode} mode via subprocess`);
|
|
613
622
|
async function runOne(runNumber) {
|
|
614
623
|
console.log(`[elasticdash] === Run ${runNumber}: Starting workflow "${workflowName}" ===`);
|
|
615
|
-
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput)
|
|
624
|
+
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, toolMockConfig ? { toolMockConfig } : undefined)
|
|
616
625
|
.catch(err => {
|
|
617
626
|
throw { ok: false, error: `Workflow subprocess failed: ${formatError(err)}` };
|
|
618
627
|
});
|
|
@@ -1186,13 +1195,200 @@ function getDashboardHtml() {
|
|
|
1186
1195
|
}
|
|
1187
1196
|
};
|
|
1188
1197
|
|
|
1198
|
+
// ---- Tool Mock Config State ----
|
|
1199
|
+
window._toolMockConfig = {}; // { toolName: { mode: 'live'|'mock-all'|'mock-specific', callIndices: [], mockData: {} } }
|
|
1200
|
+
|
|
1201
|
+
function getToolsFromTrace() {
|
|
1202
|
+
// Extract unique tool names and their call details from the uploaded trace observations
|
|
1203
|
+
const toolCalls = {};
|
|
1204
|
+
currentObservations.forEach(function(obs, i) {
|
|
1205
|
+
if (obs.type !== 'TOOL') return;
|
|
1206
|
+
const name = obs.name || '(unknown)';
|
|
1207
|
+
if (!toolCalls[name]) toolCalls[name] = [];
|
|
1208
|
+
toolCalls[name].push({ index: toolCalls[name].length + 1, obsIndex: i, input: obs.input, output: obs.output });
|
|
1209
|
+
});
|
|
1210
|
+
return toolCalls;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function getAllRegisteredTools() {
|
|
1214
|
+
// From codeIndex.tools (fetched at page load from /api/code-index)
|
|
1215
|
+
return (codeIndex.tools || []).map(function(t) { return t.name; });
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function buildToolMockConfigFromUI() {
|
|
1219
|
+
const config = {};
|
|
1220
|
+
const rows = document.querySelectorAll('.tool-mock-row');
|
|
1221
|
+
rows.forEach(function(row) {
|
|
1222
|
+
const toolName = row.dataset.toolName;
|
|
1223
|
+
const modeSelect = row.querySelector('.tool-mock-mode');
|
|
1224
|
+
const mode = modeSelect ? modeSelect.value : 'live';
|
|
1225
|
+
if (mode === 'live') return;
|
|
1226
|
+
const entry = { mode: mode };
|
|
1227
|
+
if (mode === 'mock-specific') {
|
|
1228
|
+
const checkboxes = row.querySelectorAll('.tool-call-checkbox:checked');
|
|
1229
|
+
entry.callIndices = Array.from(checkboxes).map(function(cb) { return parseInt(cb.value, 10); });
|
|
1230
|
+
if (entry.callIndices.length === 0) return; // No calls selected, treat as live
|
|
1231
|
+
}
|
|
1232
|
+
// Collect mock data
|
|
1233
|
+
entry.mockData = {};
|
|
1234
|
+
const dataInputs = row.querySelectorAll('.tool-mock-data-input');
|
|
1235
|
+
dataInputs.forEach(function(inp) {
|
|
1236
|
+
const callIdx = parseInt(inp.dataset.callIdx, 10);
|
|
1237
|
+
if (!inp.value.trim()) return;
|
|
1238
|
+
try { entry.mockData[callIdx] = JSON.parse(inp.value); }
|
|
1239
|
+
catch(e) { entry.mockData[callIdx] = inp.value; }
|
|
1240
|
+
});
|
|
1241
|
+
config[toolName] = entry;
|
|
1242
|
+
});
|
|
1243
|
+
return config;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function cleanValue(value) {
|
|
1247
|
+
if (typeof value === "string") {
|
|
1248
|
+
value = value.replaceAll('\\\\"', '');
|
|
1249
|
+
// remove surrounding quotes if they exist
|
|
1250
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
1251
|
+
return value.slice(1, -1);
|
|
1252
|
+
}
|
|
1253
|
+
return value;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (Array.isArray(value)) {
|
|
1257
|
+
return value.map(cleanValue);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (typeof value === "object" && value !== null) {
|
|
1261
|
+
const result = {};
|
|
1262
|
+
for (const key in value) {
|
|
1263
|
+
result[key] = cleanValue(value[key]);
|
|
1264
|
+
}
|
|
1265
|
+
return result;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
return value;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function convert(input) {
|
|
1272
|
+
const parsed = JSON.parse(input);
|
|
1273
|
+
return cleanValue(parsed);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function renderToolMockSection(showAll) {
|
|
1277
|
+
const traceTools = getToolsFromTrace();
|
|
1278
|
+
const allToolNames = getAllRegisteredTools();
|
|
1279
|
+
const traceToolNames = Object.keys(traceTools);
|
|
1280
|
+
const toolNames = showAll
|
|
1281
|
+
? Array.from(new Set([...traceToolNames, ...allToolNames]))
|
|
1282
|
+
: traceToolNames;
|
|
1283
|
+
|
|
1284
|
+
if (toolNames.length === 0) {
|
|
1285
|
+
return '<div style="color:#999;font-size:13px;padding:6px 0;">No tools detected.</div>';
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
let html = '<div style="max-height:260px;overflow-y:auto;border:1px solid #e0e0e0;border-radius:6px;">';
|
|
1289
|
+
html += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
|
|
1290
|
+
html += '<thead><tr style="background:#f5f5f5;">';
|
|
1291
|
+
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Tool</th>';
|
|
1292
|
+
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Calls in Trace</th>';
|
|
1293
|
+
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Mock Mode</th>';
|
|
1294
|
+
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Details</th>';
|
|
1295
|
+
html += '</tr></thead><tbody>';
|
|
1296
|
+
|
|
1297
|
+
toolNames.forEach(function(name) {
|
|
1298
|
+
const calls = traceTools[name] || [];
|
|
1299
|
+
const inTrace = traceToolNames.includes(name);
|
|
1300
|
+
const existing = window._toolMockConfig[name] || { mode: 'live' };
|
|
1301
|
+
const nameStyle = inTrace ? '' : 'color:#999;';
|
|
1302
|
+
|
|
1303
|
+
html += '<tr class="tool-mock-row" data-tool-name="' + esc(name) + '" style="border-bottom:1px solid #f0f0f0;">';
|
|
1304
|
+
html += '<td style="padding:6px 10px;font-family:Monaco,monospace;' + nameStyle + '">' + esc(name) + (inTrace ? '' : ' <span style="font-size:10px;color:#aaa;">(not in trace)</span>') + '</td>';
|
|
1305
|
+
html += '<td style="padding:6px 10px;">' + calls.length + '</td>';
|
|
1306
|
+
html += '<td style="padding:6px 10px;">';
|
|
1307
|
+
html += '<select class="tool-mock-mode" style="font-size:12px;padding:2px 4px;" onchange="window.onToolMockModeChange(\\'' + esc(name) + '\\', this.value)">';
|
|
1308
|
+
html += '<option value="live"' + (existing.mode === 'live' ? ' selected' : '') + '>Live</option>';
|
|
1309
|
+
html += '<option value="mock-all"' + (existing.mode === 'mock-all' ? ' selected' : '') + '>Mock All Calls</option>';
|
|
1310
|
+
if (calls.length > 0) {
|
|
1311
|
+
html += '<option value="mock-specific"' + (existing.mode === 'mock-specific' ? ' selected' : '') + '>Mock Specific Calls</option>';
|
|
1312
|
+
}
|
|
1313
|
+
html += '</select>';
|
|
1314
|
+
html += '</td>';
|
|
1315
|
+
|
|
1316
|
+
// Details column: per-call checkboxes + mock data inputs
|
|
1317
|
+
html += '<td style="padding:6px 10px;">';
|
|
1318
|
+
if (existing.mode === 'mock-all') {
|
|
1319
|
+
let defaultData = (existing.mockData && existing.mockData[0] !== undefined) ? JSON.stringify(existing.mockData[0]) : (calls.length > 0 ? JSON.stringify(calls[0].output) : '');
|
|
1320
|
+
defaultData = convert(defaultData);
|
|
1321
|
+
html += '<div style="font-size:11px;color:#555;margin-bottom:4px;">Mock data (JSON):</div>';
|
|
1322
|
+
html += '<textarea class="tool-mock-data-input" data-call-idx="0" style="width:100%;font-size:11px;font-family:Monaco,monospace;padding:4px;border:1px solid #ddd;border-radius:4px;min-height:32px;resize:vertical;" placeholder="Return value for all calls">' + esc(defaultData) + '</textarea>';
|
|
1323
|
+
} else if (existing.mode === 'mock-specific' && calls.length > 0) {
|
|
1324
|
+
html += '<div style="font-size:11px;color:#555;margin-bottom:4px;">Select calls to mock:</div>';
|
|
1325
|
+
calls.forEach(function(call) {
|
|
1326
|
+
const isChecked = existing.callIndices && existing.callIndices.includes(call.index);
|
|
1327
|
+
const inputPreview = typeof call.input === 'string' ? call.input.slice(0, 40) : JSON.stringify(call.input || '').slice(0, 40);
|
|
1328
|
+
let mockVal = (existing.mockData && existing.mockData[call.index] !== undefined) ? JSON.stringify(existing.mockData[call.index]) : JSON.stringify(call.output);
|
|
1329
|
+
mockVal = convert(mockVal);
|
|
1330
|
+
html += '<div style="margin-bottom:6px;padding:4px;background:#fafafa;border-radius:4px;border:1px solid #eee;">';
|
|
1331
|
+
html += '<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;">';
|
|
1332
|
+
html += '<input type="checkbox" class="tool-call-checkbox" value="' + call.index + '"' + (isChecked ? ' checked' : '') + ' onchange="window.onToolCallCheckChange(\\'' + esc(name) + '\\',' + call.index + ',this.checked)">';
|
|
1333
|
+
html += '<span>Call #' + call.index + '</span>';
|
|
1334
|
+
html += '<span style="color:#888;font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + esc(inputPreview) + '</span>';
|
|
1335
|
+
html += '</label>';
|
|
1336
|
+
if (isChecked) {
|
|
1337
|
+
html += '<textarea class="tool-mock-data-input" data-call-idx="' + call.index + '" style="width:100%;font-size:11px;font-family:Monaco,monospace;padding:4px;border:1px solid #ddd;border-radius:4px;min-height:28px;resize:vertical;margin-top:4px;" placeholder="Mock return value (JSON)">' + esc(mockVal) + '</textarea>';
|
|
1338
|
+
}
|
|
1339
|
+
html += '</div>';
|
|
1340
|
+
});
|
|
1341
|
+
} else {
|
|
1342
|
+
html += '<span style="color:#aaa;font-size:11px;">—</span>';
|
|
1343
|
+
}
|
|
1344
|
+
html += '</td>';
|
|
1345
|
+
html += '</tr>';
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
html += '</tbody></table></div>';
|
|
1349
|
+
return html;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
window.onToolMockModeChange = function(toolName, mode) {
|
|
1353
|
+
if (!window._toolMockConfig[toolName]) window._toolMockConfig[toolName] = { mode: 'live' };
|
|
1354
|
+
// Save current mock data before switching
|
|
1355
|
+
window._toolMockConfig[toolName] = { ...window._toolMockConfig[toolName], mode: mode };
|
|
1356
|
+
if (mode === 'mock-specific' && !window._toolMockConfig[toolName].callIndices) {
|
|
1357
|
+
window._toolMockConfig[toolName].callIndices = [];
|
|
1358
|
+
}
|
|
1359
|
+
// Re-render tool mock section
|
|
1360
|
+
const showAll = document.getElementById('showAllToolsToggle');
|
|
1361
|
+
const container = document.getElementById('toolMockContainer');
|
|
1362
|
+
if (container) container.innerHTML = renderToolMockSection(showAll && showAll.checked);
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
window.onToolCallCheckChange = function(toolName, callIdx, checked) {
|
|
1366
|
+
if (!window._toolMockConfig[toolName]) window._toolMockConfig[toolName] = { mode: 'mock-specific', callIndices: [] };
|
|
1367
|
+
const indices = window._toolMockConfig[toolName].callIndices || [];
|
|
1368
|
+
if (checked && !indices.includes(callIdx)) {
|
|
1369
|
+
indices.push(callIdx);
|
|
1370
|
+
} else if (!checked) {
|
|
1371
|
+
const pos = indices.indexOf(callIdx);
|
|
1372
|
+
if (pos >= 0) indices.splice(pos, 1);
|
|
1373
|
+
}
|
|
1374
|
+
window._toolMockConfig[toolName].callIndices = indices;
|
|
1375
|
+
const showAll = document.getElementById('showAllToolsToggle');
|
|
1376
|
+
const container = document.getElementById('toolMockContainer');
|
|
1377
|
+
if (container) container.innerHTML = renderToolMockSection(showAll && showAll.checked);
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1189
1380
|
window.openLiveValidationDialog = function() {
|
|
1190
1381
|
if (window.liveValidationDialog) return;
|
|
1382
|
+
window._toolMockConfig = {}; // Reset mock config each time dialog opens
|
|
1383
|
+
|
|
1384
|
+
const hasTraceTools = currentObservations.some(function(o) { return o.type === 'TOOL'; });
|
|
1385
|
+
const hasRegisteredTools = codeIndex.tools && codeIndex.tools.length > 0;
|
|
1386
|
+
|
|
1191
1387
|
window.liveValidationDialog = document.createElement('div');
|
|
1192
1388
|
window.liveValidationDialog.id = 'liveValidationDialog';
|
|
1193
1389
|
window.liveValidationDialog.style = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.25);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
|
1194
1390
|
window.liveValidationDialog.innerHTML = \`
|
|
1195
|
-
<div style="background:white;padding:32px 28px;border-radius:12px;box-shadow:0 2px 24px #0002;min-width:
|
|
1391
|
+
<div style="background:white;padding:32px 28px;border-radius:12px;box-shadow:0 2px 24px #0002;min-width:680px;max-width:90vw;max-height:90vh;overflow-y:auto;">
|
|
1196
1392
|
<h3 style="margin-top:0;margin-bottom:18px;font-size:20px;">Validate Updated Flow with Live Data</h3>
|
|
1197
1393
|
<label style="font-size:15px;display:block;margin-bottom:8px;">How many times do you want to run the flow with live data?</label>
|
|
1198
1394
|
<input id="liveValidationCount" type="number" min="1" value="1" style="width:100%;font-size:16px;padding:6px 10px;margin-bottom:18px;" />
|
|
@@ -1200,6 +1396,17 @@ function getDashboardHtml() {
|
|
|
1200
1396
|
<input id="liveValidationSequential" type="checkbox" />
|
|
1201
1397
|
Run in sequence instead of parallel
|
|
1202
1398
|
</label>
|
|
1399
|
+
\${(hasTraceTools || hasRegisteredTools) ? \`
|
|
1400
|
+
<div style="border-top:1px solid #eee;padding-top:16px;margin-bottom:16px;">
|
|
1401
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
|
1402
|
+
<div style="font-size:15px;font-weight:600;">Tool Mocking</div>
|
|
1403
|
+
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
|
1404
|
+
<input id="showAllToolsToggle" type="checkbox" onchange="document.getElementById('toolMockContainer').innerHTML = renderToolMockSection(this.checked);" />
|
|
1405
|
+
Show all registered tools
|
|
1406
|
+
</label>
|
|
1407
|
+
</div>
|
|
1408
|
+
<div id="toolMockContainer"></div>
|
|
1409
|
+
</div>\` : ''}
|
|
1203
1410
|
<div style="display:flex;gap:12px;justify-content:space-between;align-items:center;">
|
|
1204
1411
|
<span id="liveValidationProgress" style="font-size:14px;color:#555;"></span>
|
|
1205
1412
|
<div style="display:flex;gap:12px;">
|
|
@@ -1210,6 +1417,11 @@ function getDashboardHtml() {
|
|
|
1210
1417
|
</div>
|
|
1211
1418
|
\`;
|
|
1212
1419
|
document.body.appendChild(window.liveValidationDialog);
|
|
1420
|
+
// Render the tool mock section after DOM insertion
|
|
1421
|
+
const toolMockContainer = document.getElementById('toolMockContainer');
|
|
1422
|
+
if (toolMockContainer) {
|
|
1423
|
+
toolMockContainer.innerHTML = renderToolMockSection(false);
|
|
1424
|
+
}
|
|
1213
1425
|
document.getElementById('cancelLiveValidation').onclick = function() {
|
|
1214
1426
|
window.liveValidationDialog.remove();
|
|
1215
1427
|
window.liveValidationDialog = null;
|
|
@@ -1218,65 +1430,76 @@ function getDashboardHtml() {
|
|
|
1218
1430
|
const count = parseInt(document.getElementById('liveValidationCount').value, 10);
|
|
1219
1431
|
const sequential = document.getElementById('liveValidationSequential').checked;
|
|
1220
1432
|
if (count >= 1) {
|
|
1433
|
+
// Build the tool mock config from UI state
|
|
1434
|
+
const toolMockConfig = buildToolMockConfigFromUI();
|
|
1221
1435
|
const submitBtn = document.getElementById('submitLiveValidation');
|
|
1222
1436
|
submitBtn.disabled = true;
|
|
1223
1437
|
submitBtn.textContent = 'Validating...';
|
|
1224
1438
|
const progressEl = document.getElementById('liveValidationProgress');
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
// Wrap the fetch in a progress simulation for user feedback
|
|
1228
|
-
const payload = { workflowName: selectedWorkflow?.name, runCount: count, sequential, observations: currentObservations };
|
|
1229
|
-
// Start the fetch but also update progress as we go
|
|
1230
|
-
const progressInterval = setInterval(() => {
|
|
1231
|
-
if (progressEl && completed < count) {
|
|
1232
|
-
progressEl.textContent = \`\${completed} of \${count} workflow runs\`;
|
|
1233
|
-
}
|
|
1234
|
-
}, 200);
|
|
1235
|
-
try {
|
|
1236
|
-
const responsePromise = fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
|
1237
|
-
// Simulate progress incrementing as if runs are happening (for demo, increment every 500ms)
|
|
1238
|
-
while (completed < count) {
|
|
1239
|
-
await new Promise(r => setTimeout(r, 500));
|
|
1240
|
-
completed++;
|
|
1241
|
-
if (progressEl) progressEl.textContent = \`\${completed} of \${count} workflow runs\`;
|
|
1242
|
-
}
|
|
1243
|
-
const response = await responsePromise;
|
|
1244
|
-
const data = await response.json();
|
|
1245
|
-
clearInterval(progressInterval);
|
|
1439
|
+
|
|
1440
|
+
function finishValidation(collectedTraces, errorMsg, usedSequential) {
|
|
1246
1441
|
if (progressEl) progressEl.textContent = '';
|
|
1247
1442
|
window.liveValidationDialog.remove();
|
|
1248
1443
|
window.liveValidationDialog = null;
|
|
1249
1444
|
window.liveValidationCount = count;
|
|
1250
|
-
window.liveValidationSequential =
|
|
1445
|
+
window.liveValidationSequential = usedSequential;
|
|
1251
1446
|
window.step5SelectedTrace = 0;
|
|
1252
1447
|
window.step5SelectedObservation = 0;
|
|
1253
1448
|
currentStep = 5;
|
|
1254
1449
|
updateModalTitle();
|
|
1255
1450
|
updateFooterButtons();
|
|
1256
|
-
if (
|
|
1257
|
-
step5RunTraces = Array.isArray(data.traces) ? data.traces : [];
|
|
1258
|
-
persistTraces();
|
|
1259
|
-
step5RunMeta = { loading: false, error: '', runCount: typeof data.runCount === 'number' ? data.runCount : count, sequential: data.mode === 'sequential' };
|
|
1260
|
-
} else {
|
|
1451
|
+
if (errorMsg && collectedTraces.length === 0) {
|
|
1261
1452
|
step5RunTraces = [];
|
|
1262
1453
|
localStorage.removeItem('ed_step5RunTraces');
|
|
1263
|
-
step5RunMeta = { loading: false, error:
|
|
1454
|
+
step5RunMeta = { loading: false, error: errorMsg, runCount: count, sequential: usedSequential };
|
|
1455
|
+
} else {
|
|
1456
|
+
step5RunTraces = collectedTraces;
|
|
1457
|
+
persistTraces();
|
|
1458
|
+
step5RunMeta = { loading: false, error: '', runCount: collectedTraces.length, sequential: usedSequential };
|
|
1459
|
+
}
|
|
1460
|
+
if (window.step5SelectedTrace > step5RunTraces.length) window.step5SelectedTrace = 0;
|
|
1461
|
+
window.step5SelectedObservation = 0;
|
|
1462
|
+
renderObservationTable();
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (sequential) {
|
|
1466
|
+
// Sequential mode: fire one request per run so progress reflects real completion
|
|
1467
|
+
if (progressEl) progressEl.textContent = \`0 of \${count} workflow runs completed\`;
|
|
1468
|
+
const collectedTraces = [];
|
|
1469
|
+
let fatalError = null;
|
|
1470
|
+
for (let i = 0; i < count; i++) {
|
|
1471
|
+
const singlePayload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig };
|
|
1472
|
+
try {
|
|
1473
|
+
const response = await fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(singlePayload) });
|
|
1474
|
+
const data = await response.json();
|
|
1475
|
+
if (response.ok && data.ok && Array.isArray(data.traces) && data.traces.length > 0) {
|
|
1476
|
+
collectedTraces.push({ ...data.traces[0], runNumber: i + 1 });
|
|
1477
|
+
} else {
|
|
1478
|
+
// Push an error trace so the run is still visible in Step 5
|
|
1479
|
+
collectedTraces.push({ runNumber: i + 1, ok: false, error: data.error || 'Workflow validation failed.', observations: [], workflowTrace: null });
|
|
1480
|
+
}
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
collectedTraces.push({ runNumber: i + 1, ok: false, error: err && err.message ? err.message : String(err), observations: [], workflowTrace: null });
|
|
1483
|
+
}
|
|
1484
|
+
if (progressEl) progressEl.textContent = \`\${i + 1} of \${count} workflow runs completed\`;
|
|
1485
|
+
}
|
|
1486
|
+
finishValidation(collectedTraces, fatalError, true);
|
|
1487
|
+
} else {
|
|
1488
|
+
// Parallel mode: single bulk request
|
|
1489
|
+
if (progressEl) progressEl.textContent = \`Running \${count} workflow run\${count !== 1 ? 's' : ''} in parallel…\`;
|
|
1490
|
+
const payload = { workflowName: selectedWorkflow?.name, runCount: count, sequential: false, observations: currentObservations, toolMockConfig };
|
|
1491
|
+
try {
|
|
1492
|
+
const response = await fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
|
1493
|
+
const data = await response.json();
|
|
1494
|
+
if (response.ok && data.ok) {
|
|
1495
|
+
finishValidation(Array.isArray(data.traces) ? data.traces : [], null, false);
|
|
1496
|
+
} else {
|
|
1497
|
+
finishValidation([], data.error || 'Workflow validation failed.', false);
|
|
1498
|
+
}
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
finishValidation([], err && err.message ? err.message : String(err), false);
|
|
1264
1501
|
}
|
|
1265
|
-
} catch (err) {
|
|
1266
|
-
clearInterval(progressInterval);
|
|
1267
|
-
if (progressEl) progressEl.textContent = '';
|
|
1268
|
-
window.liveValidationDialog.remove();
|
|
1269
|
-
window.liveValidationDialog = null;
|
|
1270
|
-
currentStep = 5;
|
|
1271
|
-
updateModalTitle();
|
|
1272
|
-
updateFooterButtons();
|
|
1273
|
-
step5RunTraces = [];
|
|
1274
|
-
localStorage.removeItem('ed_step5RunTraces');
|
|
1275
|
-
step5RunMeta = { loading: false, error: err && err.message ? err.message : String(err), runCount: count, sequential };
|
|
1276
1502
|
}
|
|
1277
|
-
if (window.step5SelectedTrace > step5RunTraces.length) window.step5SelectedTrace = 0;
|
|
1278
|
-
window.step5SelectedObservation = 0;
|
|
1279
|
-
renderObservationTable();
|
|
1280
1503
|
} else {
|
|
1281
1504
|
document.getElementById('liveValidationCount').style.borderColor = 'red';
|
|
1282
1505
|
}
|