elasticdash-test 0.1.13-alpha → 0.1.13-alpha-2
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 +16 -9
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/dashboard-server.d.ts +8 -0
- package/dist/dashboard-server.d.ts.map +1 -1
- package/dist/dashboard-server.js +229 -1758
- package/dist/dashboard-server.js.map +1 -1
- package/dist/http.d.ts +9 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +9 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/telemetry-push.d.ts +21 -0
- package/dist/interceptors/telemetry-push.d.ts.map +1 -0
- package/dist/interceptors/telemetry-push.js +56 -0
- package/dist/interceptors/telemetry-push.js.map +1 -0
- package/dist/interceptors/tool.d.ts.map +1 -1
- package/dist/interceptors/tool.js +71 -22
- package/dist/interceptors/tool.js.map +1 -1
- package/dist/interceptors/workflow-ai.d.ts.map +1 -1
- package/dist/interceptors/workflow-ai.js +99 -17
- package/dist/interceptors/workflow-ai.js.map +1 -1
- package/package.json +15 -2
- package/src/browser-ui.ts +281 -0
- package/src/capture/event.ts +26 -0
- package/src/capture/index.ts +3 -0
- package/src/capture/recorder.ts +62 -0
- package/src/capture/replay.ts +55 -0
- package/src/cli.ts +315 -0
- package/src/core/agent-state.ts +162 -0
- package/src/core/registry.ts +92 -0
- package/src/dashboard-server.ts +1862 -0
- package/src/html/dashboard.html +1782 -0
- package/src/http.ts +8 -0
- package/src/index.ts +32 -0
- package/src/interceptors/ai-interceptor.ts +542 -0
- package/src/interceptors/db-auto.ts +147 -0
- package/src/interceptors/db.ts +81 -0
- package/src/interceptors/http.ts +367 -0
- package/src/interceptors/side-effects.ts +83 -0
- package/src/interceptors/telemetry-push.ts +67 -0
- package/src/interceptors/tool.ts +231 -0
- package/src/interceptors/workflow-ai.ts +117 -0
- package/src/internals/conditional-recorder.ts +63 -0
- package/src/internals/mock-resolver.ts +92 -0
- package/src/matchers/index.ts +814 -0
- package/src/proxy/llm-capture.ts +301 -0
- package/src/reporter.ts +81 -0
- package/src/runWorkflowSubprocess.ts +74 -0
- package/src/runner.ts +178 -0
- package/src/test-setup.ts +16 -0
- package/src/tool-runner-worker.ts +64 -0
- package/src/trace-adapter/context.ts +156 -0
- package/src/tracing.ts +62 -0
- package/src/types/agent.d.ts +63 -0
- package/src/types/expect.d.ts +81 -0
- package/src/types/modules.d.ts +2 -0
- package/src/workflow-runner-worker.ts +357 -0
- package/src/workflow-runner.ts +55 -0
- package/dist/internals/async-storage.d.ts +0 -21
- package/dist/internals/async-storage.d.ts.map +0 -1
- package/dist/internals/async-storage.js +0 -46
- package/dist/internals/async-storage.js.map +0 -1
- package/dist/internals/node-crypto.d.ts +0 -12
- package/dist/internals/node-crypto.d.ts.map +0 -1
- package/dist/internals/node-crypto.js +0 -38
- package/dist/internals/node-crypto.js.map +0 -1
package/dist/dashboard-server.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { pathToFileURL } from 'url';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
6
7
|
import { callProviderLLM } from './matchers/index.js';
|
|
7
8
|
import chokidar from 'chokidar';
|
|
8
9
|
import express from 'express';
|
|
@@ -951,1752 +952,7 @@ function openBrowser(url) {
|
|
|
951
952
|
*/
|
|
952
953
|
function getDashboardHtml() {
|
|
953
954
|
/* DASHBOARD_HTML_START */
|
|
954
|
-
return
|
|
955
|
-
<html lang="en">
|
|
956
|
-
<head>
|
|
957
|
-
<meta charset="UTF-8">
|
|
958
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
959
|
-
<title>ElasticDash Dashboard</title>
|
|
960
|
-
<style>
|
|
961
|
-
/* Ensure first cell in observation-table never overflows parent */
|
|
962
|
-
.observation-table td:first-child {
|
|
963
|
-
max-width: 120px;
|
|
964
|
-
overflow: auto;
|
|
965
|
-
white-space: nowrap;
|
|
966
|
-
}
|
|
967
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
968
|
-
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; }
|
|
969
|
-
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
970
|
-
header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
971
|
-
h1 { font-size: 28px; margin-bottom: 8px; color: #1a1a1a; }
|
|
972
|
-
.subtitle { font-size: 14px; color: #666; margin-bottom: 16px; }
|
|
973
|
-
.search-box { display: flex; gap: 10px; }
|
|
974
|
-
input[type="text"] { flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
|
975
|
-
input[type="text"]:focus { outline: none; border-color: #0066cc; box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1); }
|
|
976
|
-
.result-count { padding: 10px 12px; background: #f0f0f0; border-radius: 6px; font-size: 14px; color: #666; }
|
|
977
|
-
.workflows-list { background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; max-height: 65vh; display: flex; flex-direction: column; }
|
|
978
|
-
.workflows-table { width: 100%; border-collapse: collapse; }
|
|
979
|
-
.workflows-table thead { background: #f5f5f5; position: sticky; top: 0; z-index: 10; }
|
|
980
|
-
.workflows-table th { padding: 12px 16px; text-align: left; font-weight: 600; font-size: 13px; color: #333; border-bottom: 2px solid #ddd; }
|
|
981
|
-
.workflows-table td { padding: 12px 16px; border-bottom: 1px solid #eee; }
|
|
982
|
-
.workflows-table tbody tr { cursor: pointer; transition: background-color 0.2s; }
|
|
983
|
-
.workflows-table tbody tr:hover { background-color: #f9f9f9; }
|
|
984
|
-
.workflow-name-cell { font-family: Monaco, monospace; font-weight: 600; color: #0066cc; }
|
|
985
|
-
.workflow-path-cell { font-family: Monaco, monospace; font-size: 12px; color: #666; }
|
|
986
|
-
.async-badge { display: inline-block; background: #e8f3ff; color: #0066cc; padding: 2px 8px; border-radius: 4px; font-size: 11px; margin-left: 8px; }
|
|
987
|
-
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; align-items: center; justify-content: center; }
|
|
988
|
-
.modal.open { display: flex; }
|
|
989
|
-
.modal-content { background: white; border-radius: 12px; width: 92%; max-width: 1100px; max-height: 90vh; overflow-y: auto; padding: 30px; }
|
|
990
|
-
.modal-header { display: flex; justify-content: space-between; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; }
|
|
991
|
-
.modal-title { font-size: 20px; font-weight: 600; }
|
|
992
|
-
.modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #999; }
|
|
993
|
-
.upload-area { border: 2px dashed #ddd; border-radius: 8px; padding: 30px; text-align: center; cursor: pointer; background: #fafafa; overflow: hidden; height: 520px; position: relative; }
|
|
994
|
-
.upload-area:hover { border-color: #0066cc; background: #f0f7ff; }
|
|
995
|
-
.upload-area > div {
|
|
996
|
-
position: absolute;
|
|
997
|
-
top: 50%;
|
|
998
|
-
left: 50%;
|
|
999
|
-
transform: translate(-50%, -50%);
|
|
1000
|
-
}
|
|
1001
|
-
.upload-icon { font-size: 32px; margin-bottom: 12px; }
|
|
1002
|
-
input[type="file"] { display: none; }
|
|
1003
|
-
.upload-status { margin-top: 20px; padding: 12px; border-radius: 6px; display: none; }
|
|
1004
|
-
.upload-status.success { display: block; background: #e8f5e9; color: #2e7d32; }
|
|
1005
|
-
.upload-status.error { display: block; background: #ffebee; color: #c62828; }
|
|
1006
|
-
.hidden { display: none !important; }
|
|
1007
|
-
.trace-viewer { display: none; margin-top: 20px; }
|
|
1008
|
-
.trace-viewer.visible { display: block; }
|
|
1009
|
-
.trace-layout { display: grid; grid-template-columns: 40% calc(60% - 16px); gap: 16px; overflow: auto; height: 520px; }
|
|
1010
|
-
.trace-layout.step-5 { display: grid; grid-template-columns: calc(30% - 16px) calc(30% - 16px) 40%; gap: 16px; overflow: auto; height: 520px; }
|
|
1011
|
-
.trace-layout.step-4 { display: grid; grid-template-columns: calc(30% - 16px) calc(30% - 16px) 40%; gap: 16px; overflow: auto; height: 520px; }
|
|
1012
|
-
.trace-left, .trace-right { background: #f9f9f9; border-radius: 8px; padding: 14px; border: 1px solid #eee; }
|
|
1013
|
-
.trace-section-title { font-size: 14px; font-weight: 600; margin-bottom: 10px; }
|
|
1014
|
-
.observation-table-wrap { max-height: 460px; overflow: auto; background: white; border-radius: 6px; border: 1px solid #eee; }
|
|
1015
|
-
.observation-table { width: 100%; border-collapse: collapse; }
|
|
1016
|
-
.observation-table thead { background: #f5f5f5; position: sticky; top: 0; z-index: 1; }
|
|
1017
|
-
.observation-table th { text-align: left; font-size: 12px; font-weight: 600; color: #555; padding: 10px 12px; border-bottom: 1px solid #e8e8e8; }
|
|
1018
|
-
.observation-table td { font-size: 13px; padding: 10px 12px; border-bottom: 1px solid #f0f0f0; }
|
|
1019
|
-
.observation-table tbody tr { cursor: pointer; }
|
|
1020
|
-
.observation-table tbody tr:hover { background: #f7fbff; }
|
|
1021
|
-
.observation-table tbody tr.selected { background: #e8f3ff; }
|
|
1022
|
-
.obs-type { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; background: #e6e6e6; color: #333; }
|
|
1023
|
-
.obs-type.tool { background: #e8f7ef; color: #1f7a44; }
|
|
1024
|
-
.obs-type.ai { background: #e8f1ff; color: #1f5fbf; }
|
|
1025
|
-
.run-from-bp-btn { font-size: 11px; padding: 2px 8px; border: 1px solid #bbb; border-radius: 4px; background: #f5f5f5; color: #333; cursor: pointer; white-space: nowrap; }
|
|
1026
|
-
.run-from-bp-btn:hover { background: #e0edff; border-color: #5a8fd8; color: #1f5fbf; }
|
|
1027
|
-
.run-from-bp-btn:disabled { opacity: 0.6; cursor: default; }
|
|
1028
|
-
.resume-agent-btn { font-size: 11px; padding: 2px 8px; border: 1px solid #b8a0d8; border-radius: 4px; background: #f3eeff; color: #5a2d9c; cursor: pointer; white-space: nowrap; margin-left: 6px; }
|
|
1029
|
-
.resume-agent-btn:hover { background: #e6d8ff; border-color: #7c52b8; color: #3d1f7a; }
|
|
1030
|
-
.resume-agent-btn:disabled { opacity: 0.6; cursor: default; }
|
|
1031
|
-
.agent-task-badge { display: inline-block; padding: 1px 6px; border-radius: 8px; font-size: 10px; background: #f0e8ff; color: #6a2fb0; border: 1px solid #d4b8f0; margin-left: 6px; font-weight: 600; }
|
|
1032
|
-
.agent-task-row { background: #f7f0ff; border-left: 3px solid #8560c6; }
|
|
1033
|
-
.frozen-row { background: #eef7ff; border-left: 3px solid #8cbcf5; }
|
|
1034
|
-
.observation-table tbody tr.selected.frozen-row { background: #dcebff; }
|
|
1035
|
-
.frozen-tag { display: inline-block; padding: 1px 6px; border-radius: 8px; font-size: 10px; background: #dcecff; color: #1f5fbf; border: 1px solid #a9c8f3; margin-left: 6px; font-weight: 600; }
|
|
1036
|
-
.detail-sections { display: flex; flex-direction: column; gap: 12px; height: 486.5px; overflow-y: auto; }
|
|
1037
|
-
.detail-section { background: white; border: 1px solid #eee; border-radius: 6px; padding: 10px; }
|
|
1038
|
-
.detail-title { font-size: 12px; font-weight: 600; margin-bottom: 8px; color: #555; text-transform: uppercase; letter-spacing: 0.02em; }
|
|
1039
|
-
.detail-pre { margin: 0; font-family: Monaco, monospace; font-size: 12px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; background: #fafafa; border-radius: 4px; padding: 10px; border: 1px solid #f0f0f0; min-height: 56px; max-height: 340px; overflow-y: auto; }
|
|
1040
|
-
.modal-footer { display: flex; margin-top: 24px; padding-top: 20px; border-top: 1px solid #eee; gap: 12px; justify-content: space-between; }
|
|
1041
|
-
.btn { padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; border: none; }
|
|
1042
|
-
.btn-secondary { background: #f0f0f0; color: #333; }
|
|
1043
|
-
.btn-secondary:hover { background: #e0e0e0; }
|
|
1044
|
-
.btn-primary { background: #0066cc; color: white; }
|
|
1045
|
-
.btn-primary:hover { background: #0052a3; }
|
|
1046
|
-
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
1047
|
-
.btn-primary:disabled:hover { background: #0066cc; }
|
|
1048
|
-
.btn-secondary:disabled:hover { background: #f0f0f0; }
|
|
1049
|
-
.obs-checkbox { width: 18px; height: 18px; cursor: pointer; }
|
|
1050
|
-
.rerun-status { display: inline-block; margin-left: 8px; font-size: 11px; font-weight: 600; }
|
|
1051
|
-
.rerun-status.running { color: #666; }
|
|
1052
|
-
.rerun-status.success { color: #1f7a44; }
|
|
1053
|
-
.rerun-status.error { color: #c62828; }
|
|
1054
|
-
@media (max-width: 900px) {
|
|
1055
|
-
.trace-layout { grid-template-columns: 1fr; }
|
|
1056
|
-
}
|
|
1057
|
-
</style>
|
|
1058
|
-
<script>
|
|
1059
|
-
const updatedInputs = new Map();
|
|
1060
|
-
</script>
|
|
1061
|
-
</head>
|
|
1062
|
-
<body>
|
|
1063
|
-
<div class="container">
|
|
1064
|
-
<header>
|
|
1065
|
-
<h1>Workflow Functions</h1>
|
|
1066
|
-
<div class="subtitle">Select a workflow to debug with trace analysis</div>
|
|
1067
|
-
<div class="search-box">
|
|
1068
|
-
<input type="text" id="searchInput" placeholder="Search by name or path..." autocomplete="off">
|
|
1069
|
-
<div class="result-count"><span id="resultCount">0</span> workflows</div>
|
|
1070
|
-
</div>
|
|
1071
|
-
</header>
|
|
1072
|
-
<div class="workflows-list">
|
|
1073
|
-
<table class="workflows-table">
|
|
1074
|
-
<thead><tr><th style="width: 35%">Function Name</th><th>File Path</th></tr></thead>
|
|
1075
|
-
<tbody id="workflowsTableBody"><tr><td colspan="2" style="text-align: center; padding: 40px;">Loading...</td></tr></tbody>
|
|
1076
|
-
</table>
|
|
1077
|
-
</div>
|
|
1078
|
-
</div>
|
|
1079
|
-
<div id="traceModal" class="modal">
|
|
1080
|
-
<div class="modal-content">
|
|
1081
|
-
<div class="modal-header">
|
|
1082
|
-
<h2 class="modal-title">Import Trace for Analysis</h2>
|
|
1083
|
-
<button class="modal-close" id="closeModal">×</button>
|
|
1084
|
-
</div>
|
|
1085
|
-
<div id="uploadArea" class="upload-area">
|
|
1086
|
-
<div>Drag and Drop <br />or <br />Click to Upload</div>
|
|
1087
|
-
<input type="file" id="traceFile" accept=".json" />
|
|
1088
|
-
</div>
|
|
1089
|
-
<div id="uploadStatus" class="upload-status"></div>
|
|
1090
|
-
<div id="traceViewer" class="trace-viewer">
|
|
1091
|
-
<div class="trace-layout">
|
|
1092
|
-
<div class="trace-left">
|
|
1093
|
-
<div class="trace-section-title">Observations</div>
|
|
1094
|
-
<div class="observation-table-wrap">
|
|
1095
|
-
<table class="observation-table">
|
|
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>
|
|
1097
|
-
<tbody id="observationTableBody"></tbody>
|
|
1098
|
-
</table>
|
|
1099
|
-
</div>
|
|
1100
|
-
</div>
|
|
1101
|
-
<div class="trace-right">
|
|
1102
|
-
<div id="observationDetail"></div>
|
|
1103
|
-
</div>
|
|
1104
|
-
</div>
|
|
1105
|
-
</div>
|
|
1106
|
-
<div id="modalFooter" class="modal-footer">
|
|
1107
|
-
<button class="btn btn-secondary" id="changeTraceBtn">Change Trace File</button>
|
|
1108
|
-
<button class="btn btn-primary" id="nextBtn">Next</button>
|
|
1109
|
-
</div>
|
|
1110
|
-
</div>
|
|
1111
|
-
</div>
|
|
1112
|
-
<script>
|
|
1113
|
-
console.log("[Dashboard] Script starting...");
|
|
1114
|
-
let allWorkflows = [], codeIndex = {workflows: [], tools: []}, selectedWorkflow = null;
|
|
1115
|
-
let currentObservations = [], selectedObservationIndex = -1;
|
|
1116
|
-
let rerunHistory = new Map();
|
|
1117
|
-
let rerunInFlight = new Set();
|
|
1118
|
-
let step4SelectedRun = -1;
|
|
1119
|
-
let step5RunTraces = [];
|
|
1120
|
-
let repoRoot = ''; // Will be fetched from API
|
|
1121
|
-
try {
|
|
1122
|
-
const _saved = localStorage.getItem('ed_step5RunTraces');
|
|
1123
|
-
if (_saved) step5RunTraces = JSON.parse(_saved);
|
|
1124
|
-
} catch {}
|
|
1125
|
-
let step5RunMeta = { loading: false, error: '', runCount: 0, sequential: false };
|
|
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
|
-
|
|
1168
|
-
function persistTraces() {
|
|
1169
|
-
try {
|
|
1170
|
-
// Store compact version — strip bulky workflowTrace.events (snapshot is on server)
|
|
1171
|
-
const compact = step5RunTraces.map(function(t) {
|
|
1172
|
-
const { workflowTrace, ...rest } = t;
|
|
1173
|
-
return rest;
|
|
1174
|
-
});
|
|
1175
|
-
localStorage.setItem('ed_step5RunTraces', JSON.stringify(compact));
|
|
1176
|
-
} catch {}
|
|
1177
|
-
}
|
|
1178
|
-
const tbody = document.getElementById("workflowsTableBody");
|
|
1179
|
-
const countEl = document.getElementById("resultCount");
|
|
1180
|
-
const modal = document.getElementById("traceModal");
|
|
1181
|
-
const uploadArea = document.getElementById("uploadArea");
|
|
1182
|
-
const fileInput = document.getElementById("traceFile");
|
|
1183
|
-
const modalFooter = document.getElementById("modalFooter");
|
|
1184
|
-
const uploadStatus = document.getElementById("uploadStatus");
|
|
1185
|
-
const traceViewer = document.getElementById("traceViewer");
|
|
1186
|
-
let observationTableBody = document.getElementById("observationTableBody");
|
|
1187
|
-
let observationDetail = document.getElementById("observationDetail");
|
|
1188
|
-
const modalTitle = document.querySelector(".modal-title");
|
|
1189
|
-
console.log("[Dashboard] DOM elements loaded, tbody:", tbody);
|
|
1190
|
-
|
|
1191
|
-
let currentStep = 0; // 0=upload, 3=mark, 4=verify, 5=validate
|
|
1192
|
-
let checkedObservations = new Set();
|
|
1193
|
-
|
|
1194
|
-
document.getElementById("closeModal").onclick = () => {
|
|
1195
|
-
modal.classList.remove("open");
|
|
1196
|
-
resetTraceModal();
|
|
1197
|
-
};
|
|
1198
|
-
modal.onclick = (e) => {
|
|
1199
|
-
if (e.target === modal) {
|
|
1200
|
-
modal.classList.remove("open");
|
|
1201
|
-
resetTraceModal();
|
|
1202
|
-
}
|
|
1203
|
-
};
|
|
1204
|
-
|
|
1205
|
-
document.getElementById("changeTraceBtn").onclick = () => {
|
|
1206
|
-
if (currentStep === 3) {
|
|
1207
|
-
resetTraceModal();
|
|
1208
|
-
} else if (currentStep === 4) {
|
|
1209
|
-
// Go back to Step 3
|
|
1210
|
-
currentStep = 3;
|
|
1211
|
-
checkedObservations.clear(); // Clear to allow reselecting different steps
|
|
1212
|
-
updateModalTitle();
|
|
1213
|
-
updateFooterButtons();
|
|
1214
|
-
renderObservationTable();
|
|
1215
|
-
// Auto-select first observation
|
|
1216
|
-
if (currentObservations.length > 0) {
|
|
1217
|
-
selectObservation(0);
|
|
1218
|
-
}
|
|
1219
|
-
} else if (currentStep === 5) {
|
|
1220
|
-
// Go back to Step 3 (Still Failing)
|
|
1221
|
-
currentStep = 3;
|
|
1222
|
-
checkedObservations.clear(); // Clear to allow reselecting different steps
|
|
1223
|
-
updateModalTitle();
|
|
1224
|
-
updateFooterButtons();
|
|
1225
|
-
renderObservationTable();
|
|
1226
|
-
// Auto-select first observation
|
|
1227
|
-
if (currentObservations.length > 0) {
|
|
1228
|
-
selectObservation(0);
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
};
|
|
1232
|
-
|
|
1233
|
-
document.getElementById("nextBtn").onclick = () => {
|
|
1234
|
-
if (currentStep < 3) {
|
|
1235
|
-
alert("Please upload a trace file to continue");
|
|
1236
|
-
return;
|
|
1237
|
-
}
|
|
1238
|
-
if (currentStep === 3) {
|
|
1239
|
-
// Validate that at least one checkbox is checked
|
|
1240
|
-
if (checkedObservations.size === 0) {
|
|
1241
|
-
alert("Please select at least one step to mark as broken");
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
// Move to Step 4
|
|
1245
|
-
currentStep = 4;
|
|
1246
|
-
updateModalTitle();
|
|
1247
|
-
updateFooterButtons();
|
|
1248
|
-
renderObservationTable();
|
|
1249
|
-
// Auto-select first checked observation
|
|
1250
|
-
const checkedArray = Array.from(checkedObservations);
|
|
1251
|
-
if (checkedArray.length > 0) {
|
|
1252
|
-
window.step4SelectObservation(checkedArray[0]);
|
|
1253
|
-
}
|
|
1254
|
-
} else if (currentStep === 4) {
|
|
1255
|
-
// Show prompt-update confirmation (if needed), then live validation dialog
|
|
1256
|
-
window.openPromptConfirmation(() => window.openLiveValidationDialog());
|
|
1257
|
-
return;
|
|
1258
|
-
} else if (currentStep === 5) {
|
|
1259
|
-
modal.classList.remove("open");
|
|
1260
|
-
resetTraceModal();
|
|
1261
|
-
customFooter.remove();
|
|
1262
|
-
}
|
|
1263
|
-
};
|
|
1264
|
-
|
|
1265
|
-
// ---- Tool Mock Config State ----
|
|
1266
|
-
window._toolMockConfig = {}; // { toolName: { mode: 'live'|'mock-all'|'mock-specific', callIndices: [], mockData: {} } }
|
|
1267
|
-
|
|
1268
|
-
function getToolsFromTrace() {
|
|
1269
|
-
// Extract unique tool names and their call details from the uploaded trace observations
|
|
1270
|
-
const toolCalls = {};
|
|
1271
|
-
currentObservations.forEach(function(obs, i) {
|
|
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)');
|
|
1278
|
-
if (!toolCalls[name]) toolCalls[name] = [];
|
|
1279
|
-
toolCalls[name].push({ index: toolCalls[name].length + 1, obsIndex: i, input: obs.input, output: obs.output });
|
|
1280
|
-
});
|
|
1281
|
-
return toolCalls;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
function getAllRegisteredTools() {
|
|
1285
|
-
// From codeIndex.tools (fetched at page load from /api/code-index)
|
|
1286
|
-
return (codeIndex.tools || []).map(function(t) { return t.name; });
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
function buildToolMockConfigFromUI() {
|
|
1290
|
-
const config = {};
|
|
1291
|
-
const rows = document.querySelectorAll('.tool-mock-row');
|
|
1292
|
-
rows.forEach(function(row) {
|
|
1293
|
-
const toolName = row.dataset.toolName;
|
|
1294
|
-
const modeSelect = row.querySelector('.tool-mock-mode');
|
|
1295
|
-
const mode = modeSelect ? modeSelect.value : 'live';
|
|
1296
|
-
if (mode === 'live') return;
|
|
1297
|
-
const entry = { mode: mode };
|
|
1298
|
-
if (mode === 'mock-specific') {
|
|
1299
|
-
const checkboxes = row.querySelectorAll('.tool-call-checkbox:checked');
|
|
1300
|
-
entry.callIndices = Array.from(checkboxes).map(function(cb) { return parseInt(cb.value, 10); });
|
|
1301
|
-
if (entry.callIndices.length === 0) return; // No calls selected, treat as live
|
|
1302
|
-
}
|
|
1303
|
-
// Collect mock data
|
|
1304
|
-
entry.mockData = {};
|
|
1305
|
-
const dataInputs = row.querySelectorAll('.tool-mock-data-input');
|
|
1306
|
-
dataInputs.forEach(function(inp) {
|
|
1307
|
-
const callIdx = parseInt(inp.dataset.callIdx, 10);
|
|
1308
|
-
if (!inp.value.trim()) return;
|
|
1309
|
-
try { entry.mockData[callIdx] = JSON.parse(inp.value); }
|
|
1310
|
-
catch(e) { entry.mockData[callIdx] = inp.value; }
|
|
1311
|
-
});
|
|
1312
|
-
config[toolName] = entry;
|
|
1313
|
-
});
|
|
1314
|
-
return config;
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
function cleanValue(value) {
|
|
1318
|
-
if (typeof value === "string") {
|
|
1319
|
-
value = value.replaceAll('\\\\"', '');
|
|
1320
|
-
// remove surrounding quotes if they exist
|
|
1321
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
1322
|
-
return value.slice(1, -1);
|
|
1323
|
-
}
|
|
1324
|
-
return value;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
if (Array.isArray(value)) {
|
|
1328
|
-
return value.map(cleanValue);
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
if (typeof value === "object" && value !== null) {
|
|
1332
|
-
const result = {};
|
|
1333
|
-
for (const key in value) {
|
|
1334
|
-
result[key] = cleanValue(value[key]);
|
|
1335
|
-
}
|
|
1336
|
-
return result;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
return value;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
function convert(input) {
|
|
1343
|
-
const parsed = JSON.parse(input);
|
|
1344
|
-
return cleanValue(parsed);
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
function renderToolMockSection(showAll) {
|
|
1348
|
-
const traceTools = getToolsFromTrace();
|
|
1349
|
-
const allToolNames = getAllRegisteredTools();
|
|
1350
|
-
const traceToolNames = Object.keys(traceTools);
|
|
1351
|
-
const toolNames = showAll
|
|
1352
|
-
? Array.from(new Set([...traceToolNames, ...allToolNames]))
|
|
1353
|
-
: traceToolNames;
|
|
1354
|
-
|
|
1355
|
-
if (toolNames.length === 0) {
|
|
1356
|
-
return '<div style="color:#999;font-size:13px;padding:6px 0;">No tools detected.</div>';
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
let html = '<div style="max-height:260px;overflow-y:auto;border:1px solid #e0e0e0;border-radius:6px;">';
|
|
1360
|
-
html += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
|
|
1361
|
-
html += '<thead><tr style="background:#f5f5f5;">';
|
|
1362
|
-
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Tool</th>';
|
|
1363
|
-
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Calls in Trace</th>';
|
|
1364
|
-
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Mock Mode</th>';
|
|
1365
|
-
html += '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid #e0e0e0;">Details</th>';
|
|
1366
|
-
html += '</tr></thead><tbody>';
|
|
1367
|
-
|
|
1368
|
-
toolNames.forEach(function(name) {
|
|
1369
|
-
const calls = traceTools[name] || [];
|
|
1370
|
-
const inTrace = traceToolNames.includes(name);
|
|
1371
|
-
const existing = window._toolMockConfig[name] || { mode: 'live' };
|
|
1372
|
-
const nameStyle = inTrace ? '' : 'color:#999;';
|
|
1373
|
-
|
|
1374
|
-
html += '<tr class="tool-mock-row" data-tool-name="' + esc(name) + '" style="border-bottom:1px solid #f0f0f0;">';
|
|
1375
|
-
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>';
|
|
1376
|
-
html += '<td style="padding:6px 10px;">' + calls.length + '</td>';
|
|
1377
|
-
html += '<td style="padding:6px 10px;">';
|
|
1378
|
-
html += '<select class="tool-mock-mode" style="font-size:12px;padding:2px 4px;" onchange="window.onToolMockModeChange(\\'' + esc(name) + '\\', this.value)">';
|
|
1379
|
-
html += '<option value="live"' + (existing.mode === 'live' ? ' selected' : '') + '>Live</option>';
|
|
1380
|
-
html += '<option value="mock-all"' + (existing.mode === 'mock-all' ? ' selected' : '') + '>Mock All Calls</option>';
|
|
1381
|
-
if (calls.length > 0) {
|
|
1382
|
-
html += '<option value="mock-specific"' + (existing.mode === 'mock-specific' ? ' selected' : '') + '>Mock Specific Calls</option>';
|
|
1383
|
-
}
|
|
1384
|
-
html += '</select>';
|
|
1385
|
-
html += '</td>';
|
|
1386
|
-
|
|
1387
|
-
// Details column: per-call checkboxes + mock data inputs
|
|
1388
|
-
html += '<td style="padding:6px 10px;">';
|
|
1389
|
-
if (existing.mode === 'mock-all') {
|
|
1390
|
-
let defaultData = (existing.mockData && existing.mockData[0] !== undefined) ? JSON.stringify(existing.mockData[0]) : (calls.length > 0 ? JSON.stringify(calls[0].output) : '');
|
|
1391
|
-
defaultData = convert(defaultData);
|
|
1392
|
-
html += '<div style="font-size:11px;color:#555;margin-bottom:4px;">Mock data (JSON):</div>';
|
|
1393
|
-
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>';
|
|
1394
|
-
} else if (existing.mode === 'mock-specific' && calls.length > 0) {
|
|
1395
|
-
html += '<div style="font-size:11px;color:#555;margin-bottom:4px;">Select calls to mock:</div>';
|
|
1396
|
-
calls.forEach(function(call) {
|
|
1397
|
-
const isChecked = existing.callIndices && existing.callIndices.includes(call.index);
|
|
1398
|
-
const inputPreview = typeof call.input === 'string' ? call.input.slice(0, 40) : JSON.stringify(call.input || '').slice(0, 40);
|
|
1399
|
-
let mockVal = (existing.mockData && existing.mockData[call.index] !== undefined) ? JSON.stringify(existing.mockData[call.index]) : JSON.stringify(call.output);
|
|
1400
|
-
mockVal = convert(mockVal);
|
|
1401
|
-
html += '<div style="margin-bottom:6px;padding:4px;background:#fafafa;border-radius:4px;border:1px solid #eee;">';
|
|
1402
|
-
html += '<label style="display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;">';
|
|
1403
|
-
html += '<input type="checkbox" class="tool-call-checkbox" value="' + call.index + '"' + (isChecked ? ' checked' : '') + ' onchange="window.onToolCallCheckChange(\\'' + esc(name) + '\\',' + call.index + ',this.checked)">';
|
|
1404
|
-
html += '<span>Call #' + call.index + '</span>';
|
|
1405
|
-
html += '<span style="color:#888;font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + esc(inputPreview) + '</span>';
|
|
1406
|
-
html += '</label>';
|
|
1407
|
-
if (isChecked) {
|
|
1408
|
-
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>';
|
|
1409
|
-
}
|
|
1410
|
-
html += '</div>';
|
|
1411
|
-
});
|
|
1412
|
-
} else {
|
|
1413
|
-
html += '<span style="color:#aaa;font-size:11px;">—</span>';
|
|
1414
|
-
}
|
|
1415
|
-
html += '</td>';
|
|
1416
|
-
html += '</tr>';
|
|
1417
|
-
});
|
|
1418
|
-
|
|
1419
|
-
html += '</tbody></table></div>';
|
|
1420
|
-
return html;
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
window.onToolMockModeChange = function(toolName, mode) {
|
|
1424
|
-
if (!window._toolMockConfig[toolName]) window._toolMockConfig[toolName] = { mode: 'live' };
|
|
1425
|
-
// Save current mock data before switching
|
|
1426
|
-
window._toolMockConfig[toolName] = { ...window._toolMockConfig[toolName], mode: mode };
|
|
1427
|
-
if (mode === 'mock-specific' && !window._toolMockConfig[toolName].callIndices) {
|
|
1428
|
-
window._toolMockConfig[toolName].callIndices = [];
|
|
1429
|
-
}
|
|
1430
|
-
// Re-render tool mock section
|
|
1431
|
-
const showAll = document.getElementById('showAllToolsToggle');
|
|
1432
|
-
const container = document.getElementById('toolMockContainer');
|
|
1433
|
-
if (container) container.innerHTML = renderToolMockSection(showAll && showAll.checked);
|
|
1434
|
-
};
|
|
1435
|
-
|
|
1436
|
-
window.onToolCallCheckChange = function(toolName, callIdx, checked) {
|
|
1437
|
-
if (!window._toolMockConfig[toolName]) window._toolMockConfig[toolName] = { mode: 'mock-specific', callIndices: [] };
|
|
1438
|
-
const indices = window._toolMockConfig[toolName].callIndices || [];
|
|
1439
|
-
if (checked && !indices.includes(callIdx)) {
|
|
1440
|
-
indices.push(callIdx);
|
|
1441
|
-
} else if (!checked) {
|
|
1442
|
-
const pos = indices.indexOf(callIdx);
|
|
1443
|
-
if (pos >= 0) indices.splice(pos, 1);
|
|
1444
|
-
}
|
|
1445
|
-
window._toolMockConfig[toolName].callIndices = indices;
|
|
1446
|
-
const showAll = document.getElementById('showAllToolsToggle');
|
|
1447
|
-
const container = document.getElementById('toolMockContainer');
|
|
1448
|
-
if (container) container.innerHTML = renderToolMockSection(showAll && showAll.checked);
|
|
1449
|
-
};
|
|
1450
|
-
|
|
1451
|
-
window.openLiveValidationDialog = function() {
|
|
1452
|
-
if (window.liveValidationDialog) return;
|
|
1453
|
-
window._toolMockConfig = {}; // Reset mock config each time dialog opens
|
|
1454
|
-
|
|
1455
|
-
const hasTraceTools = currentObservations.some(function(o) { return o.type === 'TOOL'; });
|
|
1456
|
-
const hasRegisteredTools = codeIndex.tools && codeIndex.tools.length > 0;
|
|
1457
|
-
|
|
1458
|
-
window.liveValidationDialog = document.createElement('div');
|
|
1459
|
-
window.liveValidationDialog.id = 'liveValidationDialog';
|
|
1460
|
-
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;';
|
|
1461
|
-
window.liveValidationDialog.innerHTML = \`
|
|
1462
|
-
<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;">
|
|
1463
|
-
<h3 style="margin-top:0;margin-bottom:18px;font-size:20px;">Validate Updated Flow with Live Data</h3>
|
|
1464
|
-
<label style="font-size:15px;display:block;margin-bottom:8px;">How many times do you want to run the flow with live data?</label>
|
|
1465
|
-
<input id="liveValidationCount" type="number" min="1" value="1" style="width:100%;font-size:16px;padding:6px 10px;margin-bottom:18px;" />
|
|
1466
|
-
<label style="display:flex;align-items:center;gap:8px;font-size:14px;margin-bottom:18px;">
|
|
1467
|
-
<input id="liveValidationSequential" type="checkbox" />
|
|
1468
|
-
Run in sequence instead of parallel
|
|
1469
|
-
</label>
|
|
1470
|
-
\${(hasTraceTools || hasRegisteredTools) ? \`
|
|
1471
|
-
<div style="border-top:1px solid #eee;padding-top:16px;margin-bottom:16px;">
|
|
1472
|
-
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
|
1473
|
-
<div style="font-size:15px;font-weight:600;">Tool Mocking</div>
|
|
1474
|
-
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
|
1475
|
-
<input id="showAllToolsToggle" type="checkbox" onchange="document.getElementById('toolMockContainer').innerHTML = renderToolMockSection(this.checked);" />
|
|
1476
|
-
Show all registered tools
|
|
1477
|
-
</label>
|
|
1478
|
-
</div>
|
|
1479
|
-
<div id="toolMockContainer"></div>
|
|
1480
|
-
</div>\` : ''}
|
|
1481
|
-
<div style="display:flex;gap:12px;justify-content:space-between;align-items:center;">
|
|
1482
|
-
<span id="liveValidationProgress" style="font-size:14px;color:#555;"></span>
|
|
1483
|
-
<div style="display:flex;gap:12px;">
|
|
1484
|
-
<button id="cancelLiveValidation" class="btn btn-secondary">Cancel</button>
|
|
1485
|
-
<button id="submitLiveValidation" class="btn btn-primary">Validate</button>
|
|
1486
|
-
</div>
|
|
1487
|
-
</div>
|
|
1488
|
-
</div>
|
|
1489
|
-
\`;
|
|
1490
|
-
document.body.appendChild(window.liveValidationDialog);
|
|
1491
|
-
// Render the tool mock section after DOM insertion
|
|
1492
|
-
const toolMockContainer = document.getElementById('toolMockContainer');
|
|
1493
|
-
if (toolMockContainer) {
|
|
1494
|
-
toolMockContainer.innerHTML = renderToolMockSection(false);
|
|
1495
|
-
}
|
|
1496
|
-
document.getElementById('cancelLiveValidation').onclick = function() {
|
|
1497
|
-
window.liveValidationDialog.remove();
|
|
1498
|
-
window.liveValidationDialog = null;
|
|
1499
|
-
};
|
|
1500
|
-
document.getElementById('submitLiveValidation').onclick = async function() {
|
|
1501
|
-
const count = parseInt(document.getElementById('liveValidationCount').value, 10);
|
|
1502
|
-
const sequential = document.getElementById('liveValidationSequential').checked;
|
|
1503
|
-
if (count >= 1) {
|
|
1504
|
-
// Build the tool mock config from UI state and persist for "Run from here"
|
|
1505
|
-
const toolMockConfig = buildToolMockConfigFromUI();
|
|
1506
|
-
window._toolMockConfig = toolMockConfig;
|
|
1507
|
-
const submitBtn = document.getElementById('submitLiveValidation');
|
|
1508
|
-
submitBtn.disabled = true;
|
|
1509
|
-
submitBtn.textContent = 'Validating...';
|
|
1510
|
-
const progressEl = document.getElementById('liveValidationProgress');
|
|
1511
|
-
|
|
1512
|
-
function finishValidation(collectedTraces, errorMsg, usedSequential) {
|
|
1513
|
-
if (progressEl) progressEl.textContent = '';
|
|
1514
|
-
window.liveValidationDialog.remove();
|
|
1515
|
-
window.liveValidationDialog = null;
|
|
1516
|
-
window.liveValidationCount = count;
|
|
1517
|
-
window.liveValidationSequential = usedSequential;
|
|
1518
|
-
window.step5SelectedTrace = 0;
|
|
1519
|
-
window.step5SelectedObservation = 0;
|
|
1520
|
-
currentStep = 5;
|
|
1521
|
-
updateModalTitle();
|
|
1522
|
-
updateFooterButtons();
|
|
1523
|
-
if (errorMsg && collectedTraces.length === 0) {
|
|
1524
|
-
step5RunTraces = [];
|
|
1525
|
-
localStorage.removeItem('ed_step5RunTraces');
|
|
1526
|
-
step5RunMeta = { loading: false, error: errorMsg, runCount: count, sequential: usedSequential };
|
|
1527
|
-
} else {
|
|
1528
|
-
step5RunTraces = collectedTraces;
|
|
1529
|
-
persistTraces();
|
|
1530
|
-
step5RunMeta = { loading: false, error: '', runCount: collectedTraces.length, sequential: usedSequential };
|
|
1531
|
-
}
|
|
1532
|
-
if (window.step5SelectedTrace > step5RunTraces.length) window.step5SelectedTrace = 0;
|
|
1533
|
-
window.step5SelectedObservation = 0;
|
|
1534
|
-
renderObservationTable();
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
if (sequential) {
|
|
1538
|
-
// Sequential mode: fire one request per run so progress reflects real completion
|
|
1539
|
-
if (progressEl) progressEl.textContent = \`0 of \${count} workflow runs completed\`;
|
|
1540
|
-
const collectedTraces = [];
|
|
1541
|
-
let fatalError = null;
|
|
1542
|
-
for (let i = 0; i < count; i++) {
|
|
1543
|
-
const singlePayload = { workflowName: selectedWorkflow?.name, runCount: 1, sequential: false, observations: currentObservations, toolMockConfig };
|
|
1544
|
-
try {
|
|
1545
|
-
const response = await fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(singlePayload) });
|
|
1546
|
-
const data = await response.json();
|
|
1547
|
-
if (response.ok && data.ok && Array.isArray(data.traces) && data.traces.length > 0) {
|
|
1548
|
-
collectedTraces.push({ ...data.traces[0], runNumber: i + 1 });
|
|
1549
|
-
} else {
|
|
1550
|
-
// Push an error trace so the run is still visible in Step 5
|
|
1551
|
-
collectedTraces.push({ runNumber: i + 1, ok: false, error: data.error || 'Workflow validation failed.', observations: [], workflowTrace: null });
|
|
1552
|
-
}
|
|
1553
|
-
} catch (err) {
|
|
1554
|
-
collectedTraces.push({ runNumber: i + 1, ok: false, error: err && err.message ? err.message : String(err), observations: [], workflowTrace: null });
|
|
1555
|
-
}
|
|
1556
|
-
if (progressEl) progressEl.textContent = \`\${i + 1} of \${count} workflow runs completed\`;
|
|
1557
|
-
}
|
|
1558
|
-
finishValidation(collectedTraces, fatalError, true);
|
|
1559
|
-
} else {
|
|
1560
|
-
// Parallel mode: single bulk request
|
|
1561
|
-
if (progressEl) progressEl.textContent = \`Running \${count} workflow run\${count !== 1 ? 's' : ''} in parallel…\`;
|
|
1562
|
-
const payload = { workflowName: selectedWorkflow?.name, runCount: count, sequential: false, observations: currentObservations, toolMockConfig };
|
|
1563
|
-
try {
|
|
1564
|
-
const response = await fetch('/api/validate-workflow', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
|
1565
|
-
const data = await response.json();
|
|
1566
|
-
if (response.ok && data.ok) {
|
|
1567
|
-
finishValidation(Array.isArray(data.traces) ? data.traces : [], null, false);
|
|
1568
|
-
} else {
|
|
1569
|
-
finishValidation([], data.error || 'Workflow validation failed.', false);
|
|
1570
|
-
}
|
|
1571
|
-
} catch (err) {
|
|
1572
|
-
finishValidation([], err && err.message ? err.message : String(err), false);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
} else {
|
|
1576
|
-
document.getElementById('liveValidationCount').style.borderColor = 'red';
|
|
1577
|
-
}
|
|
1578
|
-
};
|
|
1579
|
-
};
|
|
1580
|
-
|
|
1581
|
-
window.openPromptConfirmation = function(onConfirm) {
|
|
1582
|
-
const genObs = currentObservations
|
|
1583
|
-
.map((o, i) => ({ obs: o, idx: i }))
|
|
1584
|
-
.filter(({ obs, idx }) => obs.type === 'GENERATION' && checkedObservations.has(idx));
|
|
1585
|
-
if (genObs.length === 0) { onConfirm(); return; }
|
|
1586
|
-
|
|
1587
|
-
const tableRows = genObs.map(({ obs, idx }) => {
|
|
1588
|
-
const preview = toDisplayText(obs.input, obs.type).replace(/\\s+/g, ' ').trim().slice(0, 50);
|
|
1589
|
-
const model = obs.model || '—';
|
|
1590
|
-
return \`<tr>
|
|
1591
|
-
<td style="padding:8px 10px;font-size:13px;color:#555;">\${idx + 1}</td>
|
|
1592
|
-
<td style="padding:8px 10px;font-size:13px;font-family:Monaco,monospace;">\${esc(obs.name || 'AI call')}</td>
|
|
1593
|
-
<td style="padding:8px 10px;font-size:12px;font-family:Monaco,monospace;color:#444;max-width:320px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">\${esc(preview)}\${preview.length === 50 ? '…' : ''}</td>
|
|
1594
|
-
<td style="padding:8px 10px;font-size:12px;font-family:Monaco,monospace;color:#555;">\${esc(model)}</td>
|
|
1595
|
-
</tr>\`;
|
|
1596
|
-
}).join('');
|
|
1597
|
-
|
|
1598
|
-
const dlg = document.createElement('div');
|
|
1599
|
-
dlg.style = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.35);display:flex;align-items:center;justify-content:center;z-index:9998;';
|
|
1600
|
-
dlg.innerHTML = \`
|
|
1601
|
-
<div style="background:white;padding:32px 28px;border-radius:12px;box-shadow:0 2px 24px #0003;min-width:640px;max-width:92vw;">
|
|
1602
|
-
<h3 style="margin-top:0;margin-bottom:8px;font-size:20px;">Have you updated your AI prompts?</h3>
|
|
1603
|
-
<p style="margin:0 0 18px;font-size:14px;color:#555;">The following AI generation steps were found in your trace. Make sure you have edited the prompts in Step 4 before validating with live data.</p>
|
|
1604
|
-
<div style="border:1px solid #e0e0e0;border-radius:6px;overflow:hidden;margin-bottom:24px;">
|
|
1605
|
-
<table style="width:100%;border-collapse:collapse;">
|
|
1606
|
-
<thead>
|
|
1607
|
-
<tr style="background:#f5f5f5;">
|
|
1608
|
-
<th style="padding:8px 10px;text-align:left;font-size:12px;color:#555;border-bottom:1px solid #e0e0e0;">#</th>
|
|
1609
|
-
<th style="padding:8px 10px;text-align:left;font-size:12px;color:#555;border-bottom:1px solid #e0e0e0;">Name</th>
|
|
1610
|
-
<th style="padding:8px 10px;text-align:left;font-size:12px;color:#555;border-bottom:1px solid #e0e0e0;">Input preview</th>
|
|
1611
|
-
<th style="padding:8px 10px;text-align:left;font-size:12px;color:#555;border-bottom:1px solid #e0e0e0;">Model</th>
|
|
1612
|
-
</tr>
|
|
1613
|
-
</thead>
|
|
1614
|
-
<tbody>\${tableRows}</tbody>
|
|
1615
|
-
</table>
|
|
1616
|
-
</div>
|
|
1617
|
-
<div style="display:flex;gap:12px;justify-content:flex-end;">
|
|
1618
|
-
<button id="cancelPromptConfirm" class="btn btn-secondary">Cancel</button>
|
|
1619
|
-
<button id="proceedPromptConfirm" class="btn btn-primary">Yes, Proceed</button>
|
|
1620
|
-
</div>
|
|
1621
|
-
</div>
|
|
1622
|
-
\`;
|
|
1623
|
-
document.body.appendChild(dlg);
|
|
1624
|
-
dlg.querySelector('#cancelPromptConfirm').onclick = () => dlg.remove();
|
|
1625
|
-
dlg.querySelector('#proceedPromptConfirm').onclick = () => { dlg.remove(); onConfirm(); };
|
|
1626
|
-
};
|
|
1627
|
-
|
|
1628
|
-
uploadArea.onclick = () => fileInput.click();
|
|
1629
|
-
|
|
1630
|
-
// Drag and drop handlers
|
|
1631
|
-
uploadArea.ondragover = (e) => {
|
|
1632
|
-
e.preventDefault();
|
|
1633
|
-
e.stopPropagation();
|
|
1634
|
-
uploadArea.style.borderColor = '#0066cc';
|
|
1635
|
-
uploadArea.style.background = '#f0f7ff';
|
|
1636
|
-
};
|
|
1637
|
-
|
|
1638
|
-
uploadArea.ondragleave = (e) => {
|
|
1639
|
-
e.preventDefault();
|
|
1640
|
-
e.stopPropagation();
|
|
1641
|
-
uploadArea.style.borderColor = '#ddd';
|
|
1642
|
-
uploadArea.style.background = '#fafafa';
|
|
1643
|
-
};
|
|
1644
|
-
|
|
1645
|
-
uploadArea.ondrop = (e) => {
|
|
1646
|
-
e.preventDefault();
|
|
1647
|
-
e.stopPropagation();
|
|
1648
|
-
uploadArea.style.borderColor = '#ddd';
|
|
1649
|
-
uploadArea.style.background = '#fafafa';
|
|
1650
|
-
|
|
1651
|
-
const files = e.dataTransfer.files;
|
|
1652
|
-
if (files.length === 0) return;
|
|
1653
|
-
|
|
1654
|
-
const file = files[0];
|
|
1655
|
-
// Check if it's a JSON file
|
|
1656
|
-
if (!file.name.toLowerCase().endsWith('.json')) {
|
|
1657
|
-
uploadStatus.className = "upload-status error";
|
|
1658
|
-
uploadStatus.textContent = "Please drop a JSON file";
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
handleFileUpload(file);
|
|
1663
|
-
};
|
|
1664
|
-
|
|
1665
|
-
fileInput.onchange = (e) => {
|
|
1666
|
-
if (!e.target.files[0]) return;
|
|
1667
|
-
const file = e.target.files[0];
|
|
1668
|
-
// Always clear file input so same file can be uploaded again
|
|
1669
|
-
fileInput.value = "";
|
|
1670
|
-
handleFileUpload(file);
|
|
1671
|
-
};
|
|
1672
|
-
|
|
1673
|
-
function handleFileUpload(file) {
|
|
1674
|
-
// Clear observations before loading new trace
|
|
1675
|
-
resetTraceModal();
|
|
1676
|
-
const reader = new FileReader();
|
|
1677
|
-
reader.onload = (e) => {
|
|
1678
|
-
try {
|
|
1679
|
-
const data = JSON.parse(e.target.result);
|
|
1680
|
-
uploadStatus.className = "upload-status";
|
|
1681
|
-
uploadStatus.textContent = "";
|
|
1682
|
-
displayTrace(data);
|
|
1683
|
-
} catch (err) {
|
|
1684
|
-
uploadArea.classList.remove("hidden");
|
|
1685
|
-
traceViewer.classList.remove("visible");
|
|
1686
|
-
uploadStatus.className = "upload-status error";
|
|
1687
|
-
uploadStatus.textContent = "Invalid JSON";
|
|
1688
|
-
}
|
|
1689
|
-
};
|
|
1690
|
-
reader.readAsText(file);
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
function displayTrace(data) {
|
|
1694
|
-
let obs = [];
|
|
1695
|
-
if (Array.isArray(data)) {
|
|
1696
|
-
obs = data.filter(o =>
|
|
1697
|
-
(o.type === "GENERATION" || o.type === "TOOL" || o.type === "SPAN") &&
|
|
1698
|
-
(o.input !== null && o.input !== undefined) &&
|
|
1699
|
-
(o.output !== null && o.output !== undefined)
|
|
1700
|
-
);
|
|
1701
|
-
} else {
|
|
1702
|
-
const trace = data.trace || data;
|
|
1703
|
-
// Handle multiple formats: data.data, data.observations, trace.observations
|
|
1704
|
-
const rawObs = data.data || data.observations || trace.observations || [];
|
|
1705
|
-
obs = rawObs.filter(o =>
|
|
1706
|
-
(o.type === "GENERATION" || o.type === "TOOL" || o.type === "SPAN") &&
|
|
1707
|
-
(o.input !== null && o.input !== undefined) &&
|
|
1708
|
-
(o.output !== null && o.output !== undefined)
|
|
1709
|
-
);
|
|
1710
|
-
}
|
|
1711
|
-
// Sort by startTime ascending
|
|
1712
|
-
obs = obs.sort((a, b) => {
|
|
1713
|
-
const timeA = new Date(a.startTime || 0).getTime();
|
|
1714
|
-
const timeB = new Date(b.startTime || 0).getTime();
|
|
1715
|
-
return timeA - timeB;
|
|
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
|
-
}
|
|
1738
|
-
currentObservations = obs;
|
|
1739
|
-
selectedObservationIndex = -1;
|
|
1740
|
-
checkedObservations.clear();
|
|
1741
|
-
observationDetail.innerHTML = "";
|
|
1742
|
-
uploadArea.classList.add("hidden");
|
|
1743
|
-
traceViewer.classList.add("visible");
|
|
1744
|
-
currentStep = 3;
|
|
1745
|
-
updateModalTitle();
|
|
1746
|
-
updateFooterButtons();
|
|
1747
|
-
renderObservationTable();
|
|
1748
|
-
// Auto-select first observation
|
|
1749
|
-
if (currentObservations.length > 0) {
|
|
1750
|
-
selectObservation(0);
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
function renderObservationTable() {
|
|
1755
|
-
// Capture scroll positions for step 5 columns before rebuilding DOM
|
|
1756
|
-
let _step5Col1Scroll = 0, _step5Col2Scroll = 0;
|
|
1757
|
-
if (currentStep === 5) {
|
|
1758
|
-
const tls = document.querySelectorAll('.trace-left');
|
|
1759
|
-
_step5Col1Scroll = tls[0]?.querySelector('.observation-table-wrap')?.scrollTop ?? 0;
|
|
1760
|
-
_step5Col2Scroll = tls[1]?.querySelector('.observation-table-wrap')?.scrollTop ?? 0;
|
|
1761
|
-
}
|
|
1762
|
-
// For other steps, preserve scroll position of the first .observation-table-wrap
|
|
1763
|
-
let obsTableWrap = document.querySelector('.observation-table-wrap');
|
|
1764
|
-
let prevScrollTop = obsTableWrap ? obsTableWrap.scrollTop : null;
|
|
1765
|
-
if (currentStep === 5) {
|
|
1766
|
-
// Initialize selections if not set
|
|
1767
|
-
if (window.step5SelectedTrace === undefined || window.step5SelectedTrace === null) {
|
|
1768
|
-
window.step5SelectedTrace = 0;
|
|
1769
|
-
}
|
|
1770
|
-
if (window.step5SelectedObservation === undefined || window.step5SelectedObservation === null) {
|
|
1771
|
-
window.step5SelectedObservation = 0;
|
|
1772
|
-
}
|
|
1773
|
-
// Step 5: Validate Updated Flow with Live Data
|
|
1774
|
-
// Render traceTable before observationsTable
|
|
1775
|
-
const traces = Array.isArray(step5RunTraces) ? step5RunTraces : [];
|
|
1776
|
-
const traceCount = traces.length + 1;
|
|
1777
|
-
document.getElementsByClassName("trace-layout")[0].classList.add("step-5");
|
|
1778
|
-
let traceTable = \`<div class="trace-section-title">Traces</div>
|
|
1779
|
-
<div class="observation-table-wrap">
|
|
1780
|
-
<table class="observation-table">
|
|
1781
|
-
<tbody>\`;
|
|
1782
|
-
for (let i = 0; i < traceCount; i++) {
|
|
1783
|
-
const isSelected = i === window.step5SelectedTrace;
|
|
1784
|
-
const run = i === 0 ? null : traces[i - 1];
|
|
1785
|
-
const status = run ? (run.ok ? ' ✓' : ' ✗') : '';
|
|
1786
|
-
const label = i === 0 ? "Original Trace" : \`\${run?.traceName ?? \`Trace-\${run?.runNumber ?? i}\`}\${status}\`;
|
|
1787
|
-
traceTable += \`<tr class="\${isSelected ? "selected" : ""}" onclick="window.step5SelectedTrace=\${i};window.step5SelectedObservation=0;renderObservationTable();"><td>\${label}</td></tr>\`;
|
|
1788
|
-
}
|
|
1789
|
-
traceTable += \`</tbody></table></div>\`;
|
|
1790
|
-
|
|
1791
|
-
// Observations table for selected trace
|
|
1792
|
-
let observationsTable = "";
|
|
1793
|
-
let detailsSection = "";
|
|
1794
|
-
|
|
1795
|
-
if (window.step5SelectedTrace > traces.length) {
|
|
1796
|
-
window.step5SelectedTrace = 0;
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
if (window.step5SelectedTrace === 0) {
|
|
1800
|
-
// Original Trace: show all currentObservations (same as step 3)
|
|
1801
|
-
observationsTable += \`<div class="trace-section-title">Observations</div>
|
|
1802
|
-
<div class="observation-table-wrap">
|
|
1803
|
-
<table class="observation-table">
|
|
1804
|
-
<thead><tr><th>Name</th><th>Type</th><th style="width:80px;">Duration</th></tr></thead>
|
|
1805
|
-
<tbody>\`;
|
|
1806
|
-
observationsTable += currentObservations.map((obs, j) => {
|
|
1807
|
-
const isSelected = j === window.step5SelectedObservation;
|
|
1808
|
-
const name = obs.name || obs.id || ("Observation " + (j + 1));
|
|
1809
|
-
const type = obs.type || "UNKNOWN";
|
|
1810
|
-
const typeClass = type === "TOOL" ? "tool" : "ai";
|
|
1811
|
-
const agentBadge = obs.agentTaskIndex != null ? \`<span class="agent-task-badge">T\${obs.agentTaskIndex + 1}</span>\` : '';
|
|
1812
|
-
const rowClass = obs.agentTaskIndex != null ? 'agent-task-row' : '';
|
|
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>\`;
|
|
1814
|
-
}).join("");
|
|
1815
|
-
observationsTable += \`</tbody></table></div>\`;
|
|
1816
|
-
// Details for selected observation
|
|
1817
|
-
const selObs = currentObservations[window.step5SelectedObservation];
|
|
1818
|
-
if (selObs) {
|
|
1819
|
-
const inputText = toDisplayText(selObs.input, selObs.type);
|
|
1820
|
-
const outputText = toDisplayText(selObs.output, selObs.type);
|
|
1821
|
-
const detailId = 'step5-orig-' + window.step5SelectedObservation;
|
|
1822
|
-
const filePathHtml = selObs.type === "GENERATION"
|
|
1823
|
-
? '<div class="file-path-placeholder"></div>'
|
|
1824
|
-
: renderFilePath(getObsFilePath(selObs));
|
|
1825
|
-
const _dur5orig = computeDurationMs(selObs);
|
|
1826
|
-
detailsSection = \`<div class="detail-sections" id="\${detailId}">
|
|
1827
|
-
\${filePathHtml}
|
|
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)}
|
|
1831
|
-
<div class="detail-section">
|
|
1832
|
-
<div class="detail-title">Input</div>
|
|
1833
|
-
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
1834
|
-
</div>
|
|
1835
|
-
<div class="detail-section">
|
|
1836
|
-
<div class="detail-title">Output</div>
|
|
1837
|
-
<pre class="detail-pre">\${esc(outputText)}</pre>
|
|
1838
|
-
</div>
|
|
1839
|
-
</div>\`;
|
|
1840
|
-
if (selObs.type === "GENERATION") setTimeout(() => resolveGenFilePath(selObs, detailId), 0);
|
|
1841
|
-
}
|
|
1842
|
-
} else {
|
|
1843
|
-
// Live traces: index 1 → traces[0], index 2 → traces[1], …
|
|
1844
|
-
const liveTrace = traces[window.step5SelectedTrace - 1];
|
|
1845
|
-
if (liveTrace) {
|
|
1846
|
-
const actions = Array.isArray(liveTrace.observations) ? liveTrace.observations : [];
|
|
1847
|
-
const traceIdx = window.step5SelectedTrace - 1;
|
|
1848
|
-
observationsTable += \`<div class="trace-section-title">Observations</div>
|
|
1849
|
-
<div class="observation-table-wrap">
|
|
1850
|
-
<table class="observation-table">
|
|
1851
|
-
<thead><tr><th>Name</th><th>Type</th><th style="width:80px;">Duration</th></tr></thead>
|
|
1852
|
-
<tbody>\`;
|
|
1853
|
-
observationsTable += actions.map((action, j) => {
|
|
1854
|
-
const isSelected = j === window.step5SelectedObservation;
|
|
1855
|
-
const name = action.name || action.id || ("Observation " + (j + 1));
|
|
1856
|
-
const type = action.type || "UNKNOWN";
|
|
1857
|
-
const typeClass = type === "TOOL" ? "tool" : "ai";
|
|
1858
|
-
const agentBadge = action.agentTaskIndex != null ? \`<span class="agent-task-badge">T\${action.agentTaskIndex + 1}</span>\` : '';
|
|
1859
|
-
const frozenBadge = action.isFrozen ? '<span class="frozen-tag">Frozen</span>' : '';
|
|
1860
|
-
const rowClasses = [
|
|
1861
|
-
isSelected ? 'selected' : '',
|
|
1862
|
-
action.agentTaskIndex != null ? 'agent-task-row' : '',
|
|
1863
|
-
action.isFrozen ? 'frozen-row' : '',
|
|
1864
|
-
].filter(Boolean).join(' ');
|
|
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>\`;
|
|
1866
|
-
}).join("");
|
|
1867
|
-
observationsTable += \`</tbody></table></div>\`;
|
|
1868
|
-
// Details for selected observation
|
|
1869
|
-
if (actions[window.step5SelectedObservation]) {
|
|
1870
|
-
const obs = actions[window.step5SelectedObservation];
|
|
1871
|
-
const inputText = toDisplayText(obs.input, obs.type);
|
|
1872
|
-
const outputText = toDisplayText(obs.output, obs.type);
|
|
1873
|
-
const detailId = 'step5-live-' + (window.step5SelectedTrace - 1) + '-' + window.step5SelectedObservation;
|
|
1874
|
-
const filePathHtml = obs.type === "GENERATION"
|
|
1875
|
-
? '<div class="file-path-placeholder"></div>'
|
|
1876
|
-
: renderFilePath(getObsFilePath(obs));
|
|
1877
|
-
|
|
1878
|
-
// For the first observation (workflow output), show Original Output for comparison
|
|
1879
|
-
let originalOutputSection = '';
|
|
1880
|
-
if (window.step5SelectedObservation === 0 && currentObservations[0]) {
|
|
1881
|
-
const originalOutputText = toDisplayText(currentObservations[0].output, currentObservations[0].type);
|
|
1882
|
-
originalOutputSection = \`<div class="detail-section">
|
|
1883
|
-
<div class="detail-title">Original Output</div>
|
|
1884
|
-
<pre class="detail-pre">\${esc(originalOutputText)}</pre>
|
|
1885
|
-
</div>\`;
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
// Agent steps: always show "Resume from Task X" button
|
|
1889
|
-
// Non-agent steps: show "Run from here" only if they have workflowEventId (HTTP/DB events)
|
|
1890
|
-
// Steps without agentTaskIndex and without workflowEventId get no button
|
|
1891
|
-
let runFromBpHtml = '';
|
|
1892
|
-
if (obs.agentTaskIndex != null) {
|
|
1893
|
-
runFromBpHtml = \`<div class="detail-section" style="padding:8px 12px;"><button class="resume-agent-btn" onclick="resumeAgentFromTask(\${traceIdx},\${window.step5SelectedObservation},\${obs.agentTaskIndex},event)">▶ Resume from Task \${obs.agentTaskIndex + 1}</button></div>\`;
|
|
1894
|
-
} else if (obs.workflowEventId != null) {
|
|
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>\`;
|
|
1896
|
-
}
|
|
1897
|
-
const _dur5live = computeDurationMs(obs);
|
|
1898
|
-
detailsSection = \`<div class="detail-sections" id="\${detailId}">
|
|
1899
|
-
\${filePathHtml}
|
|
1900
|
-
\${runFromBpHtml}
|
|
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)}
|
|
1904
|
-
<div class="detail-section">
|
|
1905
|
-
<div class="detail-title">Input</div>
|
|
1906
|
-
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
1907
|
-
</div>
|
|
1908
|
-
\${originalOutputSection}
|
|
1909
|
-
<div class="detail-section">
|
|
1910
|
-
<div class="detail-title">Output</div>
|
|
1911
|
-
<pre class="detail-pre">\${esc(outputText)}</pre>
|
|
1912
|
-
</div>
|
|
1913
|
-
</div>\`;
|
|
1914
|
-
if (obs.type === "GENERATION") setTimeout(() => resolveGenFilePath(obs, detailId), 0);
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
if (step5RunMeta.loading) {
|
|
1920
|
-
detailsSection = \`<div class="detail-sections">
|
|
1921
|
-
<div class="detail-section">
|
|
1922
|
-
<div class="detail-title">Validation</div>
|
|
1923
|
-
<pre class="detail-pre">Running \${step5RunMeta.runCount || 1} workflow run(s) in \${step5RunMeta.sequential ? 'sequence' : 'parallel'} mode...</pre>
|
|
1924
|
-
</div>
|
|
1925
|
-
</div>\`;
|
|
1926
|
-
} else if (step5RunMeta.error && !detailsSection) {
|
|
1927
|
-
detailsSection = \`<div class="detail-sections">
|
|
1928
|
-
<div class="detail-section">
|
|
1929
|
-
<div class="detail-title">Validation Error</div>
|
|
1930
|
-
<pre class="detail-pre">\${esc(step5RunMeta.error)}</pre>
|
|
1931
|
-
</div>
|
|
1932
|
-
</div>\`;
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
// Render 3 sibling columns inside the CSS grid
|
|
1936
|
-
const traceLayout = document.getElementsByClassName("trace-layout")[0];
|
|
1937
|
-
traceLayout.innerHTML = \`
|
|
1938
|
-
<div class="trace-left">\${traceTable}</div>
|
|
1939
|
-
<div class="trace-left">\${observationsTable || '<div class="trace-section-title">Observations</div>'}</div>
|
|
1940
|
-
<div class="trace-right">\${detailsSection}</div>
|
|
1941
|
-
\`;
|
|
1942
|
-
// Restore scroll positions after DOM rebuild
|
|
1943
|
-
const _newTls = traceLayout.querySelectorAll('.trace-left');
|
|
1944
|
-
const _w1 = _newTls[0]?.querySelector('.observation-table-wrap');
|
|
1945
|
-
const _w2 = _newTls[1]?.querySelector('.observation-table-wrap');
|
|
1946
|
-
if (_w1) _w1.scrollTop = _step5Col1Scroll;
|
|
1947
|
-
if (_w2) _w2.scrollTop = _step5Col2Scroll;
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
if (currentStep === 4) {
|
|
1952
|
-
const traceLayout = document.getElementsByClassName("trace-layout")[0];
|
|
1953
|
-
traceLayout.classList.remove("step-5");
|
|
1954
|
-
traceLayout.classList.add("step-4");
|
|
1955
|
-
const obsIndices = Array.from(checkedObservations);
|
|
1956
|
-
|
|
1957
|
-
// Column 1: Steps (observation list)
|
|
1958
|
-
let col1 = \`<div class="trace-section-title">Steps</div>
|
|
1959
|
-
<div class="observation-table-wrap">
|
|
1960
|
-
<table class="observation-table">
|
|
1961
|
-
<thead><tr><th>Name</th><th>Type</th><th style="width:80px;">Duration</th></tr></thead>
|
|
1962
|
-
<tbody>\`;
|
|
1963
|
-
col1 += obsIndices.map(idx => {
|
|
1964
|
-
const obs = currentObservations[idx];
|
|
1965
|
-
const isSelected = idx === selectedObservationIndex;
|
|
1966
|
-
const name = obs.name || obs.id || ('Observation ' + (idx + 1));
|
|
1967
|
-
const type = obs.type || 'UNKNOWN';
|
|
1968
|
-
const typeClass = type === 'TOOL' ? 'tool' : 'ai';
|
|
1969
|
-
const history = rerunHistory.get(idx) || [];
|
|
1970
|
-
const latest = history[history.length - 1];
|
|
1971
|
-
const badge = latest
|
|
1972
|
-
? (latest.running ? ' <span class="rerun-status running">⟳</span>'
|
|
1973
|
-
: latest.ok ? ' <span class="rerun-status success">✓</span>'
|
|
1974
|
-
: ' <span class="rerun-status error">✗</span>')
|
|
1975
|
-
: '';
|
|
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>\`;
|
|
1977
|
-
}).join('');
|
|
1978
|
-
col1 += \`</tbody></table></div>\`;
|
|
1979
|
-
|
|
1980
|
-
// Column 2: Runs for the selected observation
|
|
1981
|
-
const inFlight = selectedObservationIndex >= 0 && rerunInFlight.has(selectedObservationIndex);
|
|
1982
|
-
let col2 = \`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
|
|
1983
|
-
<div class="trace-section-title" style="margin-bottom:0">Runs</div>
|
|
1984
|
-
\${selectedObservationIndex >= 0 ? \`<button class="btn btn-primary" style="padding:4px 12px;font-size:12px;" onclick="rerunObservation(\${selectedObservationIndex})" \${inFlight ? 'disabled' : ''}>\${inFlight ? 'Running...' : 'Rerun'}</button>\` : ''}
|
|
1985
|
-
</div>\`;
|
|
1986
|
-
if (selectedObservationIndex >= 0) {
|
|
1987
|
-
const history = rerunHistory.get(selectedObservationIndex) || [];
|
|
1988
|
-
if (history.length === 0) {
|
|
1989
|
-
col2 += \`<div style="color:#999;font-size:13px;padding:8px;">No runs yet. Click Rerun to start.</div>\`;
|
|
1990
|
-
} else {
|
|
1991
|
-
col2 += \`<div class="observation-table-wrap"><table class="observation-table"><thead><tr><th>Run</th><th>Status</th></tr></thead><tbody>\`;
|
|
1992
|
-
col2 += history.map((run, ri) => {
|
|
1993
|
-
const isSelRun = ri === step4SelectedRun;
|
|
1994
|
-
const status = run.running ? '⟳ Running' : (run.ok ? '✓ Complete' : '✗ Failed');
|
|
1995
|
-
return \`<tr class="\${isSelRun ? 'selected' : ''}" onclick="window.step4SelectRun(\${ri})"><td>Run \${run.runNumber}</td><td>\${status}</td></tr>\`;
|
|
1996
|
-
}).join('');
|
|
1997
|
-
col2 += \`</tbody></table></div>\`;
|
|
1998
|
-
}
|
|
1999
|
-
} else {
|
|
2000
|
-
col2 += \`<div style="color:#999;font-size:13px;padding:8px;">Select a step to view runs.</div>\`;
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
// Column 3: Details for the selected run
|
|
2004
|
-
let col3 = '';
|
|
2005
|
-
if (selectedObservationIndex >= 0) {
|
|
2006
|
-
const obs = currentObservations[selectedObservationIndex];
|
|
2007
|
-
const history = rerunHistory.get(selectedObservationIndex) || [];
|
|
2008
|
-
const run = step4SelectedRun >= 0 ? history[step4SelectedRun] : null;
|
|
2009
|
-
const inputText = toDisplayText(obs.input, obs.type);
|
|
2010
|
-
const outputText = toDisplayText(obs.output, obs.type);
|
|
2011
|
-
const detailId = 'step4-detail-' + selectedObservationIndex + '-' + step4SelectedRun;
|
|
2012
|
-
const filePathHtml = obs.type === "GENERATION"
|
|
2013
|
-
? '<div class="file-path-placeholder"></div>'
|
|
2014
|
-
: renderFilePath(getObsFilePath(obs));
|
|
2015
|
-
let currentOutputSection = '';
|
|
2016
|
-
if (run) {
|
|
2017
|
-
if (run.running) {
|
|
2018
|
-
currentOutputSection = \`<div class="detail-section"><div class="detail-title">Current Output</div><pre class="detail-pre">Running...</pre></div>\`;
|
|
2019
|
-
} else if (run.ok) {
|
|
2020
|
-
currentOutputSection = \`<div class="detail-section"><div class="detail-title">Current Output</div><pre class="detail-pre">\${esc(toDisplayText(run.output, obs.type))}</pre></div>\`;
|
|
2021
|
-
} else {
|
|
2022
|
-
currentOutputSection = \`<div class="detail-section"><div class="detail-title">Current Output</div><pre class="detail-pre">Rerun failed: \${esc(run.error || 'Unknown error')}</pre></div>\`;
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
// Editable AI input UI
|
|
2026
|
-
let inputSection = '';
|
|
2027
|
-
if (obs.type === "GENERATION") {
|
|
2028
|
-
const hasUpdate = updatedInputs.has(selectedObservationIndex);
|
|
2029
|
-
inputSection += \`<div class="detail-section">
|
|
2030
|
-
<div class="detail-title">Input</div>
|
|
2031
|
-
<pre class="detail-pre" style="position:relative;">\${esc(toDisplayText(obs.input, obs.type))}
|
|
2032
|
-
<button class="btn btn-secondary edit-btn" style="position:absolute;top:8px;right:8px;padding:2px 10px;font-size:12px;\${hasUpdate ? 'display:none;' : ''}" onclick="window.enableInputEditing('\${detailId}', \${selectedObservationIndex})">Edit</button>
|
|
2033
|
-
<button class="btn btn-secondary reset-btn" style="position:absolute;top:8px;right:8px;padding:2px 10px;font-size:12px;\${hasUpdate ? '' : 'display:none;'}" onclick="window.resetInput('\${detailId}', \${selectedObservationIndex})">Reset</button>
|
|
2034
|
-
</pre>
|
|
2035
|
-
</div>\`;
|
|
2036
|
-
if (hasUpdate) {
|
|
2037
|
-
inputSection += \`<div class="detail-section updated-input-section">
|
|
2038
|
-
<div class="detail-title">Update Input</div>
|
|
2039
|
-
<textarea id="editInputTextarea" class="detail-pre" style="width:100%;height:400px;">\${esc(updatedInputs.get(selectedObservationIndex))}</textarea>
|
|
2040
|
-
<button class="btn btn-secondary save-btn" style="margin-top:8px;float:right;padding:2px 10px;font-size:12px;" onclick="window.saveUpdatedInput(\${selectedObservationIndex})">Save</button>
|
|
2041
|
-
</div>\`;
|
|
2042
|
-
}
|
|
2043
|
-
} else {
|
|
2044
|
-
inputSection += \`<div class="detail-section"><div class="detail-title">Input</div><pre class="detail-pre">\${esc(toDisplayText(obs.input, obs.type))}</pre></div>\`;
|
|
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
|
-
: '';
|
|
2057
|
-
col3 = \`<div class="detail-sections" id="\${detailId}">
|
|
2058
|
-
\${filePathHtml}
|
|
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}
|
|
2064
|
-
\${inputSection}
|
|
2065
|
-
<div class="detail-section"><div class="detail-title">Output</div><pre class="detail-pre">\${esc(outputText)}</pre></div>
|
|
2066
|
-
\${currentOutputSection}
|
|
2067
|
-
</div>\`;
|
|
2068
|
-
if (obs.type === "GENERATION") setTimeout(() => resolveGenFilePath(obs, detailId), 0);
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
traceLayout.innerHTML = \`
|
|
2072
|
-
<div class="trace-left">\${col1}</div>
|
|
2073
|
-
<div class="trace-left">\${col2}</div>
|
|
2074
|
-
<div class="trace-right">\${col3}</div>
|
|
2075
|
-
\`;
|
|
2076
|
-
// Auto-select first observation if none selected
|
|
2077
|
-
if (obsIndices.length > 0 && selectedObservationIndex === -1) {
|
|
2078
|
-
window.step4SelectObservation(obsIndices[0]);
|
|
2079
|
-
}
|
|
2080
|
-
return;
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
const traceLayoutEl = document.getElementsByClassName("trace-layout")[0];
|
|
2084
|
-
if (traceLayoutEl.classList.contains("step-5") || traceLayoutEl.classList.contains("step-4")) {
|
|
2085
|
-
traceLayoutEl.classList.remove("step-5");
|
|
2086
|
-
traceLayoutEl.classList.remove("step-4");
|
|
2087
|
-
let headerHtml = '';
|
|
2088
|
-
if (currentStep === 3) {
|
|
2089
|
-
headerHtml = '<tr><th style="width: 40px;">Check</th><th>Name</th><th>Type</th></tr>';
|
|
2090
|
-
}
|
|
2091
|
-
traceLayoutEl.innerHTML = \`
|
|
2092
|
-
<div class="trace-left">
|
|
2093
|
-
<div class="trace-section-title">Observations</div>
|
|
2094
|
-
<div class="observation-table-wrap">
|
|
2095
|
-
<table class="observation-table">
|
|
2096
|
-
<thead id="observationTableHead">\${headerHtml}</thead>
|
|
2097
|
-
<tbody id="observationTableBody"></tbody>
|
|
2098
|
-
</table>
|
|
2099
|
-
</div>
|
|
2100
|
-
</div>
|
|
2101
|
-
<div class="trace-right">
|
|
2102
|
-
<div id="observationDetail"></div>
|
|
2103
|
-
</div>
|
|
2104
|
-
\`;
|
|
2105
|
-
observationTableBody = document.getElementById("observationTableBody");
|
|
2106
|
-
observationDetail = document.getElementById("observationDetail");
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
const obsToRender = currentObservations;
|
|
2110
|
-
const indices = currentObservations.map((_, i) => i);
|
|
2111
|
-
|
|
2112
|
-
if (!obsToRender.length) {
|
|
2113
|
-
observationTableBody.innerHTML = '<tr><td colspan="3" style="padding: 16px; color: #777;">No observations found.</td></tr>';
|
|
2114
|
-
return;
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
observationTableBody.innerHTML = obsToRender.map((obs, displayIndex) => {
|
|
2118
|
-
const actualIndex = indices[displayIndex];
|
|
2119
|
-
const isSelected = actualIndex === selectedObservationIndex;
|
|
2120
|
-
const isChecked = checkedObservations.has(actualIndex);
|
|
2121
|
-
const name = obs.name || obs.id || ("Observation " + (displayIndex + 1));
|
|
2122
|
-
const type = obs.type || "UNKNOWN";
|
|
2123
|
-
const typeClass = type === "TOOL" ? "tool" : "ai";
|
|
2124
|
-
// Step 3: Mark broken - show checkboxes
|
|
2125
|
-
return \`<tr class="\${isSelected ? "selected" : ""}">
|
|
2126
|
-
<td style="width: 40px;"><input type="checkbox" class="obs-checkbox" value="\${actualIndex}" \${isChecked ? "checked" : ""}></td>
|
|
2127
|
-
<td onclick="selectObservation(\${actualIndex})">\${esc(name)}</td>
|
|
2128
|
-
<td><span class="obs-type \${typeClass}">\${esc(type)}</span></td>
|
|
2129
|
-
<td style="color:#888;font-size:12px;">\${formatDuration(computeDurationMs(obs))}</td>
|
|
2130
|
-
</tr>\`;
|
|
2131
|
-
}).join("");
|
|
2132
|
-
|
|
2133
|
-
document.querySelectorAll(".obs-checkbox").forEach(checkbox => {
|
|
2134
|
-
checkbox.onchange = (e) => {
|
|
2135
|
-
const idx = parseInt(e.target.value);
|
|
2136
|
-
if (e.target.checked) {
|
|
2137
|
-
checkedObservations.add(idx);
|
|
2138
|
-
} else {
|
|
2139
|
-
checkedObservations.delete(idx);
|
|
2140
|
-
}
|
|
2141
|
-
};
|
|
2142
|
-
});
|
|
2143
|
-
// Restore scroll position if previously saved
|
|
2144
|
-
if (currentStep === 5 && prevStep5ObsScrollTop !== null) {
|
|
2145
|
-
// After rendering, restore scroll for the observations table in the second .trace-left
|
|
2146
|
-
const traceLefts = document.querySelectorAll('.trace-left');
|
|
2147
|
-
if (traceLefts.length > 1) {
|
|
2148
|
-
const obsWrap = traceLefts[1].querySelector('.observation-table-wrap');
|
|
2149
|
-
if (obsWrap) obsWrap.scrollTop = prevStep5ObsScrollTop;
|
|
2150
|
-
}
|
|
2151
|
-
} else {
|
|
2152
|
-
obsTableWrap = document.querySelector('.observation-table-wrap');
|
|
2153
|
-
if (obsTableWrap && prevScrollTop !== null) {
|
|
2154
|
-
obsTableWrap.scrollTop = prevScrollTop;
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
function selectObservation(index) {
|
|
2160
|
-
// Preserve scroll position of observation-table-wrap
|
|
2161
|
-
let obsTableWrap = document.querySelector('.observation-table-wrap');
|
|
2162
|
-
let prevScrollTop = obsTableWrap ? obsTableWrap.scrollTop : null;
|
|
2163
|
-
selectedObservationIndex = index;
|
|
2164
|
-
renderObservationTable();
|
|
2165
|
-
// Restore scroll position after rendering
|
|
2166
|
-
obsTableWrap = document.querySelector('.observation-table-wrap');
|
|
2167
|
-
if (obsTableWrap && prevScrollTop !== null) {
|
|
2168
|
-
obsTableWrap.scrollTop = prevScrollTop;
|
|
2169
|
-
}
|
|
2170
|
-
const obs = currentObservations[index];
|
|
2171
|
-
const inputText = toDisplayText(obs.input, obs.type);
|
|
2172
|
-
const outputText = toDisplayText(obs.output, obs.type);
|
|
2173
|
-
const detailId = 'obs-detail-' + index;
|
|
2174
|
-
const filePathHtml = obs.type === "GENERATION"
|
|
2175
|
-
? '<div class="file-path-placeholder"></div>'
|
|
2176
|
-
: renderFilePath(getObsFilePath(obs));
|
|
2177
|
-
|
|
2178
|
-
const _dur3 = computeDurationMs(obs);
|
|
2179
|
-
observationDetail.innerHTML = \`<div class="detail-sections" id="\${detailId}">
|
|
2180
|
-
\${filePathHtml}
|
|
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)}
|
|
2184
|
-
<div class="detail-section">
|
|
2185
|
-
<div class="detail-title">Input</div>
|
|
2186
|
-
<pre class="detail-pre">\${esc(inputText)}</pre>
|
|
2187
|
-
</div>
|
|
2188
|
-
<div class="detail-section">
|
|
2189
|
-
<div class="detail-title">Output</div>
|
|
2190
|
-
<pre class="detail-pre">\${esc(outputText)}</pre>
|
|
2191
|
-
</div>
|
|
2192
|
-
</div>\`;
|
|
2193
|
-
if (obs.type === "GENERATION") resolveGenFilePath(obs, detailId);
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
async function rerunObservation(index) {
|
|
2197
|
-
const obs = currentObservations[index];
|
|
2198
|
-
let inputToUse = obs.input;
|
|
2199
|
-
if (obs.type === "GENERATION" && updatedInputs.has(index)) {
|
|
2200
|
-
inputToUse = updatedInputs.get(index);
|
|
2201
|
-
}
|
|
2202
|
-
// Proceed with rerun logic using inputToUse
|
|
2203
|
-
console.log('Rerunning observation with input:', inputToUse);
|
|
2204
|
-
const history = rerunHistory.get(index) || [];
|
|
2205
|
-
const newRun = { runNumber: history.length + 1, running: true, ok: null };
|
|
2206
|
-
history.push(newRun);
|
|
2207
|
-
rerunHistory.set(index, history);
|
|
2208
|
-
rerunInFlight.add(index);
|
|
2209
|
-
if (selectedObservationIndex === index) {
|
|
2210
|
-
step4SelectedRun = history.length - 1;
|
|
2211
|
-
}
|
|
2212
|
-
renderObservationTable();
|
|
2213
|
-
try {
|
|
2214
|
-
const payload = { observation: { ...obs, input: inputToUse } };
|
|
2215
|
-
const response = await fetch('/api/rerun-observation', {
|
|
2216
|
-
method: 'POST',
|
|
2217
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2218
|
-
body: JSON.stringify(payload),
|
|
2219
|
-
});
|
|
2220
|
-
const data = await response.json();
|
|
2221
|
-
newRun.running = false;
|
|
2222
|
-
if (response.ok && data.ok) {
|
|
2223
|
-
newRun.ok = true;
|
|
2224
|
-
newRun.output = data.currentOutput;
|
|
2225
|
-
newRun.currentDurationMs = data.currentDurationMs ?? null;
|
|
2226
|
-
newRun.currentUsage = data.currentUsage ?? null;
|
|
2227
|
-
} else {
|
|
2228
|
-
newRun.ok = false;
|
|
2229
|
-
newRun.error = data.error || 'Rerun failed.';
|
|
2230
|
-
}
|
|
2231
|
-
} catch (err) {
|
|
2232
|
-
newRun.running = false;
|
|
2233
|
-
newRun.ok = false;
|
|
2234
|
-
newRun.error = err && err.message ? err.message : String(err);
|
|
2235
|
-
} finally {
|
|
2236
|
-
rerunInFlight.delete(index);
|
|
2237
|
-
renderObservationTable();
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
|
-
async function runFromBreakpoint(traceIdx, obsIdx, evt) {
|
|
2242
|
-
const btn = evt.target;
|
|
2243
|
-
const liveTrace = step5RunTraces[traceIdx];
|
|
2244
|
-
if (!liveTrace) return;
|
|
2245
|
-
const obs = liveTrace.observations[obsIdx];
|
|
2246
|
-
if (!obs || obs.workflowEventId == null) return;
|
|
2247
|
-
|
|
2248
|
-
btn.disabled = true;
|
|
2249
|
-
btn.textContent = 'Running…';
|
|
2250
|
-
|
|
2251
|
-
try {
|
|
2252
|
-
const payload = {
|
|
2253
|
-
workflowName: selectedWorkflow ? selectedWorkflow.name : '',
|
|
2254
|
-
checkpoint: obs.workflowEventId,
|
|
2255
|
-
snapshotId: liveTrace.snapshotId,
|
|
2256
|
-
observations: currentObservations,
|
|
2257
|
-
toolMockConfig: window._toolMockConfig || {},
|
|
2258
|
-
};
|
|
2259
|
-
const response = await fetch('/api/run-from-breakpoint', {
|
|
2260
|
-
method: 'POST',
|
|
2261
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2262
|
-
body: JSON.stringify(payload),
|
|
2263
|
-
});
|
|
2264
|
-
const data = await response.json();
|
|
2265
|
-
if (response.ok) {
|
|
2266
|
-
// Sub-trace naming: "Trace-N-M" where N = parent name, M = resume count from parent
|
|
2267
|
-
const parentTraceName = liveTrace.traceName ?? \`Trace-\${traceIdx + 1}\`;
|
|
2268
|
-
const siblingCount = step5RunTraces.filter(t => t.parentTraceName === parentTraceName).length;
|
|
2269
|
-
const traceName = \`\${parentTraceName}-\${siblingCount + 1}\`;
|
|
2270
|
-
const newTrace = { ...data, runNumber: step5RunTraces.length + 1, traceName, parentTraceName };
|
|
2271
|
-
step5RunTraces.push(newTrace);
|
|
2272
|
-
persistTraces();
|
|
2273
|
-
renderObservationTable();
|
|
2274
|
-
} else {
|
|
2275
|
-
btn.textContent = '✗ ' + (data.error || 'Failed');
|
|
2276
|
-
btn.disabled = false;
|
|
2277
|
-
}
|
|
2278
|
-
} catch (err) {
|
|
2279
|
-
btn.textContent = '✗ Error';
|
|
2280
|
-
btn.disabled = false;
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
async function resumeAgentFromTask(traceIdx, obsIdx, taskIndex, evt) {
|
|
2285
|
-
const btn = evt.target;
|
|
2286
|
-
const liveTrace = step5RunTraces[traceIdx];
|
|
2287
|
-
if (!liveTrace) return;
|
|
2288
|
-
|
|
2289
|
-
// Extract AgentPlan from the workflow's currentOutput
|
|
2290
|
-
const currentOutput = liveTrace.currentOutput;
|
|
2291
|
-
const agentPlan = currentOutput && typeof currentOutput === 'object' && Array.isArray(currentOutput.tasks)
|
|
2292
|
-
? currentOutput
|
|
2293
|
-
: null;
|
|
2294
|
-
if (!agentPlan) {
|
|
2295
|
-
alert('No agent plan found in this trace. Ensure the workflow returns an AgentPlan from executorAgent() or resumeAgentFromTrace().');
|
|
2296
|
-
return;
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
btn.disabled = true;
|
|
2300
|
-
btn.textContent = 'Resuming…';
|
|
2301
|
-
|
|
2302
|
-
try {
|
|
2303
|
-
const payload = {
|
|
2304
|
-
workflowName: selectedWorkflow ? selectedWorkflow.name : '',
|
|
2305
|
-
taskIndex: taskIndex,
|
|
2306
|
-
agentState: {
|
|
2307
|
-
plan: agentPlan,
|
|
2308
|
-
trace: [],
|
|
2309
|
-
resumeFromTaskIndex: taskIndex,
|
|
2310
|
-
},
|
|
2311
|
-
snapshotId: liveTrace.snapshotId,
|
|
2312
|
-
toolMockConfig: window._toolMockConfig || {},
|
|
2313
|
-
};
|
|
2314
|
-
const response = await fetch('/api/resume-agent-from-task', {
|
|
2315
|
-
method: 'POST',
|
|
2316
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2317
|
-
body: JSON.stringify(payload),
|
|
2318
|
-
});
|
|
2319
|
-
const data = await response.json();
|
|
2320
|
-
if (response.ok) {
|
|
2321
|
-
// Sub-trace naming: "Trace-N-M" where N = parent name, M = resume count from parent
|
|
2322
|
-
const parentTraceName = liveTrace.traceName ?? \`Trace-\${traceIdx + 1}\`;
|
|
2323
|
-
const siblingCount = step5RunTraces.filter(t => t.parentTraceName === parentTraceName).length;
|
|
2324
|
-
const traceName = \`\${parentTraceName}-\${siblingCount + 1}\`;
|
|
2325
|
-
const newTrace = { ...data, runNumber: step5RunTraces.length + 1, traceName, parentTraceName };
|
|
2326
|
-
step5RunTraces.push(newTrace);
|
|
2327
|
-
persistTraces();
|
|
2328
|
-
renderObservationTable();
|
|
2329
|
-
} else {
|
|
2330
|
-
btn.textContent = '✗ ' + (data.error || 'Failed');
|
|
2331
|
-
btn.disabled = false;
|
|
2332
|
-
}
|
|
2333
|
-
} catch (err) {
|
|
2334
|
-
btn.textContent = '✗ Error';
|
|
2335
|
-
btn.disabled = false;
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
window.enableInputEditing = function(detailId, index) {
|
|
2340
|
-
const obs = currentObservations[index];
|
|
2341
|
-
const current = updatedInputs.has(index)
|
|
2342
|
-
? updatedInputs.get(index)
|
|
2343
|
-
: toDisplayText(obs.input, obs.type);
|
|
2344
|
-
updatedInputs.set(index, current);
|
|
2345
|
-
renderObservationTable();
|
|
2346
|
-
};
|
|
2347
|
-
|
|
2348
|
-
window.resetInput = function(detailId, index) {
|
|
2349
|
-
updatedInputs.delete(index);
|
|
2350
|
-
renderObservationTable();
|
|
2351
|
-
};
|
|
2352
|
-
|
|
2353
|
-
window.saveUpdatedInput = function(index) {
|
|
2354
|
-
const ta = document.getElementById('editInputTextarea');
|
|
2355
|
-
if (!ta) return;
|
|
2356
|
-
updatedInputs.set(index, ta.value);
|
|
2357
|
-
renderObservationTable();
|
|
2358
|
-
};
|
|
2359
|
-
|
|
2360
|
-
function getObsFilePath(obs) {
|
|
2361
|
-
if (obs && obs.type === "TOOL") {
|
|
2362
|
-
const tool = codeIndex.tools.find(t => t.name === obs.name);
|
|
2363
|
-
if (tool) return tool.lineNumber ? tool.filePath + ':' + tool.lineNumber : tool.filePath;
|
|
2364
|
-
}
|
|
2365
|
-
return null;
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
function extractSystemPrompt(obs) {
|
|
2369
|
-
if (!obs || obs.type !== "GENERATION") return null;
|
|
2370
|
-
let input = obs.input;
|
|
2371
|
-
// Parse input if it's a JSON string
|
|
2372
|
-
if (
|
|
2373
|
-
typeof input === 'string' &&
|
|
2374
|
-
(input.startsWith('{') || input.startsWith('[')) &&
|
|
2375
|
-
(input.endsWith('}') || input.endsWith(']'))
|
|
2376
|
-
) {
|
|
2377
|
-
try {
|
|
2378
|
-
input = JSON.parse(input);
|
|
2379
|
-
} catch { /* not JSON */ }
|
|
2380
|
-
}
|
|
2381
|
-
const messages = Array.isArray(input) ? input
|
|
2382
|
-
: (input && Array.isArray(input.messages)) ? input.messages : [];
|
|
2383
|
-
const sys = messages.find(m => m && m.role === "system");
|
|
2384
|
-
if (!sys || !sys.content) return null;
|
|
2385
|
-
return String(sys.content).trim();
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
|
-
function getSearchFragments(systemPrompt) {
|
|
2389
|
-
if (!systemPrompt || systemPrompt.length < 10) return [];
|
|
2390
|
-
const fragments = [];
|
|
2391
|
-
|
|
2392
|
-
// 1. First sentence (often unique: "You are an expert SQL generator...")
|
|
2393
|
-
const firstSentence = systemPrompt.split(/[.!?]\\n/)[0].trim();
|
|
2394
|
-
if (firstSentence.length >= 20 && firstSentence.length <= 100) {
|
|
2395
|
-
fragments.push(firstSentence);
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
// 2. First 50 chars
|
|
2399
|
-
if (systemPrompt.length >= 40) {
|
|
2400
|
-
fragments.push(systemPrompt.slice(0, 50).trim());
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
// 3. First 80 chars (if different from above)
|
|
2404
|
-
if (systemPrompt.length >= 80) {
|
|
2405
|
-
const fragment80 = systemPrompt.slice(0, 80).trim();
|
|
2406
|
-
if (!fragments.some(f => fragment80.startsWith(f))) {
|
|
2407
|
-
fragments.push(fragment80);
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
// 4. Try to find a unique phrase after common prefixes
|
|
2412
|
-
const afterYouAre = systemPrompt.match(/You are (?:an? )?([^.\\n]{15,60})/);
|
|
2413
|
-
if (afterYouAre && afterYouAre[1]) {
|
|
2414
|
-
fragments.push(afterYouAre[1].trim());
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
return fragments.filter(f => f.length >= 15);
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
async function resolveGenFilePath(obs, containerId) {
|
|
2421
|
-
const systemPrompt = extractSystemPrompt(obs);
|
|
2422
|
-
if (!systemPrompt) return;
|
|
2423
|
-
|
|
2424
|
-
const fragments = getSearchFragments(systemPrompt);
|
|
2425
|
-
if (fragments.length === 0) return;
|
|
2426
|
-
|
|
2427
|
-
try {
|
|
2428
|
-
// Try each fragment until we find a match
|
|
2429
|
-
for (const fragment of fragments) {
|
|
2430
|
-
const res = await fetch('/api/search-source?q=' + encodeURIComponent(fragment));
|
|
2431
|
-
const data = await res.json();
|
|
2432
|
-
if (data.filePath) {
|
|
2433
|
-
const label = data.lineNumber ? data.filePath + ':' + data.lineNumber : data.filePath;
|
|
2434
|
-
const container = document.getElementById(containerId);
|
|
2435
|
-
if (!container) return;
|
|
2436
|
-
const placeholder = container.querySelector('.file-path-placeholder');
|
|
2437
|
-
if (placeholder) placeholder.outerHTML = renderFilePath(label);
|
|
2438
|
-
return; // Success, stop trying
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
} catch { /* ignore network errors */ }
|
|
2442
|
-
}
|
|
2443
|
-
|
|
2444
|
-
function stripAbsolutePath(filePath) {
|
|
2445
|
-
console.log('stripAbsolutePath called with:', filePath, 'repoRoot:', repoRoot);
|
|
2446
|
-
if (!filePath) return filePath;
|
|
2447
|
-
|
|
2448
|
-
// If we have a repo root, strip it from the absolute path
|
|
2449
|
-
if (repoRoot) {
|
|
2450
|
-
// Normalize path separators and handle case-insensitive filesystems
|
|
2451
|
-
let normalizedRoot = repoRoot.replace(/\\\\/g, '/').toLowerCase();
|
|
2452
|
-
let normalizedPath = filePath.replace(/\\\\/g, '/').toLowerCase();
|
|
2453
|
-
|
|
2454
|
-
// Ensure root ends with / for proper matching
|
|
2455
|
-
if (!normalizedRoot.endsWith('/')) {
|
|
2456
|
-
normalizedRoot += '/';
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
if (normalizedPath.startsWith(normalizedRoot)) {
|
|
2460
|
-
// Return the original case of the file path after the root
|
|
2461
|
-
return filePath.replace(/\\\\/g, '/').substring(repoRoot.length).replace(/^\\//, '');
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
// Also try without trailing slash
|
|
2465
|
-
normalizedRoot = normalizedRoot.slice(0, -1);
|
|
2466
|
-
if (normalizedPath.startsWith(normalizedRoot + '/')) {
|
|
2467
|
-
return filePath.replace(/\\\\/g, '/').substring(repoRoot.length).replace(/^\\//, '');
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
// Fallback if repo root not available or path doesn't match
|
|
2472
|
-
return filePath;
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
function renderFilePath(filePath) {
|
|
2476
|
-
if (!filePath) return '';
|
|
2477
|
-
const cleanPath = stripAbsolutePath(filePath);
|
|
2478
|
-
return \`<div class="detail-section">
|
|
2479
|
-
<div class="detail-title">File Path</div>
|
|
2480
|
-
<pre class="detail-pre">\${esc(cleanPath)}</pre>
|
|
2481
|
-
</div>\`;
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
function renderModel(obs) {
|
|
2485
|
-
if (!obs || obs.type !== "GENERATION" || !obs.model) return '';
|
|
2486
|
-
let label = obs.model;
|
|
2487
|
-
if (obs.modelParameters) {
|
|
2488
|
-
const parts = [];
|
|
2489
|
-
if (obs.modelParameters.temperature != null) parts.push('temp=' + obs.modelParameters.temperature);
|
|
2490
|
-
if (obs.modelParameters.max_tokens != null) parts.push('max_tokens=' + obs.modelParameters.max_tokens);
|
|
2491
|
-
if (parts.length) label += ' (' + parts.join(', ') + ')';
|
|
2492
|
-
}
|
|
2493
|
-
return \`<div class="detail-section">
|
|
2494
|
-
<div class="detail-title">Model</div>
|
|
2495
|
-
<pre class="detail-pre">\${esc(label)}</pre>
|
|
2496
|
-
</div>\`;
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
function stripMarkdownCodeFence(text) {
|
|
2500
|
-
// Remove markdown code fences like \`\`\`sql, \`\`\`json, \`\`\`, etc.
|
|
2501
|
-
return text.replace(/^\`\`\`[\\w]*\\n?/gm, '').replace(/\\n?\`\`\`\$/gm, '').trim();
|
|
2502
|
-
}
|
|
2503
|
-
|
|
2504
|
-
function toDisplayText(value, type) {
|
|
2505
|
-
if (value === null || value === undefined || value === "") {
|
|
2506
|
-
return "No data";
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
// If it's a string, try to parse it first (might be JSON)
|
|
2510
|
-
if (typeof value === "string") {
|
|
2511
|
-
if (value.startsWith('[') || value.startsWith('{')) {
|
|
2512
|
-
try {
|
|
2513
|
-
const parsed = JSON.parse(value);
|
|
2514
|
-
value = parsed; // Use parsed version for further processing
|
|
2515
|
-
} catch {
|
|
2516
|
-
// Not JSON, strip markdown and return
|
|
2517
|
-
return stripMarkdownCodeFence(value);
|
|
2518
|
-
}
|
|
2519
|
-
} else {
|
|
2520
|
-
return stripMarkdownCodeFence(value);
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
// GENERATION input: raw messages array
|
|
2525
|
-
if (type === "GENERATION" && Array.isArray(value)) {
|
|
2526
|
-
return JSON.stringify(value, null, 2);
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
// GENERATION input wrapped in {messages:[...]} (legacy / Langfuse format)
|
|
2530
|
-
if (type === "GENERATION" && value.messages) {
|
|
2531
|
-
return JSON.stringify(value.messages, null, 2);
|
|
2532
|
-
}
|
|
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
|
-
|
|
2564
|
-
// For other objects, stringify them
|
|
2565
|
-
try {
|
|
2566
|
-
return JSON.stringify(value, null, 2);
|
|
2567
|
-
} catch {
|
|
2568
|
-
return String(value);
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
function resetTraceModal() {
|
|
2573
|
-
uploadArea.classList.remove("hidden");
|
|
2574
|
-
traceViewer.classList.remove("visible");
|
|
2575
|
-
let customFooter = document.getElementById("step5FooterBtns");
|
|
2576
|
-
if (customFooter) customFooter.remove();
|
|
2577
|
-
uploadStatus.className = "upload-status";
|
|
2578
|
-
uploadStatus.textContent = "";
|
|
2579
|
-
fileInput.value = "";
|
|
2580
|
-
currentObservations = [];
|
|
2581
|
-
selectedObservationIndex = -1;
|
|
2582
|
-
checkedObservations.clear();
|
|
2583
|
-
rerunHistory.clear();
|
|
2584
|
-
rerunInFlight.clear();
|
|
2585
|
-
step4SelectedRun = -1;
|
|
2586
|
-
step5RunTraces = [];
|
|
2587
|
-
localStorage.removeItem('ed_step5RunTraces');
|
|
2588
|
-
step5RunMeta = { loading: false, error: '', runCount: 0, sequential: false };
|
|
2589
|
-
observationTableBody.innerHTML = "";
|
|
2590
|
-
observationDetail.innerHTML = "";
|
|
2591
|
-
currentStep = 0;
|
|
2592
|
-
updateModalTitle();
|
|
2593
|
-
}
|
|
2594
|
-
|
|
2595
|
-
function updateModalTitle() {
|
|
2596
|
-
const titles = {
|
|
2597
|
-
0: "Step 2: Import Failed Trace",
|
|
2598
|
-
3: "Step 3: Mark broken step",
|
|
2599
|
-
4: "Step 4: Validate your Fixes",
|
|
2600
|
-
5: "Step 5: Validate Updated Flow with Live Data"
|
|
2601
|
-
};
|
|
2602
|
-
modalTitle.textContent = titles[currentStep] || "Step 2: Import Failed Trace";
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
function updateFooterButtons() {
|
|
2606
|
-
const changeBtn = document.getElementById("changeTraceBtn");
|
|
2607
|
-
const nextBtn = document.getElementById("nextBtn");
|
|
2608
|
-
|
|
2609
|
-
if (currentStep <= 2) {
|
|
2610
|
-
changeBtn.textContent = "Change Workflow Function";
|
|
2611
|
-
nextBtn.textContent = "Next";
|
|
2612
|
-
nextBtn.disabled = true;
|
|
2613
|
-
} else if (currentStep === 3) {
|
|
2614
|
-
changeBtn.textContent = "Change Trace File";
|
|
2615
|
-
nextBtn.textContent = "Next";
|
|
2616
|
-
nextBtn.disabled = false;
|
|
2617
|
-
} else if (currentStep === 4) {
|
|
2618
|
-
changeBtn.textContent = "Select Different Steps";
|
|
2619
|
-
nextBtn.textContent = "Fix Works as Expected";
|
|
2620
|
-
nextBtn.disabled = false;
|
|
2621
|
-
} else if (currentStep === 5) {
|
|
2622
|
-
// Step 5 - show custom buttons
|
|
2623
|
-
changeBtn.textContent = "Still Failing";
|
|
2624
|
-
nextBtn.textContent = "Done";
|
|
2625
|
-
} else {
|
|
2626
|
-
let customFooter = document.getElementById("step5FooterBtns");
|
|
2627
|
-
if (customFooter) customFooter.remove();
|
|
2628
|
-
}
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
fetch("/api/repo-root").then(r => r.json()).then(d => {
|
|
2632
|
-
console.log("[Dashboard] Repo root fetched:", d);
|
|
2633
|
-
repoRoot = d.repoRoot || '';
|
|
2634
|
-
console.log("[Dashboard] repoRoot set to:", repoRoot);
|
|
2635
|
-
|
|
2636
|
-
// Now fetch workflows after repo root is loaded
|
|
2637
|
-
return fetch("/api/workflows");
|
|
2638
|
-
}).then(r => r.json()).then(d => {
|
|
2639
|
-
console.log("[Dashboard] Workflows fetched:", d);
|
|
2640
|
-
allWorkflows = d.workflows || [];
|
|
2641
|
-
console.log("[Dashboard] Calling render with", allWorkflows.length, "workflows");
|
|
2642
|
-
render();
|
|
2643
|
-
}).catch(err => {
|
|
2644
|
-
console.error("Failed to fetch repo root or workflows:", err);
|
|
2645
|
-
tbody.innerHTML = '<tr><td colspan="2" style="text-align: center; padding: 40px; color: #c62828;">Error loading workflows</td></tr>';
|
|
2646
|
-
});
|
|
2647
|
-
|
|
2648
|
-
fetch("/api/code-index").then(r => r.json()).then(d => {
|
|
2649
|
-
codeIndex = d;
|
|
2650
|
-
console.log("Code index:", d);
|
|
2651
|
-
}).catch(err => {
|
|
2652
|
-
console.error("Failed to fetch code index:", err);
|
|
2653
|
-
});
|
|
2654
|
-
|
|
2655
|
-
function render(search = "") {
|
|
2656
|
-
console.log("[Dashboard] render() called with search:", search, "workflows:", allWorkflows.length);
|
|
2657
|
-
const filtered = search ? allWorkflows.filter(w =>
|
|
2658
|
-
w.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
2659
|
-
w.filePath.toLowerCase().includes(search.toLowerCase())
|
|
2660
|
-
) : allWorkflows;
|
|
2661
|
-
countEl.textContent = filtered.length;
|
|
2662
|
-
console.log("[Dashboard] Rendering", filtered.length, "workflows");
|
|
2663
|
-
tbody.innerHTML = filtered.length ? filtered.map((w, i) => \`<tr onclick="showModal(\${i},'\${search}')">
|
|
2664
|
-
<td><div class="workflow-name-cell">\${esc(w.name)}\${w.isAsync ? '<span class="async-badge">async</span>' : ""}</div></td>
|
|
2665
|
-
<td><div class="workflow-path-cell">\${esc(stripAbsolutePath(w.filePath))}</div></td>
|
|
2666
|
-
</tr>\`).join("") : \`<tr><td colspan="2" style="text-align: center; padding: 40px; color: #999;">No workflows found</td></tr>\`;
|
|
2667
|
-
console.log("[Dashboard] tbody updated");
|
|
2668
|
-
}
|
|
2669
|
-
|
|
2670
|
-
function showModal(index, search) {
|
|
2671
|
-
const filtered = search ? allWorkflows.filter(w =>
|
|
2672
|
-
w.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
2673
|
-
w.filePath.toLowerCase().includes(search.toLowerCase())
|
|
2674
|
-
) : allWorkflows;
|
|
2675
|
-
selectedWorkflow = filtered[index];
|
|
2676
|
-
modal.classList.add("open");
|
|
2677
|
-
resetTraceModal();
|
|
2678
|
-
updateFooterButtons();
|
|
2679
|
-
}
|
|
2680
|
-
|
|
2681
|
-
window.showModal = showModal;
|
|
2682
|
-
window.selectObservation = selectObservation;
|
|
2683
|
-
window.step4SelectObservation = function(idx) {
|
|
2684
|
-
selectedObservationIndex = idx;
|
|
2685
|
-
const history = rerunHistory.get(idx) || [];
|
|
2686
|
-
step4SelectedRun = history.length > 0 ? history.length - 1 : -1;
|
|
2687
|
-
renderObservationTable();
|
|
2688
|
-
};
|
|
2689
|
-
window.step4SelectRun = function(ri) {
|
|
2690
|
-
step4SelectedRun = ri;
|
|
2691
|
-
renderObservationTable();
|
|
2692
|
-
};
|
|
2693
|
-
function esc(t) { const d = document.createElement("div"); d.textContent = t; return d.innerHTML; }
|
|
2694
|
-
window.esc = esc;
|
|
2695
|
-
|
|
2696
|
-
document.getElementById("searchInput").oninput = (e) => render(e.target.value);
|
|
2697
|
-
</script>
|
|
2698
|
-
</body>
|
|
2699
|
-
</html>`;
|
|
955
|
+
return readFileSync(path.join(__dirname, 'html', 'dashboard.html'), 'utf8');
|
|
2700
956
|
/* DASHBOARD_HTML_END */
|
|
2701
957
|
}
|
|
2702
958
|
const SEARCH_SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.next', '.turbo', 'build', 'coverage']);
|
|
@@ -2771,16 +1027,144 @@ function searchInFiles(dir, query) {
|
|
|
2771
1027
|
}
|
|
2772
1028
|
return null;
|
|
2773
1029
|
}
|
|
1030
|
+
/** Load elasticdash.config.ts via a tsx-enabled subprocess and return the parsed object. */
|
|
1031
|
+
async function loadElasticDashConfig(cwd) {
|
|
1032
|
+
const configPath = resolveRuntimeModule(cwd, 'elasticdash.config');
|
|
1033
|
+
if (!configPath)
|
|
1034
|
+
return {};
|
|
1035
|
+
return new Promise((resolve) => {
|
|
1036
|
+
const nodeOptions = process.env.NODE_OPTIONS ?? '';
|
|
1037
|
+
const tsxFlag = '--import tsx';
|
|
1038
|
+
const childNodeOptions = nodeOptions.includes('tsx') ? nodeOptions : `${nodeOptions} ${tsxFlag}`.trim();
|
|
1039
|
+
const childEnv = { ...process.env, NODE_OPTIONS: isDenoProject(cwd) ? nodeOptions : childNodeOptions };
|
|
1040
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
1041
|
+
const child = spawn(process.execPath, ['--input-type=module'], {
|
|
1042
|
+
env: childEnv,
|
|
1043
|
+
cwd,
|
|
1044
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
1045
|
+
});
|
|
1046
|
+
let output = '';
|
|
1047
|
+
child.stdout.on('data', (chunk) => { output += chunk.toString(); });
|
|
1048
|
+
child.on('close', () => {
|
|
1049
|
+
try {
|
|
1050
|
+
resolve(JSON.parse(output));
|
|
1051
|
+
}
|
|
1052
|
+
catch {
|
|
1053
|
+
resolve({});
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
child.on('error', () => resolve({}));
|
|
1057
|
+
child.stdin.write(`import m from '${configUrl}'; process.stdout.write(JSON.stringify(m.default ?? m))`);
|
|
1058
|
+
child.stdin.end();
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
/** Resolve {{env.VAR}} and {{input.field}} placeholders in a value. */
|
|
1062
|
+
function resolveTemplateValue(value, input) {
|
|
1063
|
+
if (typeof value === 'string') {
|
|
1064
|
+
return value.replace(/\{\{env\.([^}]+)\}\}/g, (_, k) => process.env[k] ?? '')
|
|
1065
|
+
.replace(/\{\{input\.([^}]+)\}\}/g, (_, k) => String(input[k] ?? ''))
|
|
1066
|
+
.replace(/\{\{timestamp\}\}/g, () => String(Date.now()));
|
|
1067
|
+
}
|
|
1068
|
+
if (Array.isArray(value))
|
|
1069
|
+
return value.map(v => resolveTemplateValue(v, input));
|
|
1070
|
+
if (value && typeof value === 'object') {
|
|
1071
|
+
const out = {};
|
|
1072
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1073
|
+
out[k] = resolveTemplateValue(v, input);
|
|
1074
|
+
}
|
|
1075
|
+
return out;
|
|
1076
|
+
}
|
|
1077
|
+
return value;
|
|
1078
|
+
}
|
|
1079
|
+
async function runHttpWorkflow(opts) {
|
|
1080
|
+
const { workflowName, workflowInput, frozenEvents = [], pushedEvents, runConfigs, config, dashboardPort } = opts;
|
|
1081
|
+
const runId = randomUUID();
|
|
1082
|
+
// Register run config so the user's server can fetch frozen events
|
|
1083
|
+
pushedEvents.set(runId, []);
|
|
1084
|
+
runConfigs.set(runId, { frozenEvents });
|
|
1085
|
+
try {
|
|
1086
|
+
const inputObj = workflowInput && typeof workflowInput === 'object' ? workflowInput : {};
|
|
1087
|
+
const method = config.method ?? 'POST';
|
|
1088
|
+
const resolvedHeaders = {};
|
|
1089
|
+
for (const [k, v] of Object.entries(config.headers ?? {})) {
|
|
1090
|
+
resolvedHeaders[k] = resolveTemplateValue(v, inputObj);
|
|
1091
|
+
}
|
|
1092
|
+
resolvedHeaders['x-elasticdash-run-id'] = runId;
|
|
1093
|
+
resolvedHeaders['x-elasticdash-server'] = `http://localhost:${dashboardPort}`;
|
|
1094
|
+
const body = config.bodyTemplate
|
|
1095
|
+
? JSON.stringify(resolveTemplateValue(config.bodyTemplate, inputObj))
|
|
1096
|
+
: undefined;
|
|
1097
|
+
if (body && !resolvedHeaders['Content-Type']) {
|
|
1098
|
+
resolvedHeaders['Content-Type'] = 'application/json';
|
|
1099
|
+
}
|
|
1100
|
+
console.log(`[elasticdash] HTTP workflow "${workflowName}" → ${method} ${config.url} (runId=${runId})`);
|
|
1101
|
+
const response = await fetch(config.url, { method, headers: resolvedHeaders, body });
|
|
1102
|
+
let currentOutput;
|
|
1103
|
+
if (config.responseFormat === 'vercel-ai-stream' && response.body) {
|
|
1104
|
+
const decoder = new TextDecoder();
|
|
1105
|
+
const reader = response.body.getReader();
|
|
1106
|
+
let text = '';
|
|
1107
|
+
for (;;) {
|
|
1108
|
+
const { done, value } = await reader.read();
|
|
1109
|
+
if (done)
|
|
1110
|
+
break;
|
|
1111
|
+
text += decoder.decode(value, { stream: true });
|
|
1112
|
+
}
|
|
1113
|
+
// Extract final text from Vercel AI stream data lines
|
|
1114
|
+
let finalText = '';
|
|
1115
|
+
for (const line of text.split('\n')) {
|
|
1116
|
+
if (!line.startsWith('0:'))
|
|
1117
|
+
continue;
|
|
1118
|
+
try {
|
|
1119
|
+
finalText += JSON.parse(line.slice(2));
|
|
1120
|
+
}
|
|
1121
|
+
catch { /* skip */ }
|
|
1122
|
+
}
|
|
1123
|
+
currentOutput = finalText || text;
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
try {
|
|
1127
|
+
currentOutput = await response.clone().json();
|
|
1128
|
+
}
|
|
1129
|
+
catch {
|
|
1130
|
+
currentOutput = await response.text();
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (!response.ok) {
|
|
1134
|
+
return { ok: false, error: `HTTP ${response.status}: ${response.statusText}`, currentOutput };
|
|
1135
|
+
}
|
|
1136
|
+
// Drain window: wait for in-flight push POSTs to arrive
|
|
1137
|
+
const drainMs = parseInt(process.env.ELASTICDASH_HTTP_DRAIN_MS ?? '300', 10);
|
|
1138
|
+
await new Promise(resolve => setTimeout(resolve, drainMs));
|
|
1139
|
+
const events = (pushedEvents.get(runId) ?? []).sort((a, b) => a.timestamp - b.timestamp);
|
|
1140
|
+
const workflowTrace = { traceId: runId, events };
|
|
1141
|
+
return { ok: true, currentOutput, workflowTrace, steps: [], llmSteps: [], toolCalls: [], customSteps: [] };
|
|
1142
|
+
}
|
|
1143
|
+
catch (error) {
|
|
1144
|
+
return { ok: false, error: `HTTP workflow failed: ${formatError(error)}` };
|
|
1145
|
+
}
|
|
1146
|
+
finally {
|
|
1147
|
+
pushedEvents.delete(runId);
|
|
1148
|
+
runConfigs.delete(runId);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
2774
1151
|
/**
|
|
2775
1152
|
* Start the dashboard server
|
|
2776
1153
|
*/
|
|
2777
1154
|
export async function startDashboardServer(cwd, options = {}) {
|
|
2778
1155
|
const port = options.port ?? 4573;
|
|
2779
1156
|
const autoOpen = options.autoOpen ?? true;
|
|
2780
|
-
//
|
|
1157
|
+
// In-memory store for telemetry events pushed from HTTP workflow mode runs.
|
|
1158
|
+
// Maps runId -> accumulated WorkflowEvent[]
|
|
1159
|
+
const pushedEvents = new Map();
|
|
1160
|
+
// Per-run config for HTTP workflow mode (frozen events for replay).
|
|
1161
|
+
// Maps runId -> { frozenEvents }
|
|
1162
|
+
const runConfigs = new Map();
|
|
1163
|
+
// Scan workflows, tools, and config once at startup
|
|
2781
1164
|
const workflows = scanWorkflows(cwd);
|
|
2782
1165
|
const tools = scanTools(cwd);
|
|
2783
1166
|
const codeIndex = { workflows, tools };
|
|
1167
|
+
const elasticdashConfig = await loadElasticDashConfig(cwd);
|
|
2784
1168
|
console.log(`[elasticdash] Scanned: ${workflows.length} workflows, ${tools.length} tools`);
|
|
2785
1169
|
// Create HTTP server
|
|
2786
1170
|
const server = http.createServer((req, res) => {
|
|
@@ -2831,6 +1215,41 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
2831
1215
|
(async () => {
|
|
2832
1216
|
try {
|
|
2833
1217
|
const body = (await readJsonBody(req));
|
|
1218
|
+
const workflowName = typeof body.workflowName === 'string' ? body.workflowName.trim() : '';
|
|
1219
|
+
const httpConfig = elasticdashConfig.workflows?.[workflowName];
|
|
1220
|
+
if (httpConfig?.mode === 'http') {
|
|
1221
|
+
// HTTP workflow mode — call user's dev server instead of subprocess
|
|
1222
|
+
const runCount = typeof body.runCount === 'number' ? Math.max(1, Math.min(50, body.runCount)) : 1;
|
|
1223
|
+
const sequential = body.sequential === true;
|
|
1224
|
+
const resolvedInput = resolveWorkflowArgsFromObservations(body, workflowName);
|
|
1225
|
+
const workflowInput = resolvedInput.input ?? null;
|
|
1226
|
+
const traces = [];
|
|
1227
|
+
const runOne = async (runNumber) => {
|
|
1228
|
+
const result = await runHttpWorkflow({
|
|
1229
|
+
workflowName, workflowInput, pushedEvents, runConfigs,
|
|
1230
|
+
config: httpConfig, dashboardPort: port,
|
|
1231
|
+
});
|
|
1232
|
+
const traceStub = { getSteps: () => [], getLLMSteps: () => [], getToolCalls: () => [], getCustomSteps: () => [], recordLLMStep: () => { }, recordToolCall: () => { }, recordCustomStep: () => { } };
|
|
1233
|
+
return {
|
|
1234
|
+
runNumber, ok: result.ok, error: result.error,
|
|
1235
|
+
observations: buildValidationObservations(workflowName, workflowInput, result.currentOutput, result.error, traceStub, result.workflowTrace),
|
|
1236
|
+
workflowTrace: result.workflowTrace,
|
|
1237
|
+
currentOutput: result.currentOutput,
|
|
1238
|
+
snapshotId: result.workflowTrace ? saveSnapshot(cwd, result.workflowTrace) : undefined,
|
|
1239
|
+
};
|
|
1240
|
+
};
|
|
1241
|
+
if (sequential) {
|
|
1242
|
+
for (let i = 1; i <= runCount; i++)
|
|
1243
|
+
traces.push(await runOne(i));
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
traces.push(...await Promise.all(Array.from({ length: runCount }, (_, i) => runOne(i + 1))));
|
|
1247
|
+
}
|
|
1248
|
+
const ok = traces.some(t => t.ok);
|
|
1249
|
+
res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
1250
|
+
res.end(JSON.stringify({ ok, mode: sequential ? 'sequential' : 'parallel', runCount, traces }));
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
2834
1253
|
const result = await validateWorkflowRuns(cwd, body);
|
|
2835
1254
|
const statusCode = result.ok ? 200 : 400;
|
|
2836
1255
|
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
@@ -2862,12 +1281,6 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
2862
1281
|
else {
|
|
2863
1282
|
history = Array.isArray(body.history) ? body.history : [];
|
|
2864
1283
|
}
|
|
2865
|
-
const workflowsModulePath = resolveWorkflowModule(cwd);
|
|
2866
|
-
if (!workflowsModulePath) {
|
|
2867
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2868
|
-
res.end(JSON.stringify({ ok: false, error: 'Cannot find ed_workflows.ts/js in workspace root.' }));
|
|
2869
|
-
return;
|
|
2870
|
-
}
|
|
2871
1284
|
const validationBody = { workflowName, observations: body.observations };
|
|
2872
1285
|
const resolvedInput = resolveWorkflowArgsFromObservations(validationBody, workflowName);
|
|
2873
1286
|
if (resolvedInput.error) {
|
|
@@ -2875,16 +1288,45 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
2875
1288
|
res.end(JSON.stringify({ ok: false, error: resolvedInput.error }));
|
|
2876
1289
|
return;
|
|
2877
1290
|
}
|
|
2878
|
-
const workflowArgs = resolvedInput.args ?? [];
|
|
2879
1291
|
const workflowInput = resolvedInput.input ?? null;
|
|
1292
|
+
const frozenEvents = history.filter((event) => (event.id <= checkpoint
|
|
1293
|
+
&& (event.type === 'ai' || event.type === 'tool' || event.type === 'http' || event.type === 'db')));
|
|
1294
|
+
const frozenEventIds = new Set(frozenEvents.map((e) => e.id));
|
|
1295
|
+
const httpConfig = elasticdashConfig.workflows?.[workflowName];
|
|
1296
|
+
if (httpConfig?.mode === 'http') {
|
|
1297
|
+
// HTTP workflow mode — call user's dev server with frozen events for step replay
|
|
1298
|
+
console.log(`[elasticdash] Run from breakpoint (HTTP mode): workflow="${workflowName}" checkpoint=${checkpoint} frozen=${frozenEvents.length}`);
|
|
1299
|
+
const result = await runHttpWorkflow({
|
|
1300
|
+
workflowName, workflowInput, pushedEvents, runConfigs,
|
|
1301
|
+
config: httpConfig, dashboardPort: port,
|
|
1302
|
+
frozenEvents,
|
|
1303
|
+
});
|
|
1304
|
+
const traceStub = { getSteps: () => [], getLLMSteps: () => [], getToolCalls: () => [], getCustomSteps: () => [], recordLLMStep: () => { }, recordToolCall: () => { }, recordCustomStep: () => { } };
|
|
1305
|
+
const snapshotId = result.workflowTrace ? saveSnapshot(cwd, result.workflowTrace) : undefined;
|
|
1306
|
+
const trace = {
|
|
1307
|
+
runNumber: 0,
|
|
1308
|
+
ok: result.ok,
|
|
1309
|
+
error: result.ok ? undefined : result.error,
|
|
1310
|
+
observations: buildValidationObservations(workflowName, workflowInput, result.currentOutput, result.ok ? undefined : result.error, traceStub, result.workflowTrace, frozenEventIds),
|
|
1311
|
+
workflowTrace: result.workflowTrace,
|
|
1312
|
+
currentOutput: result.currentOutput,
|
|
1313
|
+
snapshotId,
|
|
1314
|
+
};
|
|
1315
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1316
|
+
res.end(JSON.stringify(trace));
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const workflowsModulePath = resolveWorkflowModule(cwd);
|
|
1320
|
+
if (!workflowsModulePath) {
|
|
1321
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1322
|
+
res.end(JSON.stringify({ ok: false, error: 'Cannot find ed_workflows.ts/js in workspace root.' }));
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
const workflowArgs = resolvedInput.args ?? [];
|
|
2880
1326
|
const toolsModulePath = resolveRuntimeModule(cwd, 'ed_tools') ?? null;
|
|
2881
1327
|
const toolMockConfig = body.toolMockConfig && typeof body.toolMockConfig === 'object' && !Array.isArray(body.toolMockConfig)
|
|
2882
1328
|
? body.toolMockConfig
|
|
2883
1329
|
: undefined;
|
|
2884
|
-
const frozenEventIds = new Set(history
|
|
2885
|
-
.filter((event) => (event.id <= checkpoint
|
|
2886
|
-
&& (event.type === 'ai' || event.type === 'tool' || event.type === 'http' || event.type === 'db')))
|
|
2887
|
-
.map((event) => event.id));
|
|
2888
1330
|
console.log(`[elasticdash] Run from breakpoint: workflow="${workflowName}" checkpoint=${checkpoint} historyLen=${history.length}`);
|
|
2889
1331
|
const result = await runWorkflowInSubprocess(workflowsModulePath, toolsModulePath, workflowName, workflowArgs, workflowInput, { replayMode: true, checkpoint, history, ...(toolMockConfig ? { toolMockConfig } : {}) });
|
|
2890
1332
|
const traceStub = {
|
|
@@ -3005,6 +1447,35 @@ export async function startDashboardServer(cwd, options = {}) {
|
|
|
3005
1447
|
}
|
|
3006
1448
|
}
|
|
3007
1449
|
}
|
|
1450
|
+
else if (url.pathname.startsWith('/api/run-configs/') && req.method === 'GET') {
|
|
1451
|
+
const runId = url.pathname.slice('/api/run-configs/'.length);
|
|
1452
|
+
const cfg = runConfigs.get(runId);
|
|
1453
|
+
res.writeHead(cfg ? 200 : 404, { 'Content-Type': 'application/json' });
|
|
1454
|
+
res.end(JSON.stringify({ frozenEvents: cfg?.frozenEvents ?? [] }));
|
|
1455
|
+
}
|
|
1456
|
+
else if (url.pathname === '/api/trace-events' && req.method === 'POST') {
|
|
1457
|
+
// Receive telemetry events pushed from wrapAI / wrapTool in HTTP workflow mode
|
|
1458
|
+
;
|
|
1459
|
+
(async () => {
|
|
1460
|
+
try {
|
|
1461
|
+
const body = (await readJsonBody(req));
|
|
1462
|
+
if (typeof body.runId !== 'string' || !body.runId || !body.event || typeof body.event !== 'object') {
|
|
1463
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1464
|
+
res.end(JSON.stringify({ ok: false, error: 'runId (string) and event (object) are required.' }));
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const existing = pushedEvents.get(body.runId) ?? [];
|
|
1468
|
+
existing.push(body.event);
|
|
1469
|
+
pushedEvents.set(body.runId, existing);
|
|
1470
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1471
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1472
|
+
}
|
|
1473
|
+
catch (error) {
|
|
1474
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1475
|
+
res.end(JSON.stringify({ ok: false, error: formatError(error) }));
|
|
1476
|
+
}
|
|
1477
|
+
})();
|
|
1478
|
+
}
|
|
3008
1479
|
else {
|
|
3009
1480
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
3010
1481
|
res.end(getDashboardHtml());
|