@tea-agent/loop-agent 0.11.0 → 0.12.0

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 (76) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +3 -2
  3. package/dist/application/dag/generate-task-dag.js +15 -0
  4. package/dist/application/dag/run-dag.js +10 -0
  5. package/dist/application/dag/validate-dag.js +11 -0
  6. package/dist/commands/init.js +74 -7
  7. package/dist/shared/package-metadata.js +135 -0
  8. package/dist/task/config-types.js +1 -0
  9. package/dist/worker/cli.js +3 -1
  10. package/dist/worker/observability/event-history.js +216 -0
  11. package/dist/worker/observability/read-model.js +312 -83
  12. package/dist/worker/observe/paths.js +17 -0
  13. package/dist/worker/observe/routes.js +165 -21
  14. package/dist/worker/observe/server.js +59 -1
  15. package/dist/worker/observe/static/api.js +27 -0
  16. package/dist/worker/observe/static/app.js +120 -2598
  17. package/dist/worker/observe/static/constants.js +148 -0
  18. package/dist/worker/observe/static/copy.js +67 -0
  19. package/dist/worker/observe/static/dag-helpers.js +172 -0
  20. package/dist/worker/observe/static/dag-model.js +72 -0
  21. package/dist/worker/observe/static/dom.js +61 -0
  22. package/dist/worker/observe/static/format-pool.js +67 -0
  23. package/dist/worker/observe/static/format.js +292 -0
  24. package/dist/worker/observe/static/index.html +300 -82
  25. package/dist/worker/observe/static/kpi.js +94 -0
  26. package/dist/worker/observe/static/relations.js +128 -0
  27. package/dist/worker/observe/static/router.js +85 -0
  28. package/dist/worker/observe/static/run-processing.js +148 -0
  29. package/dist/worker/observe/static/shell-chrome.js +68 -0
  30. package/dist/worker/observe/static/state.js +253 -0
  31. package/dist/worker/observe/static/styles.css +1719 -495
  32. package/dist/worker/observe/static/views/batch.js +226 -0
  33. package/dist/worker/observe/static/views/dag-graph.js +172 -0
  34. package/dist/worker/observe/static/views/dag-inspector.js +477 -0
  35. package/dist/worker/observe/static/views/dag.js +362 -0
  36. package/dist/worker/observe/static/views/dashboard.js +442 -0
  37. package/dist/worker/observe/static/views/failures.js +143 -0
  38. package/dist/worker/observe/static/views/feature.js +453 -0
  39. package/dist/worker/observe/static/views/pool.js +347 -0
  40. package/dist/worker/observe/static/views/run.js +453 -0
  41. package/dist/worker/observe/static/views/session-timeline.js +205 -0
  42. package/dist/worker/observe/static/views/shell.js +7 -0
  43. package/dist/worker/observe/static/views/task.js +260 -0
  44. package/dist/worker/observe/static/views/timeline.js +163 -0
  45. package/dist/workflows/dag/controller-identity.js +104 -0
  46. package/dist/workflows/dag/init-hybrid.js +396 -3
  47. package/dist/workflows/dag/node-execution.js +123 -29
  48. package/dist/workflows/dag/repair-artifact.js +91 -0
  49. package/dist/workflows/dag/report.js +50 -0
  50. package/dist/workflows/dag/retry-policy.js +138 -0
  51. package/dist/workflows/dag/runner.js +32 -0
  52. package/dist/workflows/dag/runtime-contract.js +87 -0
  53. package/dist/workflows/dag/skill-snapshot.js +2 -0
  54. package/dist/workflows/dag/types.js +44 -1
  55. package/dist/workflows/dag/validate.js +68 -4
  56. package/docs/agent-dag-runner.md +26 -1
  57. package/docs/architecture/dag-execution.md +6 -0
  58. package/docs/architecture/evolution.md +4 -3
  59. package/docs/architecture/facts-and-state.md +1 -1
  60. package/docs/design/README.md +4 -3
  61. package/docs/exec-plans/active/README.md +1 -3
  62. package/docs/exec-plans/completed/README.md +11 -0
  63. package/docs/feature-workflow.md +28 -0
  64. package/docs/progress/README.md +18 -0
  65. package/docs/reports/README.md +8 -2
  66. package/docs/templates/agent-dag-report.schema.json +17 -0
  67. package/docs/templates/agent-dag.schema.json +69 -1
  68. package/docs/templates/agent-dag.supervised-implementation.json +8 -2
  69. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
  70. package/docs/templates/backend-test-dag.json +276 -0
  71. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
  72. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
  73. package/package.json +1 -1
  74. package/skills/loop-agent/references/command-reference.md +1 -0
  75. package/skills/loop-agent/references/hybrid-dag.md +22 -3
  76. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
