@tea-agent/loop-agent 0.13.0-beta.0 → 0.14.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 (127) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +56 -305
  3. package/README.md +13 -19
  4. package/dist/commands/init.js +92 -23
  5. package/dist/executors/pi-event-serializer.js +33 -11
  6. package/dist/executors/shell-executor.js +200 -21
  7. package/dist/infrastructure/evaluation/candidate-store.js +5 -1
  8. package/dist/worker/observe/spec-evidence.js +19 -10
  9. package/dist/worker/observe/static/app.js +4 -3
  10. package/dist/worker/observe/static/constants.js +10 -2
  11. package/dist/worker/observe/static/dag-helpers.js +37 -8
  12. package/dist/worker/observe/static/dom.js +159 -0
  13. package/dist/worker/observe/static/format-pool.d.ts +71 -0
  14. package/dist/worker/observe/static/format-pool.js +67 -0
  15. package/dist/worker/observe/static/format.js +27 -2
  16. package/dist/worker/observe/static/index.html +76 -34
  17. package/dist/worker/observe/static/kpi.js +12 -6
  18. package/dist/worker/observe/static/markdown-render.js +124 -0
  19. package/dist/worker/observe/static/shell-chrome.js +8 -2
  20. package/dist/worker/observe/static/state.js +20 -0
  21. package/dist/worker/observe/static/styles.css +662 -60
  22. package/dist/worker/observe/static/views/dag-inspector.js +65 -142
  23. package/dist/worker/observe/static/views/dag.js +9 -0
  24. package/dist/worker/observe/static/views/dashboard.js +512 -269
  25. package/dist/worker/observe/static/views/pool.js +595 -237
  26. package/dist/worker/observe/static/views/session-timeline.js +577 -11
  27. package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
  28. package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
  29. package/dist/workflows/dag/backend-test-result-contract.js +568 -0
  30. package/dist/workflows/dag/decision-envelope.js +57 -2
  31. package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
  32. package/dist/workflows/dag/frontend-project-capability.js +309 -0
  33. package/dist/workflows/dag/frontend-repair.js +341 -0
  34. package/dist/workflows/dag/frontend-risk.js +161 -0
  35. package/dist/workflows/dag/frontend-verification-trace.js +190 -0
  36. package/dist/workflows/dag/init-hybrid.js +1020 -125
  37. package/dist/workflows/dag/repair-artifact.js +43 -3
  38. package/dist/workflows/dag/skill-instructions.js +4 -2
  39. package/dist/workflows/dag/types.js +29 -8
  40. package/docs/README.md +2 -0
  41. package/docs/agent-dag-recovery-playbook.md +3 -3
  42. package/docs/agent-dag-runner.md +3 -3
  43. package/docs/architecture/README.md +3 -3
  44. package/docs/architecture/dag-execution.md +1 -1
  45. package/docs/architecture/evolution.md +13 -13
  46. package/docs/architecture/facts-and-state.md +1 -1
  47. package/docs/architecture/runtime-boundaries.md +7 -7
  48. package/docs/architecture/system-overview.md +3 -3
  49. package/docs/architecture/worker-and-feature.md +3 -3
  50. package/docs/design/README.md +7 -7
  51. package/docs/development-principles.md +4 -4
  52. package/docs/exec-plans/active/README.md +2 -4
  53. package/docs/exec-plans/completed/README.md +29 -6
  54. package/docs/feature-workflow.md +57 -32
  55. package/docs/init-surface.manifest.json +21 -3
  56. package/docs/loop-agent-harness.md +8 -8
  57. package/docs/production-readiness.md +1 -1
  58. package/docs/progress/README.md +20 -3
  59. package/docs/reports/README.md +53 -7
  60. package/docs/templates/agent-dag.supervised-implementation.json +127 -8
  61. package/docs/templates/backend-test-case-manifest.schema.json +190 -0
  62. package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
  63. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +6 -4
  64. package/docs/templates/backend-test-dag.json +269 -21
  65. package/docs/templates/backend-test-dag.retrospect.prompt.md +44 -30
  66. package/docs/templates/backend-test-dag.review-cases.prompt.md +6 -4
  67. package/docs/templates/backend-test-execution.schema.json +133 -0
  68. package/docs/templates/backend-test-result.schema.json +99 -0
  69. package/docs/templates/branch-merge-report.md +93 -0
  70. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
  71. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
  72. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
  73. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
  74. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
  75. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
  76. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
  77. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
  78. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
  79. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
  80. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
  81. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
  82. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
  83. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
  84. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
  85. package/docs/templates/frontend-eval/metrics.md +138 -0
  86. package/docs/templates/frontend-eval/smoke-targets.md +53 -0
  87. package/docs/templates/frontend-implementation-contract.schema.json +27 -0
  88. package/docs/verification-matrix.md +1 -1
  89. package/examples/decision-gate-agent-dag.json +4 -4
  90. package/examples/hybrid-loop-agent-dag.json +1 -1
  91. package/package.json +2 -2
  92. package/skills/ai-engineering-context/SKILL.md +2 -2
  93. package/skills/browser-tools/SKILL.md +196 -0
  94. package/skills/browser-tools/browser-content.js +103 -0
  95. package/skills/browser-tools/browser-cookies.js +35 -0
  96. package/skills/browser-tools/browser-eval.js +53 -0
  97. package/skills/browser-tools/browser-hn-scraper.js +108 -0
  98. package/skills/browser-tools/browser-nav.js +44 -0
  99. package/skills/browser-tools/browser-pick.js +162 -0
  100. package/skills/browser-tools/browser-screenshot.js +34 -0
  101. package/skills/browser-tools/browser-start.js +86 -0
  102. package/skills/browser-tools/package-lock.json +2556 -0
  103. package/skills/browser-tools/package.json +19 -0
  104. package/skills/frontend-implementation/SKILL.md +3 -1
  105. package/skills/frontend-implementation/references/node-contracts.md +17 -66
  106. package/skills/frontend-verification/SKILL.md +1 -1
  107. package/skills/grill-with-docs/SKILL.md +5 -5
  108. package/skills/grill-with-docs/adr-format.md +3 -3
  109. package/skills/init-capability-evolution/SKILL.md +5 -5
  110. package/skills/loop-agent/SKILL.md +5 -5
  111. package/skills/loop-agent/references/README.md +3 -3
  112. package/skills/loop-agent/references/command-reference.md +39 -17
  113. package/skills/loop-agent/references/docs-converge.md +15 -15
  114. package/skills/loop-agent/references/harness-policy.md +2 -2
  115. package/skills/loop-agent/references/hybrid-dag.md +20 -15
  116. package/skills/loop-agent/references/multi-worktree.md +1 -1
  117. package/skills/loop-agent/references/orchestrator-and-interventions.md +8 -8
  118. package/skills/loop-agent/references/task-workflow.md +1 -1
  119. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -4
  120. package/skills/requesting-code-review/SKILL.md +1 -1
  121. package/skills/systematic-debugging/CREATION-LOG.md +3 -3
  122. package/skills/systematic-debugging/SKILL.md +1 -1
  123. package/skills/systematic-debugging/test-academic.md +1 -1
  124. package/skills/systematic-debugging/test-pressure-1.md +1 -1
  125. package/skills/systematic-debugging/test-pressure-2.md +1 -1
  126. package/skills/systematic-debugging/test-pressure-3.md +1 -1
  127. package/skills/verification-before-completion/SKILL.md +1 -1
