@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.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +56 -305
- package/README.md +13 -19
- package/dist/commands/init.js +92 -23
- package/dist/executors/pi-event-serializer.js +33 -11
- package/dist/executors/shell-executor.js +200 -21
- package/dist/infrastructure/evaluation/candidate-store.js +5 -1
- package/dist/worker/observe/spec-evidence.js +19 -10
- package/dist/worker/observe/static/app.js +4 -3
- package/dist/worker/observe/static/constants.js +10 -2
- package/dist/worker/observe/static/dag-helpers.js +37 -8
- package/dist/worker/observe/static/dom.js +159 -0
- package/dist/worker/observe/static/format-pool.d.ts +71 -0
- package/dist/worker/observe/static/format-pool.js +67 -0
- package/dist/worker/observe/static/format.js +27 -2
- package/dist/worker/observe/static/index.html +76 -34
- package/dist/worker/observe/static/kpi.js +12 -6
- package/dist/worker/observe/static/markdown-render.js +124 -0
- package/dist/worker/observe/static/shell-chrome.js +8 -2
- package/dist/worker/observe/static/state.js +20 -0
- package/dist/worker/observe/static/styles.css +662 -60
- package/dist/worker/observe/static/views/dag-inspector.js +65 -142
- package/dist/worker/observe/static/views/dag.js +9 -0
- package/dist/worker/observe/static/views/dashboard.js +512 -269
- package/dist/worker/observe/static/views/pool.js +595 -237
- package/dist/worker/observe/static/views/session-timeline.js +577 -11
- package/dist/workflows/dag/backend-test-case-manifest.js +503 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +353 -0
- package/dist/workflows/dag/backend-test-result-contract.js +568 -0
- package/dist/workflows/dag/decision-envelope.js +57 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +240 -0
- package/dist/workflows/dag/frontend-project-capability.js +309 -0
- package/dist/workflows/dag/frontend-repair.js +341 -0
- package/dist/workflows/dag/frontend-risk.js +161 -0
- package/dist/workflows/dag/frontend-verification-trace.js +190 -0
- package/dist/workflows/dag/init-hybrid.js +1020 -125
- package/dist/workflows/dag/repair-artifact.js +43 -3
- package/dist/workflows/dag/skill-instructions.js +4 -2
- package/dist/workflows/dag/types.js +29 -8
- package/docs/README.md +2 -0
- package/docs/agent-dag-recovery-playbook.md +3 -3
- package/docs/agent-dag-runner.md +3 -3
- package/docs/architecture/README.md +3 -3
- package/docs/architecture/dag-execution.md +1 -1
- package/docs/architecture/evolution.md +13 -13
- package/docs/architecture/facts-and-state.md +1 -1
- package/docs/architecture/runtime-boundaries.md +7 -7
- package/docs/architecture/system-overview.md +3 -3
- package/docs/architecture/worker-and-feature.md +3 -3
- package/docs/design/README.md +7 -7
- package/docs/development-principles.md +4 -4
- package/docs/exec-plans/active/README.md +2 -4
- package/docs/exec-plans/completed/README.md +29 -6
- package/docs/feature-workflow.md +57 -32
- package/docs/init-surface.manifest.json +21 -3
- package/docs/loop-agent-harness.md +8 -8
- package/docs/production-readiness.md +1 -1
- package/docs/progress/README.md +20 -3
- package/docs/reports/README.md +53 -7
- package/docs/templates/agent-dag.supervised-implementation.json +127 -8
- package/docs/templates/backend-test-case-manifest.schema.json +190 -0
- package/docs/templates/backend-test-dag.classify.prompt.md +75 -0
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +6 -4
- package/docs/templates/backend-test-dag.json +269 -21
- package/docs/templates/backend-test-dag.retrospect.prompt.md +44 -30
- package/docs/templates/backend-test-dag.review-cases.prompt.md +6 -4
- package/docs/templates/backend-test-execution.schema.json +133 -0
- package/docs/templates/backend-test-result.schema.json +99 -0
- package/docs/templates/branch-merge-report.md +93 -0
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -0
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -0
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -0
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -0
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -0
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -0
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -0
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -0
- package/docs/templates/frontend-eval/metrics.md +138 -0
- package/docs/templates/frontend-eval/smoke-targets.md +53 -0
- package/docs/templates/frontend-implementation-contract.schema.json +27 -0
- package/docs/verification-matrix.md +1 -1
- package/examples/decision-gate-agent-dag.json +4 -4
- package/examples/hybrid-loop-agent-dag.json +1 -1
- package/package.json +2 -2
- package/skills/ai-engineering-context/SKILL.md +2 -2
- package/skills/browser-tools/SKILL.md +196 -0
- package/skills/browser-tools/browser-content.js +103 -0
- package/skills/browser-tools/browser-cookies.js +35 -0
- package/skills/browser-tools/browser-eval.js +53 -0
- package/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/skills/browser-tools/browser-nav.js +44 -0
- package/skills/browser-tools/browser-pick.js +162 -0
- package/skills/browser-tools/browser-screenshot.js +34 -0
- package/skills/browser-tools/browser-start.js +86 -0
- package/skills/browser-tools/package-lock.json +2556 -0
- package/skills/browser-tools/package.json +19 -0
- package/skills/frontend-implementation/SKILL.md +3 -1
- package/skills/frontend-implementation/references/node-contracts.md +17 -66
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/grill-with-docs/SKILL.md +5 -5
- package/skills/grill-with-docs/adr-format.md +3 -3
- package/skills/init-capability-evolution/SKILL.md +5 -5
- package/skills/loop-agent/SKILL.md +5 -5
- package/skills/loop-agent/references/README.md +3 -3
- package/skills/loop-agent/references/command-reference.md +39 -17
- package/skills/loop-agent/references/docs-converge.md +15 -15
- package/skills/loop-agent/references/harness-policy.md +2 -2
- package/skills/loop-agent/references/hybrid-dag.md +20 -15
- package/skills/loop-agent/references/multi-worktree.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +8 -8
- package/skills/loop-agent/references/task-workflow.md +1 -1
- package/skills/loop-agent/references/verification-and-failure-handling.md +6 -4
- package/skills/requesting-code-review/SKILL.md +1 -1
- package/skills/systematic-debugging/CREATION-LOG.md +3 -3
- package/skills/systematic-debugging/SKILL.md +1 -1
- package/skills/systematic-debugging/test-academic.md +1 -1
- package/skills/systematic-debugging/test-pressure-1.md +1 -1
- package/skills/systematic-debugging/test-pressure-2.md +1 -1
- package/skills/systematic-debugging/test-pressure-3.md +1 -1
- 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
|
|
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
|
-
|
|
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-${
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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);
|