@tea-agent/loop-agent 0.30.0 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/dist/executors/dag-pi-executor.js +72 -4
- package/dist/executors/pi-sdk-executor.js +61 -0
- package/dist/executors/shell-executor.js +136 -79
- package/dist/worker/observability/dag-execution-trajectory.js +591 -0
- package/dist/worker/observability/read-model.js +258 -31
- package/dist/worker/observe/dag-node-execution-output.js +180 -0
- package/dist/worker/observe/routes.js +53 -6
- package/dist/worker/observe/static/dag-edge-routing.js +368 -0
- package/dist/worker/observe/static/dag-history-labels.js +95 -0
- package/dist/worker/observe/static/dag-layout.d.ts +12 -7
- package/dist/worker/observe/static/dag-layout.js +101 -21
- package/dist/worker/observe/static/favicon.svg +37 -0
- package/dist/worker/observe/static/format.js +31 -1
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/state.js +102 -0
- package/dist/worker/observe/static/styles.css +267 -7
- package/dist/worker/observe/static/views/dag-graph.js +414 -154
- package/dist/worker/observe/static/views/dag-inspector.js +478 -27
- package/dist/worker/observe/static/views/dag-trajectory.js +313 -0
- package/dist/worker/observe/static/views/dag.js +20 -3
- package/dist/workflows/dag/backend-test-pytest-collection.js +162 -7
- package/dist/workflows/dag/backend-test-result-contract.js +105 -67
- package/dist/workflows/dag/backend-test-scenario-param.js +92 -30
- package/dist/workflows/dag/backend-test-writer-completeness.js +55 -0
- package/dist/workflows/dag/init-hybrid.js +46 -49
- package/dist/workflows/dag/rerun-task.js +86 -0
- package/docs/architecture/README.md +4 -0
- package/docs/architecture/dag-execution.md +1 -1
- package/docs/architecture/worker-and-feature.md +1 -1
- package/docs/governance/README.md +3 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/backend-test-dag.json +40 -60
- package/docs/templates/frontend-test-dag.json +1 -1
- package/harness.json +3 -3
- package/package.json +3 -2
- package/scripts/kb-bootstrap-init-skeleton.sh +1 -1
|
@@ -10,13 +10,18 @@ import {
|
|
|
10
10
|
UI_TEXT,
|
|
11
11
|
} from "../constants.js";
|
|
12
12
|
import { clearNode, el } from "../dom.js";
|
|
13
|
+
import { attemptFailureLabel } from "../dag-history-labels.js";
|
|
13
14
|
import { fetchJson, fetchSpecEvidenceFile, fetchDagNodeInput } from "../api.js";
|
|
14
|
-
import { badge } from "../format.js";
|
|
15
|
+
import { badge, formatMs } from "../format.js";
|
|
15
16
|
import { renderMarkdown } from "../markdown-render.js";
|
|
16
17
|
import {
|
|
17
18
|
uiState,
|
|
18
19
|
makeSessionEventIdentity,
|
|
19
20
|
makeDagNodeInputIdentity,
|
|
21
|
+
makeDagNodeExecutionOutputIdentity,
|
|
22
|
+
getDagNodeExecutionOutputCache,
|
|
23
|
+
setDagNodeExecutionOutputCache,
|
|
24
|
+
clearDagNodeExecutionOutputCache,
|
|
20
25
|
openSpecEvidenceDetail,
|
|
21
26
|
closeSpecEvidenceDetail,
|
|
22
27
|
updateDagTimelineViewportState,
|
|
@@ -47,28 +52,59 @@ function renderDagInspectorForCurrent() {
|
|
|
47
52
|
);
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
const nodeChanged = uiState.selectedDagNodeId !== nodeId;
|
|
55
|
+
function resetInspectorNodeSelection(dagRunId, nodeId) {
|
|
52
56
|
uiState.selectedDagNodeId = nodeId;
|
|
53
57
|
uiState.dagInspectorOpen = true;
|
|
58
|
+
// Reset the per-attempt selection so an old node's attempt cannot leak
|
|
59
|
+
// into the newly selected node (always defaults back to the latest).
|
|
60
|
+
uiState.dagInspectorAttempt = null;
|
|
61
|
+
uiState.sessionEventOffset = 0;
|
|
62
|
+
uiState.sessionEvents = [];
|
|
63
|
+
// Drop any open spec-evidence preview so a stale A detail cannot survive
|
|
64
|
+
// into node B's spec-evidence panel (AC-007 regression guard).
|
|
65
|
+
uiState.specEvidenceDetail = null;
|
|
66
|
+
uiState.specEvidenceListEvidence = null;
|
|
67
|
+
uiState.dagNodeInputData = null;
|
|
68
|
+
uiState.dagNodeInputError = null;
|
|
69
|
+
uiState.dagNodeInputLoading = false;
|
|
70
|
+
// Drop execution-output cache/identity so node A's bodies never leak into B.
|
|
71
|
+
clearDagNodeExecutionOutputCache();
|
|
72
|
+
// Bump the session-event identity token so any in-flight response for the
|
|
73
|
+
// previous node is rejected before it can mutate the current node cache.
|
|
74
|
+
uiState.sessionEventIdentity = makeSessionEventIdentity(dagRunId, nodeId);
|
|
75
|
+
uiState.dagNodeInputIdentity = makeDagNodeInputIdentity(dagRunId, nodeId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function selectDagNode(dagRunId, nodeId) {
|
|
79
|
+
const nodeChanged = uiState.selectedDagNodeId !== nodeId;
|
|
54
80
|
if (nodeChanged) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// into node B's spec-evidence panel (AC-007 regression guard).
|
|
59
|
-
uiState.specEvidenceDetail = null;
|
|
60
|
-
uiState.specEvidenceListEvidence = null;
|
|
61
|
-
uiState.dagNodeInputData = null;
|
|
62
|
-
uiState.dagNodeInputError = null;
|
|
63
|
-
uiState.dagNodeInputLoading = false;
|
|
64
|
-
// Bump the session-event identity token so any in-flight response for the
|
|
65
|
-
// previous node is rejected before it can mutate the current node cache.
|
|
66
|
-
uiState.sessionEventIdentity = makeSessionEventIdentity(dagRunId, nodeId);
|
|
67
|
-
uiState.dagNodeInputIdentity = makeDagNodeInputIdentity(dagRunId, nodeId);
|
|
81
|
+
resetInspectorNodeSelection(dagRunId, nodeId);
|
|
82
|
+
} else {
|
|
83
|
+
uiState.dagInspectorOpen = true;
|
|
68
84
|
}
|
|
69
85
|
void import("./dag.js").then((m) => m.renderDagDetail(dagRunId, false));
|
|
70
86
|
}
|
|
71
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 从执行轨迹 occurrence 打开同一 Inspector 并选中对应 execution(AC-005)。
|
|
90
|
+
* `executionKey` 为既有 pass-<n> key(@pass-<n> 映射)或 null(@base/@final
|
|
91
|
+
* 默认最新)。uiState.dagInspectorAttempt 跨 poll 保持。
|
|
92
|
+
*/
|
|
93
|
+
export function selectDagExecution(dagRunId, nodeId, executionKey) {
|
|
94
|
+
const nodeChanged = uiState.selectedDagNodeId !== nodeId;
|
|
95
|
+
if (nodeChanged) {
|
|
96
|
+
resetInspectorNodeSelection(dagRunId, nodeId);
|
|
97
|
+
} else {
|
|
98
|
+
uiState.dagInspectorOpen = true;
|
|
99
|
+
}
|
|
100
|
+
uiState.dagInspectorAttempt = {
|
|
101
|
+
dagRunId,
|
|
102
|
+
nodeId,
|
|
103
|
+
key: executionKey ? String(executionKey) : null,
|
|
104
|
+
};
|
|
105
|
+
void import("./dag.js").then((m) => m.renderDagDetail(dagRunId, false));
|
|
106
|
+
}
|
|
107
|
+
|
|
72
108
|
export function closeDagInspector() {
|
|
73
109
|
uiState.dagInspectorOpen = false;
|
|
74
110
|
const panel = document.getElementById("dag-inspector");
|
|
@@ -1063,6 +1099,373 @@ function ensureInspectorContentScrollCapture(content, dagRunId, nodeId, tab) {
|
|
|
1063
1099
|
});
|
|
1064
1100
|
}
|
|
1065
1101
|
|
|
1102
|
+
function attemptPreviewStatus(status) {
|
|
1103
|
+
if (!status) return null;
|
|
1104
|
+
const normalized = String(status).toLowerCase();
|
|
1105
|
+
if (
|
|
1106
|
+
["success", "finished", "completed", "succeeded", "done"].includes(
|
|
1107
|
+
normalized,
|
|
1108
|
+
)
|
|
1109
|
+
) {
|
|
1110
|
+
return { cls: "succeeded", label: "成功" };
|
|
1111
|
+
}
|
|
1112
|
+
if (
|
|
1113
|
+
[
|
|
1114
|
+
"failure",
|
|
1115
|
+
"failed",
|
|
1116
|
+
"error",
|
|
1117
|
+
"partial_failed",
|
|
1118
|
+
"partial-failed",
|
|
1119
|
+
"interrupted",
|
|
1120
|
+
].includes(normalized)
|
|
1121
|
+
) {
|
|
1122
|
+
return { cls: "failed", label: "失败" };
|
|
1123
|
+
}
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Attempt selector options for a node: per-attempt when attempts>1,
|
|
1129
|
+
* per-pass when convergence passes>1, none otherwise (legacy/single).
|
|
1130
|
+
* 主文案为中文:首次尝试 / 第 N 次尝试 / 初始执行 / 修复轮次 N-1
|
|
1131
|
+
* (AC-001/AC-002);option value 保持 raw attempt-<n>/pass-<n>(AC-003)。
|
|
1132
|
+
*/
|
|
1133
|
+
function attemptSelectorOptions(node) {
|
|
1134
|
+
const summary = node.executionSummary;
|
|
1135
|
+
if (summary?.attempts && summary.attempts.length > 1) {
|
|
1136
|
+
return summary.attempts.map((attempt) => ({
|
|
1137
|
+
key: `attempt-${attempt.attempt}`,
|
|
1138
|
+
label:
|
|
1139
|
+
attempt.attempt === 1 ? "首次尝试" : `第 ${attempt.attempt} 次尝试`,
|
|
1140
|
+
preview: attempt,
|
|
1141
|
+
}));
|
|
1142
|
+
}
|
|
1143
|
+
const passes = node.passes ?? [];
|
|
1144
|
+
if (passes.length > 1) {
|
|
1145
|
+
const passPreviews = summary?.passes ?? [];
|
|
1146
|
+
return passes.map((pass) => {
|
|
1147
|
+
const preview = passPreviews.find((entry) => entry.pass === pass) ?? {
|
|
1148
|
+
pass,
|
|
1149
|
+
};
|
|
1150
|
+
// 仅显示文案做人类轮次映射:raw pass=1 → 初始执行,raw pass>=2 →
|
|
1151
|
+
// 修复轮次 N-1(AC-001/AC-002);option value 保持 raw pass-<n>(AC-003)。
|
|
1152
|
+
return {
|
|
1153
|
+
key: `pass-${pass}`,
|
|
1154
|
+
label: pass <= 1 ? "初始执行" : `修复轮次 ${pass - 1}`,
|
|
1155
|
+
preview,
|
|
1156
|
+
};
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
return [];
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** Selected attempt key for the current node; null = latest. */
|
|
1163
|
+
function resolveSelectedAttemptKey(dagRunId, node) {
|
|
1164
|
+
const selection = uiState.dagInspectorAttempt;
|
|
1165
|
+
if (
|
|
1166
|
+
selection &&
|
|
1167
|
+
selection.dagRunId === dagRunId &&
|
|
1168
|
+
selection.nodeId === node.nodeId
|
|
1169
|
+
) {
|
|
1170
|
+
return selection.key;
|
|
1171
|
+
}
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Sticky selector + preview strip shown at the top of output/timeline tabs
|
|
1177
|
+
* when the node carries multi-attempt (or multi-pass) history. Preview shows
|
|
1178
|
+
* status badge / failureCategory / durationMs for the selected attempt; for a
|
|
1179
|
+
* non-latest attempt the full body stays in the immutable attempt artifact.
|
|
1180
|
+
*/
|
|
1181
|
+
function buildAttemptBar(dagRunId, node) {
|
|
1182
|
+
const options = attemptSelectorOptions(node);
|
|
1183
|
+
if (options.length === 0) return null;
|
|
1184
|
+
const latestKey = options[options.length - 1].key;
|
|
1185
|
+
const requestedKey = resolveSelectedAttemptKey(dagRunId, node);
|
|
1186
|
+
const selectedKey = options.some((option) => option.key === requestedKey)
|
|
1187
|
+
? requestedKey
|
|
1188
|
+
: latestKey;
|
|
1189
|
+
|
|
1190
|
+
const bar = el("div", "dag-inspector-attempt-bar");
|
|
1191
|
+
bar.appendChild(el("label", "dag-inspector-attempt-label", "执行分次"));
|
|
1192
|
+
const select = document.createElement("select");
|
|
1193
|
+
select.className = "dag-inspector-attempt-select";
|
|
1194
|
+
select.setAttribute("aria-label", "选择执行分次");
|
|
1195
|
+
for (const option of options) {
|
|
1196
|
+
const optionEl = document.createElement("option");
|
|
1197
|
+
optionEl.value = option.key;
|
|
1198
|
+
optionEl.textContent = option.label;
|
|
1199
|
+
select.appendChild(optionEl);
|
|
1200
|
+
}
|
|
1201
|
+
select.value = selectedKey;
|
|
1202
|
+
select.addEventListener("change", () => {
|
|
1203
|
+
uiState.dagInspectorAttempt = {
|
|
1204
|
+
dagRunId,
|
|
1205
|
+
nodeId: node.nodeId,
|
|
1206
|
+
key: select.value,
|
|
1207
|
+
};
|
|
1208
|
+
renderDagInspectorForCurrent();
|
|
1209
|
+
});
|
|
1210
|
+
bar.appendChild(select);
|
|
1211
|
+
|
|
1212
|
+
const selected =
|
|
1213
|
+
options.find((option) => option.key === selectedKey) ??
|
|
1214
|
+
options[options.length - 1];
|
|
1215
|
+
const data = selected.preview ?? {};
|
|
1216
|
+
const preview = el("div", "dag-inspector-attempt-preview");
|
|
1217
|
+
const status = attemptPreviewStatus(data.status);
|
|
1218
|
+
if (status) {
|
|
1219
|
+
preview.appendChild(el("span", `badge badge-${status.cls}`, status.label));
|
|
1220
|
+
}
|
|
1221
|
+
if (status?.cls === "failed" && data.failureCategory) {
|
|
1222
|
+
// 仅失败尝试显示原因;部分 runtime 会在成功尝试保留
|
|
1223
|
+
// failureCategory="success",不能将其误映射为「其他失败原因」。
|
|
1224
|
+
// 未知失败分类仍 fail-closed,raw token 仅保留在 title 诊断中。
|
|
1225
|
+
const reason = el(
|
|
1226
|
+
"span",
|
|
1227
|
+
"dag-inspector-attempt-category",
|
|
1228
|
+
`原因 ${attemptFailureLabel(data.failureCategory)}`,
|
|
1229
|
+
);
|
|
1230
|
+
reason.setAttribute("title", `failureCategory ${data.failureCategory}`);
|
|
1231
|
+
preview.appendChild(reason);
|
|
1232
|
+
}
|
|
1233
|
+
if (data.durationMs !== undefined) {
|
|
1234
|
+
preview.appendChild(
|
|
1235
|
+
el(
|
|
1236
|
+
"span",
|
|
1237
|
+
"dag-inspector-attempt-duration",
|
|
1238
|
+
`耗时 ${formatMs(data.durationMs)}`,
|
|
1239
|
+
),
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
// Historical bodies load via execution-output endpoint; do not surface
|
|
1243
|
+
// raw artifact paths as the primary path for full text (AC-003/004).
|
|
1244
|
+
bar.appendChild(preview);
|
|
1245
|
+
return { bar, selectedKey, latestKey, options };
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Resolve whether the current selection is the semantic latest execution.
|
|
1250
|
+
* Users selecting the last option still write a concrete attempt-N/pass-N key;
|
|
1251
|
+
* treat that as latest so preview/body is never blank on "切回最新".
|
|
1252
|
+
*/
|
|
1253
|
+
function isLatestExecutionSelection(dagRunId, node) {
|
|
1254
|
+
const options = attemptSelectorOptions(node);
|
|
1255
|
+
if (options.length === 0) return true;
|
|
1256
|
+
const latestKey = options[options.length - 1].key;
|
|
1257
|
+
const requested = resolveSelectedAttemptKey(dagRunId, node);
|
|
1258
|
+
return requested === null || requested === latestKey;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function effectiveExecutionKey(dagRunId, node) {
|
|
1262
|
+
const options = attemptSelectorOptions(node);
|
|
1263
|
+
if (options.length === 0) return null;
|
|
1264
|
+
const latestKey = options[options.length - 1].key;
|
|
1265
|
+
const requested = resolveSelectedAttemptKey(dagRunId, node);
|
|
1266
|
+
if (requested === null) return latestKey;
|
|
1267
|
+
return options.some((option) => option.key === requested)
|
|
1268
|
+
? requested
|
|
1269
|
+
: latestKey;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Inline read-only fetch for execution-output (api.js is outside writeSet).
|
|
1274
|
+
* Mirrors fetchJsonResult: preserves status, never throws on transport errors.
|
|
1275
|
+
*/
|
|
1276
|
+
async function fetchExecutionOutput(dagRunId, nodeId, key) {
|
|
1277
|
+
const url = `/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/execution-output?key=${encodeURIComponent(key)}`;
|
|
1278
|
+
try {
|
|
1279
|
+
const res = await fetch(url);
|
|
1280
|
+
let body = null;
|
|
1281
|
+
try {
|
|
1282
|
+
body = await res.json();
|
|
1283
|
+
} catch {
|
|
1284
|
+
body = null;
|
|
1285
|
+
}
|
|
1286
|
+
return { ok: res.ok, status: res.status, body };
|
|
1287
|
+
} catch {
|
|
1288
|
+
return { ok: false, status: 0, body: null };
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function composeExecutionBodyText(payload) {
|
|
1293
|
+
if (!payload || typeof payload !== "object") return "";
|
|
1294
|
+
const assistant =
|
|
1295
|
+
typeof payload.assistantText === "string" && payload.assistantText
|
|
1296
|
+
? payload.assistantText
|
|
1297
|
+
: "";
|
|
1298
|
+
const stdout =
|
|
1299
|
+
typeof payload.stdout === "string" && payload.stdout ? payload.stdout : "";
|
|
1300
|
+
// Dedupe when both streams are non-empty and equal after trim (AC-001);
|
|
1301
|
+
// keep order assistantText → stdout when they differ (AC-002).
|
|
1302
|
+
if (assistant && stdout) {
|
|
1303
|
+
if (assistant.trim() === stdout.trim()) return assistant;
|
|
1304
|
+
return `${assistant}\n\n${stdout}`;
|
|
1305
|
+
}
|
|
1306
|
+
return assistant || stdout;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function composeExecutionErrorText(payload) {
|
|
1310
|
+
if (!payload || typeof payload !== "object") return "";
|
|
1311
|
+
return typeof payload.stderr === "string" ? payload.stderr : "";
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function appendOutputBody(content, outputText, errorText, options = null) {
|
|
1315
|
+
if (!outputText && !errorText) {
|
|
1316
|
+
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (outputText) content.appendChild(renderMarkdown(outputText));
|
|
1320
|
+
// Truncation note only for full/cached execution-output bodies (AC-003);
|
|
1321
|
+
// preview path never invents truncated/maxBytes.
|
|
1322
|
+
if (options?.truncated === true) {
|
|
1323
|
+
const maxBytes = options.maxBytes ?? 0;
|
|
1324
|
+
content.appendChild(
|
|
1325
|
+
el(
|
|
1326
|
+
"p",
|
|
1327
|
+
"muted execution-output-truncated",
|
|
1328
|
+
`内容过长,已截断(maxBytes=${maxBytes})`,
|
|
1329
|
+
),
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
if (errorText) {
|
|
1333
|
+
const error = el("section", "markdown-error");
|
|
1334
|
+
error.appendChild(el("p", "markdown-error-title", "错误输出"));
|
|
1335
|
+
error.appendChild(renderMarkdown(errorText));
|
|
1336
|
+
content.appendChild(error);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function renderCachedOrPreviewBody(content, dagRunId, node, key, isLatest) {
|
|
1341
|
+
const cached = key
|
|
1342
|
+
? getDagNodeExecutionOutputCache(dagRunId, node.nodeId, key)
|
|
1343
|
+
: null;
|
|
1344
|
+
if (cached?.ok && cached.body) {
|
|
1345
|
+
appendOutputBody(
|
|
1346
|
+
content,
|
|
1347
|
+
composeExecutionBodyText(cached.body),
|
|
1348
|
+
composeExecutionErrorText(cached.body),
|
|
1349
|
+
{
|
|
1350
|
+
truncated: cached.body.truncated === true,
|
|
1351
|
+
maxBytes: cached.body.maxBytes,
|
|
1352
|
+
},
|
|
1353
|
+
);
|
|
1354
|
+
return "cached";
|
|
1355
|
+
}
|
|
1356
|
+
if (isLatest) {
|
|
1357
|
+
appendOutputBody(
|
|
1358
|
+
content,
|
|
1359
|
+
node.outputPreview ?? "",
|
|
1360
|
+
node.errorPreview ?? "",
|
|
1361
|
+
);
|
|
1362
|
+
return "preview";
|
|
1363
|
+
}
|
|
1364
|
+
if (cached && cached.ok === false) {
|
|
1365
|
+
const message =
|
|
1366
|
+
cached.error || "该次执行输出暂不可用(artifact 缺失或无法读取)。";
|
|
1367
|
+
const error = el("p", "empty", message);
|
|
1368
|
+
error.setAttribute("role", "status");
|
|
1369
|
+
content.appendChild(error);
|
|
1370
|
+
return "missing";
|
|
1371
|
+
}
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
async function loadAndRenderExecutionOutput(
|
|
1376
|
+
content,
|
|
1377
|
+
dagRunId,
|
|
1378
|
+
node,
|
|
1379
|
+
key,
|
|
1380
|
+
isLatest,
|
|
1381
|
+
) {
|
|
1382
|
+
const nodeId = node.nodeId;
|
|
1383
|
+
const identity = makeDagNodeExecutionOutputIdentity(dagRunId, nodeId, key);
|
|
1384
|
+
uiState.dagNodeExecutionOutputIdentity = identity;
|
|
1385
|
+
uiState.dagNodeExecutionOutputKey = key;
|
|
1386
|
+
uiState.dagNodeExecutionOutputLoading = true;
|
|
1387
|
+
uiState.dagNodeExecutionOutputError = null;
|
|
1388
|
+
|
|
1389
|
+
const stillCurrent = () =>
|
|
1390
|
+
uiState.dagNodeExecutionOutputIdentity === identity &&
|
|
1391
|
+
uiState.selectedDagNodeId === nodeId &&
|
|
1392
|
+
// currentDagRunId may be unset in isolated unit stubs; prefer dataset match.
|
|
1393
|
+
(uiState.currentDagRunId == null || uiState.currentDagRunId === dagRunId) &&
|
|
1394
|
+
uiState.dagInspectorTab === "output" &&
|
|
1395
|
+
content.isConnected &&
|
|
1396
|
+
content.dataset.dagNodeId === String(nodeId) &&
|
|
1397
|
+
content.dataset.dagRunId === String(dagRunId) &&
|
|
1398
|
+
effectiveExecutionKey(dagRunId, inspectorSelection.node ?? node) === key;
|
|
1399
|
+
|
|
1400
|
+
const result = await fetchExecutionOutput(dagRunId, nodeId, key);
|
|
1401
|
+
if (!stillCurrent()) return;
|
|
1402
|
+
|
|
1403
|
+
uiState.dagNodeExecutionOutputLoading = false;
|
|
1404
|
+
if (!result.ok || !result.body) {
|
|
1405
|
+
const message =
|
|
1406
|
+
result.body?.error ||
|
|
1407
|
+
(result.status === 0
|
|
1408
|
+
? "加载执行输出失败(网络错误)。"
|
|
1409
|
+
: result.status === 404
|
|
1410
|
+
? "该次执行输出暂不可用(artifact 缺失或无法读取)。"
|
|
1411
|
+
: `加载执行输出失败(HTTP ${result.status})。`);
|
|
1412
|
+
// Cache the miss for this key only — never wipe other valid bodies.
|
|
1413
|
+
setDagNodeExecutionOutputCache(dagRunId, nodeId, key, {
|
|
1414
|
+
ok: false,
|
|
1415
|
+
error: message,
|
|
1416
|
+
status: result.status,
|
|
1417
|
+
});
|
|
1418
|
+
uiState.dagNodeExecutionOutputError = message;
|
|
1419
|
+
// Rebuild body area: keep attempt bar, replace status/body.
|
|
1420
|
+
repaintOutputTabBody(content, dagRunId, node);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
setDagNodeExecutionOutputCache(dagRunId, nodeId, key, {
|
|
1425
|
+
ok: true,
|
|
1426
|
+
body: result.body,
|
|
1427
|
+
});
|
|
1428
|
+
uiState.dagNodeExecutionOutputError = null;
|
|
1429
|
+
repaintOutputTabBody(content, dagRunId, node);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* Rebuild the output tab body under the attempt bar using cache + preview
|
|
1434
|
+
* without losing selection identity (poll / async completion).
|
|
1435
|
+
*/
|
|
1436
|
+
function repaintOutputTabBody(content, dagRunId, node) {
|
|
1437
|
+
if (
|
|
1438
|
+
!content?.isConnected ||
|
|
1439
|
+
content.dataset.dagNodeId !== String(node.nodeId ?? "") ||
|
|
1440
|
+
content.dataset.dagRunId !== String(dagRunId ?? "") ||
|
|
1441
|
+
activeInspectorTab() !== "output"
|
|
1442
|
+
) {
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
// Remove everything after the attempt bar (or all if no bar).
|
|
1446
|
+
const bar = content.querySelector(".dag-inspector-attempt-bar");
|
|
1447
|
+
const keep = bar ? [bar] : [];
|
|
1448
|
+
while (content.firstChild) content.removeChild(content.firstChild);
|
|
1449
|
+
for (const nodeEl of keep) content.appendChild(nodeEl);
|
|
1450
|
+
|
|
1451
|
+
const key = effectiveExecutionKey(dagRunId, node);
|
|
1452
|
+
const latest = isLatestExecutionSelection(dagRunId, node);
|
|
1453
|
+
const rendered = renderCachedOrPreviewBody(
|
|
1454
|
+
content,
|
|
1455
|
+
dagRunId,
|
|
1456
|
+
node,
|
|
1457
|
+
key,
|
|
1458
|
+
latest,
|
|
1459
|
+
);
|
|
1460
|
+
if (rendered === null) {
|
|
1461
|
+
const loading = el("p", "muted", "正在加载执行输出…");
|
|
1462
|
+
loading.setAttribute("role", "status");
|
|
1463
|
+
loading.setAttribute("aria-live", "polite");
|
|
1464
|
+
content.appendChild(loading);
|
|
1465
|
+
}
|
|
1466
|
+
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "output");
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1066
1469
|
function fillDagInspectorContent(content, dagRunId, node) {
|
|
1067
1470
|
const previousRenderedNode = content.dataset.dagNodeId ?? "";
|
|
1068
1471
|
const previousRenderedRun = content.dataset.dagRunId ?? "";
|
|
@@ -1110,21 +1513,39 @@ function fillDagInspectorContent(content, dagRunId, node) {
|
|
|
1110
1513
|
if (activeTab === "spec-evidence") {
|
|
1111
1514
|
void renderSpecEvidence(content, dagRunId, node.nodeId);
|
|
1112
1515
|
} else if (activeTab === "timeline") {
|
|
1516
|
+
const attemptBuilt = buildAttemptBar(dagRunId, node);
|
|
1517
|
+
if (attemptBuilt?.bar) content.appendChild(attemptBuilt.bar);
|
|
1113
1518
|
renderSessionTimeline(content, node.nodeId);
|
|
1114
1519
|
} else if (activeTab === "input") {
|
|
1115
1520
|
void renderDagNodeInput(content, dagRunId, node);
|
|
1116
|
-
} else if (!node.outputPreview && !node.errorPreview) {
|
|
1117
|
-
content.appendChild(el("p", "empty", UI_TEXT.noOutput));
|
|
1118
|
-
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "output");
|
|
1119
1521
|
} else {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
if (
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
content
|
|
1522
|
+
// output tab: attempt bar + latest preview immediate / historical async body.
|
|
1523
|
+
const attemptBuilt = buildAttemptBar(dagRunId, node);
|
|
1524
|
+
if (attemptBuilt?.bar) content.appendChild(attemptBuilt.bar);
|
|
1525
|
+
const key = effectiveExecutionKey(dagRunId, node);
|
|
1526
|
+
const latest = isLatestExecutionSelection(dagRunId, node);
|
|
1527
|
+
const rendered = renderCachedOrPreviewBody(
|
|
1528
|
+
content,
|
|
1529
|
+
dagRunId,
|
|
1530
|
+
node,
|
|
1531
|
+
key,
|
|
1532
|
+
latest,
|
|
1533
|
+
);
|
|
1534
|
+
// Historical (or multi-exec) selection without cache → loading then fetch.
|
|
1535
|
+
if (key && rendered === null) {
|
|
1536
|
+
const loading = el("p", "muted", "正在加载执行输出…");
|
|
1537
|
+
loading.setAttribute("role", "status");
|
|
1538
|
+
loading.setAttribute("aria-live", "polite");
|
|
1539
|
+
content.appendChild(loading);
|
|
1540
|
+
void loadAndRenderExecutionOutput(content, dagRunId, node, key, latest);
|
|
1541
|
+
} else if (key && rendered === "preview" && latest) {
|
|
1542
|
+
// Optional parallel enhance: load full artifact body for latest.
|
|
1543
|
+
const cached = getDagNodeExecutionOutputCache(dagRunId, node.nodeId, key);
|
|
1544
|
+
if (!cached) {
|
|
1545
|
+
void loadAndRenderExecutionOutput(content, dagRunId, node, key, latest);
|
|
1546
|
+
}
|
|
1127
1547
|
}
|
|
1548
|
+
// Single-execution nodes (!key) already painted preview via renderCachedOrPreviewBody.
|
|
1128
1549
|
restoreInspectorContentScroll(content, dagRunId, node.nodeId, "output");
|
|
1129
1550
|
}
|
|
1130
1551
|
}
|
|
@@ -1223,16 +1644,46 @@ function syncDagInspectorHeader(panel, dagRunId, node) {
|
|
|
1223
1644
|
|
|
1224
1645
|
function dagInspectorSignature(dagRunId, node) {
|
|
1225
1646
|
const tab = activeInspectorTab();
|
|
1647
|
+
const execKey = effectiveExecutionKey(dagRunId, node);
|
|
1648
|
+
const cachedBody = execKey
|
|
1649
|
+
? getDagNodeExecutionOutputCache(dagRunId, node.nodeId, execKey)
|
|
1650
|
+
: null;
|
|
1651
|
+
const cachedRevision = cachedBody?.ok
|
|
1652
|
+
? [
|
|
1653
|
+
cachedBody.body?.assistantText ?? "",
|
|
1654
|
+
cachedBody.body?.stdout ?? "",
|
|
1655
|
+
cachedBody.body?.stderr ?? "",
|
|
1656
|
+
cachedBody.body?.truncated === true ? "trunc" : "full",
|
|
1657
|
+
cachedBody.body?.maxBytes ?? "",
|
|
1658
|
+
]
|
|
1659
|
+
: cachedBody
|
|
1660
|
+
? ["err", cachedBody.error ?? ""]
|
|
1661
|
+
: ["none"];
|
|
1226
1662
|
const contentRevision =
|
|
1227
1663
|
tab === "output"
|
|
1228
|
-
? [
|
|
1664
|
+
? [
|
|
1665
|
+
node.outputPreview ?? "",
|
|
1666
|
+
node.errorPreview ?? "",
|
|
1667
|
+
...cachedRevision,
|
|
1668
|
+
uiState.dagNodeExecutionOutputLoading ? "loading" : "idle",
|
|
1669
|
+
]
|
|
1229
1670
|
: tab === "timeline"
|
|
1230
1671
|
? [uiState.sessionEventOffset]
|
|
1231
1672
|
: [node.status ?? ""];
|
|
1673
|
+
// Attempt dimension: selection identity + history shape. Poll refreshes keep
|
|
1674
|
+
// the DOM while selection/history are unchanged; switching attempt or a
|
|
1675
|
+
// changed attempt count rebuilds the selector.
|
|
1676
|
+
const summary = node.executionSummary;
|
|
1677
|
+
const attemptRevision = [
|
|
1678
|
+
summary?.totalAttemptCount ?? 0,
|
|
1679
|
+
(node.passes ?? []).join(","),
|
|
1680
|
+
execKey ?? "latest",
|
|
1681
|
+
];
|
|
1232
1682
|
return JSON.stringify([
|
|
1233
1683
|
dagRunId,
|
|
1234
1684
|
node.nodeId,
|
|
1235
1685
|
tab,
|
|
1686
|
+
attemptRevision,
|
|
1236
1687
|
contentRevision,
|
|
1237
1688
|
]);
|
|
1238
1689
|
}
|