@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/application/evaluation/budget.js +19 -1
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/executors/dag-pi-executor.js +762 -252
  5. package/dist/executors/pi-executor.js +20 -1
  6. package/dist/executors/pi-sdk-executor.js +64 -1
  7. package/dist/executors/shell-executor.js +5 -2
  8. package/dist/shared/frontend-execution-policy.js +22 -0
  9. package/dist/task/config-types.js +4 -0
  10. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  11. package/dist/task/source-prepare/ledger-review.js +6 -9
  12. package/dist/task/source-prepare/semantic-intake.js +16 -26
  13. package/dist/task/source-prepare/source-fidelity-pi.js +26 -7
  14. package/dist/worker/observe/node-transparency.js +81 -72
  15. package/dist/worker/observe/routes.js +20 -1
  16. package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
  17. package/dist/worker/observe/static/dom.js +20 -1
  18. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  19. package/dist/worker/observe/static/format-pool.js +6 -0
  20. package/dist/worker/observe/static/format.js +7 -0
  21. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  22. package/dist/worker/observe/static/inspector-submission.js +32 -0
  23. package/dist/worker/observe/static/kpi.js +1 -0
  24. package/dist/worker/observe/static/relations.js +2 -0
  25. package/dist/worker/observe/static/router.js +13 -0
  26. package/dist/worker/observe/static/run-processing.js +2 -0
  27. package/dist/worker/observe/static/shell-chrome.js +36 -3
  28. package/dist/worker/observe/static/state.js +35 -2
  29. package/dist/worker/observe/static/styles.css +260 -39
  30. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  31. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  32. package/dist/worker/observe/static/task-history.js +12 -0
  33. package/dist/worker/observe/static/views/batch.js +6 -13
  34. package/dist/worker/observe/static/views/dag-graph.js +50 -3
  35. package/dist/worker/observe/static/views/dag-inspector.js +746 -265
  36. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  37. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  38. package/dist/worker/observe/static/views/dag.js +48 -10
  39. package/dist/worker/observe/static/views/dags.js +2 -0
  40. package/dist/worker/observe/static/views/dashboard.js +21 -12
  41. package/dist/worker/observe/static/views/failures.js +21 -11
  42. package/dist/worker/observe/static/views/feature.js +11 -29
  43. package/dist/worker/observe/static/views/pool.js +37 -28
  44. package/dist/worker/observe/static/views/run.js +48 -5
  45. package/dist/worker/observe/static/views/session-timeline.js +189 -240
  46. package/dist/worker/observe/static/views/task.js +81 -62
  47. package/dist/workflows/dag/budget-enforcement.js +53 -3
  48. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  49. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  50. package/dist/workflows/dag/frontend-implementation-contract.js +9 -0
  51. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  52. package/dist/workflows/dag/frontend-plan-render.js +10 -3
  53. package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
  54. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  55. package/dist/workflows/dag/frontend-recovery-run.js +4 -0
  56. package/dist/workflows/dag/frontend-review-scopes.js +117 -0
  57. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  58. package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
  59. package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
  60. package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
  61. package/dist/workflows/dag/init-hybrid.js +16 -10
  62. package/dist/workflows/dag/node-execution.js +32 -155
  63. package/dist/workflows/dag/prompt.js +4 -0
  64. package/dist/workflows/dag/rerun-plan.js +7 -1
  65. package/dist/workflows/dag/runner.js +26 -1
  66. package/dist/workflows/dag/types.js +6 -0
  67. package/docs/operations/README.md +1 -0
  68. package/docs/templates/frontend-design-contract.md +4 -4
  69. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  70. package/docs/templates/frontend-implementation-dag.json +5 -5
  71. package/package.json +1 -1
  72. package/skills/frontend-contract/SKILL.md +2 -1
  73. package/skills/frontend-contract/references/contract-protocol.md +19 -3
  74. package/skills/frontend-design-review/SKILL.md +12 -11
  75. package/skills/frontend-plan/SKILL.md +2 -2
  76. package/skills/frontend-plan/references/decision-contract.md +18 -5
  77. package/skills/frontend-review/SKILL.md +10 -11
  78. package/skills/frontend-scout/references/scout-evidence.md +4 -0
@@ -1,12 +1,14 @@
1
1
  /** Session event timeline helpers (Observe UI R5 + rich process view). */
2
2
  import { UI_TEXT } from "../constants.js";
3
- import { el } from "../dom.js";
3
+ import { el, iconButton } from "../dom.js";
4
4
  import { formatTs, truncateText } from "../format.js";
5
5
  import { renderMarkdown } from "../markdown-render.js";
6
6
  import { fetchJson } from "../api.js";
7
7
  import {
8
8
  uiState,
9
9
  updateDagTimelineViewportState,
10
+ inspectorReadingState,
11
+ isScrollRegionAboveBottom,
10
12
  } from "../state.js";
11
13
 
12
14
  /** Default visible lines for tool result preview before expand. */
@@ -17,6 +19,10 @@ function isTimelineAutoScrollEnabled() {
17
19
  }
18
20
 