@@ -1,7 +1,8 @@
1
- /** Session event timeline helpers (Observe UI R5). */
1
+ /** Session event timeline helpers (Observe UI R5 + rich process view). */
2
2
  import { UI_TEXT } from "../constants.js";
3
3
  import { el } from "../dom.js";
4
- import { formatTs } from "../format.js";
4
+ import { formatTs, truncateText } from "../format.js";
5
+ import { renderMarkdown } from "../markdown-render.js";
5
6
  import { fetchJson } from "../api.js";
6
7
  import {
7
8
  uiState,
@@ -9,6 +10,30 @@ import {
9
10
  computeFollowLatest,
10
11
  } from "../state.js";
11
12
 
13
+ /** Default visible lines for tool result preview before expand. */
14
+ export const TOOL_RESULT_PREVIEW_LINES = 20;
15
+
16
+ export function normalizeSessionEventFields(event) {
17
+ if (!event || typeof event !== "object") return {};
18
+ const args = event.args ?? event.input ?? event.toolInput;
19
+ const result = event.result ?? event.toolResult;
20
+ return {
21
+ ...event,
22
+ ...(args !== undefined ? { args, input: event.input ?? args } : {}),
23
+ ...(result !== undefined ? { result } : {}),
24
+ };
25
+ }
26
+
27
+ export function sessionEventArgs(event) {
28
+ const normalized = normalizeSessionEventFields(event);
29
+ const args = normalized.args;
30
+ return args && typeof args === "object" && !Array.isArray(args) ? args : {};
31
+ }
32
+
33
+ export function sessionEventResult(event) {
34
+ return normalizeSessionEventFields(event).result;
35
+ }
36
+
12
37
  export function extractAssistantSummary(event) {
13
38
  if (!event || typeof event !== "object") return "—";
14
39
  const record = event;
@@ -33,6 +58,121 @@ export function extractAssistantSummary(event) {
33
58
  return "—";
34
59
  }
35
60
 
61
+ export function extractThinkingBlocks(event) {
62
+ const content = event?.message?.content;
63
+ if (!Array.isArray(content)) return [];
64
+ return content
65
+ .filter(
66
+ (part) =>
67
+ part?.type === "thinking" && typeof part.thinking === "string",
68
+ )
69
+ .map((part) => part.thinking);
70
+ }
71
+
72
+ /** Tool calls declared on a turn_end / assistant message (name + args + id). */
73
+ export function extractToolCallsFromTurn(event) {
74
+ const content = event?.message?.content;
75
+ if (!Array.isArray(content)) return [];
76
+ const calls = [];
77
+ for (const part of content) {
78
+ if (!part || typeof part !== "object") continue;
79
+ if (part.type !== "toolCall" && part.type !== "tool_use") continue;
80
+ const name = part.name ?? part.toolName ?? "tool";
81
+ const args =
82
+ (part.arguments && typeof part.arguments === "object"
83
+ ? part.arguments
84
+ : null) ??
85
+ (part.args && typeof part.args === "object" ? part.args : null) ??
86
+ {};
87
+ calls.push({
88
+ id: typeof part.id === "string" ? part.id : "",
89
+ name: String(name),
90
+ args,
91
+ summary: formatToolSummary(String(name), args),
92
+ });
93
+ }
94
+ return calls;
95
+ }
96
+
97
+ export function formatToolSummary(toolName, args) {
98
+ const name = toolName || "tool";
99
+ const input = args && typeof args === "object" ? args : {};
100
+ if (name === "bash" || name === "bashExecution") {
101
+ const cmd = typeof input.command === "string" ? input.command : "";
102
+ return cmd ? `${name} · ${truncateText(cmd, 80)}` : name;
103
+ }
104
+ if (name === "read" || name === "write" || name === "edit") {
105
+ const path = typeof input.path === "string" ? input.path : "";
106
+ return path ? `${name} · ${truncateText(path, 80)}` : name;
107
+ }
108
+ if (name === "grep") {
109
+ const pattern = typeof input.pattern === "string" ? input.pattern : "";
110
+ const scope =
111
+ (typeof input.glob === "string" && input.glob) ||
112
+ (typeof input.path === "string" && input.path) ||
113
+ "";
114
+ if (pattern && scope) return `grep · ${truncateText(pattern, 48)} in ${truncateText(scope, 40)}`;
115
+ if (pattern) return `grep · ${truncateText(pattern, 80)}`;
116
+ return "grep";
117
+ }
118
+ if (name === "find" || name === "glob") {
119
+ const pattern = typeof input.pattern === "string" ? input.pattern : "";
120
+ return pattern ? `${name} · ${truncateText(pattern, 80)}` : name;
121
+ }
122
+ if (name === "ls") {
123
+ const path = typeof input.path === "string" ? input.path : "";
124
+ return path ? `ls · ${truncateText(path, 80)}` : "ls";
125
+ }
126
+ try {
127
+ const json = JSON.stringify(input);
128
+ if (json && json !== "{}") return `${name} · ${truncateText(json, 80)}`;
129
+ } catch {
130
+ /* ignore */
131
+ }
132
+ return name;
133
+ }
134
+
135
+ export function formatToolResultText(result) {
136
+ if (result == null) return "";
137
+ if (typeof result === "string") return result;
138
+ if (typeof result !== "object") return String(result);
139
+ if (Array.isArray(result.content)) {
140
+ return result.content
141
+ .map((part) => {
142
+ if (typeof part === "string") return part;
143
+ if (part?.type === "text" && typeof part.text === "string") return part.text;
144
+ return "";
145
+ })
146
+ .filter(Boolean)
147
+ .join("\n");
148
+ }
149
+ if (typeof result.content === "string") return result.content;
150
+ if (typeof result.text === "string") return result.text;
151
+ if (result.error != null) {
152
+ return typeof result.error === "string"
153
+ ? result.error
154
+ : JSON.stringify(result.error, null, 2);
155
+ }
156
+ try {
157
+ return JSON.stringify(result, null, 2);
158
+ } catch {
159
+ return String(result);
160
+ }
161
+ }
162
+
163
+ export function splitPreviewLines(text, maxLines = TOOL_RESULT_PREVIEW_LINES) {
164
+ const normalized = String(text ?? "");
165
+ const lines = normalized.split("\n");
166
+ if (lines.length <= maxLines) {
167
+ return { preview: normalized, truncated: false, totalLines: lines.length };
168
+ }
169
+ return {
170
+ preview: lines.slice(0, maxLines).join("\n"),
171
+ truncated: true,
172
+ totalLines: lines.length,
173
+ };
174
+ }
175
+
36
176
  export function formatSessionEventLabel(event) {
37
177
  if (!event || typeof event !== "object") return "未知类型";
38
178
  const type = typeof event.type === "string" ? event.type : "unknown";
@@ -42,7 +182,7 @@ export function formatSessionEventLabel(event) {
42
182
  if (type === "agent_settled") return "Agent 已稳定";
43
183
  if (type === "turn_start") return "开始新一轮处理";
44
184
  if (type === "tool_execution_start" || type === "tool_start") {
45
- return `调用工具 · ${toolName}`;
185
+ return `调用工具 · ${formatToolSummary(toolName, sessionEventArgs(event))}`;
46
186
  }
47
187
  if (type === "tool_execution_end" || type === "tool_end") {
48
188
  return `工具调用完成 · ${toolName}`;
@@ -88,6 +228,7 @@ export function sessionEventKind(event) {
88
228
  return "protocol";
89
229
  }
90
230
 
231
+ /** Legacy flat item builder (kept for label-only consumers / tests). */
91
232
  export function buildSessionTimelineItems(events) {
92
233
  const items = [];
93
234
  for (let index = 0; index < events.length; index += 1) {
@@ -109,11 +250,391 @@ export function buildSessionTimelineItems(events) {
109
250
  return items;
110
251
  }
111
252
 
253
+ /**
254
+ * Build rich timeline items: merge tool start/end by toolCallId, collapse
255
+ * protocol noise, surface turn thinking/text without dumping agent_end.messages.
256
+ */
257
+ export function buildRichTimelineItems(events, options = {}) {
258
+ const showProtocol = Boolean(options.showProtocol);
259
+ const items = [];
260
+ const pendingTools = new Map();
261
+
262
+ const flushTool = (toolCallId) => {
263
+ const pending = pendingTools.get(toolCallId);
264
+ if (!pending) return;
265
+ pendingTools.delete(toolCallId);
266
+ items.push(pending);
267
+ };
268
+
269
+ const flushAllPending = () => {
270
+ for (const id of [...pendingTools.keys()]) flushTool(id);
271
+ };
272
+
273
+ for (let index = 0; index < events.length; index += 1) {
274
+ const event = normalizeSessionEventFields(events[index]);
275
+ const type = typeof event?.type === "string" ? event.type : "unknown";
276
+
277
+ if (type === "message_start" || type === "message_end") {
278
+ let count = 1;
279
+ const protocolEvents = [event];
280
+ while (index + 1 < events.length) {
281
+ const nextType = events[index + 1]?.type;
282
+ if (nextType !== "message_start" && nextType !== "message_end") break;
283
+ count += 1;
284
+ index += 1;
285
+ protocolEvents.push(normalizeSessionEventFields(events[index]));
286
+ }
287
+ if (showProtocol) {
288
+ items.push({
289
+ kind: "protocol",
290
+ event,
291
+ protocolCount: count,
292
+ protocolEvents,
293
+ detailKey: `protocol-${items.length}`,
294
+ });
295
+ } else {
296
+ items.push({
297
+ kind: "protocol",
298
+ event,
299
+ protocolCount: count,
300
+ collapsed: true,
301
+ detailKey: `protocol-${items.length}`,
302
+ });
303
+ }
304
+ continue;
305
+ }
306
+
307
+ if (type === "tool_execution_start" || type === "tool_start") {
308
+ const toolCallId =
309
+ typeof event.toolCallId === "string" && event.toolCallId
310
+ ? event.toolCallId
311
+ : `anon-start-${index}`;
312
+ const toolName = event.toolName ?? event.name ?? "tool";
313
+ const args = sessionEventArgs(event);
314
+ pendingTools.set(toolCallId, {
315
+ kind: "tool",
316
+ toolCallId,
317
+ toolName,
318
+ args,
319
+ startEvent: event,
320
+ endEvent: null,
321
+ result: null,
322
+ isError: false,
323
+ pending: true,
324
+ detailKey: `tool-${toolCallId}`,
325
+ event,
326
+ });
327
+ continue;
328
+ }
329
+
330
+ // Streaming progress between start/end — do not flush pending tools.
331
+ if (
332
+ type === "tool_execution_update" ||
333
+ type === "tool_update" ||
334
+ type === "thinking_delta" ||
335
+ type === "message_update"
336
+ ) {
337
+ continue;
338
+ }
339
+
340
+ if (type === "tool_execution_end" || type === "tool_end") {
341
+ const toolCallId =
342
+ typeof event.toolCallId === "string" && event.toolCallId
343
+ ? event.toolCallId
344
+ : `anon-end-${index}`;
345
+ const toolName = event.toolName ?? event.name ?? "tool";
346
+ const result = sessionEventResult(event);
347
+ const isError = event.isError === true;
348
+ const existing = pendingTools.get(toolCallId);
349
+ if (existing) {
350
+ existing.endEvent = event;
351
+ existing.result = result;
352
+ existing.isError = isError;
353
+ existing.pending = false;
354
+ existing.toolName = existing.toolName || toolName;
355
+ existing.event = event;
356
+ flushTool(toolCallId);
357
+ } else {
358
+ items.push({
359
+ kind: "tool",
360
+ toolCallId,
361
+ toolName,
362
+ args: sessionEventArgs(event),
363
+ startEvent: null,
364
+ endEvent: event,
365
+ result,
366
+ isError,
367
+ pending: false,
368
+ detailKey: `tool-${toolCallId}`,
369
+ event,
370
+ });
371
+ }
372
+ continue;
373
+ }
374
+
375
+ if (
376
+ type === "turn_end" ||
377
+ type === "assistant" ||
378
+ type === "assistant_message"
379
+ ) {
380
+ // Do not flush pending tools here: tool_execution_update / out-of-order
381
+ // ends may still arrive after turn_end in some streams.
382
+ items.push({
383
+ kind: "turn",
384
+ event,
385
+ thinking: extractThinkingBlocks(event),
386
+ text: extractAssistantSummary(event),
387
+ toolCalls: extractToolCallsFromTurn(event),
388
+ detailKey: `turn-${items.length}`,
389
+ });
390
+ continue;
391
+ }
392
+
393
+ if (type === "agent_end") {
394
+ const messages = Array.isArray(event.messages) ? event.messages : [];
395
+ items.push({
396
+ kind: "lifecycle",
397
+ event,
398
+ agentEnd: true,
399
+ messageCount: messages.length,
400
+ willRetry: event.willRetry === true,
401
+ detailKey: `agent-end-${items.length}`,
402
+ });
403
+ continue;
404
+ }
405
+
406
+ // Lifecycle / unknown noise must not flush in-flight tool starts.
407
+ if (
408
+ type === "agent_start" ||
409
+ type === "turn_start" ||
410
+ type === "agent_settled" ||
411
+ type === "auto_retry_start" ||
412
+ type === "auto_retry_end" ||
413
+ type.startsWith("agent_")
414
+ ) {
415
+ items.push({
416
+ kind: sessionEventKind(event),
417
+ event,
418
+ detailKey: `evt-${items.length}-${type}`,
419
+ });
420
+ continue;
421
+ }
422
+
423
+ items.push({
424
+ kind: sessionEventKind(event),
425
+ event,
426
+ detailKey: `evt-${items.length}-${type}`,
427
+ });
428
+ }
429
+
430
+ // Only after the full event list: leftover starts with no end.
431
+ flushAllPending();
432
+ return items;
433
+ }
434
+
435
+ function ensureOpenDetailsSet() {
436
+ if (!(uiState.sessionTimelineOpenDetails instanceof Set)) {
437
+ uiState.sessionTimelineOpenDetails = new Set();
438
+ }
439
+ return uiState.sessionTimelineOpenDetails;
440
+ }
441
+
442
+ function bindDetailsToggle(details, detailKey) {
443
+ const openSet = ensureOpenDetailsSet();
444
+ if (openSet.has(detailKey)) details.open = true;
445
+ details.addEventListener("toggle", () => {
446
+ if (details.open) openSet.add(detailKey);
447
+ else openSet.delete(detailKey);
448
+ });
449
+ }
450
+
451
+ function appendPreBlock(parent, className, text) {
452
+ const pre = el("pre", className);
453
+ pre.textContent = text;
454
+ parent.appendChild(pre);
455
+ return pre;
456
+ }
457
+
458
+ function renderToolCard(item) {
459
+ const card = el(
460
+ "div",
461
+ `process-timeline-card process-timeline-tool-card${item.isError ? " is-error" : ""}${item.pending ? " is-pending" : ""}`,
462
+ );
463
+ const summary = formatToolSummary(item.toolName, item.args);
464
+ const status = item.pending ? "进行中" : item.isError ? "失败" : "完成";
465
+ const details = document.createElement("details");
466
+ details.className = "process-timeline-details";
467
+ details.dataset.detailKey = item.detailKey;
468
+ bindDetailsToggle(details, item.detailKey);
469
+
470
+ const summaryEl = document.createElement("summary");
471
+ summaryEl.className = "process-timeline-summary";
472
+ summaryEl.appendChild(
473
+ el("span", "process-timeline-summary-title", summary),
474
+ );
475
+ summaryEl.appendChild(
476
+ el(
477
+ "span",
478
+ `process-timeline-summary-status status-${item.pending ? "pending" : item.isError ? "failed" : "succeeded"}`,
479
+ status,
480
+ ),
481
+ );
482
+ details.appendChild(summaryEl);
483
+
484
+ const body = el("div", "process-timeline-card-body");
485
+ const argsJson = (() => {
486
+ try {
487
+ return JSON.stringify(item.args ?? {}, null, 2);
488
+ } catch {
489
+ return String(item.args ?? {});
490
+ }
491
+ })();
492
+ body.appendChild(el("div", "process-timeline-section-label", "参数"));
493
+ appendPreBlock(body, "process-timeline-code", argsJson);
494
+
495
+ const resultText = formatToolResultText(item.result);
496
+ if (resultText) {
497
+ body.appendChild(el("div", "process-timeline-section-label", "结果"));
498
+ const { preview, truncated, totalLines } = splitPreviewLines(resultText);
499
+ appendPreBlock(body, "process-timeline-code", preview);
500
+ if (truncated) {
501
+ const more = document.createElement("details");
502
+ more.className = "process-timeline-more";
503
+ more.dataset.detailKey = `${item.detailKey}-full`;
504
+ bindDetailsToggle(more, `${item.detailKey}-full`);
505
+ const moreSummary = document.createElement("summary");
506
+ moreSummary.textContent = `展开全部结果(${totalLines} 行)`;
507
+ more.appendChild(moreSummary);
508
+ appendPreBlock(more, "process-timeline-code", resultText);
509
+ body.appendChild(more);
510
+ }
511
+ } else if (!item.pending) {
512
+ body.appendChild(el("p", "muted", "无结果正文"));
513
+ }
514
+ details.appendChild(body);
515
+ card.appendChild(details);
516
+ return card;
517
+ }
518
+
519
+ function renderTurnCard(item) {
520
+ const card = el("div", "process-timeline-card process-timeline-turn-card");
521
+ const toolCalls = Array.isArray(item.toolCalls) ? item.toolCalls : [];
522
+ const hasText = Boolean(item.text && item.text !== "—");
523
+ const title = hasText
524
+ ? `AI 回复 · ${truncateText(item.text, 80)}`
525
+ : toolCalls.length > 0
526
+ ? `本回合仅调用工具 · ${toolCalls.length} 个`
527
+ : item.thinking.length > 0
528
+ ? "AI 回复 · 含思考过程"
529
+ : "AI 回复";
530
+
531
+ if (item.thinking.length > 0) {
532
+ for (const [i, thinking] of item.thinking.entries()) {
533
+ const details = document.createElement("details");
534
+ details.className = "process-timeline-details process-timeline-thinking";
535
+ details.dataset.detailKey = `${item.detailKey}-thinking-${i}`;
536
+ // Default open for first thinking block so anomalies are visible.
537
+ if (i === 0) details.open = true;
538
+ bindDetailsToggle(details, `${item.detailKey}-thinking-${i}`);
539
+ const summary = document.createElement("summary");
540
+ summary.className = "process-timeline-summary";
541
+ summary.textContent = "思考过程";
542
+ details.appendChild(summary);
543
+ appendPreBlock(details, "process-timeline-thinking-body", thinking);
544
+ card.appendChild(details);
545
+ }
546
+ }
547
+
548
+ const textBlock = el("div", "process-timeline-turn-text");
549
+ textBlock.appendChild(el("div", "process-timeline-section-label", title));
550
+ if (hasText) {
551
+ textBlock.appendChild(renderMarkdown(item.text));
552
+ } else if (toolCalls.length > 0) {
553
+ textBlock.appendChild(
554
+ el(
555
+ "p",
556
+ "muted process-timeline-tool-only-note",
557
+ "本回合没有文本回复,模型直接发起了下列工具调用(详情见上方工具卡):",
558
+ ),
559
+ );
560
+ const list = el("ul", "process-timeline-turn-tools");
561
+ for (const call of toolCalls) {
562
+ list.appendChild(el("li", null, call.summary || call.name));
563
+ }
564
+ textBlock.appendChild(list);
565
+ } else {
566
+ textBlock.appendChild(el("p", "muted", "无文本回复"));
567
+ }
568
+ card.appendChild(textBlock);
569
+ return card;
570
+ }
571
+
572
+ function renderLifecycleCard(item) {
573
+ if (item.agentEnd) {
574
+ const card = el("div", "process-timeline-card process-timeline-lifecycle-card");
575
+ const parts = ["Agent 已结束"];
576
+ if (item.messageCount > 0) parts.push(`${item.messageCount} 条消息`);
577
+ if (item.willRetry) parts.push("将重试");
578
+ card.appendChild(el("div", "process-timeline-label", parts.join(" · ")));
579
+ card.appendChild(
580
+ el(
581
+ "p",
582
+ "muted process-timeline-agent-end-note",
583
+ "完整会话 messages 体积可能很大,此处不展开;请查看上方工具与回合卡片。",
584
+ ),
585
+ );
586
+ return card;
587
+ }
588
+ return el("span", "process-timeline-label", formatSessionEventLabel(item.event));
589
+ }
590
+
591
+ function renderProtocolCard(item) {
592
+ if (item.collapsed) {
593
+ return el(
594
+ "span",
595
+ "process-timeline-label",
596
+ `消息收发 · 已合并 ${item.protocolCount} 条协议事件`,
597
+ );
598
+ }
599
+ const details = document.createElement("details");
600
+ details.className = "process-timeline-details";
601
+ details.dataset.detailKey = item.detailKey;
602
+ bindDetailsToggle(details, item.detailKey);
603
+ const summary = document.createElement("summary");
604
+ summary.className = "process-timeline-summary";
605
+ summary.textContent = `消息收发 · ${item.protocolCount} 条协议事件`;
606
+ details.appendChild(summary);
607
+ const body = el("div", "process-timeline-card-body");
608
+ for (const event of item.protocolEvents ?? [item.event]) {
609
+ const role = event?.message?.role ?? event?.type ?? "event";
610
+ body.appendChild(el("div", "process-timeline-section-label", String(role)));
611
+ try {
612
+ appendPreBlock(
613
+ body,
614
+ "process-timeline-code",
615
+ JSON.stringify(event, null, 2),
616
+ );
617
+ } catch {
618
+ appendPreBlock(body, "process-timeline-code", String(event));
619
+ }
620
+ }
621
+ details.appendChild(body);
622
+ return details;
623
+ }
624
+
112
625
  export async function mergeSessionEvents(dagRunId, nodeId) {
113
626
  if (!dagRunId || !nodeId) return;
627
+ const identity = uiState.sessionEventIdentity;
114
628
  const page = await fetchJson(
115
629
  `/api/dag-runs/${encodeURIComponent(dagRunId)}/nodes/${encodeURIComponent(nodeId)}/session-events?after=${uiState.sessionEventOffset}`,
116
630
  );
631
+ if (
632
+ identity !== uiState.sessionEventIdentity ||
633
+ uiState.currentDagRunId !== dagRunId ||
634
+ uiState.selectedDagNodeId !== nodeId
635
+ ) {
636
+ return;
637
+ }
117
638
  if (!page) return;
118
639
  if (Array.isArray(page.events) && page.events.length > 0) {
119
640
  uiState.sessionEvents = uiState.sessionEvents.concat(page.events);
@@ -132,7 +653,31 @@ export function renderSessionTimeline(content, nodeId) {
132
653
  return;
133
654
  }
134
655
 
135
- const timelineItems = buildSessionTimelineItems(uiState.sessionEvents);
656
+ const showProtocol = Boolean(uiState.sessionTimelineShowProtocol);
657
+ const toolbar = el("div", "process-timeline-toolbar");
658
+ const protocolToggle = document.createElement("button");
659
+ protocolToggle.type = "button";
660
+ protocolToggle.className = "process-timeline-toggle";
661
+ protocolToggle.setAttribute("aria-pressed", String(showProtocol));
662
+ protocolToggle.textContent = showProtocol
663
+ ? "隐藏原始协议事件"
664
+ : "展开原始协议事件";
665
+ protocolToggle.addEventListener("click", () => {
666
+ uiState.sessionTimelineShowProtocol = !showProtocol;
667
+ const parent = content;
668
+ const scrollTop = parent.scrollTop;
669
+ const scrollLeft = parent.scrollLeft;
670
+ while (parent.firstChild) parent.removeChild(parent.firstChild);
671
+ renderSessionTimeline(parent, nodeId);
672
+ parent.scrollTop = scrollTop;
673
+ parent.scrollLeft = scrollLeft;
674
+ });
675
+ toolbar.appendChild(protocolToggle);
676
+ content.appendChild(toolbar);
677
+
678
+ const timelineItems = buildRichTimelineItems(uiState.sessionEvents, {
679
+ showProtocol,
680
+ });
136
681
  const hasEventTimes = uiState.sessionEvents.some((event) =>
137
682
  Boolean(sessionEventTimestamp(event)),
138
683
  );
@@ -145,12 +690,23 @@ export function renderSessionTimeline(content, nodeId) {
145
690
  ),
146
691
  );
147
692
  }
148
- const ul = el("ul", "process-timeline");
693
+
694
+ const ul = el("ul", "process-timeline process-timeline-rich");
149
695
  for (let index = 0; index < timelineItems.length; index += 1) {
150
696
  const item = timelineItems[index];
697
+ const kindClass =
698
+ item.kind === "tool"
699
+ ? item.isError
700
+ ? "tool-end is-error"
701
+ : item.pending
702
+ ? "tool-start"
703
+ : "tool-end"
704
+ : item.kind === "turn"
705
+ ? "assistant"
706
+ : item.kind;
151
707
  const li = el(
152
708
  "li",
153
- `process-timeline-item process-timeline-${item.kind}${hasEventTimes ? " has-time" : ""}`,
709
+ `process-timeline-item process-timeline-${kindClass}${hasEventTimes ? " has-time" : ""}`,
154
710
  );
155
711
  if (index === 0) li.classList.add("is-first");
156
712
  if (index === timelineItems.length - 1) li.classList.add("is-last");
@@ -162,11 +718,21 @@ export function renderSessionTimeline(content, nodeId) {
162
718
  el("span", "process-timeline-time", formatSessionEventTime(item.event)),
163
719
  );
164
720
  }
165
- const label =
166
- item.protocolCount > 1
167
- ? `消息收发 · 已合并 ${item.protocolCount} 条协议事件`
168
- : formatSessionEventLabel(item.event);
169
- li.appendChild(el("span", "process-timeline-label", label));
721
+ const body = el("div", "process-timeline-body");
722
+ if (item.kind === "tool") {
723
+ body.appendChild(renderToolCard(item));
724
+ } else if (item.kind === "turn") {
725
+ body.appendChild(renderTurnCard(item));
726
+ } else if (item.kind === "lifecycle" && item.agentEnd) {
727
+ body.appendChild(renderLifecycleCard(item));
728
+ } else if (item.kind === "protocol") {
729
+ body.appendChild(renderProtocolCard(item));
730
+ } else {
731
+ body.appendChild(
732
+ el("span", "process-timeline-label", formatSessionEventLabel(item.event)),
733
+ );
734
+ }
735
+ li.appendChild(body);
170
736
  ul.appendChild(li);
171
737
  }
172
738
  content.appendChild(ul);