@tea-agent/loop-agent 0.11.0 → 0.13.0-alpha.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 (123) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/README.md +33 -4
  3. package/dist/application/dag/generate-task-dag.js +45 -0
  4. package/dist/application/dag/run-dag.js +10 -0
  5. package/dist/application/dag/validate-dag.js +11 -0
  6. package/dist/cli/command-definitions.js +10 -3
  7. package/dist/commands/init.js +74 -7
  8. package/dist/commands/knowledge.js +129 -31
  9. package/dist/governance/manifest-types.js +3 -0
  10. package/dist/shared/package-metadata.js +135 -0
  11. package/dist/task/config-types.js +6 -1
  12. package/dist/worker/cli.js +99 -2
  13. package/dist/worker/delivery/package.js +3 -3
  14. package/dist/worker/feature/decision-loader.js +37 -6
  15. package/dist/worker/feature/next-action.js +10 -2
  16. package/dist/worker/feature/ready-plan-projection.js +81 -0
  17. package/dist/worker/feature/reducer.js +2 -1
  18. package/dist/worker/feature/review.js +19 -2
  19. package/dist/worker/feature/run.js +27 -2
  20. package/dist/worker/follow-up/approve.js +5 -2
  21. package/dist/worker/follow-up/factory.js +1 -1
  22. package/dist/worker/observability/event-history.js +216 -0
  23. package/dist/worker/observability/read-model.js +552 -118
  24. package/dist/worker/observe/paths.js +17 -0
  25. package/dist/worker/observe/routes.js +310 -23
  26. package/dist/worker/observe/server.js +59 -1
  27. package/dist/worker/observe/spec-evidence.js +281 -0
  28. package/dist/worker/observe/static/api.js +46 -0
  29. package/dist/worker/observe/static/app.js +120 -2598
  30. package/dist/worker/observe/static/constants.js +148 -0
  31. package/dist/worker/observe/static/copy.js +67 -0
  32. package/dist/worker/observe/static/dag-helpers.js +172 -0
  33. package/dist/worker/observe/static/dag-model.js +72 -0
  34. package/dist/worker/observe/static/dom.js +61 -0
  35. package/dist/worker/observe/static/format-pool.js +67 -0
  36. package/dist/worker/observe/static/format.js +292 -0
  37. package/dist/worker/observe/static/index.html +300 -82
  38. package/dist/worker/observe/static/kpi.js +94 -0
  39. package/dist/worker/observe/static/relations.js +133 -0
  40. package/dist/worker/observe/static/router.js +93 -0
  41. package/dist/worker/observe/static/run-processing.js +148 -0
  42. package/dist/worker/observe/static/shell-chrome.js +68 -0
  43. package/dist/worker/observe/static/state.js +253 -0
  44. package/dist/worker/observe/static/styles.css +1731 -495
  45. package/dist/worker/observe/static/views/batch.js +227 -0
  46. package/dist/worker/observe/static/views/dag-graph.js +172 -0
  47. package/dist/worker/observe/static/views/dag-inspector.js +596 -0
  48. package/dist/worker/observe/static/views/dag.js +362 -0
  49. package/dist/worker/observe/static/views/dashboard.js +445 -0
  50. package/dist/worker/observe/static/views/failures.js +143 -0
  51. package/dist/worker/observe/static/views/feature.js +492 -0
  52. package/dist/worker/observe/static/views/pool.js +350 -0
  53. package/dist/worker/observe/static/views/run.js +453 -0
  54. package/dist/worker/observe/static/views/session-timeline.js +205 -0
  55. package/dist/worker/observe/static/views/shell.js +7 -0
  56. package/dist/worker/observe/static/views/task.js +314 -0
  57. package/dist/worker/observe/static/views/timeline.js +163 -0
  58. package/dist/worker/pool/doctor.js +165 -0
  59. package/dist/worker/pool/migrate-state.js +303 -0
  60. package/dist/worker/pool/run-store.js +205 -17
  61. package/dist/worker/pool/types.js +17 -1
  62. package/dist/worker/pool/validation.js +100 -15
  63. package/dist/worker/report/morning-report.js +12 -2
  64. package/dist/worker/runner/run-ready.js +41 -26
  65. package/dist/worker/task-graph/ready-planner.js +136 -0
  66. package/dist/workflows/dag/controller-identity.js +104 -0
  67. package/dist/workflows/dag/convergence/controller.js +16 -8
  68. package/dist/workflows/dag/failure-routing.js +12 -1
  69. package/dist/workflows/dag/init-hybrid.js +1233 -11
  70. package/dist/workflows/dag/node-execution.js +123 -29
  71. package/dist/workflows/dag/repair-artifact.js +91 -0
  72. package/dist/workflows/dag/report.js +50 -0
  73. package/dist/workflows/dag/retry-policy.js +138 -0
  74. package/dist/workflows/dag/runner.js +32 -0
  75. package/dist/workflows/dag/runtime-contract.js +87 -0
  76. package/dist/workflows/dag/skill-snapshot.js +2 -0
  77. package/dist/workflows/dag/types.js +45 -1
  78. package/dist/workflows/dag/validate.js +68 -4
  79. package/docs/README.md +1 -1
  80. package/docs/agent-dag-recovery-playbook.md +9 -0
  81. package/docs/agent-dag-runner.md +26 -1
  82. package/docs/architecture/dag-execution.md +6 -0
  83. package/docs/architecture/evolution.md +7 -5
  84. package/docs/architecture/facts-and-state.md +15 -2
  85. package/docs/architecture/worker-and-feature.md +6 -2
  86. package/docs/decisions/README.md +3 -0
  87. package/docs/design/README.md +12 -3
  88. package/docs/exec-plans/active/README.md +2 -2
  89. package/docs/exec-plans/completed/README.md +12 -0
  90. package/docs/feature-workflow.md +108 -2
  91. package/docs/loop-agent-harness.md +15 -4
  92. package/docs/progress/README.md +22 -0
  93. package/docs/reports/README.md +14 -2
  94. package/docs/templates/agent-dag-report.schema.json +17 -0
  95. package/docs/templates/agent-dag.schema.json +69 -1
  96. package/docs/templates/agent-dag.supervised-implementation.json +8 -2
  97. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
  98. package/docs/templates/backend-test-dag.json +288 -0
  99. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
  100. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
  101. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
  102. package/docs/templates/knowledge-sync-dag.json +177 -0
  103. package/docs/templates/knowledge-sync-draft.schema.json +71 -0
  104. package/docs/verification-matrix.md +2 -1
  105. package/package.json +8 -2
  106. package/scripts/kb-bootstrap-init-skeleton.sh +239 -0
  107. package/scripts/kb-graph-incremental-prepare.mjs +372 -0
  108. package/scripts/kb-graph-incremental-prepare.sh +5 -0
  109. package/scripts/kb-graph-materialize.mjs +105 -0
  110. package/scripts/kb-graph-materialize.sh +4 -0
  111. package/scripts/kb-graph-promote.mjs +153 -0
  112. package/scripts/kb-graph-promote.sh +4 -0
  113. package/scripts/kb-query.mjs +554 -0
  114. package/scripts/kb-query.sh +5 -0
  115. package/skills/agent-worker/SKILL.md +3 -1
  116. package/skills/agent-worker/references/agent-worker-operator.md +18 -1
  117. package/skills/frontend-design-review/SKILL.md +26 -24
  118. package/skills/frontend-implementation/SKILL.md +29 -26
  119. package/skills/frontend-implementation/references/node-contracts.md +50 -19
  120. package/skills/frontend-review/SKILL.md +1 -1
  121. package/skills/loop-agent/references/command-reference.md +2 -0
  122. package/skills/loop-agent/references/hybrid-dag.md +22 -3
  123. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
