elasticdash-test 0.1.12 → 0.1.13-alpha
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 +123 -4
- package/dist/capture/event.d.ts +6 -0
- package/dist/capture/event.d.ts.map +1 -1
- package/dist/dashboard-server.d.ts.map +1 -1
- package/dist/dashboard-server.js +240 -82
- package/dist/dashboard-server.js.map +1 -1
- package/dist/interceptors/ai-interceptor.d.ts.map +1 -1
- package/dist/interceptors/ai-interceptor.js +65 -10
- package/dist/interceptors/ai-interceptor.js.map +1 -1
- package/dist/interceptors/tool.d.ts.map +1 -1
- package/dist/interceptors/tool.js +12 -7
- package/dist/interceptors/tool.js.map +1 -1
- package/dist/matchers/index.d.ts +10 -1
- package/dist/matchers/index.d.ts.map +1 -1
- package/dist/matchers/index.js +57 -12
- package/dist/matchers/index.js.map +1 -1
- package/dist/tool-runner-worker.js +2 -2
- package/dist/tool-runner-worker.js.map +1 -1
- package/dist/trace-adapter/context.d.ts +2 -0
- package/dist/trace-adapter/context.d.ts.map +1 -1
- package/dist/trace-adapter/context.js +2 -2
- package/dist/trace-adapter/context.js.map +1 -1
- package/dist/tracing.d.ts +1 -1
- package/dist/tracing.d.ts.map +1 -1
- package/dist/tracing.js +4 -4
- package/dist/tracing.js.map +1 -1
- package/dist/workflow-runner-worker.js +37 -23
- package/dist/workflow-runner-worker.js.map +1 -1
- package/package.json +1 -1
package/dist/dashboard-server.js
CHANGED
|
@@ -24,6 +24,9 @@ function loadSnapshot(cwd, snapshotId) {
|
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
function isDenoProject(dir) {
|
|
28
|
+
return existsSync(path.join(dir, 'deno.json')) || existsSync(path.join(dir, 'deno.jsonc'));
|
|
29
|
+
}
|
|
27
30
|
function resolveRuntimeModule(cwd, baseName) {
|
|
28
31
|
for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
|
|
29
32
|
const candidate = path.join(cwd, `${baseName}${ext}`);
|
|
@@ -164,12 +167,21 @@ function formatError(error) {
|
|
|
164
167
|
}
|
|
165
168
|
function runToolInSubprocess(toolsModulePath, toolName, args) {
|
|
166
169
|
return new Promise((resolve) => {
|
|
170
|
+
const startMs = Date.now();
|
|
167
171
|
const workerScript = new URL('./tool-runner-worker.js', import.meta.url).pathname;
|
|
168
|
-
|
|
172
|
+
const projectDir = path.dirname(toolsModulePath);
|
|
173
|
+
const denoProject = isDenoProject(projectDir);
|
|
174
|
+
// For Deno projects use `deno run --allow-all` so that https:// imports and
|
|
175
|
+
// TypeScript are handled natively. For Node projects keep the existing tsx path.
|
|
169
176
|
const nodeOptions = process.env.NODE_OPTIONS ?? '';
|
|
170
|
-
const
|
|
171
|
-
const
|
|
177
|
+
const tsxFlag = '--import tsx';
|
|
178
|
+
const childNodeOptions = nodeOptions.includes('tsx') ? nodeOptions : `${nodeOptions} ${tsxFlag}`.trim();
|
|
179
|
+
const childEnv = { ...process.env, NODE_OPTIONS: denoProject ? nodeOptions : childNodeOptions };
|
|
180
|
+
const runtime = denoProject ? 'deno' : process.execPath;
|
|
181
|
+
const runtimeArgs = denoProject ? ['run', '--allow-all', workerScript] : [workerScript];
|
|
182
|
+
const child = spawn(runtime, runtimeArgs, {
|
|
172
183
|
env: childEnv,
|
|
184
|
+
cwd: projectDir,
|
|
173
185
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
174
186
|
});
|
|
175
187
|
const RESULT_PREFIX = '__ELASTICDASH_RESULT__:';
|
|
@@ -191,17 +203,21 @@ function runToolInSubprocess(toolsModulePath, toolName, args) {
|
|
|
191
203
|
process.stderr.write(chunk);
|
|
192
204
|
});
|
|
193
205
|
child.on('close', () => {
|
|
206
|
+
const currentDurationMs = Date.now() - startMs;
|
|
194
207
|
if (resultLine) {
|
|
195
208
|
try {
|
|
196
|
-
resolve(JSON.parse(resultLine));
|
|
209
|
+
resolve({ ...JSON.parse(resultLine), currentDurationMs });
|
|
197
210
|
return;
|
|
198
211
|
}
|
|
199
212
|
catch { /* fall through */ }
|
|
200
213
|
}
|
|
201
|
-
resolve({ ok: false, error: stderr.trim() || 'Tool subprocess produced no output.' });
|
|
214
|
+
resolve({ ok: false, error: stderr.trim() || 'Tool subprocess produced no output.', currentDurationMs });
|
|
202
215
|
});
|
|
203
216
|
child.on('error', (err) => {
|
|
204
|
-
|
|
217
|
+
const hint = denoProject && err.code === 'ENOENT'
|
|
218
|
+
? ' (Deno project detected — ensure "deno" is installed and available in PATH)'
|
|
219
|
+
: '';
|
|
220
|
+
resolve({ ok: false, error: `Failed to spawn tool subprocess: ${err.message}${hint}`, currentDurationMs: Date.now() - startMs });
|
|
205
221
|
});
|
|
206
222
|
// Always use absolute file URL for toolsModulePath
|
|
207
223
|
const payload = JSON.stringify({
|
|
@@ -216,17 +232,40 @@ function runToolInSubprocess(toolsModulePath, toolName, args) {
|
|
|
216
232
|
function runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, args, input, options) {
|
|
217
233
|
return new Promise((resolve) => {
|
|
218
234
|
const workerScript = new URL('./workflow-runner-worker.js', import.meta.url).pathname;
|
|
219
|
-
|
|
235
|
+
const projectDir = path.dirname(workflowsModulePath);
|
|
236
|
+
const denoProject = isDenoProject(projectDir);
|
|
237
|
+
// For Deno projects use `deno run --allow-all` so that https:// imports and
|
|
238
|
+
// TypeScript are handled natively. For Node projects keep the existing tsx path.
|
|
220
239
|
const nodeOptions = process.env.NODE_OPTIONS ?? '';
|
|
221
|
-
const
|
|
222
|
-
const
|
|
240
|
+
const tsxFlag = '--import tsx';
|
|
241
|
+
const childNodeOptions = nodeOptions.includes('tsx') ? nodeOptions : `${nodeOptions} ${tsxFlag}`.trim();
|
|
242
|
+
const childEnv = { ...process.env, NODE_OPTIONS: denoProject ? nodeOptions : childNodeOptions };
|
|
243
|
+
const runtime = denoProject ? 'deno' : process.execPath;
|
|
244
|
+
const runtimeArgs = denoProject ? ['run', '--allow-all', workerScript] : [workerScript];
|
|
245
|
+
const child = spawn(runtime, runtimeArgs, {
|
|
223
246
|
env: childEnv,
|
|
247
|
+
cwd: projectDir,
|
|
224
248
|
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
|
225
249
|
});
|
|
226
250
|
let fd3Data = '';
|
|
227
251
|
let stderr = '';
|
|
252
|
+
// Line-buffer stdout so that large result JSON lines split across multiple
|
|
253
|
+
// data events are reassembled before processing.
|
|
254
|
+
const WORKFLOW_RESULT_PREFIX = '__ELASTICDASH_RESULT__:';
|
|
255
|
+
let stdoutBuf = '';
|
|
228
256
|
child.stdout.on('data', (chunk) => {
|
|
229
|
-
|
|
257
|
+
stdoutBuf += chunk.toString();
|
|
258
|
+
const lines = stdoutBuf.split('\n');
|
|
259
|
+
stdoutBuf = lines.pop() ?? ''; // keep last (possibly incomplete) line
|
|
260
|
+
for (const line of lines) {
|
|
261
|
+
if (line.startsWith(WORKFLOW_RESULT_PREFIX)) {
|
|
262
|
+
// Stdout fallback channel (used by Deno when fd3 is unavailable)
|
|
263
|
+
fd3Data += line.slice(WORKFLOW_RESULT_PREFIX.length);
|
|
264
|
+
}
|
|
265
|
+
else if (line) {
|
|
266
|
+
process.stdout.write(line + '\n');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
230
269
|
});
|
|
231
270
|
child.stderr.on('data', (chunk) => {
|
|
232
271
|
stderr += chunk.toString();
|
|
@@ -237,6 +276,13 @@ function runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowN
|
|
|
237
276
|
fd3Data += chunk.toString();
|
|
238
277
|
});
|
|
239
278
|
child.on('close', () => {
|
|
279
|
+
// Flush any remaining buffered stdout line (e.g. result with no trailing newline)
|
|
280
|
+
if (stdoutBuf.startsWith(WORKFLOW_RESULT_PREFIX)) {
|
|
281
|
+
fd3Data += stdoutBuf.slice(WORKFLOW_RESULT_PREFIX.length);
|
|
282
|
+
}
|
|
283
|
+
else if (stdoutBuf) {
|
|
284
|
+
process.stdout.write(stdoutBuf + '\n');
|
|
285
|
+
}
|
|
240
286
|
if (fd3Data) {
|
|
241
287
|
try {
|
|
242
288
|
resolve(JSON.parse(fd3Data));
|
|
@@ -247,7 +293,10 @@ function runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowN
|
|
|
247
293
|
resolve({ ok: false, error: stderr.trim() || 'Workflow subprocess produced no output.' });
|
|
248
294
|
});
|
|
249
295
|
child.on('error', (err) => {
|
|
250
|
-
|
|
296
|
+
const hint = denoProject && err.code === 'ENOENT'
|
|
297
|
+
? ' (Deno project detected — ensure "deno" is installed and available in PATH)'
|
|
298
|
+
: '';
|
|
299
|
+
resolve({ ok: false, error: `Failed to spawn workflow subprocess: ${err.message}${hint}` });
|
|
251
300
|
});
|
|
252
301
|
// Always use absolute file URL for workflowsModulePath and toolsModulePath
|
|
253
302
|
const payload = JSON.stringify({
|
|
@@ -301,8 +350,8 @@ async function runGenerationObservation(observation) {
|
|
|
301
350
|
const model = observation.model;
|
|
302
351
|
const temperature = typeof observation.modelParameters?.temperature === 'number' ? observation.modelParameters.temperature : 0;
|
|
303
352
|
const maxTokens = typeof observation.modelParameters?.max_tokens === 'number' ? observation.modelParameters.max_tokens : 512;
|
|
304
|
-
const
|
|
305
|
-
return { ok: true, currentOutput:
|
|
353
|
+
const result = await callProviderLLM(prompt, { provider, model }, systemPrompt ?? 'You are a helpful assistant.', maxTokens, temperature);
|
|
354
|
+
return { ok: true, currentOutput: result.content, currentDurationMs: result.durationMs, currentUsage: result.usage };
|
|
306
355
|
}
|
|
307
356
|
catch (error) {
|
|
308
357
|
return { ok: false, error: `Generation rerun failed: ${formatError(error)}` };
|
|
@@ -418,64 +467,21 @@ function toObservationFromWorkflowEvent(event) {
|
|
|
418
467
|
const inp = event.input;
|
|
419
468
|
const out = event.output;
|
|
420
469
|
const provider = inp?.provider ?? '';
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
else if (provider === 'gemini') {
|
|
427
|
-
const candidates = out.candidates;
|
|
428
|
-
if (Array.isArray(candidates) && candidates.length > 0) {
|
|
429
|
-
const first = candidates[0];
|
|
430
|
-
if (first.content && typeof first.content === 'object') {
|
|
431
|
-
const parts = first.content.parts;
|
|
432
|
-
if (Array.isArray(parts) && parts.length > 0) {
|
|
433
|
-
completion = String(parts[0].text ?? '');
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
const choices = out.choices;
|
|
440
|
-
if (Array.isArray(choices) && choices.length > 0) {
|
|
441
|
-
const first = choices[0];
|
|
442
|
-
if (first.message && typeof first.message === 'object') {
|
|
443
|
-
completion = String(first.message.content ?? '');
|
|
444
|
-
}
|
|
445
|
-
else if (typeof first.text === 'string') {
|
|
446
|
-
completion = first.text;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
// Embedding response: data[].embedding
|
|
450
|
-
if (!completion) {
|
|
451
|
-
const data = out.data;
|
|
452
|
-
if (Array.isArray(data) && data.length > 0) {
|
|
453
|
-
const first = data[0];
|
|
454
|
-
if (Array.isArray(first?.embedding)) {
|
|
455
|
-
completion = `[${data.length} embedding(s), ${first.embedding.length} dimensions]`;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
// Gemini embedding response: embeddings[].values
|
|
460
|
-
if (!completion) {
|
|
461
|
-
const embeddings = out.embeddings;
|
|
462
|
-
if (Array.isArray(embeddings) && embeddings.length > 0) {
|
|
463
|
-
const first = embeddings[0];
|
|
464
|
-
if (Array.isArray(first?.values)) {
|
|
465
|
-
completion = `[${embeddings.length} embedding(s), ${first.values.length} dimensions]`;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
+
// For streaming events, out is { streamed: true, completion } — extract text for fallback
|
|
471
|
+
let streamedCompletion;
|
|
472
|
+
if (out?.streamed === true && typeof out.completion === 'string') {
|
|
473
|
+
streamedCompletion = out.completion;
|
|
470
474
|
}
|
|
471
475
|
return {
|
|
472
476
|
type: 'GENERATION',
|
|
473
477
|
name: provider || 'llm',
|
|
474
478
|
provider: provider || undefined,
|
|
475
479
|
model: inp?.model ?? event.name,
|
|
476
|
-
input: inp?.prompt,
|
|
477
|
-
output:
|
|
480
|
+
input: inp?.messages ?? inp?.prompt,
|
|
481
|
+
output: streamedCompletion !== undefined ? streamedCompletion : out,
|
|
478
482
|
startTime: normalizeStartTime(event.timestamp),
|
|
483
|
+
durationMs: event.durationMs,
|
|
484
|
+
usage: event.usage,
|
|
479
485
|
workflowEventId: event.id,
|
|
480
486
|
...agentFields,
|
|
481
487
|
};
|
|
@@ -487,6 +493,7 @@ function toObservationFromWorkflowEvent(event) {
|
|
|
487
493
|
input: event.input,
|
|
488
494
|
output: event.output,
|
|
489
495
|
startTime: normalizeStartTime(event.timestamp),
|
|
496
|
+
durationMs: event.durationMs,
|
|
490
497
|
workflowEventId: event.id,
|
|
491
498
|
...agentFields,
|
|
492
499
|
};
|
|
@@ -499,6 +506,7 @@ function toObservationFromWorkflowEvent(event) {
|
|
|
499
506
|
input: event.input,
|
|
500
507
|
output: event.output,
|
|
501
508
|
startTime: normalizeStartTime(event.timestamp),
|
|
509
|
+
durationMs: event.durationMs,
|
|
502
510
|
workflowEventId: event.id,
|
|
503
511
|
...agentFields,
|
|
504
512
|
};
|
|
@@ -510,6 +518,7 @@ function toObservationFromWorkflowEvent(event) {
|
|
|
510
518
|
input: event.input,
|
|
511
519
|
output: event.output,
|
|
512
520
|
startTime: normalizeStartTime(event.timestamp),
|
|
521
|
+
durationMs: event.durationMs,
|
|
513
522
|
workflowEventId: event.id,
|
|
514
523
|
...agentFields,
|
|
515
524
|
};
|
|
@@ -520,6 +529,7 @@ function toObservationFromWorkflowEvent(event) {
|
|
|
520
529
|
input: event.input,
|
|
521
530
|
output: event.output,
|
|
522
531
|
startTime: normalizeStartTime(event.timestamp),
|
|
532
|
+
durationMs: event.durationMs,
|
|
523
533
|
workflowEventId: event.id,
|
|
524
534
|
...agentFields,
|
|
525
535
|
};
|
|
@@ -571,6 +581,22 @@ function buildValidationObservations(workflowName, workflowInput, workflowOutput
|
|
|
571
581
|
}
|
|
572
582
|
}
|
|
573
583
|
}
|
|
584
|
+
// Compute total duration and aggregate token usage for the container observation
|
|
585
|
+
if (workflowTrace && workflowTrace.events.length > 0) {
|
|
586
|
+
const endTime = workflowTrace.events.reduce((max, e) => Math.max(max, e.timestamp + e.durationMs), workflowStartTime);
|
|
587
|
+
observations[0].durationMs = endTime - workflowStartTime;
|
|
588
|
+
let inputTokens = 0, outputTokens = 0, totalTokens = 0;
|
|
589
|
+
for (const e of workflowTrace.events) {
|
|
590
|
+
if (e.type === 'ai' && e.usage) {
|
|
591
|
+
inputTokens += e.usage.inputTokens ?? 0;
|
|
592
|
+
outputTokens += e.usage.outputTokens ?? 0;
|
|
593
|
+
totalTokens += e.usage.totalTokens ?? 0;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (totalTokens > 0) {
|
|
597
|
+
observations[0].usage = { inputTokens, outputTokens, totalTokens };
|
|
598
|
+
}
|
|
599
|
+
}
|
|
574
600
|
// Sort all observations except the workflow entry (index 0) by startTime
|
|
575
601
|
const [workflowEntry, ...rest] = observations;
|
|
576
602
|
rest.sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
|
|
@@ -1067,7 +1093,7 @@ function getDashboardHtml() {
|
|
|
1067
1093
|
<div class="trace-section-title">Observations</div>
|
|
1068
1094
|
<div class="observation-table-wrap">
|
|
1069
1095
|
<table class="observation-table">
|
|
1070
|
-
<thead id="observationTableHead"><tr><th style="width: 40px;">Check</th><th>Name</th><th>Type</th></tr></thead>
|
|
1096
|
+
<thead id="observationTableHead"><tr><th style="width: 40px;">Check</th><th>Name</th><th>Type</th><th style="width:80px;">Duration</th></tr></thead>
|
|
1071
1097
|
<tbody id="observationTableBody"></tbody>
|
|
1072
1098
|
</table>
|
|
1073
1099
|
</div>
|
|
@@ -1098,6 +1124,47 @@ function getDashboardHtml() {
|
|
|
1098
1124
|
} catch {}
|
|
1099
1125
|
let step5RunMeta = { loading: false, error: '', runCount: 0, sequential: false };
|
|
1100
1126
|
|
|
1127
|
+
function computeDurationMs(obs) {
|
|
1128
|
+
if (obs.durationMs != null) return obs.durationMs;
|
|
1129
|
+
if (obs.latency != null && obs.latency > 0) return Math.round(obs.latency * 1000);
|
|
1130
|
+
if (obs.startTime && obs.endTime) {
|
|
1131
|
+
const diff = new Date(obs.endTime).getTime() - new Date(obs.startTime).getTime();
|
|
1132
|
+
if (Number.isFinite(diff) && diff >= 0) return diff;
|
|
1133
|
+
}
|
|
1134
|
+
if (obs.latency != null) return 0;
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function formatDuration(ms) {
|
|
1139
|
+
if (ms == null) return '—';
|
|
1140
|
+
if (ms < 1000) return ms + ' ms';
|
|
1141
|
+
return (ms / 1000).toFixed(2) + ' s';
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function extractUsage(obs) {
|
|
1145
|
+
if (obs.usage && (obs.usage.inputTokens != null || obs.usage.outputTokens != null)) {
|
|
1146
|
+
return obs.usage;
|
|
1147
|
+
}
|
|
1148
|
+
if (obs.usageDetails && (obs.usageDetails.input != null || obs.usageDetails.output != null)) {
|
|
1149
|
+
return { inputTokens: obs.usageDetails.input, outputTokens: obs.usageDetails.output, totalTokens: obs.usageDetails.total };
|
|
1150
|
+
}
|
|
1151
|
+
if (obs.inputUsage != null || obs.outputUsage != null) {
|
|
1152
|
+
return { inputTokens: obs.inputUsage, outputTokens: obs.outputUsage, totalTokens: obs.totalUsage };
|
|
1153
|
+
}
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function renderUsage(obs) {
|
|
1158
|
+
const u = extractUsage(obs);
|
|
1159
|
+
if (!u || !(u.inputTokens > 0 || u.outputTokens > 0 || u.totalTokens > 0)) return '';
|
|
1160
|
+
const lines = [];
|
|
1161
|
+
if (u.inputTokens != null) lines.push('Input tokens: ' + u.inputTokens);
|
|
1162
|
+
if (u.outputTokens != null) lines.push('Output tokens: ' + u.outputTokens);
|
|
1163
|
+
if (u.totalTokens != null) lines.push('Total tokens: ' + u.totalTokens);
|
|
1164
|
+
if (!lines.length) return '';
|
|
1165
|
+
return \`<div class="detail-section"><div class="detail-title">Usage</div><pre class="detail-pre">\${lines.join('\\n')}</pre></div>\`;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1101
1168
|
function persistTraces() {
|
|
1102
1169
|
try {
|
|
1103
1170
|
// Store compact version — strip bulky workflowTrace.events (snapshot is on server)
|
|
@@ -1202,8 +1269,12 @@ function getDashboardHtml() {
|
|
|
1202
1269
|
// Extract unique tool names and their call details from the uploaded trace observations
|
|
1203
1270
|
const toolCalls = {};
|
|
1204
1271
|
currentObservations.forEach(function(obs, i) {
|
|
1205
|
-
|
|
1206
|
-
const
|
|
1272
|
+
const isToolByType = obs.type === 'TOOL';
|
|
1273
|
+
const isToolByName = typeof obs.name === 'string' && obs.name.startsWith('tool-');
|
|
1274
|
+
if (!isToolByType && !isToolByName) return;
|
|
1275
|
+
const name = isToolByName && obs.type !== 'TOOL'
|
|
1276
|
+
? obs.name.slice(5)
|
|
1277
|
+
: (obs.name || '(unknown)');
|
|
1207
1278
|
if (!toolCalls[name]) toolCalls[name] = [];
|
|
1208
1279
|
toolCalls[name].push({ index: toolCalls[name].length + 1, obsIndex: i, input: obs.input, output: obs.output });
|
|
1209
1280
|
});
|
|
@@ -1430,8 +1501,9 @@ function getDashboardHtml() {
|
|
|
1430
1501
|
const count = parseInt(document.getElementById('liveValidationCount').value, 10);
|
|
1431
1502
|
const sequential = document.getElementById('liveValidationSequential').checked;
|
|
1432
1503
|
if (count >= 1) {
|
|
1433
|
-
// Build the tool mock config from UI state
|
|
1504
|
+
// Build the tool mock config from UI state and persist for "Run from here"
|
|
1434
1505
|
const toolMockConfig = buildToolMockConfigFromUI();
|
|
1506
|
+
window._toolMockConfig = toolMockConfig;
|
|
1435
1507
|
const submitBtn = document.getElementById('submitLiveValidation');
|
|
1436
1508
|
submitBtn.disabled = true;
|
|
1437
1509
|
submitBtn.textContent = 'Validating...';
|
|
@@ -1642,6 +1714,27 @@ function getDashboardHtml() {
|
|
|
1642
1714
|
const timeB = new Date(b.startTime || 0).getTime();
|
|
1643
1715
|
return timeA - timeB;
|
|
1644
1716
|
});
|
|
1717
|
+
// Aggregate token usage onto the workflow container SPAN if it has none
|
|
1718
|
+
if (selectedWorkflow?.name) {
|
|
1719
|
+
const containerIdx = obs.findIndex(
|
|
1720
|
+
o => o.type === 'SPAN' && o.name === selectedWorkflow.name
|
|
1721
|
+
);
|
|
1722
|
+
const _existingUsage = extractUsage(obs[containerIdx]);
|
|
1723
|
+
if (containerIdx >= 0 && (!_existingUsage || !(_existingUsage.totalTokens > 0))) {
|
|
1724
|
+
let inputTokens = 0, outputTokens = 0, totalTokens = 0;
|
|
1725
|
+
for (const o of obs) {
|
|
1726
|
+
if (o.type !== 'GENERATION') continue;
|
|
1727
|
+
const u = extractUsage(o);
|
|
1728
|
+
if (!u) continue;
|
|
1729
|
+
inputTokens += u.inputTokens ?? 0;
|
|
1730
|
+
outputTokens += u.outputTokens ?? 0;
|
|
1731
|
+
totalTokens += u.totalTokens ?? 0;
|
|
1732
|
+
}
|
|
1733
|
+
if (totalTokens > 0) {
|
|
1734
|
+
obs[containerIdx].usage = { inputTokens, outputTokens, totalTokens };
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1645
1738
|
currentObservations = obs;
|
|
1646
1739
|
selectedObservationIndex = -1;
|
|
1647
1740
|
checkedObservations.clear();
|
|
@@ -1708,7 +1801,7 @@ function getDashboardHtml() {
|
|
|
1708
1801
|
observationsTable += \`<div class="trace-section-title">Observations</div>
|
|
1709
1802
|
<div class="observation-table-wrap">
|
|
1710
1803
|
<table class="observation-table">
|
|
1711
|
-
<thead><tr><th>Name</th><th>Type</th></tr></thead>
|
|
1804
|
+
<thead><tr><th>Name</th><th>Type</th><th style="width:80px;">Duration</th></tr></thead>
|
|
1712
1805
|
<tbody>\`;
|
|
1713
1806
|
observationsTable += currentObservations.map((obs, j) => {
|
|
1714
1807
|
const isSelected = j === window.step5SelectedObservation;
|
|
@@ -1717,7 +1810,7 @@ function getDashboardHtml() {
|
|
|
1717
1810
|
const typeClass = type === "TOOL" ? "tool" : "ai";
|
|
1718
1811
|
const agentBadge = obs.agentTaskIndex != null ? \`<span class="agent-task-badge">T\${obs.agentTaskIndex + 1}</span>\` : '';
|
|
1719
1812
|
const rowClass = obs.agentTaskIndex != null ? 'agent-task-row' : '';
|
|
1720
|
-
return \`<tr class="\${isSelected ? "selected" : ""} \${rowClass}" onclick="window.step5SelectedObservation=\${j};renderObservationTable();"><td>\${esc(name)}\${agentBadge}</td><td><span class="obs-type \${typeClass}">\${esc(type)}</span></td></tr>\`;
|
|
1813
|
+
return \`<tr class="\${isSelected ? "selected" : ""} \${rowClass}" onclick="window.step5SelectedObservation=\${j};renderObservationTable();"><td>\${esc(name)}\${agentBadge}</td><td><span class="obs-type \${typeClass}">\${esc(type)}</span></td><td style="color:#888;font-size:12px;">\${formatDuration(computeDurationMs(obs))}</td></tr>\`;
|
|
1721
1814
|
}).join("");
|
|
1722
1815
|
observationsTable += \`</tbody></table></div>\`;
|
|
1723
1816
|
// Details for selected observation
|
|
@@ -1729,9 +1822,12 @@ function getDashboardHtml() {
|
|
|
1729
1822
|
const filePathHtml = selObs.type === "GENERATION"
|
|
1730
1823
|
? '<div class="file-path-placeholder"></div>'
|
|
1731
1824
|
: renderFilePath(getObsFilePath(selObs));
|
|
1825
|
+
const _dur5orig = computeDurationMs(selObs);
|
|
1732
1826
|
detailsSection = \`<div class="detail-sections" id="\${detailId}">
|
|
1733
1827
|
\${filePathHtml}
|
|
1734
1828
|
\${renderModel(selObs)}
|
|
1829
|
+
\${_dur5orig != null ? \`<div class="detail-section"><div class="detail-title">Duration</div><pre class="detail-pre">\${formatDuration(_dur5orig)}</pre></div>\` : ''}
|
|
1830
|
+
\${renderUsage(selObs)}
|
|
1735
1831
|
<div class="detail-section">
|
|
1736
1832
|
<div class="detail-title">Input</div>
|
|
1737
1833
|
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
@@ -1752,7 +1848,7 @@ function getDashboardHtml() {
|
|
|
1752
1848
|
observationsTable += \`<div class="trace-section-title">Observations</div>
|
|
1753
1849
|
<div class="observation-table-wrap">
|
|
1754
1850
|
<table class="observation-table">
|
|
1755
|
-
<thead><tr><th>Name</th><th>Type</th></tr></thead>
|
|
1851
|
+
<thead><tr><th>Name</th><th>Type</th><th style="width:80px;">Duration</th></tr></thead>
|
|
1756
1852
|
<tbody>\`;
|
|
1757
1853
|
observationsTable += actions.map((action, j) => {
|
|
1758
1854
|
const isSelected = j === window.step5SelectedObservation;
|
|
@@ -1766,7 +1862,7 @@ function getDashboardHtml() {
|
|
|
1766
1862
|
action.agentTaskIndex != null ? 'agent-task-row' : '',
|
|
1767
1863
|
action.isFrozen ? 'frozen-row' : '',
|
|
1768
1864
|
].filter(Boolean).join(' ');
|
|
1769
|
-
return \`<tr class="\${rowClasses}" onclick="window.step5SelectedObservation=\${j};renderObservationTable();"><td>\${esc(name)}\${agentBadge}\${frozenBadge}</td><td><span class="obs-type \${typeClass}">\${esc(type)}</span></td></tr>\`;
|
|
1865
|
+
return \`<tr class="\${rowClasses}" onclick="window.step5SelectedObservation=\${j};renderObservationTable();"><td>\${esc(name)}\${agentBadge}\${frozenBadge}</td><td><span class="obs-type \${typeClass}">\${esc(type)}</span></td><td style="color:#888;font-size:12px;">\${formatDuration(computeDurationMs(action))}</td></tr>\`;
|
|
1770
1866
|
}).join("");
|
|
1771
1867
|
observationsTable += \`</tbody></table></div>\`;
|
|
1772
1868
|
// Details for selected observation
|
|
@@ -1798,10 +1894,13 @@ function getDashboardHtml() {
|
|
|
1798
1894
|
} else if (obs.workflowEventId != null) {
|
|
1799
1895
|
runFromBpHtml = \`<div class="detail-section" style="padding:8px 12px;"><button class="run-from-bp-btn" onclick="runFromBreakpoint(\${traceIdx},\${window.step5SelectedObservation},event)">▶ Run from here</button></div>\`;
|
|
1800
1896
|
}
|
|
1897
|
+
const _dur5live = computeDurationMs(obs);
|
|
1801
1898
|
detailsSection = \`<div class="detail-sections" id="\${detailId}">
|
|
1802
1899
|
\${filePathHtml}
|
|
1803
1900
|
\${runFromBpHtml}
|
|
1804
1901
|
\${renderModel(obs)}
|
|
1902
|
+
\${_dur5live != null ? \`<div class="detail-section"><div class="detail-title">Duration</div><pre class="detail-pre">\${formatDuration(_dur5live)}</pre></div>\` : ''}
|
|
1903
|
+
\${renderUsage(obs)}
|
|
1805
1904
|
<div class="detail-section">
|
|
1806
1905
|
<div class="detail-title">Input</div>
|
|
1807
1906
|
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
@@ -1859,7 +1958,7 @@ function getDashboardHtml() {
|
|
|
1859
1958
|
let col1 = \`<div class="trace-section-title">Steps</div>
|
|
1860
1959
|
<div class="observation-table-wrap">
|
|
1861
1960
|
<table class="observation-table">
|
|
1862
|
-
<thead><tr><th>Name</th><th>Type</th></tr></thead>
|
|
1961
|
+
<thead><tr><th>Name</th><th>Type</th><th style="width:80px;">Duration</th></tr></thead>
|
|
1863
1962
|
<tbody>\`;
|
|
1864
1963
|
col1 += obsIndices.map(idx => {
|
|
1865
1964
|
const obs = currentObservations[idx];
|
|
@@ -1874,7 +1973,7 @@ function getDashboardHtml() {
|
|
|
1874
1973
|
: latest.ok ? ' <span class="rerun-status success">✓</span>'
|
|
1875
1974
|
: ' <span class="rerun-status error">✗</span>')
|
|
1876
1975
|
: '';
|
|
1877
|
-
return \`<tr class="\${isSelected ? 'selected' : ''}" onclick="window.step4SelectObservation(\${idx})"><td>\${esc(name)}\${badge}</td><td><span class="obs-type \${typeClass}">\${esc(type)}</span></td></tr>\`;
|
|
1976
|
+
return \`<tr class="\${isSelected ? 'selected' : ''}" onclick="window.step4SelectObservation(\${idx})"><td>\${esc(name)}\${badge}</td><td><span class="obs-type \${typeClass}">\${esc(type)}</span></td><td style="color:#888;font-size:12px;">\${formatDuration(computeDurationMs(obs))}</td></tr>\`;
|
|
1878
1977
|
}).join('');
|
|
1879
1978
|
col1 += \`</tbody></table></div>\`;
|
|
1880
1979
|
|
|
@@ -1944,9 +2043,24 @@ function getDashboardHtml() {
|
|
|
1944
2043
|
} else {
|
|
1945
2044
|
inputSection += \`<div class="detail-section"><div class="detail-title">Input</div><pre class="detail-pre">\${esc(toDisplayText(obs.input, obs.type))}</pre></div>\`;
|
|
1946
2045
|
}
|
|
2046
|
+
const _dur4 = computeDurationMs(obs);
|
|
2047
|
+
const currentDurSection = (run && !run.running && run.ok && run.currentDurationMs != null)
|
|
2048
|
+
? \`<div class="detail-section"><div class="detail-title">Current Duration</div><pre class="detail-pre">\${formatDuration(run.currentDurationMs)}</pre></div>\`
|
|
2049
|
+
: '';
|
|
2050
|
+
const currentUsageSection = (run && !run.running && run.ok && run.currentUsage && run.currentUsage.totalTokens > 0)
|
|
2051
|
+
? \`<div class="detail-section"><div class="detail-title">Current Usage</div><pre class="detail-pre">\${[
|
|
2052
|
+
run.currentUsage.inputTokens != null ? 'Input tokens: ' + run.currentUsage.inputTokens : null,
|
|
2053
|
+
run.currentUsage.outputTokens != null ? 'Output tokens: ' + run.currentUsage.outputTokens : null,
|
|
2054
|
+
run.currentUsage.totalTokens != null ? 'Total tokens: ' + run.currentUsage.totalTokens : null,
|
|
2055
|
+
].filter(Boolean).join('\\n')}</pre></div>\`
|
|
2056
|
+
: '';
|
|
1947
2057
|
col3 = \`<div class="detail-sections" id="\${detailId}">
|
|
1948
2058
|
\${filePathHtml}
|
|
1949
2059
|
\${renderModel(obs)}
|
|
2060
|
+
\${_dur4 != null ? \`<div class="detail-section"><div class="detail-title">Duration</div><pre class="detail-pre">\${formatDuration(_dur4)}</pre></div>\` : ''}
|
|
2061
|
+
\${currentDurSection}
|
|
2062
|
+
\${renderUsage(obs)}
|
|
2063
|
+
\${currentUsageSection}
|
|
1950
2064
|
\${inputSection}
|
|
1951
2065
|
<div class="detail-section"><div class="detail-title">Output</div><pre class="detail-pre">\${esc(outputText)}</pre></div>
|
|
1952
2066
|
\${currentOutputSection}
|
|
@@ -2012,6 +2126,7 @@ function getDashboardHtml() {
|
|
|
2012
2126
|
<td style="width: 40px;"><input type="checkbox" class="obs-checkbox" value="\${actualIndex}" \${isChecked ? "checked" : ""}></td>
|
|
2013
2127
|
<td onclick="selectObservation(\${actualIndex})">\${esc(name)}</td>
|
|
2014
2128
|
<td><span class="obs-type \${typeClass}">\${esc(type)}</span></td>
|
|
2129
|
+
<td style="color:#888;font-size:12px;">\${formatDuration(computeDurationMs(obs))}</td>
|
|
2015
2130
|
</tr>\`;
|
|
2016
2131
|
}).join("");
|
|
2017
2132
|
|
|
@@ -2060,9 +2175,12 @@ function getDashboardHtml() {
|
|
|
2060
2175
|
? '<div class="file-path-placeholder"></div>'
|
|
2061
2176
|
: renderFilePath(getObsFilePath(obs));
|
|
2062
2177
|
|
|
2178
|
+
const _dur3 = computeDurationMs(obs);
|
|
2063
2179
|
observationDetail.innerHTML = \`<div class="detail-sections" id="\${detailId}">
|
|
2064
2180
|
\${filePathHtml}
|
|
2065
2181
|
\${renderModel(obs)}
|
|
2182
|
+
\${_dur3 != null ? \`<div class="detail-section"><div class="detail-title">Duration</div><pre class="detail-pre">\${formatDuration(_dur3)}</pre></div>\` : ''}
|
|
2183
|
+
\${renderUsage(obs)}
|
|
2066
2184
|
<div class="detail-section">
|
|
2067
2185
|
<div class="detail-title">Input</div>
|
|
2068
2186
|
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
@@ -2104,6 +2222,8 @@ function getDashboardHtml() {
|
|
|
2104
2222
|
if (response.ok && data.ok) {
|
|
2105
2223
|
newRun.ok = true;
|
|
2106
2224
|
newRun.output = data.currentOutput;
|
|
2225
|
+
newRun.currentDurationMs = data.currentDurationMs ?? null;
|
|
2226
|
+
newRun.currentUsage = data.currentUsage ?? null;
|
|
2107
2227
|
} else {
|
|
2108
2228
|
newRun.ok = false;
|
|
2109
2229
|
newRun.error = data.error || 'Rerun failed.';
|
|
@@ -2134,6 +2254,7 @@ function getDashboardHtml() {
|
|
|
2134
2254
|
checkpoint: obs.workflowEventId,
|
|
2135
2255
|
snapshotId: liveTrace.snapshotId,
|
|
2136
2256
|
observations: currentObservations,
|
|
2257
|
+
toolMockConfig: window._toolMockConfig || {},
|
|
2137
2258
|
};
|
|
2138
2259
|
const response = await fetch('/api/run-from-breakpoint', {
|
|
2139
2260
|
method: 'POST',
|
|
@@ -2188,6 +2309,7 @@ function getDashboardHtml() {
|
|
|
2188
2309
|
resumeFromTaskIndex: taskIndex,
|
|
2189
2310
|
},
|
|
2190
2311
|
snapshotId: liveTrace.snapshotId,
|
|
2312
|
+
toolMockConfig: window._toolMockConfig || {},
|
|
2191
2313
|
};
|
|
2192
2314
|
const response = await fetch('/api/resume-agent-from-task', {
|
|
2193
2315
|
method: 'POST',
|
|
@@ -2399,16 +2521,46 @@ function getDashboardHtml() {
|
|
|
2399
2521
|
}
|
|
2400
2522
|
}
|
|
2401
2523
|
|
|
2402
|
-
//
|
|
2403
|
-
if (type === "GENERATION" &&
|
|
2404
|
-
return
|
|
2524
|
+
// GENERATION input: raw messages array
|
|
2525
|
+
if (type === "GENERATION" && Array.isArray(value)) {
|
|
2526
|
+
return JSON.stringify(value, null, 2);
|
|
2405
2527
|
}
|
|
2406
|
-
|
|
2407
|
-
//
|
|
2528
|
+
|
|
2529
|
+
// GENERATION input wrapped in {messages:[...]} (legacy / Langfuse format)
|
|
2408
2530
|
if (type === "GENERATION" && value.messages) {
|
|
2409
2531
|
return JSON.stringify(value.messages, null, 2);
|
|
2410
2532
|
}
|
|
2411
|
-
|
|
2533
|
+
|
|
2534
|
+
// GENERATION output: assistant message object
|
|
2535
|
+
if (type === "GENERATION" && value.role) {
|
|
2536
|
+
const parts = [];
|
|
2537
|
+
// Text content
|
|
2538
|
+
if (value.content && typeof value.content === 'string') {
|
|
2539
|
+
parts.push(stripMarkdownCodeFence(value.content));
|
|
2540
|
+
} else if (Array.isArray(value.content)) {
|
|
2541
|
+
// Anthropic / Gemini content block array
|
|
2542
|
+
const text = value.content
|
|
2543
|
+
.filter(b => b && (b.type === 'text' || b.text))
|
|
2544
|
+
.map(b => b.text || b.value || '')
|
|
2545
|
+
.join('');
|
|
2546
|
+
if (text) parts.push(stripMarkdownCodeFence(text));
|
|
2547
|
+
// Anthropic tool_use blocks
|
|
2548
|
+
const toolUses = value.content.filter(b => b && b.type === 'tool_use');
|
|
2549
|
+
if (toolUses.length) parts.push('Tool calls:\\n' + JSON.stringify(toolUses, null, 2));
|
|
2550
|
+
} else if (value.parts && Array.isArray(value.parts)) {
|
|
2551
|
+
// Gemini parts array
|
|
2552
|
+
const text = value.parts.filter(p => p.text).map(p => p.text).join('');
|
|
2553
|
+
if (text) parts.push(stripMarkdownCodeFence(text));
|
|
2554
|
+
const fnCalls = value.parts.filter(p => p.functionCall);
|
|
2555
|
+
if (fnCalls.length) parts.push('Function calls:\\n' + JSON.stringify(fnCalls, null, 2));
|
|
2556
|
+
}
|
|
2557
|
+
// OpenAI tool_calls array
|
|
2558
|
+
if (Array.isArray(value.tool_calls) && value.tool_calls.length) {
|
|
2559
|
+
parts.push('Tool calls:\\n' + JSON.stringify(value.tool_calls, null, 2));
|
|
2560
|
+
}
|
|
2561
|
+
return parts.length ? parts.join('\\n\\n') : 'No content';
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2412
2564
|
// For other objects, stringify them
|
|
2413
2565
|
try {
|
|
2414
2566
|
return JSON.stringify(value, null, 2);
|
|
@@ -2726,12 +2878,15 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
2726
2878
|
const workflowArgs = resolvedInput.args ?? [];
|
|
2727
2879
|
const workflowInput = resolvedInput.input ?? null;
|
|
2728
2880
|
const toolsModulePath = resolveRuntimeModule(cwd, 'ed_tools') ?? null;
|
|
2881
|
+
const toolMockConfig = body.toolMockConfig && typeof body.toolMockConfig === 'object' && !Array.isArray(body.toolMockConfig)
|
|
2882
|
+
? body.toolMockConfig
|
|
2883
|
+
: undefined;
|
|
2729
2884
|
const frozenEventIds = new Set(history
|
|
2730
2885
|
.filter((event) => (event.id <= checkpoint
|
|
2731
2886
|
&& (event.type === 'ai' || event.type === 'tool' || event.type === 'http' || event.type === 'db')))
|
|
2732
2887
|
.map((event) => event.id));
|
|
2733
2888
|
console.log(`[elasticdash] Run from breakpoint: workflow="${workflowName}" checkpoint=${checkpoint} historyLen=${history.length}`);
|
|
2734
|
-
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, { replayMode: true, checkpoint, history });
|
|
2889
|
+
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, { replayMode: true, checkpoint, history, ...(toolMockConfig ? { toolMockConfig } : {}) });
|
|
2735
2890
|
const traceStub = {
|
|
2736
2891
|
getSteps: () => (result.steps ?? []),
|
|
2737
2892
|
getLLMSteps: () => (result.llmSteps ?? []),
|
|
@@ -2799,8 +2954,11 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
2799
2954
|
return;
|
|
2800
2955
|
}
|
|
2801
2956
|
const toolsModulePath = resolveRuntimeModule(cwd, 'ed_tools') ?? null;
|
|
2957
|
+
const toolMockConfig = body.toolMockConfig && typeof body.toolMockConfig === 'object' && !Array.isArray(body.toolMockConfig)
|
|
2958
|
+
? body.toolMockConfig
|
|
2959
|
+
: undefined;
|
|
2802
2960
|
console.log(`[elasticdash] Resume agent from task: workflow="${workflowName}" taskIndex=${taskIndex}`);
|
|
2803
|
-
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, [], null, { replayMode: history.length > 0, checkpoint: 0, history, agentState });
|
|
2961
|
+
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, [], null, { replayMode: history.length > 0, checkpoint: 0, history, agentState, ...(toolMockConfig ? { toolMockConfig } : {}) });
|
|
2804
2962
|
const traceStub = {
|
|
2805
2963
|
getSteps: () => (result.steps ?? []),
|
|
2806
2964
|
getLLMSteps: () => (result.llmSteps ?? []),
|