@tea-agent/loop-agent 0.44.0-next.4 → 0.44.0-next.5

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 (77) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/shell-write-guard.js +26 -3
  4. package/dist/shared/file-identity.js +23 -0
  5. package/dist/shared/git-workspace-status.js +480 -0
  6. package/dist/shared/path-safety.js +2 -2
  7. package/dist/worker/console/chat/context-insights.js +723 -0
  8. package/dist/worker/console/chat/repo-browser.js +3 -4
  9. package/dist/worker/console/chat/routes.js +73 -1
  10. package/dist/worker/console/chat/shortcuts.js +7 -0
  11. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B_lmJjOm.js → abnfDiagram-N423BO3Z-BHuhGM1e.js} +1 -1
  12. package/dist/worker/console/static/assets/{arc-pymM4Qeh.js → arc-CKh5E_sr.js} +1 -1
  13. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw9EPAhi.js → architectureDiagram-T3A2C74G-DizhiBEv.js} +1 -1
  14. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C7_oXJp2.js → blockDiagram-VBNYF7ZC-3Neea-Z-.js} +1 -1
  15. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-LnLWDbh8.js → c4Diagram-5PPSVZJV-BCP_Qrgi.js} +1 -1
  16. package/dist/worker/console/static/assets/channel-DxgzA1GF.js +1 -0
  17. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BBiu3sCW.js → chunk-2GRJ4B5K-C_wydftK.js} +1 -1
  18. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DiXqDQml.js → chunk-2Q5K7J3B-jRPXZbzx.js} +1 -1
  19. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-lmNq9nYk.js → chunk-5RXB4S5H-BLQf2OYO.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-boBu74dU.js → chunk-5VM5RSS4-0qRq64rI.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CjxqoJLQ.js → chunk-6Q2QTUOP-ClMg8xmV.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DL0vQ9MG.js → chunk-GF5L2VYU-Coop2FlL.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DGPqE-O0.js → chunk-JWPE2WC7-CDdO-jPJ.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-v4BbT_r2.js → chunk-KBJHAD2P-D8Gkppjl.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Ci0ynvyL.js → chunk-RYQCIY6F-DA9HIySf.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CGsbJoTV.js → chunk-XXDRQBXY-DZNDRX1k.js} +1 -1
  27. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BZcm7fs6.js +1 -0
  28. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BZcm7fs6.js +1 -0
  29. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CzoRDEI7.js → cose-bilkent-JH36ORCC-CZ1zGyjq.js} +1 -1
  30. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BOwFwbX4.js → cynefin-VYW2F7L2-B8u1_FtM.js} +1 -1
  31. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2Cfg451l.js → cynefinDiagram-MW4NZA55-CPBB6lfo.js} +1 -1
  32. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DbT0D6Y5.js → dagre-VZM6K2ZE-CG3Eov3E.js} +1 -1
  33. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dfd_JoMs.js → diagram-7IWD3JNH-ycI-dOjh.js} +1 -1
  34. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DaGm82kh.js → diagram-B4RE2ZJO-Bx_GyQ7Y.js} +1 -1
  35. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DMoI8Ibc.js → diagram-LBJQPF4R-m0G6mP4d.js} +1 -1
  36. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtIFA3aY.js → diagram-Q27KOJAE-DTDJkqcJ.js} +1 -1
  37. package/dist/worker/console/static/assets/{diagram-UB23O5K3-D_2X1DTZ.js → diagram-UB23O5K3-BCfFrG-p.js} +1 -1
  38. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-yFW5ZYlZ.js → ebnfDiagram-BXEA7PRR-BIkTNPpp.js} +1 -1
  39. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DBXh5Svj.js → erDiagram-JOGREHBK-DTcMWrd2.js} +1 -1
  40. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C4ycm8KM.js → flowDiagram-UKHOOZJN-BqzDaNQ7.js} +1 -1
  41. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-qr-_Pekr.js → ganttDiagram-PKOTCBZU-BSl_vGFe.js} +1 -1
  42. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DzOSLv_A.js → gitGraphDiagram-DS77QQ5N-DCA5HXM6.js} +1 -1
  43. package/dist/worker/console/static/assets/{index-ZtyB6Ol-.css → index-BAEDjTbD.css} +1 -1
  44. package/dist/worker/console/static/assets/{index-PFjB6g6l.js → index-wKEJUaP5.js} +106 -106
  45. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C7Hl8D7l.js → infoDiagram-6WML65LV-DvqT18ge.js} +1 -1
  46. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-fBxVlr6t.js → ishikawaDiagram-WSZJBQD7-B6zGKrQ3.js} +1 -1
  47. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CoiJEVCl.js → journeyDiagram-NVQOT4AX-BpLQPI9C.js} +1 -1
  48. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-TJXpxNuV.js → kanban-definition-27J2QSJJ-CzIz_-Lt.js} +1 -1
  49. package/dist/worker/console/static/assets/{linear-CjQWz1pG.js → linear-CMQ8Jpp-.js} +1 -1
  50. package/dist/worker/console/static/assets/{mermaid.core-DY9_apIE.js → mermaid.core-BdAMrTf7.js} +5 -5
  51. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-HB0Id8gM.js → mindmap-definition-FAOFIHXS-hSwF6sOq.js} +1 -1
  52. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-0OSWgr6v.js → pegDiagram-VL7TDLO6-DZdv509B.js} +1 -1
  53. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cei1GGC2.js → pieDiagram-7S7Q4E2Y-CmD4-b0K.js} +1 -1
  54. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CDy0Adfu.js → quadrantDiagram-CIZ2JOQS-DtKxNxlS.js} +1 -1
  55. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwqzUWwN.js → railroadDiagram-AXF67PYL-D48iIoiS.js} +1 -1
  56. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Bif2IBRL.js → requirementDiagram-LRYGKXZP-C3N0K-2p.js} +1 -1
  57. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CYIoNMLg.js → sankeyDiagram-W5VNT64P-CACWR-Xs.js} +1 -1
  58. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BDS7Y0aQ.js → sequenceDiagram-SI44F4Z6-g_QgugvZ.js} +1 -1
  59. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-lZic-_Bp.js → sizeCapture-X5ZJPWSS-DqLoRPGK.js} +1 -1
  60. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-WCVlCqSd.js → stateDiagram-OKZ733FA-ES59VgMG.js} +1 -1
  61. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyCIq6jx.js +1 -0
  62. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-pGccxTw_.js → swimlanes-SLNWSIFB-fjyLVNWo.js} +2 -2
  63. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CsKtO34q.js +8 -0
  64. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D29RlrQA.js → timeline-definition-Z64GVDOM-Cd5-biKv.js} +1 -1
  65. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-01fEM1NU.js → vennDiagram-T6HMQDX7-CZTu6mPf.js} +1 -1
  66. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C4GeB6dq.js → wardleyDiagram-T6FBY63Y-DR01jjbe.js} +1 -1
  67. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BXHrXHps.js → xychartDiagram-ELKLHX3M-iZcuGVGa.js} +1 -1
  68. package/dist/worker/console/static/index.html +2 -2
  69. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  70. package/dist/worker/observe/node-context-insights.js +479 -0
  71. package/docs/README.md +1 -1
  72. package/package.json +1 -1
  73. package/dist/worker/console/static/assets/channel-Ym13M3Lb.js +0 -1
  74. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeFR1wGo.js +0 -1
  75. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeFR1wGo.js +0 -1
  76. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-cj5UUB6m.js +0 -1
  77. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-C_Qp6bnh.js +0 -8
