elasticdash-test 0.1.12-hotfix → 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 +228 -79
- 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
|
});
|
|
@@ -1643,6 +1714,27 @@ function getDashboardHtml() {
|
|
|
1643
1714
|
const timeB = new Date(b.startTime || 0).getTime();
|
|
1644
1715
|
return timeA - timeB;
|
|
1645
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
|
+
}
|
|
1646
1738
|
currentObservations = obs;
|
|
1647
1739
|
selectedObservationIndex = -1;
|
|
1648
1740
|
checkedObservations.clear();
|
|
@@ -1709,7 +1801,7 @@ function getDashboardHtml() {
|
|
|
1709
1801
|
observationsTable += \`<div class="trace-section-title">Observations</div>
|
|
1710
1802
|
<div class="observation-table-wrap">
|
|
1711
1803
|
<table class="observation-table">
|
|
1712
|
-
<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>
|
|
1713
1805
|
<tbody>\`;
|
|
1714
1806
|
observationsTable += currentObservations.map((obs, j) => {
|
|
1715
1807
|
const isSelected = j === window.step5SelectedObservation;
|
|
@@ -1718,7 +1810,7 @@ function getDashboardHtml() {
|
|
|
1718
1810
|
const typeClass = type === "TOOL" ? "tool" : "ai";
|
|
1719
1811
|
const agentBadge = obs.agentTaskIndex != null ? \`<span class="agent-task-badge">T\${obs.agentTaskIndex + 1}</span>\` : '';
|
|
1720
1812
|
const rowClass = obs.agentTaskIndex != null ? 'agent-task-row' : '';
|
|
1721
|
-
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>\`;
|
|
1722
1814
|
}).join("");
|
|
1723
1815
|
observationsTable += \`</tbody></table></div>\`;
|
|
1724
1816
|
// Details for selected observation
|
|
@@ -1730,9 +1822,12 @@ function getDashboardHtml() {
|
|
|
1730
1822
|
const filePathHtml = selObs.type === "GENERATION"
|
|
1731
1823
|
? '<div class="file-path-placeholder"></div>'
|
|
1732
1824
|
: renderFilePath(getObsFilePath(selObs));
|
|
1825
|
+
const _dur5orig = computeDurationMs(selObs);
|
|
1733
1826
|
detailsSection = \`<div class="detail-sections" id="\${detailId}">
|
|
1734
1827
|
\${filePathHtml}
|
|
1735
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)}
|
|
1736
1831
|
<div class="detail-section">
|
|
1737
1832
|
<div class="detail-title">Input</div>
|
|
1738
1833
|
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
@@ -1753,7 +1848,7 @@ function getDashboardHtml() {
|
|
|
1753
1848
|
observationsTable += \`<div class="trace-section-title">Observations</div>
|
|
1754
1849
|
<div class="observation-table-wrap">
|
|
1755
1850
|
<table class="observation-table">
|
|
1756
|
-
<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>
|
|
1757
1852
|
<tbody>\`;
|
|
1758
1853
|
observationsTable += actions.map((action, j) => {
|
|
1759
1854
|
const isSelected = j === window.step5SelectedObservation;
|
|
@@ -1767,7 +1862,7 @@ function getDashboardHtml() {
|
|
|
1767
1862
|
action.agentTaskIndex != null ? 'agent-task-row' : '',
|
|
1768
1863
|
action.isFrozen ? 'frozen-row' : '',
|
|
1769
1864
|
].filter(Boolean).join(' ');
|
|
1770
|
-
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>\`;
|
|
1771
1866
|
}).join("");
|
|
1772
1867
|
observationsTable += \`</tbody></table></div>\`;
|
|
1773
1868
|
// Details for selected observation
|
|
@@ -1799,10 +1894,13 @@ function getDashboardHtml() {
|
|
|
1799
1894
|
} else if (obs.workflowEventId != null) {
|
|
1800
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>\`;
|
|
1801
1896
|
}
|
|
1897
|
+
const _dur5live = computeDurationMs(obs);
|
|
1802
1898
|
detailsSection = \`<div class="detail-sections" id="\${detailId}">
|
|
1803
1899
|
\${filePathHtml}
|
|
1804
1900
|
\${runFromBpHtml}
|
|
1805
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)}
|
|
1806
1904
|
<div class="detail-section">
|
|
1807
1905
|
<div class="detail-title">Input</div>
|
|
1808
1906
|
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
@@ -1860,7 +1958,7 @@ function getDashboardHtml() {
|
|
|
1860
1958
|
let col1 = \`<div class="trace-section-title">Steps</div>
|
|
1861
1959
|
<div class="observation-table-wrap">
|
|
1862
1960
|
<table class="observation-table">
|
|
1863
|
-
<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>
|
|
1864
1962
|
<tbody>\`;
|
|
1865
1963
|
col1 += obsIndices.map(idx => {
|
|
1866
1964
|
const obs = currentObservations[idx];
|
|
@@ -1875,7 +1973,7 @@ function getDashboardHtml() {
|
|
|
1875
1973
|
: latest.ok ? ' <span class="rerun-status success">✓</span>'
|
|
1876
1974
|
: ' <span class="rerun-status error">✗</span>')
|
|
1877
1975
|
: '';
|
|
1878
|
-
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>\`;
|
|
1879
1977
|
}).join('');
|
|
1880
1978
|
col1 += \`</tbody></table></div>\`;
|
|
1881
1979
|
|
|
@@ -1945,9 +2043,24 @@ function getDashboardHtml() {
|
|
|
1945
2043
|
} else {
|
|
1946
2044
|
inputSection += \`<div class="detail-section"><div class="detail-title">Input</div><pre class="detail-pre">\${esc(toDisplayText(obs.input, obs.type))}</pre></div>\`;
|
|
1947
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
|
+
: '';
|
|
1948
2057
|
col3 = \`<div class="detail-sections" id="\${detailId}">
|
|
1949
2058
|
\${filePathHtml}
|
|
1950
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}
|
|
1951
2064
|
\${inputSection}
|
|
1952
2065
|
<div class="detail-section"><div class="detail-title">Output</div><pre class="detail-pre">\${esc(outputText)}</pre></div>
|
|
1953
2066
|
\${currentOutputSection}
|
|
@@ -2013,6 +2126,7 @@ function getDashboardHtml() {
|
|
|
2013
2126
|
<td style="width: 40px;"><input type="checkbox" class="obs-checkbox" value="\${actualIndex}" \${isChecked ? "checked" : ""}></td>
|
|
2014
2127
|
<td onclick="selectObservation(\${actualIndex})">\${esc(name)}</td>
|
|
2015
2128
|
<td><span class="obs-type \${typeClass}">\${esc(type)}</span></td>
|
|
2129
|
+
<td style="color:#888;font-size:12px;">\${formatDuration(computeDurationMs(obs))}</td>
|
|
2016
2130
|
</tr>\`;
|
|
2017
2131
|
}).join("");
|
|
2018
2132
|
|
|
@@ -2061,9 +2175,12 @@ function getDashboardHtml() {
|
|
|
2061
2175
|
? '<div class="file-path-placeholder"></div>'
|
|
2062
2176
|
: renderFilePath(getObsFilePath(obs));
|
|
2063
2177
|
|
|
2178
|
+
const _dur3 = computeDurationMs(obs);
|
|
2064
2179
|
observationDetail.innerHTML = \`<div class="detail-sections" id="\${detailId}">
|
|
2065
2180
|
\${filePathHtml}
|
|
2066
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)}
|
|
2067
2184
|
<div class="detail-section">
|
|
2068
2185
|
<div class="detail-title">Input</div>
|
|
2069
2186
|
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
@@ -2105,6 +2222,8 @@ function getDashboardHtml() {
|
|
|
2105
2222
|
if (response.ok && data.ok) {
|
|
2106
2223
|
newRun.ok = true;
|
|
2107
2224
|
newRun.output = data.currentOutput;
|
|
2225
|
+
newRun.currentDurationMs = data.currentDurationMs ?? null;
|
|
2226
|
+
newRun.currentUsage = data.currentUsage ?? null;
|
|
2108
2227
|
} else {
|
|
2109
2228
|
newRun.ok = false;
|
|
2110
2229
|
newRun.error = data.error || 'Rerun failed.';
|
|
@@ -2402,16 +2521,46 @@ function getDashboardHtml() {
|
|
|
2402
2521
|
}
|
|
2403
2522
|
}
|
|
2404
2523
|
|
|
2405
|
-
//
|
|
2406
|
-
if (type === "GENERATION" &&
|
|
2407
|
-
return
|
|
2524
|
+
// GENERATION input: raw messages array
|
|
2525
|
+
if (type === "GENERATION" && Array.isArray(value)) {
|
|
2526
|
+
return JSON.stringify(value, null, 2);
|
|
2408
2527
|
}
|
|
2409
|
-
|
|
2410
|
-
//
|
|
2528
|
+
|
|
2529
|
+
// GENERATION input wrapped in {messages:[...]} (legacy / Langfuse format)
|
|
2411
2530
|
if (type === "GENERATION" && value.messages) {
|
|
2412
2531
|
return JSON.stringify(value.messages, null, 2);
|
|
2413
2532
|
}
|
|
2414
|
-
|
|
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
|
+
|
|
2415
2564
|
// For other objects, stringify them
|
|
2416
2565
|
try {
|
|
2417
2566
|
return JSON.stringify(value, null, 2);
|