@@ -0,0 +1,453 @@
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
+ computeFollowLatest,
60
+ updateDagOutputViewportState,
61
+ renderViewState,
62
+ mountViewState,
63
+ } from "../state.js";
64
+ import { uiState } 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 { dagProgress } from "../dag-helpers.js";
80
+ import {
81
+ findBatchRunIdForTask,
82
+ buildStepStates,
83
+ stepMarker,
84
+ getCurrentCommand,
85
+ getOutputTail,
86
+ collectArtifacts,
87
+ } from "../run-processing.js";
88
+
89
+ export async function mergeRunEvents(workerRunId) {
90
+ const page = await fetchJson(
91
+ `/api/runs/${encodeURIComponent(workerRunId)}/events?after=${uiState.runEventOffset}`,
92
+ );
93
+ if (!page?.events) return;
94
+ uiState.runEvents = uiState.runEvents.concat(page.events);
95
+ uiState.runEventOffset += page.events.length;
96
+ }
97
+
98
+ export function renderInlineArtifactList(container, artifacts) {
99
+ if (!artifacts || artifacts.size === 0) {
100
+ container.appendChild(el("p", "empty", UI_TEXT.noArtifacts));
101
+ return;
102
+ }
103
+ const list = el("ul", "artifact-list artifact-preview-list");
104
+ for (const [label, artifactPath] of artifacts) {
105
+ const li = document.createElement("li");
106
+ li.className = "artifact-preview-item";
107
+ const head = el("div", "artifact-preview-head");
108
+ const title = el(
109
+ "span",
110
+ "artifact-preview-label",
111
+ `${label}: ${artifactPath}`,
112
+ );
113
+ const open = document.createElement("a");
114
+ open.href = artifactUrl(artifactPath);
115
+ open.textContent = "打开";
116
+ open.target = "_blank";
117
+ open.rel = "noopener";
118
+ open.className = "artifact-link";
119
+ open.addEventListener("click", (e) => e.stopPropagation());
120
+ head.appendChild(title);
121
+ head.appendChild(document.createTextNode(" "));
122
+ head.appendChild(open);
123
+ const pre = el("pre", "output-block artifact-preview-body", "加载中…");
124
+ li.appendChild(head);
125
+ li.appendChild(pre);
126
+ list.appendChild(li);
127
+ void (async () => {
128
+ try {
129
+ const res = await fetch(artifactUrl(artifactPath));
130
+ const text = await res.text();
131
+ if (!res.ok) {
132
+ let message = text;
133
+ try {
134
+ const parsed = JSON.parse(text);
135
+ if (parsed && typeof parsed.message === "string") {
136
+ message = parsed.message;
137
+ } else if (parsed && typeof parsed.error === "string") {
138
+ message = parsed.error;
139
+ }
140
+ } catch {
141
+ /* keep raw */
142
+ }
143
+ pre.textContent = `无法读取 (${res.status}): ${message || "未知错误"}`;
144
+ pre.classList.add("artifact-preview-error");
145
+ return;
146
+ }
147
+ const preview = parseArtifactPreviewResponse(text);
148
+ pre.textContent = preview.content || "(空)";
149
+ if (preview.truncated) {
150
+ pre.appendChild(document.createTextNode("\n\n[预览已截断]"));
151
+ }
152
+ } catch (err) {
153
+ pre.textContent = `无法读取: ${err?.message ?? String(err)}`;
154
+ pre.classList.add("artifact-preview-error");
155
+ }
156
+ })();
157
+ }
158
+ container.appendChild(list);
159
+ }
160
+
161
+ export function renderRunDagProgress(task, snapshot) {
162
+ const panel = document.getElementById("run-dag-progress");
163
+ clearNode(panel);
164
+ const dag = findDagForRun(snapshot, task.workerRunId, task);
165
+ if (!dag) return;
166
+
167
+ const progress = dagProgress(dag);
168
+ panel.appendChild(el("h3", null, "DAG 节点进度"));
169
+ panel.appendChild(
170
+ metaGrid([
171
+ ["DAG", dag.title || dag.dagRunId],
172
+ ["当前状态", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
173
+ ["进度", `${progress.finished}/${progress.total} 个节点`],
174
+ [
175
+ "详情",
176
+ (() => {
177
+ const a = el("a", "link", "查看 DAG 详情 →");
178
+ a.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
179
+ a.addEventListener("click", (e) => {
180
+ e.preventDefault();
181
+ navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
182
+ });
183
+ return a;
184
+ })(),
185
+ ],
186
+ ]),
187
+ );
188
+ }
189
+
190
+ export function renderRunPanels(task, events, snapshot) {
191
+ const metaEl = document.getElementById("run-meta");
192
+ const stepsEl = document.getElementById("run-steps");
193
+ const commandEl = document.getElementById("run-command");
194
+ const outputEl = document.getElementById("run-output");
195
+ const artifactsEl = document.getElementById("run-artifacts");
196
+ const diagnosisEl = document.getElementById("run-diagnosis");
197
+
198
+ const nowMs = Date.now();
199
+ const livenessNode = livenessBadge(task.liveness);
200
+ const statusCell = livenessNode
201
+ ? (() => {
202
+ const wrap = document.createDocumentFragment();
203
+ wrap.appendChild(badge(task.status));
204
+ wrap.appendChild(document.createTextNode(" "));
205
+ wrap.appendChild(livenessNode);
206
+ return wrap;
207
+ })()
208
+ : badge(task.status);
209
+
210
+ const dag = findDagForRun(snapshot, task.workerRunId, task);
211
+ const batchRunId =
212
+ task.batchRunId ?? findBatchRunIdForTask(snapshot, task) ?? null;
213
+ const runDuration = displayDurationMs(task);
214
+
215
+ mountRelationBar(
216
+ "run-relations",
217
+ {
218
+ featureId: task.featureId,
219
+ taskId: task.taskId,
220
+ batchRunId: batchRunId ?? undefined,
221
+ workerRunId: task.workerRunId,
222
+ dagRunId: dag?.dagRunId ?? task.dagRunId,
223
+ },
224
+ snapshot,
225
+ );
226
+
227
+ clearNode(metaEl);
228
+ metaEl.appendChild(el("h3", "layer-primary", "主事实"));
229
+ metaEl.appendChild(
230
+ metaGrid([
231
+ ["Worker 运行 ID", task.workerRunId ?? "—"],
232
+ ["Task", objectRouteLink("task", task.taskId, snapshot, task.featureId)],
233
+ [
234
+ "Feature",
235
+ task.featureId
236
+ ? objectRouteLink("feature", task.featureId, snapshot)
237
+ : el("span", "muted", "—"),
238
+ ],
239
+ ["状态", statusCell],
240
+ [
241
+ "Batch",
242
+ batchRunId
243
+ ? objectRouteLink("batch", batchRunId, snapshot)
244
+ : el("span", "muted", "缺失"),
245
+ ],
246
+ [
247
+ "DAG",
248
+ dag
249
+ ? objectRouteLink("dag", dag.dagRunId, snapshot)
250
+ : task.dagRunId
251
+ ? el("span", "muted", task.dagRunId)
252
+ : el("span", "muted", "缺失"),
253
+ ],
254
+ ["开始时间", formatTs(task.startedAt)],
255
+ ["结束时间", formatTs(task.finishedAt)],
256
+ ["派生耗时", formatMs(runDuration)],
257
+ ["已用时", formatDuration(task.elapsedMs)],
258
+ ["上次心跳", formatAgo(task.lastHeartbeatAt, nowMs)],
259
+ ["上次输出", formatAgo(task.lastOutputAt, nowMs)],
260
+ [
261
+ "超时截止",
262
+ task.timeoutAt
263
+ ? formatAgo(task.timeoutAt, nowMs)
264
+ : task.pid !== undefined
265
+ ? `pid ${task.pid}`
266
+ : "—",
267
+ ],
268
+ ]),
269
+ );
270
+
271
+ renderRunDagProgress(task, snapshot);
272
+
273
+ const steps = buildStepStates(events);
274
+ clearNode(stepsEl);
275
+ const stepsHeading = el("h3", null, "执行过程 · 步骤");
276
+ stepsEl.appendChild(stepsHeading);
277
+ if (steps.every((s) => s.status === "pending")) {
278
+ stepsEl.appendChild(el("p", "empty", "暂无步骤事件"));
279
+ }
280
+ const ul = el("ul", "step-timeline");
281
+ for (const step of steps) {
282
+ const li = document.createElement("li");
283
+ const marker = el(
284
+ "span",
285
+ `step-marker ${step.status}`,
286
+ stepMarker(step.status),
287
+ );
288
+ const name = el("span", "step-name", step.display);
289
+ const dur = el(
290
+ "span",
291
+ "step-duration",
292
+ step.durationMs !== undefined ? formatMs(step.durationMs) : "",
293
+ );
294
+ li.appendChild(marker);
295
+ if (step.label === "run-dag" && dag?.dagRunId) {
296
+ const link = document.createElement("a");
297
+ link.href = `#/dag/${encodeURIComponent(dag.dagRunId)}`;
298
+ link.className = "step-link";
299
+ link.textContent = "DAG →";
300
+ link.addEventListener("click", (e) => {
301
+ e.preventDefault();
302
+ navigate(`/dag/${encodeURIComponent(dag.dagRunId)}`);
303
+ });
304
+ li.appendChild(name);
305
+ li.appendChild(link);
306
+ } else {
307
+ li.appendChild(name);
308
+ }
309
+ li.appendChild(dur);
310
+ ul.appendChild(li);
311
+ }
312
+ stepsEl.appendChild(ul);
313
+
314
+ const cmd = getCurrentCommand(events);
315
+ clearNode(commandEl);
316
+ commandEl.appendChild(el("h3", null, "当前命令"));
317
+ if (!cmd) {
318
+ commandEl.appendChild(el("p", "empty", UI_TEXT.noActiveCommand));
319
+ } else {
320
+ commandEl.appendChild(
321
+ metaGrid([
322
+ ["标签", cmd.label ?? "—"],
323
+ ["命令", (cmd.command ?? []).join(" ") || "—"],
324
+ ["PID", cmd.pid !== undefined ? String(cmd.pid) : "—"],
325
+ [
326
+ "超时上限",
327
+ cmd.timeoutMs !== undefined ? formatMs(cmd.timeoutMs) : "—",
328
+ ],
329
+ [
330
+ "已用时间",
331
+ cmd.elapsedMs !== undefined ? formatMs(cmd.elapsedMs) : "—",
332
+ ],
333
+ ]),
334
+ );
335
+ }
336
+
337
+ const outputLabel = cmd?.label;
338
+ const tail = getOutputTail(events, outputLabel);
339
+ const existingPre = outputEl.querySelector("pre.output-block");
340
+ let followRunOutput = true;
341
+ let runOutputScrollLeft = 0;
342
+ let runOutputScrollTop = 0;
343
+ if (existingPre) {
344
+ followRunOutput = computeFollowLatest(
345
+ existingPre.scrollTop,
346
+ existingPre.scrollHeight,
347
+ existingPre.clientHeight,
348
+ );
349
+ runOutputScrollLeft = existingPre.scrollLeft;
350
+ runOutputScrollTop = existingPre.scrollTop;
351
+ }
352
+ const nextOutput = tail || "暂无输出";
353
+ // let pre = existingPre — preserve pre identity across polls when present.
354
+ let pre = existingPre;
355
+ if (!pre) {
356
+ clearNode(outputEl);
357
+ outputEl.appendChild(el("h3", null, "最新输出 (tail)"));
358
+ pre = el("pre", "output-block", nextOutput);
359
+ outputEl.appendChild(pre);
360
+ } else if (pre.textContent !== nextOutput) {
361
+ pre.textContent = nextOutput;
362
+ }
363
+ if (followRunOutput) {
364
+ pre.scrollTop = pre.scrollHeight;
365
+ } else {
366
+ pre.scrollTop = runOutputScrollTop;
367
+ }
368
+ pre.scrollLeft = runOutputScrollLeft;
369
+ const runRegionKey = `run:${task.workerRunId ?? task.taskId}:output`;
370
+ uiState.runOutputViewportState = updateDagOutputViewportState(
371
+ uiState.runOutputViewportState,
372
+ runRegionKey,
373
+ pre.scrollLeft,
374
+ pre.scrollTop,
375
+ followRunOutput,
376
+ );
377
+
378
+ let diagnosisHeader = diagnosisEl?.querySelector(".run-diagnosis-header");
379
+ if (diagnosisEl && !diagnosisHeader) {
380
+ diagnosisHeader = el("div", "run-diagnosis-header panel panel-meta");
381
+ diagnosisEl.insertBefore(diagnosisHeader, diagnosisEl.firstChild);
382
+ }
383
+ if (diagnosisHeader) {
384
+ clearNode(diagnosisHeader);
385
+ diagnosisHeader.appendChild(el("h3", "layer-diagnostic", "诊断证据"));
386
+ diagnosisHeader.appendChild(
387
+ metaGrid([
388
+ ["失败分类", task.failureCategory ?? "—"],
389
+ ["建议跟进", task.recommendedFollowUp ?? "—"],
390
+ ]),
391
+ );
392
+ }
393
+
394
+ const artifacts = collectArtifacts(task, events);
395
+ clearNode(artifactsEl);
396
+ artifactsEl.appendChild(el("h3", null, "产物预览"));
397
+ renderInlineArtifactList(artifactsEl, artifacts);
398
+ }
399
+
400
+ export async function renderRun(workerRunId, initial = true) {
401
+ showView("run");
402
+ setBreadcrumb([
403
+ { label: UI_TEXT.dashboard, href: "#/" },
404
+ { label: workerRunId },
405
+ ]);
406
+
407
+ if (initial) {
408
+ uiState.runEvents = [];
409
+ uiState.runEventOffset = 0;
410
+ uiState.currentRunId = workerRunId;
411
+ const runMeta = document.getElementById("run-meta");
412
+ if (runMeta) mountViewState(runMeta, "loading", "加载运行…");
413
+ } else if (uiState.currentRunId && uiState.currentRunId !== workerRunId) {
414
+ return false;
415
+ }
416
+
417
+ const [task, snapshot] = await Promise.all([
418
+ fetchJson(`/api/runs/${encodeURIComponent(workerRunId)}`),
419
+ fetchJson("/api/snapshot"),
420
+ ]);
421
+ if (!task) {
422
+ if (initial) {
423
+ const runMeta = document.getElementById("run-meta");
424
+ if (runMeta) mountViewState(runMeta, "error", "找不到该运行。");
425
+ }
426
+ return false;
427
+ }
428
+
429
+ if (snapshot) {
430
+ uiState.lastSnapshot = snapshot;
431
+ updateHeaderRefresh(snapshot.generatedAt);
432
+ }
433
+
434
+ uiState.currentBatchRunId = task.batchRunId ?? uiState.currentBatchRunId;
435
+ await mergeRunEvents(workerRunId);
436
+ renderRunPanels(task, uiState.runEvents, snapshot ?? uiState.lastSnapshot);
437
+ return !TERMINAL_RUN_STATUSES.has((task.status ?? "").toLowerCase());
438
+ }
439
+
440
+ export function startRunPolling(workerRunId, initial = true) {
441
+ if (uiState.runPollTimer) clearTimeout(uiState.runPollTimer);
442
+ const generation = ++uiState.pollingGeneration;
443
+ const poll = async (isInitial) => {
444
+ const active = await renderRun(workerRunId, isInitial);
445
+ if (generation !== uiState.pollingGeneration || !active) return;
446
+ const delay = detailPollDelay("running", isPageVisible());
447
+ uiState.runPollTimer = setTimeout(() => {
448
+ uiState.runPollTimer = null;
449
+ void poll(false);
450
+ }, delay);
451
+ };
452
+ void poll(initial);
453
+ }
@@ -0,0 +1,205 @@
1
+ /** Session event timeline helpers (Observe UI R5). */
2
+ import { UI_TEXT } from "../constants.js";
3
+ import { el } from "../dom.js";
4
+ import { formatTs } from "../format.js";
5
+ import { fetchJson } from "../api.js";
6
+ import {
7
+ uiState,
8
+ updateDagTimelineViewportState,
9
+ computeFollowLatest,
10
+ } from "../state.js";
11
+
12
+ export function extractAssistantSummary(event) {
13
+ if (!event || typeof event !== "object") return "—";
14
+ const record = event;
15
+ if (record.message && typeof record.message === "object") {
16
+ const content = record.message.content;
17
+ if (Array.isArray(content)) {
18
+ return (
19
+ content
20
+ .filter(
21
+ (part) => part?.type === "text" && typeof part.text === "string",
22
+ )
23
+ .map((part) => part.text)
24
+ .join(" ") || "—"
25
+ );
26
+ }
27
+ if (typeof record.message.content === "string") {
28
+ return record.message.content;
29
+ }
30
+ }
31
+ if (typeof record.text === "string") return record.text;
32
+ if (typeof record.outputPreview === "string") return record.outputPreview;
33
+ return "—";
34
+ }
35
+
36
+ export function formatSessionEventLabel(event) {
37
+ if (!event || typeof event !== "object") return "未知类型";
38
+ const type = typeof event.type === "string" ? event.type : "unknown";
39
+ const toolName = event.toolName ?? event.name ?? "—";
40
+ if (type === "agent_start") return "Agent 已启动";
41
+ if (type === "agent_end") return "Agent 已结束";
42
+ if (type === "agent_settled") return "Agent 已稳定";
43
+ if (type === "turn_start") return "开始新一轮处理";
44
+ if (type === "tool_execution_start" || type === "tool_start") {
45
+ return `调用工具 · ${toolName}`;
46
+ }
47
+ if (type === "tool_execution_end" || type === "tool_end") {
48
+ return `工具调用完成 · ${toolName}`;
49
+ }
50
+ if (
51
+ type === "turn_end" ||
52
+ type === "assistant" ||
53
+ type === "assistant_message"
54
+ ) {
55
+ return `AI 回复摘要 · ${truncateText(extractAssistantSummary(event), 120)}`;
56
+ }
57
+ return type;
58
+ }
59
+
60
+ export function sessionEventTimestamp(event) {
61
+ if (!event || typeof event !== "object") return "";
62
+ const ts =
63
+ (typeof event.timestamp === "string" && event.timestamp) ||
64
+ (typeof event.at === "string" && event.at) ||
65
+ (typeof event.recordedAt === "string" && event.recordedAt);
66
+ return ts || "";
67
+ }
68
+
69
+ export function formatSessionEventTime(event) {
70
+ const ts = sessionEventTimestamp(event);
71
+ if (!ts) return "未记录时间";
72
+ return formatTs(ts).replace(/\s.*/, "") || formatTs(ts);
73
+ }
74
+
75
+ export function sessionEventKind(event) {
76
+ const type = typeof event?.type === "string" ? event.type : "unknown";
77
+ if (type === "tool_execution_start" || type === "tool_start")
78
+ return "tool-start";
79
+ if (type === "tool_execution_end" || type === "tool_end") return "tool-end";
80
+ if (
81
+ type === "turn_end" ||
82
+ type === "assistant" ||
83
+ type === "assistant_message"
84
+ ) {
85
+ return "assistant";
86
+ }
87
+ if (type.startsWith("agent_") || type === "turn_start") return "lifecycle";
88
+ return "protocol";
89
+ }
90
+
91
+ export function buildSessionTimelineItems(events) {
92
+ const items = [];
93
+ for (let index = 0; index < events.length; index += 1) {
94
+ const event = events[index];
95
+ const type = typeof event?.type === "string" ? event.type : "unknown";
96
+ if (type === "message_start" || type === "message_end") {
97
+ let count = 1;
98
+ while (index + 1 < events.length) {
99
+ const nextType = events[index + 1]?.type;
100
+ if (nextType !== "message_start" && nextType !== "message_end") break;
101
+ count += 1;
102
+ index += 1;
103
+ }
104
+ items.push({ event, kind: "protocol", protocolCount: count });
105
+ continue;
106
+ }
107
+ items.push({ event, kind: sessionEventKind(event) });
108
+ }
109
+ return items;
110
+ }
111
+
112
+ export async function mergeSessionEvents(dagRunId, nodeId) {
113
+ if (!dagRunId || !nodeId) return;
114
+ const page = await fetchJson(
115
+ `/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/session-events?after=${uiState.sessionEventOffset}`,
116
+ );
117
+ if (!page) return;
118
+ if (Array.isArray(page.events) && page.events.length > 0) {
119
+ uiState.sessionEvents = uiState.sessionEvents.concat(page.events);
120
+ }
121
+ if (typeof page.nextOffset === "number") {
122
+ uiState.sessionEventOffset = page.nextOffset;
123
+ }
124
+ }
125
+
126
+ export function renderSessionTimeline(content, nodeId) {
127
+ content.dataset.dagRunId = uiState.currentDagRunId ?? "";
128
+ content.dataset.nodeId = nodeId ?? "";
129
+ content.classList.add("process-timeline-scroll");
130
+ if (!nodeId || uiState.sessionEvents.length === 0) {
131
+ content.appendChild(el("p", "empty", UI_TEXT.noSessionEvents));
132
+ return;
133
+ }
134
+
135
+ const timelineItems = buildSessionTimelineItems(uiState.sessionEvents);
136
+ const hasEventTimes = uiState.sessionEvents.some((event) =>
137
+ Boolean(sessionEventTimestamp(event)),
138
+ );
139
+ if (!hasEventTimes) {
140
+ content.appendChild(
141
+ el(
142
+ "p",
143
+ "process-timeline-note",
144
+ "事件未记录具体时间,以下按采集顺序展示。",
145
+ ),
146
+ );
147
+ }
148
+ const ul = el("ul", "process-timeline");
149
+ for (let index = 0; index < timelineItems.length; index += 1) {
150
+ const item = timelineItems[index];
151
+ const li = el(
152
+ "li",
153
+ `process-timeline-item process-timeline-${item.kind}${hasEventTimes ? " has-time" : ""}`,
154
+ );
155
+ if (index === 0) li.classList.add("is-first");
156
+ if (index === timelineItems.length - 1) li.classList.add("is-last");
157
+ const rail = el("span", "process-timeline-rail");
158
+ rail.appendChild(el("span", "process-timeline-dot"));
159
+ li.appendChild(rail);
160
+ if (hasEventTimes) {
161
+ li.appendChild(
162
+ el("span", "process-timeline-time", formatSessionEventTime(item.event)),
163
+ );
164
+ }
165
+ const label =
166
+ item.protocolCount > 1
167
+ ? `消息收发 · 已合并 ${item.protocolCount} 条协议事件`
168
+ : formatSessionEventLabel(item.event);
169
+ li.appendChild(el("span", "process-timeline-label", label));
170
+ ul.appendChild(li);
171
+ }
172
+ content.appendChild(ul);
173
+
174
+ const timelineState =
175
+ uiState.dagTimelineViewportState?.dagRunId === uiState.currentDagRunId &&
176
+ uiState.dagTimelineViewportState.nodeId === nodeId
177
+ ? uiState.dagTimelineViewportState
178
+ : null;
179
+ content.addEventListener(
180
+ "scroll",
181
+ () => {
182
+ const followLatest = computeFollowLatest(
183
+ content.scrollTop,
184
+ content.scrollHeight,
185
+ content.clientHeight,
186
+ );
187
+ uiState.dagTimelineViewportState = updateDagTimelineViewportState(
188
+ uiState.dagTimelineViewportState,
189
+ uiState.currentDagRunId,
190
+ nodeId,
191
+ content.scrollLeft,
192
+ content.scrollTop,
193
+ followLatest,
194
+ );
195
+ },
196
+ { passive: true },
197
+ );
198
+ if (timelineState?.followLatest ?? true) {
199
+ content.scrollLeft = timelineState?.scrollLeft ?? 0;
200
+ content.scrollTop = content.scrollHeight;
201
+ } else {
202
+ content.scrollLeft = timelineState.scrollLeft;
203
+ content.scrollTop = timelineState.scrollTop;
204
+ }
205
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shell service locator removed in R5 boundary final (BND-02).
3
+ * Views import focused modules directly (shell-chrome, kpi, dag-helpers, run-processing).
4
+ * This file is retained only so historical bundle paths do not 404; do not reintroduce getShell.
5
+ */
6
+
7
+ export {};