@@ -0,0 +1,362 @@
1
+ /** Observe UI view module (R5). */
2
+ import {
3
+ UI_TEXT,
4
+ LIVENESS_LABELS,
5
+ ARTIFACT_LABELS,
6
+ ARTIFACT_KEYS,
7
+ STEP_LABELS,
8
+ STEP_ORDER,
9
+ DAG_INSPECTOR_DEFAULT_WIDTH,
10
+ DAG_INSPECTOR_MIN_WIDTH,
11
+ DAG_INSPECTOR_MAX_WIDTH,
12
+ DAG_INSPECTOR_RIGHT_OFFSET,
13
+ DAG_INSPECTOR_VIEWPORT_MARGIN,
14
+ DAG_INSPECTOR_KEYBOARD_STEP,
15
+ TERMINAL_RUN_STATUSES,
16
+ DAG_EFFECTIVE_STATUS_LABELS,
17
+ STATUS_LABELS,
18
+ } from "../constants.js";
19
+ import { clearNode, el, metaGrid, buildTable } from "../dom.js";
20
+ import {
21
+ badge,
22
+ badgeClass,
23
+ statusLabel,
24
+ formatTs,
25
+ formatMs,
26
+ formatDuration,
27
+ formatAgo,
28
+ dagStatusBadge,
29
+ livenessBadge,
30
+ displayDurationMs,
31
+ derivedDurationMs,
32
+ summaryText,
33
+ truncateText,
34
+ repoBaseName,
35
+ parseMarkdownBlocks,
36
+ markdownLinkHref,
37
+ } from "../format.js";
38
+ import { parseRoute, navigate, buildHashPath } from "../router.js";
39
+ import {
40
+ fetchJson,
41
+ artifactUrl,
42
+ parseArtifactPreviewResponse,
43
+ } from "../api.js";
44
+ import {
45
+ objectRouteLink,
46
+ findFeatureInSnapshot,
47
+ featureLookupState,
48
+ buildObjectRelations,
49
+ renderObjectRelationBar,
50
+ } from "../relations.js";
51
+ import { copyText, renderRecommendedCommand } from "../copy.js";
52
+ import {
53
+ isPageVisible,
54
+ detailPollDelay,
55
+ dashboardPollDelay,
56
+ captureDagGraphViewportState,
57
+ updateDagGraphViewportState,
58
+ updateDagTimelineViewportState,
59
+ shouldReuseDagDetailShell,
60
+ shouldPreserveDiagnosticsOpen,
61
+ renderViewState,
62
+ mountViewState,
63
+ } from "../state.js";
64
+ import { uiState, dagGraphViewportStates } from "../state.js";
65
+ import { sortPoolTasks } from "../format-pool.js";
66
+ import {
67
+ dagMetaVisibility,
68
+ isDagRunActive,
69
+ dashboardVisibleFeatures,
70
+ findDagForRun,
71
+ } from "../dag-model.js";
72
+ import { layoutDag } from "../dag-layout.js";
73
+ import {
74
+ showView,
75
+ setBreadcrumb,
76
+ updateHeaderRefresh,
77
+ mountRelationBar,
78
+ } from "../shell-chrome.js";
79
+ import {
80
+ dagProgress,
81
+ isNodeFailed,
82
+ isNodeFinished,
83
+ isNodeActive,
84
+ renderRankLanes,
85
+ } from "../dag-helpers.js";
86
+ import { findBatchRunIdForTask } from "../run-processing.js";
87
+ import { renderDagGraph } from "./dag-graph.js";
88
+ import {
89
+ closeDagInspector,
90
+ selectDagNode,
91
+ renderDagInspector,
92
+ } from "./dag-inspector.js";
93
+ import { mergeSessionEvents } from "./session-timeline.js";
94
+
95
+ export async function renderDagDetail(dagRunId, initial = true) {
96
+ showView("dag");
97
+ setBreadcrumb([
98
+ { label: UI_TEXT.dashboard, href: "#/" },
99
+ { label: dagRunId },
100
+ ]);
101
+
102
+ if (initial || uiState.currentDagRunId !== dagRunId) {
103
+ uiState.currentDagRunId = dagRunId;
104
+ uiState.selectedDagNodeId = null;
105
+ uiState.sessionEventOffset = 0;
106
+ uiState.sessionEvents = [];
107
+ uiState.dagInspectorOpen = false;
108
+ uiState.dagInspectorTab = "output";
109
+ uiState.dagGraphViewportState = null;
110
+ uiState.dagTimelineViewportState = null;
111
+ }
112
+
113
+ const metaEl = document.getElementById("dag-meta");
114
+ const ranksEl = document.getElementById("dag-ranks");
115
+ const nodesEl = document.getElementById("dag-nodes");
116
+ if (initial && metaEl) mountViewState(metaEl, "loading", "加载 DAG…");
117
+ uiState.dagGraphViewportState = captureDagGraphViewportState(
118
+ uiState.dagGraphViewportState,
119
+ dagRunId,
120
+ ranksEl.querySelector(".dag-graph-viewport"),
121
+ );
122
+ if (uiState.dagGraphViewportState?.dagRunId === dagRunId) {
123
+ dagGraphViewportStates.set(dagRunId, uiState.dagGraphViewportState);
124
+ }
125
+
126
+ const [dag, snapshot] = await Promise.all([
127
+ fetchJson(`/api/dag-runs/${encodeURIComponent(dagRunId)}`),
128
+ fetchJson("/api/snapshot"),
129
+ ]);
130
+ if (snapshot) {
131
+ uiState.lastSnapshot = snapshot;
132
+ updateHeaderRefresh(snapshot.generatedAt);
133
+ }
134
+ if (!dag) {
135
+ clearNode(metaEl);
136
+ mountViewState(metaEl, "error", "找不到该 DAG 运行。");
137
+ mountRelationBar("dag-relations", {}, snapshot);
138
+ closeDagInspector();
139
+ return false;
140
+ }
141
+
142
+ // Only link feature/task when a snapshot object carries this dagRunId (no naming guess).
143
+ const snap = snapshot ?? uiState.lastSnapshot;
144
+ const linkedTask = (snap?.tasks ?? []).find(
145
+ (t) => t.dagRunId === dag.dagRunId,
146
+ );
147
+ mountRelationBar(
148
+ "dag-relations",
149
+ {
150
+ featureId: linkedTask?.featureId,
151
+ taskId: linkedTask?.taskId,
152
+ batchRunId:
153
+ linkedTask?.batchRunId ??
154
+ (linkedTask ? findBatchRunIdForTask(snap, linkedTask) : undefined),
155
+ workerRunId: linkedTask?.workerRunId,
156
+ dagRunId: dag.dagRunId,
157
+ },
158
+ snap,
159
+ );
160
+
161
+ const nodes = dag.nodes ?? [];
162
+ if (
163
+ uiState.selectedDagNodeId &&
164
+ !nodes.some((n) => n.nodeId === uiState.selectedDagNodeId)
165
+ ) {
166
+ uiState.selectedDagNodeId = null;
167
+ uiState.dagInspectorOpen = false;
168
+ if (initial) {
169
+ uiState.sessionEventOffset = 0;
170
+ uiState.sessionEvents = [];
171
+ }
172
+ }
173
+
174
+ const progress = dagProgress(dag);
175
+ const failedCount = nodes.filter((n) => isNodeFailed(n.status)).length;
176
+ const skippedCount = nodes.filter(
177
+ (n) => (n.status ?? "").toLowerCase() === "skipped",
178
+ ).length;
179
+ const succeededCount = nodes.filter(
180
+ (n) => isNodeFinished(n.status) && !isNodeFailed(n.status),
181
+ ).length;
182
+
183
+ // reuse-dag-detail-shell: on a poll refresh (non-initial) we keep the
184
+ // interactive DOM identity stable so active scroll/resize gestures and the
185
+ // diagnostics <details> open state survive the redraw. Initial paints and
186
+ // DAG-run switches rebuild everything from scratch as before.
187
+ const existingGraphWrap =
188
+ ranksEl.querySelector(":scope > .dag-graph-wrap") ?? null;
189
+ const existingViewport =
190
+ existingGraphWrap?.querySelector(":scope > .dag-graph-viewport") ?? null;
191
+ const existingDetails =
192
+ metaEl.querySelector("details.meta-diagnostics") ?? null;
193
+ const reuseShell = shouldReuseDagDetailShell(
194
+ initial,
195
+ Boolean(existingViewport),
196
+ );
197
+ const preserveDetailsOpen = shouldPreserveDiagnosticsOpen(
198
+ initial,
199
+ Boolean(existingDetails?.open),
200
+ );
201
+
202
+ const visibility = dagMetaVisibility(dag);
203
+ const livenessValue =
204
+ LIVENESS_LABELS[dag.liveness] ?? dag.liveness ?? LIVENESS_LABELS.unknown;
205
+ const primaryEntries = [
206
+ ["标题", dag.title || dag.dagRunId],
207
+ ["DAG 运行 ID", dag.dagRunId],
208
+ ["状态", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
209
+ ];
210
+ if (visibility.showExecutorStatus)
211
+ primaryEntries.push(["执行器状态", livenessValue]);
212
+ if (visibility.showConsistencyWarning)
213
+ primaryEntries.push(["状态一致性", "不一致,需要处理"]);
214
+ if (visibility.showResume) primaryEntries.push(["允许恢复", "是"]);
215
+ if (visibility.showReconcile) primaryEntries.push(["允许收口", "是"]);
216
+ primaryEntries.push(
217
+ ["开始时间", formatTs(dag.startedAt)],
218
+ ["结束时间", formatTs(dag.finishedAt)],
219
+ ["总耗时", formatDuration(dag.durationMs)],
220
+ ["进度", `${progress.finished}/${progress.total}`],
221
+ ["成功/失败/跳过", `${succeededCount}/${failedCount}/${skippedCount}`],
222
+ ["DAG 路径", dag.dagPath ?? "—"],
223
+ );
224
+ const primaryGrid = metaGrid(primaryEntries);
225
+ const diagnosticGrid = metaGrid([
226
+ ["当前判定", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
227
+ ["原始记录状态", badge(dag.status)],
228
+ ["执行器状态", livenessValue],
229
+ ["生命周期", dag.lifecycle ?? "—"],
230
+ [
231
+ "状态一致性",
232
+ visibility.showConsistencyWarning ? "不一致,需要处理" : "一致",
233
+ ],
234
+ ["允许恢复", visibility.showResume ? "是" : "否"],
235
+ ["允许收口", visibility.showReconcile ? "是" : "否"],
236
+ ]);
237
+ if (!initial && existingDetails?.parentNode === metaEl) {
238
+ const existingPrimary = metaEl.querySelector(":scope > .meta-grid");
239
+ if (existingPrimary) metaEl.replaceChild(primaryGrid, existingPrimary);
240
+ else metaEl.insertBefore(primaryGrid, existingDetails);
241
+ const existingDiagnosticGrid = existingDetails.querySelector(
242
+ ":scope > .meta-grid",
243
+ );
244
+ if (existingDiagnosticGrid) {
245
+ existingDetails.replaceChild(diagnosticGrid, existingDiagnosticGrid);
246
+ } else {
247
+ existingDetails.appendChild(diagnosticGrid);
248
+ }
249
+ existingDetails.open = preserveDetailsOpen;
250
+ } else {
251
+ clearNode(metaEl);
252
+ metaEl.appendChild(primaryGrid);
253
+ const details = el("details", "meta-diagnostics");
254
+ details.appendChild(el("summary", null, "诊断信息"));
255
+ details.appendChild(diagnosticGrid);
256
+ metaEl.appendChild(details);
257
+ }
258
+
259
+ const graphGestureActive = document.body.classList.contains(
260
+ "is-resizing-dag-graph",
261
+ );
262
+ if (nodes.length === 0) {
263
+ clearNode(ranksEl);
264
+ ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
265
+ ranksEl.appendChild(el("p", "empty", UI_TEXT.noNodes));
266
+ } else if (reuseShell && existingGraphWrap?.parentNode === ranksEl) {
267
+ // reuse-dag-graph-wrap: keep wrap identity across polls when possible.
268
+ if (!graphGestureActive) {
269
+ renderDagGraph(dag, dagRunId, existingGraphWrap);
270
+ }
271
+ } else {
272
+ clearNode(ranksEl);
273
+ ranksEl.appendChild(el("h3", null, "DAG 依赖图"));
274
+ ranksEl.appendChild(renderDagGraph(dag, dagRunId, null));
275
+ ranksEl.appendChild(renderRankLanes(dag));
276
+ }
277
+
278
+ const nodesScrollLeft = nodesEl.scrollLeft;
279
+ clearNode(nodesEl);
280
+ const nodeHeading = el("div", "dag-section-heading");
281
+ nodeHeading.appendChild(el("h3", null, "节点"));
282
+ nodeHeading.appendChild(
283
+ el("span", "dag-section-meta", `共 ${nodes.length} 个节点`),
284
+ );
285
+ nodesEl.appendChild(nodeHeading);
286
+
287
+ if (nodes.length === 0) {
288
+ mountViewState(nodesEl, "empty", UI_TEXT.noNodes);
289
+ } else {
290
+ const sortedNodes = [...nodes].sort((a, b) => {
291
+ const ra = Number(a.rank);
292
+ const rb = Number(b.rank);
293
+ if (!Number.isNaN(ra) && !Number.isNaN(rb) && ra !== rb) return ra - rb;
294
+ if (a.rank !== b.rank) return (a.rank ?? "").localeCompare(b.rank ?? "");
295
+ return a.nodeId.localeCompare(b.nodeId);
296
+ });
297
+
298
+ const rows = sortedNodes.map((node) => {
299
+ const active = isNodeActive(node.status);
300
+ const failed = isNodeFailed(node.status);
301
+ let className = "";
302
+ if (active) className = "dag-active-node";
303
+ if (failed) className = "dag-error-node";
304
+ if (node.nodeId === uiState.selectedDagNodeId) {
305
+ className = className
306
+ ? `${className} dag-node-selected`
307
+ : "dag-node-selected";
308
+ }
309
+ return {
310
+ className,
311
+ onClick: () => selectDagNode(dagRunId, node.nodeId),
312
+ cells: [
313
+ node.nodeId,
314
+ node.rank ?? "—",
315
+ node.executor ?? "—",
316
+ node.model ?? "—",
317
+ node.status ? badge(node.status) : "—",
318
+ formatMs(node.durationMs),
319
+ node.errorPreview ?? node.label ?? "—",
320
+ ],
321
+ };
322
+ });
323
+ const nodeTable = buildTable(
324
+ ["节点 ID", "依赖层", "执行方式", "模型", "状态", "耗时", "备注"],
325
+ rows,
326
+ );
327
+ nodeTable.classList.add("dag-node-table");
328
+ for (const row of nodeTable.tBodies[0]?.rows ?? []) {
329
+ for (const columnIndex of [0, 3, 6]) {
330
+ const cell = row.cells[columnIndex];
331
+ if (cell?.textContent) cell.title = cell.textContent;
332
+ }
333
+ }
334
+ nodesEl.appendChild(nodeTable);
335
+ }
336
+ nodesEl.scrollLeft = nodesScrollLeft;
337
+
338
+ if (uiState.dagInspectorOpen && uiState.selectedDagNodeId) {
339
+ await mergeSessionEvents(dagRunId, uiState.selectedDagNodeId);
340
+ }
341
+ renderDagInspector(
342
+ dagRunId,
343
+ nodes.find((node) => node.nodeId === uiState.selectedDagNodeId),
344
+ initial,
345
+ );
346
+ return isDagRunActive(dag);
347
+ }
348
+
349
+ export function startDagPolling(dagRunId, initial = true) {
350
+ if (uiState.dagPollTimer) clearTimeout(uiState.dagPollTimer);
351
+ const generation = ++uiState.pollingGeneration;
352
+ const poll = async (isInitial) => {
353
+ const active = await renderDagDetail(dagRunId, isInitial);
354
+ if (generation !== uiState.pollingGeneration || !active) return;
355
+ const delay = detailPollDelay("running", isPageVisible());
356
+ uiState.dagPollTimer = setTimeout(() => {
357
+ uiState.dagPollTimer = null;
358
+ void poll(false);
359
+ }, delay);
360
+ };
361
+ void poll(initial);
362
+ }