elasticdash-test 0.1.13 → 0.1.16

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