19
21
  function scrollTimelineToLatest(content) {
22
+ if (content._readingState) {
23
+ content._readingState.followLatest = true;
24
+ content._readingState.seenCount = content._readingState.eventCount;
25
+ }
20
26
  content.scrollLeft = 0;
21
27
  content.scrollTop = content.scrollHeight;
22
28
  uiState.dagTimelineViewportState = updateDagTimelineViewportState(
@@ -29,7 +35,9 @@ function scrollTimelineToLatest(content) {
29
35
  );
30
36
  }
31
37
 
32
- /** Floating, explicit live-follow control for the execution-record viewport. */
38
+ /** Floating chat-style jump-to-bottom control for execution-record viewports.
39
+ * 可滚动且视口离开底部即出现(终态运行同样可用),贴底或内容不足一屏时隐藏;
40
+ * 点击回到底部,活跃运行同时恢复跟随。 */
33
41
  export function createTimelineAutoScrollControl(content) {
34
42
  const dock = el("div", "process-timeline-autoscroll-dock");
35
43
  const button = document.createElement("button");
@@ -42,29 +50,21 @@ export function createTimelineAutoScrollControl(content) {
42
50
  button.appendChild(icon);
43
51
 
44
52
  const sync = () => {
45
- const enabled = isTimelineAutoScrollEnabled();
46
- const actionLabel = enabled ? "关闭自动滚动" : "自动滚动";
47
- button.className = `process-timeline-autoscroll${enabled ? " is-active" : ""}`;
48
- button.setAttribute("aria-pressed", String(enabled));
49
- button.setAttribute("aria-label", actionLabel);
50
- button.setAttribute("data-tooltip", actionLabel);
51
- icon.className = enabled ? "ri-arrow-down-line" : "ri-pause-mini-fill";
53
+ dock.hidden = !isScrollRegionAboveBottom(content);
54
+ if (dock.hidden) return;
55
+ const unread = content._readingState
56
+ ? Math.max(0, content._readingState.eventCount - content._readingState.seenCount)
57
+ : 0;
58
+ const label = unread > 0 ? `${unread} 条新记录 · 回到底部` : "回到底部";
59
+ button.className = `process-timeline-autoscroll${unread > 0 ? " has-unread" : ""}`;
60
+ button.setAttribute("aria-label", label);
61
+ button.setAttribute("data-tooltip", label);
52
62
  };
63
+ content._syncTimelineFollow = sync;
53
64
 
54
65
  button.addEventListener("click", () => {
55
- const enabled = !isTimelineAutoScrollEnabled();
56
- uiState.dagTimelineAutoScrollEnabled = enabled;
57
- if (enabled) scrollTimelineToLatest(content);
58
- else {
59
- uiState.dagTimelineViewportState = updateDagTimelineViewportState(
60
- uiState.dagTimelineViewportState,
61
- uiState.currentDagRunId,
62
- content.dataset.nodeId,
63
- content.scrollLeft,
64
- content.scrollTop,
65
- false,
66
- );
67
- }
66
+ uiState.dagTimelineAutoScrollEnabled = true;
67
+ scrollTimelineToLatest(content);
68
68
  sync();
69
69
  });
70
70
 
@@ -90,149 +90,6 @@ export function sessionEventArgs(event) {
90
90
  return args && typeof args === "object" && !Array.isArray(args) ? args : {};
91
91
  }
92
92
 
93
- function escapeHtml(text) {
94
- return String(text ?? "")
95
- .replaceAll("&", "&")
96
- .replaceAll("<", "&lt;")
97
- .replaceAll(">", "&gt;")
98
- .replaceAll('"', "&quot;")
99
- .replaceAll("'", "&#39;");
100
- }
101
-
102
- function downloadTextFile(filename, text) {
103
- const blob = new Blob([text], { type: "text/html;charset=utf-8" });
104
- const url = URL.createObjectURL(blob);
105
- const a = document.createElement("a");
106
- a.href = url;
107
- a.download = filename;
108
- document.body.appendChild(a);
109
- a.click();
110
- a.remove();
111
- URL.revokeObjectURL(url);
112
- }
113
-
114
- /** 抓取页面真实加载的样式表原文(导出内联用)。
115
- * 不猜路径:直接取当前文档 <link rel="stylesheet"> 的 href(相对当前页面解析),
116
- * 反代/子路径部署下也和页面渲染用的是同一份 CSS;fetch 失败时返回 "",
117
- * 调用方回退 EXPORT_FALLBACK_CSS。 */
118
- export async function fetchPageStylesheetText() {
119
- try {
120
- const links = [...document.querySelectorAll('link[rel="stylesheet"]')];
121
- const texts = [];
122
- for (const link of links) {
123
- const href = link.getAttribute("href");
124
- if (!href) continue;
125
- const url = new URL(href, document.baseURI).href;
126
- const res = await fetch(url, { cache: "no-cache" });
127
- if (res.ok) texts.push(await res.text());
128
- }
129
- return texts.join("\n");
130
- } catch {
131
- return "";
132
- }
133
- }
134
-
135
- /** 兜底样式:仅在无法抓取页面 /inspect/styles.css 时使用(与页面 process-timeline 排版对齐)。 */
136
- const EXPORT_FALLBACK_CSS = `
137
- :root{--canvas:#f7f7f4;--surface:#ffffff;--canvas-soft:#fafaf7;--surface-muted:#f0efe9;--ink:#26251e;--body:#5a5852;--muted:#807d72;--muted-soft:#a09c92;--hairline:#e6e5e0;--hairline-strong:#cfcdc4;--orange:#f54e00;--orange-active:#d04200;--blue:#3f6ea4;--green:#1f8a65;--amber:#a66a00;--red:#c72d53;--lavender:#e4daf1;--radius-sm:6px;--space-2:8px;--space-3:12px;--space-5:20px}
138
- body{margin:0;font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--canvas);color:var(--body);font-size:13px}
139
- .process-timeline{display:grid;gap:0;margin:0;padding:var(--space-2) 0;list-style:none}
140
- .process-timeline-item{display:grid;grid-template-columns:20px minmax(0,1fr);align-items:stretch;min-height:34px}
141
- .process-timeline-item.has-time{grid-template-columns:20px 92px minmax(0,1fr)}
142
- .process-timeline-rail{position:relative;display:grid;place-items:center;min-height:34px}
143
- .process-timeline-rail::before{position:absolute;top:0;bottom:0;width:1px;background:var(--hairline-strong);content:""}
144
- .process-timeline-item.is-first .process-timeline-rail::before{top:50%}
145
- .process-timeline-item.is-last .process-timeline-rail::before{bottom:50%}
146
- .process-timeline-dot{position:relative;z-index:1;width:8px;height:8px;border:2px solid var(--surface);border-radius:50%;background:var(--muted-soft)}
147
- .process-timeline-lifecycle .process-timeline-dot{background:var(--lavender)}
148
- .process-timeline-tool-start .process-timeline-dot{background:var(--orange)}
149
- .process-timeline-tool-end .process-timeline-dot{background:var(--green)}
150
- .process-timeline-assistant .process-timeline-dot{background:var(--blue)}
151
- .process-timeline-protocol .process-timeline-dot{background:var(--muted-soft)}
152
- .process-timeline-item.is-error .process-timeline-dot{background:var(--red)}
153
- .process-timeline-time{padding:9px 0;color:var(--muted);font-size:10px;font-variant-numeric:tabular-nums}
154
- .process-timeline-label{min-width:0;padding:8px 0 8px var(--space-2);color:var(--body);font-size:12px;line-height:1.5;overflow-wrap:anywhere}
155
- .process-timeline-protocol .process-timeline-label{color:var(--muted);font-size:11px}
156
- .process-timeline-body{min-width:0;padding:4px 0 8px var(--space-2)}
157
- .process-timeline-card{display:grid;gap:6px;min-width:0}
158
- .process-timeline-details{min-width:0;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--surface)}
159
- .process-timeline-summary{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;cursor:pointer;list-style:none;color:var(--ink);font-size:12px;font-weight:650}
160
- .process-timeline-summary::-webkit-details-marker{display:none}
161
- .process-timeline-summary-title{min-width:0;overflow-wrap:anywhere}
162
- .process-timeline-summary-status{flex-shrink:0;color:var(--muted);font-size:11px;font-weight:700}
163
- .process-timeline-summary-status.status-succeeded{color:var(--green)}
164
- .process-timeline-summary-status.status-failed{color:var(--red)}
165
- .process-timeline-summary-status.status-pending{color:var(--orange)}
166
- .process-timeline-tool-card.is-error .process-timeline-details{border-color:var(--red);background:var(--canvas-soft)}
167
- .process-timeline-card-body{display:grid;gap:6px;padding:0 10px 10px}
168
- .process-timeline-section-label{color:var(--muted);font-size:11px;font-weight:700}
169
- .process-timeline-code,.process-timeline-thinking-body{margin:0;padding:8px 10px;overflow:auto;border-radius:var(--radius-sm);background:var(--canvas-soft);color:var(--body);font-family:"JetBrains Mono","SFMono-Regular",Consolas,monospace;font-size:11px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere}
170
- .process-timeline-more{margin-top:2px}
171
- .process-timeline-more>summary{cursor:pointer;color:var(--blue);font-size:11px;font-weight:650}
172
- .process-timeline-thinking>summary{background:var(--lavender)}
173
- .process-timeline-turn-text{display:grid;gap:6px;padding:8px 10px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--surface)}
174
- .process-timeline-turn-text .markdown-output{font-size:12px}
175
- .process-timeline-tool-only-note{margin:0;font-size:12px}
176
- .process-timeline-turn-tools{display:grid;gap:4px;margin:0;padding:0 0 0 1.1em;color:var(--body);font-size:12px;font-family:"JetBrains Mono","SFMono-Regular",Consolas,monospace;overflow-wrap:anywhere}
177
- .process-timeline-turn-tools li{padding:2px 0}
178
- .process-timeline-agent-end-note{margin:0;font-size:11px}
179
- .markdown-output{display:grid;gap:var(--space-3);color:var(--body);font-size:13px;line-height:1.65}
180
- .markdown-output>:first-child{margin-top:0}
181
- .markdown-output>:last-child{margin-bottom:0}
182
- .markdown-output h1,.markdown-output h2,.markdown-output h3,.markdown-output h4{margin:var(--space-5) 0 var(--space-2);color:var(--ink);line-height:1.3}
183
- .markdown-output h1{font-size:21px}.markdown-output h2{font-size:18px}.markdown-output h3{font-size:16px}.markdown-output h4{font-size:14px}
184
- .markdown-output p,.markdown-output blockquote,.markdown-output ul,.markdown-output ol{margin:0;overflow-wrap:anywhere}
185
- .markdown-output ul,.markdown-output ol{padding-left:22px}
186
- .markdown-output blockquote{padding-left:12px}
187
- .btn{display:inline-flex;align-items:center;gap:6px;min-height:28px;padding:0 12px;border:1px solid var(--hairline-strong);border-radius:var(--radius-sm);background:var(--surface);color:var(--body);font-size:12px;font-weight:600;cursor:pointer;user-select:none}
188
- .btn-secondary{background:var(--surface-muted)}
189
- .btn:hover{border-color:var(--orange);color:var(--orange-active)}
190
- `;
191
-
192
- const EXPORT_SHELL_CSS = `
193
- /* 导出文档变量基础:页面样式把这些变量定义在 body[data-shell]/.inspect-workspace-root
194
- * 下,独立导出的 HTML 既无该属性也无该包裹,变量会整体失效——深色系统的画布
195
- * 兜底为黑底,叠加黑字后正文不可读。导出文档自带变量定义并强制 light 配色。 */
196
- :root{color-scheme:light}
197
- body{margin:0;font-family:-apple-system,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;background:var(--canvas,#f7f7f4)}
198
- .export-shell{max-width:1000px;margin:0 auto;padding:24px 28px;background:var(--surface,#ffffff);min-height:100vh;box-shadow:0 0 0 1px var(--hairline,#e6e5e0)}
199
- .export-head h1{font-size:15px;color:var(--ink,#26251e);margin:0;font-weight:700}
200
- .export-sub{color:var(--muted,#807d72);font-size:12px;margin:4px 0 16px}
201
- .export-controls{display:flex;justify-content:flex-start;margin:0 0 12px}
202
- .export-toggle{min-height:28px;padding:0 12px;border:1px solid var(--hairline-strong,#cfcdc4);border-radius:var(--radius-sm,6px);background:var(--surface,#ffffff);color:var(--body,#5a5852);font-size:11px;font-weight:600;cursor:pointer;user-select:none}
203
- .export-toggle:hover,.export-toggle:focus-visible{border-color:var(--orange,#f54e00);color:var(--orange-active,#d04200);outline:none}
204
- body.hide-protocol .process-timeline-item.process-timeline-protocol{display:none}
205
- `;
206
-
207
- /** 导出文档自带的变量基础:与页面 styles.css 的 light 主题保持一致。
208
- * 即使 fetch 到了页面 CSS,其变量也挂在 body[data-shell=…]/.inspect-workspace-root
209
- * 选择器下而不生效,必须在导出文档内重新定义。 */
210
- const EXPORT_ROOT_VARS_CSS = `:root{--canvas:#f7f7f4;--surface:#ffffff;--canvas-soft:#fafaf7;--surface-muted:#f0efe9;--surface-ink:#26251e;--ink:#26251e;--body:#5a5852;--muted:#807d72;--muted-soft:#a09c92;--hairline:#e6e5e0;--hairline-strong:#cfcdc4;--orange:#f54e00;--orange-active:#d04200;--blue:#3f6ea4;--green:#1f8a65;--amber:#a66a00;--red:#c72d53;--lavender:#e4daf1;--mint:#d8ead5;--peach:#f1d8cb;--gold:#d8b166;--radius-sm:6px;--radius-md:8px;--radius-lg:12px;--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px}`;
211
-
212
- /**
213
- * 把页面渲染出的时间线 innerHTML 封装成自包含 HTML。
214
- * @param {string} bodyHTML 页面渲染的 .process-timeline 等真实 DOM 序列化结果
215
- * @param {{nodeId?:string,dagRunId?:string}} meta
216
- * @param {string} [cssText] 页面 /inspect/styles.css 原文;缺失时用兜底样式
217
- */
218
- export function buildPageStyledDoc(bodyHTML, meta = {}, cssText = "") {
219
- const now = new Date().toLocaleString();
220
- const title = meta.nodeId ? `节点执行过程 · ${meta.nodeId}` : "节点执行过程";
221
- const style = cssText.trim()
222
- ? `${EXPORT_ROOT_VARS_CSS}\n${cssText}\n\n/* 导出外壳 */\n${EXPORT_SHELL_CSS}`
223
- : `${EXPORT_FALLBACK_CSS}\n${EXPORT_SHELL_CSS}`;
224
- // 有协议行时,在左上角提供一个「展开/隐藏原始协议事件」按钮(与页面一致),默认隐藏。
225
- const hasProtocol = bodyHTML.includes("process-timeline-protocol");
226
- const controls = hasProtocol
227
- ? `<div class="export-controls"><button type="button" id="exportToggleProtocol" class="export-toggle">展开原始协议事件</button></div><script>(function(){var b=document.body,t=document.getElementById('exportToggleProtocol'),on=false;function apply(){b.classList.toggle('hide-protocol',!on);t.textContent=on?'隐藏原始协议事件':'展开原始协议事件';}if(t){t.addEventListener('click',function(){on=!on;apply();});apply();}})();</script>`
228
- : "";
229
- // body 带 data-shell="observe-console":内联的页面 styles.css 把全部设计令牌
230
- // (--canvas/--surface/--hairline-strong …) 与部分规则 (body[data-shell] …) 定义在
231
- // 该作用域下,导出文档带上属性后令牌与前缀规则原生命中,与页面渲染一致。
232
- // EXPORT_ROOT_VARS_CSS 仍保留:fetch 页面 CSS 失败的兜底路径令牌不缺。
233
- return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>${style}</style></head><body data-shell="observe-console"><div class="export-shell"><div class="export-head"><h1>${escapeHtml(title)}</h1></div><p class="export-sub">DAG ${escapeHtml(meta.dagRunId ?? "")} · 导出于 ${escapeHtml(now)}</p>${controls}<div class="export-timeline">${bodyHTML}</div></div></body></html>`;
234
- }
235
-
236
93
  export function sessionEventResult(event) {
237
94
  return normalizeSessionEventFields(event).result;
238
95
  }
@@ -442,6 +299,7 @@ export function buildSessionTimelineItems(events) {
442
299
  while (index + 1 < events.length) {
443
300
  const nextType = events[index + 1]?.type;
444
301
  if (nextType !== "message_start" && nextType !== "message_end") break;
302
+ if (eventCategory({ kind: "protocol", event: events[index + 1] }) !== eventCategory({ kind: "protocol", event })) break;
445
303
  count += 1;
446
304
  index += 1;
447
305
  }
@@ -480,13 +338,16 @@ export function splitSessionEventsByAttempt(events) {
480
338
  * key. attempt-N shows only that attempt's segment when the shared log carries
481
339
  * multiple attempts; other keys (pass-N, null) keep the full stream.
482
340
  */
483
- export function resolveTimelineVisibleEvents(events, executionKey) {
341
+ export function resolveTimelineVisibleEvents(events, executionKey, expectedAttempts) {
484
342
  const match =
485
343
  typeof executionKey === "string"
486
344
  ? /^attempt-(\d+)$/.exec(executionKey)
487
345
  : null;
488
346
  if (!match) return { events, note: null };
489
347
  const segments = splitSessionEventsByAttempt(events);
348
+ if (Number.isSafeInteger(expectedAttempts) && segments.length !== expectedAttempts) return {
349
+ events, note: "当前显示节点共享日志:日志会话边界与尝试记录不一致,无法可靠按尝试拆分;分次产出可在“产出”中查看。",
350
+ };
490
351
  if (segments.length <= 1) return { events, note: null };
491
352
  const requested = Number(match[1]);
492
353
  if (requested >= 1 && requested <= segments.length) {
@@ -531,6 +392,7 @@ export function buildRichTimelineItems(events, options = {}) {
531
392
  while (index + 1 < events.length) {
532
393
  const nextType = events[index + 1]?.type;
533
394
  if (nextType !== "message_start" && nextType !== "message_end") break;
395
+ if (eventCategory({ kind: "protocol", event: events[index + 1] }) !== eventCategory({ kind: "protocol", event })) break;
534
396
  count += 1;
535
397
  index += 1;
536
398
  protocolEvents.push(normalizeSessionEventFields(events[index]));
@@ -548,6 +410,7 @@ export function buildRichTimelineItems(events, options = {}) {
548
410
  kind: "protocol",
549
411
  event,
550
412
  protocolCount: count,
413
+ protocolEvents,
551
414
  collapsed: true,
552
415
  detailKey: `protocol-${items.length}`,
553
416
  });
@@ -684,32 +547,28 @@ export function buildRichTimelineItems(events, options = {}) {
684
547
  }
685
548
 
686
549
  const EXECUTION_RECORD_FILTERS = Object.freeze([
687
- ["all", "全部"],
688
- ["model", "模型消息"],
689
- ["tools", "工具与进程"],
550
+ ["all", "全部"], ["model", "模型消息"], ["tools", "工具与进程"], ["system", "系统事件"],
690
551
  ]);
691
-
692
552
  function normalizeExecutionRecordFilter(value) {
693
- return value === "model" || value === "tools" ? value : "all";
553
+ return ["model", "tools", "system"].includes(value) ? value : "all";
694
554
  }
695
-
696
- export function filterExecutionRecordItems(items, filter) {
555
+ export function eventCategory(item) {
556
+ if (item.kind === "tool") return "tools";
557
+ if (item.kind === "turn") return "model";
558
+ if (item.kind === "protocol") {
559
+ const role = item.event?.message?.role ?? item.event?.role;
560
+ return ["tool", "toolResult"].includes(role) ? "tools" : ["assistant", "user"].includes(role) ? "model" : "system";
561
+ }
562
+ return "system";
563
+ }
564
+ export function isErrorRecord(item) {
565
+ const event = item.event ?? {};
566
+ return item.isError === true || event.isError === true || event.ok === false ||
567
+ ["failed", "error", "failure"].includes(event.status) || event.message?.stopReason === "error";
568
+ }
569
+ export function filterExecutionRecordItems(items, filter, errorsOnly = false) {
697
570
  const normalized = normalizeExecutionRecordFilter(filter);
698
- if (normalized === "all") return items;
699
- return items.filter((item) => {
700
- const protocolEvents = Array.isArray(item.protocolEvents)
701
- ? item.protocolEvents
702
- : [item.event];
703
- const toolProtocol =
704
- item.kind === "protocol" &&
705
- protocolEvents.some((event) => {
706
- const role = event?.message?.role ?? event?.role;
707
- return role === "tool" || role === "toolResult";
708
- });
709
- const modelFact =
710
- item.kind === "turn" || (item.kind === "protocol" && !toolProtocol);
711
- return normalized === "model" ? modelFact : !modelFact;
712
- });
571
+ return items.filter((item) => (normalized === "all" || eventCategory(item) === normalized) && (!errorsOnly || isErrorRecord(item)));
713
572
  }
714
573
 
715
574
  function ensureOpenDetailsSet() {
@@ -726,7 +585,7 @@ function closedDetailKey(detailKey) {
726
585
  function bindDetailsToggle(details, detailKey, defaultOpen = false) {
727
586
  const openSet = ensureOpenDetailsSet();
728
587
  if (defaultOpen) {
729
- if (!openSet.has(closedDetailKey(detailKey))) details.open = true;
588
+ details.open = !openSet.has(closedDetailKey(detailKey));
730
589
  } else if (openSet.has(detailKey)) {
731
590
  details.open = true;
732
591
  }
@@ -796,9 +655,9 @@ function renderToolCard(item) {
796
655
  more.dataset.detailKey = `${item.detailKey}-full`;
797
656
  bindDetailsToggle(more, `${item.detailKey}-full`);
798
657
  const moreSummary = document.createElement("summary");
799
- moreSummary.textContent = `展开全部结果(${totalLines} 行)`;
658
+ moreSummary.textContent = `展开剩余 ${totalLines - TOOL_RESULT_PREVIEW_LINES} 行(共 ${totalLines} 行)`;
800
659
  more.appendChild(moreSummary);
801
- appendPreBlock(more, "process-timeline-code", resultText);
660
+ appendPreBlock(more, "process-timeline-code", resultText.split("\n").slice(TOOL_RESULT_PREVIEW_LINES).join("\n"));
802
661
  body.appendChild(more);
803
662
  }
804
663
  } else if (!item.pending) {
@@ -827,8 +686,7 @@ function renderTurnCard(item) {
827
686
  details.className = "process-timeline-details process-timeline-thinking";
828
687
  details.dataset.detailKey = `${item.detailKey}-thinking-${i}`;
829
688
  // Default open for first thinking block so anomalies are visible.
830
- if (i === 0) details.open = true;
831
- bindDetailsToggle(details, `${item.detailKey}-thinking-${i}`);
689
+ bindDetailsToggle(details, `${item.detailKey}-thinking-${i}`, i === 0);
832
690
  const summary = document.createElement("summary");
833
691
  summary.className = "process-timeline-summary";
834
692
  summary.textContent = "思考过程";
@@ -937,9 +795,45 @@ export async function mergeSessionEvents(dagRunId, nodeId) {
937
795
  }
938
796
  }
939
797
 
798
+ // Work on text nodes only: log contents must never become HTML.
799
+ export function highlightTimelineSearch(row, query) {
800
+ for (const mark of row.querySelectorAll("mark.process-search-highlight")) {
801
+ mark.replaceWith(document.createTextNode(mark.textContent));
802
+ }
803
+ row.normalize();
804
+ if (!query) return false;
805
+ const pattern = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "giu");
806
+ const walker = document.createTreeWalker(row, 4 /* SHOW_TEXT */);
807
+ const nodes = [];
808
+ while (walker.nextNode()) nodes.push(walker.currentNode);
809
+ let matched = false;
810
+ for (const node of nodes) {
811
+ const text = node.textContent;
812
+ const parts = [];
813
+ let end = 0;
814
+ for (const match of text.matchAll(pattern)) {
815
+ parts.push(document.createTextNode(text.slice(end, match.index)));
816
+ parts.push(el("mark", "process-search-highlight", match[0]));
817
+ end = match.index + match[0].length;
818
+ }
819
+ if (!parts.length) continue;
820
+ parts.push(document.createTextNode(text.slice(end)));
821
+ node.replaceWith(...parts);
822
+ matched = true;
823
+ }
824
+ return matched;
825
+ }
826
+
940
827
  export function renderSessionTimeline(content, nodeId, options = {}) {
941
828
  content.dataset.dagRunId = uiState.currentDagRunId ?? "";
942
829
  content.dataset.nodeId = nodeId ?? "";
830
+ content.dataset.executionKey = options.executionKey ?? content.dataset.executionKey ?? "";
831
+ const reading = inspectorReadingState(uiState.currentDagRunId, nodeId, "timeline", content.dataset.executionKey, uiState.dagTimelineAutoScrollEnabled !== false);
832
+ if (reading) {
833
+ content._readingState = reading;
834
+ uiState.dagTimelineAutoScrollEnabled = reading.followLatest;
835
+ uiState.sessionTimelineOpenDetails = reading.openDetails;
836
+ }
943
837
  content.classList.add("process-timeline-scroll");
944
838
  if (!nodeId || uiState.sessionEvents.length === 0) {
945
839
  content.appendChild(el("p", "empty", UI_TEXT.noSessionEvents));
@@ -949,74 +843,116 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
949
843
  const visible = resolveTimelineVisibleEvents(
950
844
  uiState.sessionEvents,
951
845
  options.executionKey,
846
+ options.attemptCount,
952
847
  );
953
848
  if (visible.note) {
954
849
  content.appendChild(el("p", "process-timeline-note", visible.note));
955
850
  }
956
851
 
957
- const showProtocol = Boolean(uiState.sessionTimelineShowProtocol);
852
+ const showProtocol = reading?.showProtocol ?? Boolean(uiState.sessionTimelineShowProtocol);
958
853
  const activeFilter = normalizeExecutionRecordFilter(
959
- uiState.executionRecordFilter,
854
+ reading?.filter ?? uiState.executionRecordFilter,
960
855
  );
961
856
  const toolbar = el("div", "process-timeline-toolbar");
962
- const filterGroup = el("div", "process-timeline-filter-group");
963
- filterGroup.setAttribute("role", "group");
857
+ const filterGroup = el("select", "observe-select process-timeline-filter-group");
964
858
  filterGroup.setAttribute("aria-label", "筛选执行记录");
965
859
  for (const [filter, label] of EXECUTION_RECORD_FILTERS) {
966
- const button = document.createElement("button");
967
- button.type = "button";
968
- button.className = "process-timeline-toggle process-timeline-filter";
969
- button.setAttribute("aria-pressed", String(activeFilter === filter));
970
- button.textContent = label;
971
- button.addEventListener("click", () => {
972
- uiState.executionRecordFilter = filter;
973
- rerenderSessionTimeline(content, nodeId, options);
974
- });
975
- filterGroup.appendChild(button);
860
+ const option = el("option", null, label);
861
+ option.value = filter;
862
+ filterGroup.appendChild(option);
976
863
  }
864
+ filterGroup.value = activeFilter;
865
+ filterGroup.addEventListener("change", () => {
866
+ uiState.executionRecordFilter = filterGroup.value;
867
+ if (reading) reading.filter = filterGroup.value;
868
+ rerenderSessionTimeline(content, nodeId, options);
869
+ content.querySelector(".process-timeline-filter-group")?.focus();
870
+ });
977
871
  toolbar.appendChild(filterGroup);
978
- const protocolToggle = document.createElement("button");
872
+ const protocolToggle = iconButton(
873
+ showProtocol ? "隐藏原始协议事件" : "展开原始协议事件",
874
+ "ri-code-s-slash-line",
875
+ "process-timeline-protocol-toggle",
876
+ );
979
877
  protocolToggle.type = "button";
980
- protocolToggle.className = "process-timeline-toggle";
981
878
  protocolToggle.setAttribute("aria-pressed", String(showProtocol));
982
- protocolToggle.textContent = showProtocol
983
- ? "隐藏原始协议事件"
984
- : "展开原始协议事件";
985
879
  protocolToggle.addEventListener("click", () => {
986
880
  uiState.sessionTimelineShowProtocol = !showProtocol;
881
+ if (reading) reading.showProtocol = !showProtocol;
987
882
  rerenderSessionTimeline(content, nodeId, options);
883
+ content.querySelector(".process-timeline-protocol-toggle")?.focus({ preventScroll: true });
988
884
  });
989
- toolbar.appendChild(protocolToggle);
990
- const exportBtn = el("button", "btn btn-secondary", "导出 HTML");
991
- exportBtn.type = "button";
992
- exportBtn.addEventListener("click", async () => {
993
- const cssText = await fetchPageStylesheetText();
994
- // 复用页面真实渲染(含原始协议事件),保证导出的排版与页面一致。
995
- const prevShow = uiState.sessionTimelineShowProtocol;
996
- uiState.sessionTimelineShowProtocol = true;
997
- const tmp = document.createElement("div");
998
- tmp.className = "process-timeline-scroll";
999
- renderSessionTimeline(tmp, nodeId, options);
1000
- const toolbar = tmp.querySelector(".process-timeline-toolbar");
1001
- if (toolbar) toolbar.remove();
1002
- const innerHTML = tmp.innerHTML;
1003
- uiState.sessionTimelineShowProtocol = prevShow;
1004
- downloadTextFile(
1005
- `node-process-${nodeId || "node"}-${Date.now()}.html`,
1006
- buildPageStyledDoc(innerHTML, {
1007
- nodeId,
1008
- dagRunId: uiState.currentDagRunId ?? "",
1009
- }, cssText),
1010
- );
1011
- });
1012
- toolbar.appendChild(exportBtn);
1013
885
  content.appendChild(toolbar);
1014
- content.appendChild(createTimelineAutoScrollControl(content));
886
+ if (reading) {
887
+ reading.eventCount = visible.events.length;
888
+ if (!reading.eventsInitialized || reading.followLatest) reading.seenCount = reading.eventCount;
889
+ reading.eventsInitialized = true;
890
+ content.appendChild(createTimelineAutoScrollControl(content));
891
+ }
1015
892
 
1016
893
  const timelineItems = filterExecutionRecordItems(
1017
894
  buildRichTimelineItems(visible.events, { showProtocol }),
1018
895
  activeFilter,
896
+ reading?.errorsOnly,
1019
897
  );
898
+ const errors = iconButton("仅看错误", "ri-error-warning-line", "process-timeline-errors");
899
+ errors.type = "button";
900
+ errors.setAttribute("aria-pressed", String(reading?.errorsOnly === true));
901
+ errors.addEventListener("click", () => {
902
+ if (reading) reading.errorsOnly = !reading.errorsOnly;
903
+ rerenderSessionTimeline(content, nodeId, options);
904
+ });
905
+ toolbar.appendChild(errors);
906
+ const search = el("input", "process-timeline-search");
907
+ search.type = "search";
908
+ search.placeholder = "搜索当前尝试…";
909
+ search.setAttribute("aria-label", "搜索当前尝试执行记录");
910
+ search.value = reading?.query ?? "";
911
+ const resultCount = el("span", "muted");
912
+ resultCount.setAttribute("role", "status");
913
+ let matches = [], matchIndex = -1;
914
+ const searchButtons = [];
915
+ const refreshMatches = () => {
916
+ const query = search.value.trim();
917
+ if (reading) reading.query = search.value;
918
+ matches = [];
919
+ for (const row of content.querySelectorAll(".process-timeline-item")) {
920
+ const matched = highlightTimelineSearch(row, query);
921
+ row.classList.remove("is-search-current");
922
+ row.classList.toggle("is-search-match", Boolean(matched));
923
+ if (matched) matches.push(row);
924
+ }
925
+ matchIndex = -1;
926
+ for (const button of searchButtons) button.disabled = matches.length === 0;
927
+ resultCount.textContent = query ? `${matches.length} 条匹配` : "";
928
+ };
929
+ const jump = (delta) => {
930
+ if (!matches.length) return;
931
+ matchIndex = matchIndex < 0 ? (delta < 0 ? matches.length - 1 : 0) : (matchIndex + delta + matches.length) % matches.length;
932
+ for (const match of matches) match.classList.remove("is-search-current");
933
+ if (reading) reading.followLatest = false;
934
+ uiState.dagTimelineAutoScrollEnabled = false;
935
+ const row = matches[matchIndex];
936
+ row.classList.add("is-search-current");
937
+ for (const detail of row.querySelectorAll("details")) detail.open = true;
938
+ (row.querySelector("mark.process-search-highlight") ?? row).scrollIntoView({ block: "center" });
939
+ resultCount.textContent = `${matchIndex + 1} / ${matches.length}`;
940
+ content._syncTimelineFollow?.();
941
+ };
942
+ search.addEventListener("input", (event) => { if (!event.isComposing) refreshMatches(); });
943
+ search.addEventListener("compositionend", refreshMatches);
944
+ search.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.isComposing) { event.preventDefault(); jump(event.shiftKey ? -1 : 1); } });
945
+ toolbar.appendChild(search);
946
+ for (const [label, delta] of [["上一处", -1], ["下一处", 1]]) {
947
+ const button = iconButton(label, delta < 0 ? "ri-arrow-up-s-line" : "ri-arrow-down-s-line", delta < 0 ? "process-search-previous" : "process-search-next");
948
+ button.type = "button";
949
+ button.disabled = true;
950
+ searchButtons.push(button);
951
+ button.addEventListener("click", () => jump(delta));
952
+ toolbar.appendChild(button);
953
+ }
954
+ toolbar.appendChild(resultCount);
955
+ toolbar.appendChild(protocolToggle);
1020
956
  const hasEventTimes = visible.events.some((event) =>
1021
957
  Boolean(sessionEventTimestamp(event)),
1022
958
  );
@@ -1082,33 +1018,45 @@ export function renderSessionTimeline(content, nodeId, options = {}) {
1082
1018
  ul.appendChild(li);
1083
1019
  }
1084
1020
  content.appendChild(ul);
1021
+ refreshMatches();
1085
1022
 
1086
1023
  const timelineState =
1087
1024
  uiState.dagTimelineViewportState?.dagRunId === uiState.currentDagRunId &&
1088
1025
  uiState.dagTimelineViewportState.nodeId === nodeId
1089
1026
  ? uiState.dagTimelineViewportState
1090
1027
  : null;
1028
+ if (content.dataset.timelineScrollBound !== "1") {
1029
+ content.dataset.timelineScrollBound = "1";
1091
1030
  content.addEventListener(
1092
1031
  "scroll",
1093
1032
  () => {
1033
+ if (content.dataset.inspectorTab && content.dataset.inspectorTab !== "timeline") return;
1034
+ if (content.scrollHeight - content.clientHeight - content.scrollTop > 48) {
1035
+ uiState.dagTimelineAutoScrollEnabled = false;
1036
+ if (content._readingState) content._readingState.followLatest = false;
1037
+ }
1094
1038
  uiState.dagTimelineViewportState = updateDagTimelineViewportState(
1095
1039
  uiState.dagTimelineViewportState,
1096
1040
  uiState.currentDagRunId,
1097
- nodeId,
1041
+ content.dataset.nodeId,
1098
1042
  content.scrollLeft,
1099
1043
  content.scrollTop,
1100
1044
  isTimelineAutoScrollEnabled(),
1101
1045
  );
1046
+ if (content._readingState) Object.assign(content._readingState, { scrollTop: content.scrollTop, scrollLeft: content.scrollLeft });
1047
+ content._syncTimelineFollow?.();
1102
1048
  },
1103
1049
  { passive: true },
1104
1050
  );
1051
+ }
1105
1052
  if (isTimelineAutoScrollEnabled()) {
1106
1053
  content.scrollLeft = timelineState?.scrollLeft ?? 0;
1107
1054
  content.scrollTop = content.scrollHeight;
1108
1055
  } else {
1109
- content.scrollLeft = timelineState.scrollLeft;
1110
- content.scrollTop = timelineState.scrollTop;
1056
+ content.scrollLeft = reading?.scrollLeft ?? timelineState?.scrollLeft ?? 0;
1057
+ content.scrollTop = reading?.scrollTop ?? timelineState?.scrollTop ?? 0;
1111
1058
  }
1059
+ content._syncTimelineFollow?.();
1112
1060
  }
1113
1061
 
1114
1062
  function rerenderSessionTimeline(content, nodeId, options) {
@@ -1129,4 +1077,5 @@ function rerenderSessionTimeline(content, nodeId, options) {
1129
1077
  content.scrollTop = scrollTop;
1130
1078
  content.scrollLeft = scrollLeft;
1131
1079
  }
1080
+ content._syncTimelineFollow?.();
1132
1081
  }