@@ -0,0 +1,723 @@
1
+ /**
2
+ * Operator Chat — context insights projection (dsh-context parity).
3
+ *
4
+ * Folds the durable Chat event ring into a context-insight dashboard view:
5
+ * - current composition (what the window is made of, provider-anchored)
6
+ * - per-request trend with step briefs, turn grouping, delta mode support
7
+ * - context browser elements (category → items with token estimates and
8
+ * bounded content previews, diffable between steps)
9
+ * - context lifecycle events (injections / compactions / prunes) with chips
10
+ * - per-file activity (reads/writes/searches, line deltas, op log)
11
+ * - usage totals (cache hit, cost when reported) and timing stats
12
+ *
13
+ * Estimates use the same conservative fixed-density heuristic as the
14
+ * compaction policy (conservativeTokenEstimate), so figures never disagree
15
+ * with the harness's own pressure decisions. The projection is derived
16
+ * EXCLUSIVELY from persisted events + bounded previews the server already
17
+ * stores — it invents no payload text and never re-reads the live Pi session,
18
+ * so it works for idle and restored sessions alike.
19
+ */
20
+ import { conservativeTokenEstimate } from "../../../shared/pi-context-pressure/compaction-policy.js";
21
+ /** Fixed-density token estimate for preview text (chars → tokens). */
22
+ export function estimateTokensFromText(text) {
23
+ if (!text)
24
+ return 0;
25
+ return conservativeTokenEstimate(text);
26
+ }
27
+ /** Hard cap on a single element's preview content (chars). */
28
+ const ELEMENT_PREVIEW_MAX = 8_000;
29
+ /** Max elements retained per browser category. */
30
+ const ELEMENT_LIST_CAP = 200;
31
+ const COMPOSITION_CATEGORY_ORDER = [
32
+ "system",
33
+ "tools",
34
+ "user",
35
+ "injected",
36
+ "assistant",
37
+ "toolResults",
38
+ ];
39
+ const BROWSER_CATEGORY_ORDER = [
40
+ "system",
41
+ "tools",
42
+ "messages",
43
+ "injected",
44
+ "toolResults",
45
+ "skills",
46
+ ];
47
+ function emptyComposition() {
48
+ return {
49
+ totalTokens: 0,
50
+ contextWindow: null,
51
+ occupancy: null,
52
+ headroom: null,
53
+ slices: COMPOSITION_CATEGORY_ORDER.map((category) => ({
54
+ category,
55
+ tokens: 0,
56
+ items: 0,
57
+ })),
58
+ providerTotal: null,
59
+ };
60
+ }
61
+ function sliceOf(composition, category) {
62
+ const slice = composition.slices.find((item) => item.category === category);
63
+ if (!slice) {
64
+ throw new Error(`composition slice missing: ${category}`);
65
+ }
66
+ return slice;
67
+ }
68
+ function addSlice(composition, category, tokens, items = 1) {
69
+ const slice = sliceOf(composition, category);
70
+ slice.tokens += tokens;
71
+ slice.items += items;
72
+ }
73
+ function eventTimeMs(event) {
74
+ const parsed = Date.parse(event.at);
75
+ return Number.isFinite(parsed) ? parsed : 0;
76
+ }
77
+ function asRecord(value) {
78
+ return value && typeof value === "object"
79
+ ? value
80
+ : undefined;
81
+ }
82
+ function boundedText(value, max = 4_000) {
83
+ if (typeof value !== "string" || !value)
84
+ return undefined;
85
+ const trimmed = value.trim();
86
+ if (!trimmed)
87
+ return undefined;
88
+ return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
89
+ }
90
+ function elementPreview(value) {
91
+ return boundedText(value, ELEMENT_PREVIEW_MAX);
92
+ }
93
+ /** First string field on a tool-args record among candidates. */
94
+ function argString(args, keys) {
95
+ for (const key of keys) {
96
+ const value = args[key];
97
+ if (typeof value === "string" && value.trim())
98
+ return value;
99
+ }
100
+ return undefined;
101
+ }
102
+ function classifyToolOperation(toolName) {
103
+ const normalized = toolName.toLowerCase().replace(/[-_]/g, "");
104
+ if (normalized === "read" || normalized === "ls")
105
+ return "read";
106
+ if (normalized === "write" || normalized === "multiedit")
107
+ return "write";
108
+ if (normalized === "edit" ||
109
+ normalized === "strreplaceeditor" ||
110
+ normalized === "strreplacebasededittool" ||
111
+ normalized === "applypatch") {
112
+ return "edit";
113
+ }
114
+ if (normalized === "grep" ||
115
+ normalized === "rg" ||
116
+ normalized === "find" ||
117
+ normalized === "fd" ||
118
+ normalized === "glob") {
119
+ return "search";
120
+ }
121
+ if (normalized === "bash" ||
122
+ normalized === "powershell" ||
123
+ normalized === "pwsh") {
124
+ return "bash";
125
+ }
126
+ return "other";
127
+ }
128
+ const READ_PATH_KEYS = ["file_path", "path", "filePath", "filename", "name"];
129
+ const SEARCH_PATH_KEYS = ["path", "dir", "directory", "include", "glob"];
130
+ /** Extract touched file paths from a tool call's arguments preview. */
131
+ export function filePathsFromToolArgs(toolName, args) {
132
+ const kind = classifyToolOperation(toolName);
133
+ const paths = [];
134
+ if (kind === "search") {
135
+ const query = argString(args, SEARCH_PATH_KEYS);
136
+ if (query)
137
+ paths.push(query);
138
+ return paths;
139
+ }
140
+ if (kind === "bash")
141
+ return paths;
142
+ const target = argString(args, READ_PATH_KEYS);
143
+ if (target)
144
+ paths.push(target);
145
+ return paths;
146
+ }
147
+ function lineDelta(oldText, newText) {
148
+ if (!newText)
149
+ return { added: 0, removed: 0 };
150
+ const newLines = newText.split("\n").length;
151
+ if (!oldText)
152
+ return { added: newLines, removed: 0 };
153
+ const oldLines = oldText.split("\n").length;
154
+ return {
155
+ added: Math.max(0, newLines - oldLines),
156
+ removed: Math.max(0, oldLines - newLines),
157
+ };
158
+ }
159
+ /** Parse a JSON args preview back into a record (best-effort, bounded). */
160
+ function parseArgsPreview(preview) {
161
+ if (!preview)
162
+ return {};
163
+ try {
164
+ const parsed = JSON.parse(preview);
165
+ const record = asRecord(parsed);
166
+ return record ?? {};
167
+ }
168
+ catch {
169
+ return {};
170
+ }
171
+ }
172
+ function deltaFor(kind, args) {
173
+ if (kind !== "write" && kind !== "edit")
174
+ return undefined;
175
+ const delta = lineDelta(argString(args, ["oldText", "old_string", "oldStr"]), argString(args, ["content", "newText", "new_string", "newStr"]));
176
+ return {
177
+ ...(delta.added > 0 ? { linesAdded: delta.added } : {}),
178
+ ...(delta.removed > 0 ? { linesRemoved: delta.removed } : {}),
179
+ };
180
+ }
181
+ function num(value) {
182
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
183
+ return undefined;
184
+ }
185
+ return value;
186
+ }
187
+ function toolSourceChip(toolName) {
188
+ const normalized = toolName.toLowerCase();
189
+ if (normalized.startsWith("task") ||
190
+ normalized.startsWith("dag") ||
191
+ normalized.startsWith("worker") ||
192
+ normalized.startsWith("contract") ||
193
+ normalized.startsWith("observe") ||
194
+ normalized.startsWith("prepare") ||
195
+ normalized.startsWith("confirm")) {
196
+ return "operator";
197
+ }
198
+ if (normalized.startsWith("sql_"))
199
+ return "builtin:sql";
200
+ if (normalized.startsWith("mcp"))
201
+ return "mcp";
202
+ if (normalized.startsWith("agent") || normalized === "explore") {
203
+ return "builtin:subagent";
204
+ }
205
+ if (["read", "write", "edit", "bash", "grep", "find", "ls"].includes(normalized)) {
206
+ return "pi";
207
+ }
208
+ return "extension";
209
+ }
210
+ function contentBlocksThinking(blocks) {
211
+ if (!Array.isArray(blocks))
212
+ return false;
213
+ return blocks.some((block) => asRecord(block)?.type === "thinking" ||
214
+ asRecord(block)?.type === "reasoning");
215
+ }
216
+ function capElements(elements) {
217
+ return elements.slice(-ELEMENT_LIST_CAP);
218
+ }
219
+ /** Fold the event ring into the context insights view. */
220
+ export function deriveContextInsights(events, options = {}) {
221
+ const ordered = [...events].sort((a, b) => a.seq - b.seq);
222
+ const composition = emptyComposition();
223
+ const trend = [];
224
+ const contextEvents = [];
225
+ const files = new Map();
226
+ const usage = {
227
+ inputTokens: 0,
228
+ outputTokens: 0,
229
+ cacheReadTokens: 0,
230
+ cacheWriteTokens: 0,
231
+ totalTokens: 0,
232
+ cost: null,
233
+ cacheHitPercent: null,
234
+ turns: 0,
235
+ steps: 0,
236
+ humanInputs: 0,
237
+ };
238
+ const timing = {
239
+ llmMs: 0,
240
+ toolMs: 0,
241
+ busyMs: 0,
242
+ wallMs: null,
243
+ overheadMs: null,
244
+ ttftMs: null,
245
+ decodeTokensPerSecond: null,
246
+ steps: 0,
247
+ };
248
+ const turnIds = [];
249
+ const turnStartMs = new Map();
250
+ const turnUserBrief = new Map();
251
+ const stepToolsCalled = new Map();
252
+ const toolStarts = new Map();
253
+ const ttftSamples = [];
254
+ const decodeSamples = [];
255
+ let contextWindow = null;
256
+ let providerTotal = null;
257
+ let lastSeq = 0;
258
+ let firstAt = null;
259
+ let lastAt = null;
260
+ // --- Live browser fold (the "next request" makeup) ---
261
+ const browserMessages = [];
262
+ const browserInjected = [];
263
+ const browserToolResults = [];
264
+ if (options.systemPromptPreview) {
265
+ addSlice(composition, "system", estimateTokensFromText(options.systemPromptPreview));
266
+ }
267
+ else if (typeof options.systemPromptChars === "number" &&
268
+ options.systemPromptChars > 0) {
269
+ addSlice(composition, "system", conservativeTokenEstimate("x".repeat(Math.min(options.systemPromptChars, 2_000_000))));
270
+ }
271
+ const browserTools = (options.tools ?? []).map((tool, index) => {
272
+ const tokens = estimateTokensFromText(`${tool.id}${tool.description ? `: ${tool.description}` : ""}`);
273
+ return {
274
+ id: `tool:${tool.id}:${index}`,
275
+ label: tool.id,
276
+ tokens,
277
+ preview: elementPreview(tool.description),
278
+ toolSource: toolSourceChip(tool.id),
279
+ seq: 0,
280
+ at: "",
281
+ };
282
+ });
283
+ for (const tool of browserTools) {
284
+ addSlice(composition, "tools", tool.tokens);
285
+ }
286
+ const browserSkills = (options.skillInjections ?? []).map((skill, index) => ({
287
+ id: `skill:${skill.name}:${index}`,
288
+ label: skill.name,
289
+ tokens: skill.tokens,
290
+ seq: 0,
291
+ at: "",
292
+ }));
293
+ for (const event of ordered) {
294
+ lastSeq = event.seq;
295
+ const atMs = eventTimeMs(event);
296
+ if (atMs > 0) {
297
+ if (firstAt === null)
298
+ firstAt = atMs;
299
+ lastAt = atMs;
300
+ }
301
+ const data = asRecord(event.data) ?? {};
302
+ switch (event.kind) {
303
+ case "user_message": {
304
+ const message = asRecord(data.message);
305
+ const text = boundedText(message?.text, ELEMENT_PREVIEW_MAX);
306
+ if (text) {
307
+ const tokens = estimateTokensFromText(text);
308
+ addSlice(composition, "user", tokens);
309
+ browserMessages.push({
310
+ id: `user:${event.seq}`,
311
+ label: "用户",
312
+ tokens,
313
+ preview: text,
314
+ seq: event.seq,
315
+ at: event.at,
316
+ });
317
+ if (!turnUserBrief.has(event.turnId)) {
318
+ turnUserBrief.set(event.turnId, text.slice(0, 200));
319
+ }
320
+ }
321
+ usage.humanInputs += 1;
322
+ if (!turnIds.includes(event.turnId))
323
+ turnIds.push(event.turnId);
324
+ if (!turnStartMs.has(event.turnId) && atMs > 0) {
325
+ turnStartMs.set(event.turnId, atMs);
326
+ }
327
+ break;
328
+ }
329
+ case "message_update":
330
+ case "message_end": {
331
+ const text = boundedText(data.text, ELEMENT_PREVIEW_MAX);
332
+ const thinking = contentBlocksThinking(data.content);
333
+ if (event.kind === "message_end") {
334
+ usage.steps += 1;
335
+ timing.steps += 1;
336
+ if (!turnIds.includes(event.turnId))
337
+ turnIds.push(event.turnId);
338
+ if (text) {
339
+ const tokens = estimateTokensFromText(text);
340
+ addSlice(composition, "assistant", tokens);
341
+ browserMessages.push({
342
+ id: `assistant:${event.seq}`,
343
+ label: thinking ? "助手(含思考)" : "助手",
344
+ tokens,
345
+ preview: text,
346
+ seq: event.seq,
347
+ at: event.at,
348
+ });
349
+ }
350
+ const toolsCalled = stepToolsCalled.get(event.turnId) ?? [];
351
+ const recorded = asRecord(data.timing);
352
+ const llmMs = num(recorded?.llmMs) ?? null;
353
+ if (llmMs !== null)
354
+ timing.llmMs += llmMs;
355
+ const ttft = num(recorded?.ttftMs);
356
+ if (ttft !== undefined)
357
+ ttftSamples.push(ttft);
358
+ const decodeMs = num(recorded?.decodeMs);
359
+ const outputTokens = num(recorded?.outputTokens);
360
+ if (decodeMs !== undefined &&
361
+ decodeMs > 0 &&
362
+ outputTokens !== undefined) {
363
+ decodeSamples.push({ ms: decodeMs, tokens: outputTokens });
364
+ }
365
+ const start = turnStartMs.get(event.turnId);
366
+ const turnElapsedMs = start !== undefined && atMs >= start ? atMs - start : null;
367
+ const stepComposition = emptyComposition();
368
+ for (const slice of composition.slices) {
369
+ const target = sliceOf(stepComposition, slice.category);
370
+ target.tokens = slice.tokens;
371
+ target.items = slice.items;
372
+ }
373
+ stepComposition.totalTokens = composition.slices.reduce((sum, slice) => sum + slice.tokens, 0);
374
+ trend.push({
375
+ index: trend.length + 1,
376
+ turnId: event.turnId,
377
+ turnIndex: turnIds.indexOf(event.turnId) + 1,
378
+ at: event.at,
379
+ seq: event.seq,
380
+ composition: stepComposition.slices,
381
+ totalTokens: stepComposition.totalTokens,
382
+ actualPromptTokens: null,
383
+ actualOutputTokens: null,
384
+ actualCacheReadTokens: null,
385
+ userBrief: turnUserBrief.get(event.turnId),
386
+ responseBrief: text ? text.slice(0, 200) : undefined,
387
+ toolsCalled,
388
+ thinking,
389
+ turnElapsedMs,
390
+ llmMs,
391
+ });
392
+ stepToolsCalled.set(event.turnId, []);
393
+ }
394
+ break;
395
+ }
396
+ case "tool_call": {
397
+ const toolName = typeof data.toolName === "string" ? data.toolName : "";
398
+ const argsPreview = typeof data.argsPreview === "string" ? data.argsPreview : undefined;
399
+ const args = parseArgsPreview(argsPreview);
400
+ const atTime = eventTimeMs(event);
401
+ if (toolName) {
402
+ const list = stepToolsCalled.get(event.turnId) ?? [];
403
+ list.push(toolName);
404
+ stepToolsCalled.set(event.turnId, list);
405
+ }
406
+ if (argsPreview) {
407
+ const tokens = estimateTokensFromText(argsPreview);
408
+ addSlice(composition, "injected", tokens);
409
+ browserInjected.push({
410
+ id: `args:${event.seq}`,
411
+ label: toolName || "工具调用",
412
+ tokens,
413
+ preview: argsPreview,
414
+ toolSource: toolName ? toolSourceChip(toolName) : undefined,
415
+ seq: event.seq,
416
+ at: event.at,
417
+ });
418
+ }
419
+ if (toolName && atTime > 0) {
420
+ const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : "";
421
+ toolStarts.set(`${event.turnId}\u0000${toolCallId}`, atTime);
422
+ }
423
+ const kind = classifyToolOperation(toolName);
424
+ if (kind !== "other") {
425
+ const paths = filePathsFromToolArgs(toolName, args);
426
+ for (const path of paths) {
427
+ const entry = files.get(path) ??
428
+ {
429
+ path,
430
+ reads: 0,
431
+ writes: 0,
432
+ searches: 0,
433
+ otherOps: 0,
434
+ linesAdded: 0,
435
+ linesRemoved: 0,
436
+ operations: [],
437
+ };
438
+ if (kind === "read")
439
+ entry.reads += 1;
440
+ else if (kind === "write" || kind === "edit")
441
+ entry.writes += 1;
442
+ else if (kind === "search")
443
+ entry.searches += 1;
444
+ else
445
+ entry.otherOps += 1;
446
+ const opDelta = deltaFor(kind, args);
447
+ if (opDelta) {
448
+ entry.linesAdded += opDelta.linesAdded ?? 0;
449
+ entry.linesRemoved += opDelta.linesRemoved ?? 0;
450
+ }
451
+ entry.operations.push({
452
+ seq: event.seq,
453
+ at: event.at,
454
+ toolName: toolName || "unknown",
455
+ kind,
456
+ ...opDelta,
457
+ });
458
+ files.set(path, entry);
459
+ }
460
+ }
461
+ break;
462
+ }
463
+ case "tool_result": {
464
+ const resultPreview = typeof data.resultPreview === "string"
465
+ ? data.resultPreview
466
+ : undefined;
467
+ const toolCallId = typeof data.toolCallId === "string" ? data.toolCallId : "";
468
+ const key = `${event.turnId}\u0000${toolCallId}`;
469
+ const startedAt = toolStarts.get(key);
470
+ if (startedAt !== undefined && atMs >= startedAt) {
471
+ timing.toolMs += atMs - startedAt;
472
+ }
473
+ toolStarts.delete(key);
474
+ if (resultPreview) {
475
+ const tokens = estimateTokensFromText(resultPreview);
476
+ addSlice(composition, "toolResults", tokens);
477
+ browserToolResults.push({
478
+ id: `result:${event.seq}`,
479
+ label: typeof data.toolName === "string" && data.toolName
480
+ ? data.toolName
481
+ : "工具结果",
482
+ tokens,
483
+ preview: elementPreview(resultPreview),
484
+ status: data.isError === true ? "error" : "ok",
485
+ seq: event.seq,
486
+ at: event.at,
487
+ });
488
+ }
489
+ break;
490
+ }
491
+ case "usage": {
492
+ const input = num(data.inputTokens);
493
+ const output = num(data.outputTokens);
494
+ const cacheRead = num(data.cacheReadTokens);
495
+ const cacheWrite = num(data.cacheWriteTokens);
496
+ if (input !== undefined)
497
+ usage.inputTokens += input;
498
+ if (output !== undefined)
499
+ usage.outputTokens += output;
500
+ if (cacheRead !== undefined)
501
+ usage.cacheReadTokens += cacheRead;
502
+ if (cacheWrite !== undefined)
503
+ usage.cacheWriteTokens += cacheWrite;
504
+ const total = num(data.totalTokens);
505
+ if (total !== undefined)
506
+ usage.totalTokens += total;
507
+ const cost = num(data.cost);
508
+ if (cost !== undefined) {
509
+ usage.cost = (usage.cost ?? 0) + cost;
510
+ }
511
+ const contextUsage = asRecord(data.contextUsage);
512
+ const window = num(contextUsage?.contextWindow);
513
+ const tokens = num(contextUsage?.tokens);
514
+ if (window !== undefined && window > 0)
515
+ contextWindow = window;
516
+ if (tokens !== undefined)
517
+ providerTotal = tokens;
518
+ const lastStep = trend.at(-1);
519
+ if (lastStep && input !== undefined) {
520
+ lastStep.actualPromptTokens =
521
+ input + (cacheRead ?? 0) + (cacheWrite ?? 0);
522
+ lastStep.actualOutputTokens = output ?? null;
523
+ lastStep.actualCacheReadTokens = cacheRead ?? null;
524
+ }
525
+ break;
526
+ }
527
+ case "compaction_start":
528
+ case "compaction_end":
529
+ case "compact": {
530
+ const before = num(data.tokensBefore);
531
+ const after = num(data.estimatedTokensAfter);
532
+ const reclaimed = before !== undefined && after !== undefined ? before - after : null;
533
+ const pruned = data.pruned === true || data.kind === "prune";
534
+ contextEvents.push({
535
+ seq: event.seq,
536
+ at: event.at,
537
+ turnId: event.turnId,
538
+ turnIndex: turnIds.indexOf(event.turnId) + 1,
539
+ kind: pruned ? "prune" :
540
+ event.kind === "compaction_start"
541
+ ? "compact-start"
542
+ : event.kind === "compaction_end"
543
+ ? "compact-end"
544
+ : "compact",
545
+ tokenDelta: reclaimed,
546
+ ...(before !== undefined ? { tokensBefore: before } : {}),
547
+ ...(after !== undefined ? { tokensAfter: after } : {}),
548
+ ...(Array.isArray(data.shadowedSeqs) ? { shadowedItems: data.shadowedSeqs.length } : {}),
549
+ ...(typeof data.reason === "string" ? { reason: data.reason } : {}),
550
+ ...(pruned ? { pruned: true } : {}),
551
+ detail: boundedText(asRecord(data.details)?.text, 300),
552
+ });
553
+ const lastStep = trend.at(-1);
554
+ if (lastStep && event.kind !== "compaction_start") {
555
+ lastStep.eventMark = pruned ? "prune" : "compact";
556
+ }
557
+ if (reclaimed !== null && reclaimed > 0) {
558
+ const userSlice = sliceOf(composition, "user");
559
+ const assistantSlice = sliceOf(composition, "assistant");
560
+ const toolSlice = sliceOf(composition, "toolResults");
561
+ let remaining = reclaimed;
562
+ for (const slice of [userSlice, assistantSlice, toolSlice]) {
563
+ const take = Math.min(slice.tokens, remaining);
564
+ slice.tokens -= take;
565
+ remaining -= take;
566
+ if (remaining <= 0)
567
+ break;
568
+ }
569
+ }
570
+ break;
571
+ }
572
+ default:
573
+ break;
574
+ }
575
+ }
576
+ composition.totalTokens = composition.slices.reduce((sum, slice) => sum + slice.tokens, 0);
577
+ composition.contextWindow = contextWindow;
578
+ composition.providerTotal = providerTotal;
579
+ if (providerTotal !== null &&
580
+ providerTotal > 0 &&
581
+ composition.totalTokens > 0) {
582
+ const scale = providerTotal / composition.totalTokens;
583
+ for (const slice of composition.slices) {
584
+ slice.tokens = Math.round(slice.tokens * scale);
585
+ }
586
+ composition.totalTokens = providerTotal;
587
+ }
588
+ if (contextWindow !== null && contextWindow > 0) {
589
+ composition.occupancy =
590
+ Math.round((composition.totalTokens / contextWindow) * 1000) / 10;
591
+ composition.headroom = Math.max(0, contextWindow - composition.totalTokens);
592
+ }
593
+ const billed = usage.inputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
594
+ usage.cacheHitPercent =
595
+ billed > 0
596
+ ? Math.round((usage.cacheReadTokens / billed) * 1000) / 10
597
+ : null;
598
+ usage.turns = turnIds.length;
599
+ if (usage.totalTokens === 0 && billed + usage.outputTokens > 0) {
600
+ usage.totalTokens = billed + usage.outputTokens;
601
+ }
602
+ timing.busyMs = timing.llmMs + timing.toolMs;
603
+ if (firstAt !== null && lastAt !== null && lastAt >= firstAt) {
604
+ timing.wallMs = lastAt - firstAt;
605
+ timing.overheadMs = Math.max(0, timing.wallMs - timing.busyMs);
606
+ }
607
+ if (ttftSamples.length > 0) {
608
+ timing.ttftMs = Math.round(ttftSamples.reduce((sum, v) => sum + v, 0) / ttftSamples.length);
609
+ }
610
+ if (decodeSamples.length > 0) {
611
+ const ms = decodeSamples.reduce((sum, s) => sum + s.ms, 0);
612
+ const tokens = decodeSamples.reduce((sum, s) => sum + s.tokens, 0);
613
+ if (ms > 0 && tokens > 0) {
614
+ timing.decodeTokensPerSecond = Math.round((tokens / ms) * 1000);
615
+ }
616
+ }
617
+ // --- Browser snapshots: live + one per trend step (rolling elements) ---
618
+ const browserSteps = [];
619
+ const liveMessages = capElements(browserMessages);
620
+ const liveInjected = capElements(browserInjected);
621
+ const liveToolResults = capElements(browserToolResults);
622
+ const liveCategories = [];
623
+ const pushCategory = (category, elements) => {
624
+ if (elements.length === 0)
625
+ return;
626
+ liveCategories.push({
627
+ category,
628
+ elements,
629
+ totalTokens: elements.reduce((sum, e) => sum + e.tokens, 0),
630
+ });
631
+ };
632
+ pushCategory("system", options.systemPromptPreview
633
+ ? [{
634
+ id: "system:prompt",
635
+ label: "System prompt",
636
+ tokens: estimateTokensFromText(options.systemPromptPreview),
637
+ preview: elementPreview(options.systemPromptPreview),
638
+ seq: 0,
639
+ at: "",
640
+ }]
641
+ : []);
642
+ pushCategory("tools", browserTools);
643
+ pushCategory("messages", liveMessages);
644
+ pushCategory("injected", liveInjected);
645
+ pushCategory("toolResults", liveToolResults);
646
+ pushCategory("skills", browserSkills);
647
+ browserSteps.push({
648
+ stepId: "live",
649
+ categories: liveCategories,
650
+ totalTokens: liveCategories.reduce((sum, c) => sum + c.totalTokens, 0),
651
+ });
652
+ // Retained steps: rebuild each step's makeup from cumulative snapshots at
653
+ // that point in the ring (bounded to the most recent steps).
654
+ const RETAINED_BROWSER_STEPS = 30;
655
+ const recentTrend = trend.slice(-RETAINED_BROWSER_STEPS);
656
+ for (let i = 0; i < recentTrend.length; i++) {
657
+ const step = recentTrend[i];
658
+ const cutoffSeq = step.seq;
659
+ const upto = (list) => capElements(list.filter((e) => e.seq <= cutoffSeq));
660
+ const messagesAt = upto(browserMessages);
661
+ const injectedAt = upto(browserInjected);
662
+ const resultsAt = upto(browserToolResults);
663
+ const categories = [];
664
+ const push = (category, elements) => {
665
+ if (elements.length === 0)
666
+ return;
667
+ categories.push({
668
+ category,
669
+ elements,
670
+ totalTokens: elements.reduce((sum, e) => sum + e.tokens, 0),
671
+ });
672
+ };
673
+ push("system", options.systemPromptPreview
674
+ ? [{
675
+ id: "system:prompt",
676
+ label: "System prompt",
677
+ tokens: estimateTokensFromText(options.systemPromptPreview),
678
+ preview: elementPreview(options.systemPromptPreview),
679
+ seq: 0,
680
+ at: "",
681
+ }]
682
+ : []);
683
+ push("tools", browserTools);
684
+ push("messages", messagesAt);
685
+ push("injected", injectedAt);
686
+ push("toolResults", resultsAt);
687
+ push("skills", browserSkills);
688
+ browserSteps.push({
689
+ stepId: String(step.index),
690
+ turnId: step.turnId,
691
+ at: step.at,
692
+ categories,
693
+ totalTokens: categories.reduce((sum, c) => sum + c.totalTokens, 0),
694
+ });
695
+ }
696
+ return {
697
+ schemaVersion: 1,
698
+ ...(options.sessionId ? { sessionId: options.sessionId } : {}),
699
+ generatedAt: new Date().toISOString(),
700
+ lastEventSeq: lastSeq,
701
+ composition,
702
+ trend,
703
+ events: contextEvents,
704
+ files: [...files.values()].sort((a, b) => {
705
+ const opsA = a.reads + a.writes + a.searches + a.otherOps;
706
+ const opsB = b.reads + b.writes + b.searches + b.otherOps;
707
+ if (opsB !== opsA)
708
+ return opsB - opsA;
709
+ return a.path.localeCompare(b.path);
710
+ }),
711
+ browser: browserSteps,
712
+ usage,
713
+ timing,
714
+ systemPromptChars: options.systemPromptChars ?? null,
715
+ systemPromptPreview: options.systemPromptPreview ?? null,
716
+ tools: browserTools.map((t) => ({
717
+ id: t.label,
718
+ description: "",
719
+ tokens: t.tokens,
720
+ })),
721
+ skillInjections: options.skillInjections ?? [],
722
+ };
723
+ }