@chatpanel/events 0.102.0 → 0.104.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 (139) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +4 -5
  62. package/pdf-layout.js +7 -253
  63. package/policy.js +1 -0
  64. package/project-run.js +20 -444
  65. package/project.js +1 -170
  66. package/promotion.js +1 -171
  67. package/queue.js +1 -98
  68. package/reach.js +1 -23
  69. package/recipe-tool.js +2 -161
  70. package/recipe.js +1 -264
  71. package/record-list.js +2 -110
  72. package/recruit.js +3 -456
  73. package/redaction-tokens.js +1 -61
  74. package/ref.js +1 -55
  75. package/registry.js +1 -240
  76. package/route-graph.js +1 -115
  77. package/route-strategies.js +2 -232
  78. package/router.js +2 -880
  79. package/rrf.js +1 -89
  80. package/rules.js +1 -142
  81. package/schedule.js +1 -663
  82. package/scm-connection.js +1 -180
  83. package/scopes.js +1 -12
  84. package/scorecard.js +1 -335
  85. package/search-engines.js +1 -81
  86. package/skill-manifest.js +1 -330
  87. package/skill-scan.js +3 -209
  88. package/skill-sources.js +1 -162
  89. package/skill-vars.js +2 -252
  90. package/slash-commands.js +1 -158
  91. package/source-gate.js +1 -92
  92. package/sources-retrieval.js +5 -186
  93. package/sources.js +1 -273
  94. package/store.js +1 -171
  95. package/structured.js +4 -901
  96. package/subject-kinds.js +1 -5
  97. package/subject-name.js +1 -96
  98. package/sync-plan.js +1 -213
  99. package/synthesis.js +3 -123
  100. package/tags.js +1 -203
  101. package/team-board.js +6 -337
  102. package/team-cache.js +2 -63
  103. package/team-observe.js +1 -118
  104. package/team-org.js +1 -442
  105. package/team-plan.js +4 -131
  106. package/team-record.js +1 -225
  107. package/team-run.js +9 -652
  108. package/team-subtask.js +2 -191
  109. package/team-task.js +2 -124
  110. package/team-tool.js +2 -196
  111. package/team-trail.js +2 -73
  112. package/team-worklog.js +2 -149
  113. package/team.js +1 -302
  114. package/text-search.js +1 -155
  115. package/theme.js +17 -154
  116. package/titles.js +4 -285
  117. package/tool-discovery.js +1 -87
  118. package/tool-dispatch.js +5 -230
  119. package/tool-groups.js +1 -81
  120. package/tool-hints.js +10 -185
  121. package/tool-loop-guard.js +4 -182
  122. package/tool-need.js +2 -124
  123. package/tool-result.js +2 -438
  124. package/tool-round.js +0 -0
  125. package/tool-schema.js +1 -155
  126. package/tool-traits.js +1 -182
  127. package/toolset.js +2 -88
  128. package/trajectory.js +1 -530
  129. package/turn-loop.js +4 -440
  130. package/upcast.js +1 -37
  131. package/vault.js +2 -250
  132. package/view.js +1 -88
  133. package/voice-intents.js +2 -1488
  134. package/voice-speaker.js +1 -98
  135. package/weather-tool.js +1 -65
  136. package/weather.js +2 -211
  137. package/web-search-tool.js +14 -102
  138. package/web-search.js +1 -160
  139. package/widget.js +1 -153
package/trajectory.js CHANGED
@@ -1,530 +1 @@
1
- // One turn, as an ordered, inspectable sequence.
2
- //
3
- // THIS LIVES IN THE SHARED PACKAGE ON PURPOSE. A trajectory is not a browser-extension
4
- // feature: a desktop app, a mobile app and the gateway all need to answer "what happened in
5
- // this turn, in what order, and where did the time go", and three implementations of that
6
- // would drift into three different answers to the same question. What is client-specific is
7
- // only the rendering — the model is shared.
8
- //
9
- // TWO REFERENCES, TWO LESSONS.
10
- //
11
- // From DevTools: the WATERFALL. A request list that only shows totals cannot answer "why
12
- // was this slow"; laying the phases out proportionally answers it at a glance, before any
13
- // clicking. Our phases are setup (assembling tools, connecting to MCP servers), wait (to
14
- // first token), and work (tool calls and writing). A turn that spent 45s connecting and
15
- // 2s thinking looks nothing like one that spent 2s connecting and 45s writing, and the
16
- // old single duration made them identical.
17
- //
18
- // From the DeepSeek harness: the ENTRY LIST plus a DETAIL PANE. Every step is one row —
19
- // system, user, context, tool call, result, answer — and selecting a row shows the whole
20
- // of it. Rows stay short so the shape of the turn is legible; the detail pane is where
21
- // length is allowed.
22
- //
23
- // What neither does, and we must: content is not here. Each entry carries a `ref`, and the
24
- // caller resolves it from the blob store when the user actually looks. That keeps a
25
- // trajectory cheap to build for sixty runs and honest about deleted content — a ref whose
26
- // blob is gone resolves to "no longer stored" rather than silently showing nothing.
27
-
28
- import { linearize } from './order.js';
29
-
30
- /**
31
- * The name a human should see for a call.
32
- *
33
- * A dispatcher registers ONE tool and carries the real action in its arguments, so the raw
34
- * capability name is `page` for every page call. Showing that turns forty distinct actions
35
- * into forty identical rows — the same blindness that once stopped the loop guard exempting
36
- * screenshots. Lives here, not in a renderer, because every client will need it.
37
- */
38
- export function displayName(call) {
39
- const action = call?.args?.action;
40
- if (typeof action !== 'string' || !action) return call?.name || 'tool';
41
- return action === 'describe' && call.args.tool
42
- ? `${call.name}.describe(${call.args.tool})`
43
- : `${call.name}.${action}`;
44
- }
45
-
46
- /** Entry kinds, in the order they conventionally appear. Used for grouping and colour. */
47
- export const ENTRY_KINDS = Object.freeze(['system', 'user', 'context', 'route', 'privacy', 'tool', 'result', 'reasoning', 'assistant']);
48
-
49
- const short = (s, n = 120) => {
50
- const t = String(s ?? '').replace(/\s+/g, ' ').trim();
51
- return t.length > n ? `${t.slice(0, n - 1)}…` : t;
52
- };
53
-
54
- /**
55
- * Build the ordered entries for one turn.
56
- *
57
- * `events` are that turn's events; ordering comes from `linearize`, never from wall time,
58
- * so a trajectory reads identically on every host and after every export.
59
- */
60
- export function buildTrajectory(events) {
61
- const ordered = linearize(events || []);
62
- const entries = [];
63
- const calls = new Map();
64
- let startedAt = null;
65
-
66
- for (const e of ordered) {
67
- const p = e.payload || {};
68
- switch (e.type) {
69
- case 'turn.started':
70
- startedAt = e.at;
71
- break;
72
-
73
- case 'context.assembled':
74
- entries.push({
75
- kind: 'context', at: e.at, title: 'Context assembled',
76
- detail: `${(p.tools || []).length} tool${(p.tools || []).length === 1 ? '' : 's'} · ${p.used || 0} tokens`,
77
- data: { tools: p.tools || [], tokens: p.used || 0, redaction: !!p.redaction, surface: p.surface },
78
- });
79
- break;
80
-
81
- // The prompt blob holds system + messages + the toolset; split it into readable rows
82
- // at render time, since only the blob knows what was actually in it.
83
- // RETRIEVED MATERIAL IS INPUT, and it belongs beside the question rather than buried
84
- // in a tool result. 'a tool ran' and 'a tool returned five notes' are different facts,
85
- // and only the second explains the answer.
86
- case 'context.retrieved':
87
- entries.push({
88
- kind: 'context', at: e.at,
89
- title: p.count ? `Retrieved ${p.count} source${p.count === 1 ? '' : 's'}` : 'Retrieved material',
90
- detail: [p.tool, p.chars ? `${p.chars} chars` : ''].filter(Boolean).join(' · '),
91
- data: { tool: p.tool, count: p.count, sources: p.sources || [] },
92
- });
93
- break;
94
-
95
- // WHAT LEFT THE DEVICE, AND IN WHAT SHAPE. A trajectory that shows the prompt, the
96
- // tools and the answer but not the redaction cannot answer the question the product
97
- // exists to answer — "what did the model actually see?". Counts by entity type only;
98
- // the values are never in the event (see the privacy.redacted contract).
99
- case 'privacy.redacted': {
100
- const counts = p.counts || {};
101
- const total = Object.values(counts).reduce((n, v) => n + (Number(v) || 0), 0);
102
- if (!total) break;
103
- const byType = Object.entries(counts)
104
- .sort((a, b) => b[1] - a[1])
105
- .map(([type, n]) => `${n} ${type.toLowerCase().replace(/_/g, ' ')}`)
106
- .join(' · ');
107
- entries.push({
108
- kind: 'privacy', at: e.at,
109
- title: `Redacted ${total} value${total === 1 ? '' : 's'} before sending`,
110
- detail: byType,
111
- data: { counts },
112
- });
113
- break;
114
- }
115
-
116
- case 'assistant.prompted':
117
- entries.push({ kind: 'system', at: e.at, title: 'Prompt', detail: `${p.chars || 0} chars`, ref: p.ref, expandsToMessages: true });
118
- break;
119
-
120
- // WHICH MODEL, AND WHY. Routing decisions were being recorded and then not shown,
121
- // which is the least useful place for them: the log knew a turn changed model three
122
- // times and the view that exists to explain a turn did not mention it.
123
- case 'policy.changed': {
124
- if (!String(p.dial || '').startsWith('route.')) break;
125
- const applied = p.dial === 'route.applied';
126
- entries.push({
127
- kind: 'route', at: e.at,
128
- title: applied ? `Routed to ${p.to}` : `Would route to ${p.to}`,
129
- detail: (p.reasons || [])[0] || '',
130
- data: {
131
- from: p.from, to: p.to, applied,
132
- agrees: p.agrees, strategy: p.strategy,
133
- reasons: p.reasons, eligible: p.eligible, rejected: p.rejected,
134
- // Every candidate and the projected failover chain, carried through so a viewer
135
- // can DRAW the decision. Passed along rather than rebuilt here: this file turns
136
- // events into entries, it does not re-derive what the router already decided.
137
- graph: p.graph || null,
138
- },
139
- });
140
- break;
141
- }
142
-
143
- case 'automation.fired': {
144
- if (p.ruleId !== 'router:failover') {
145
- entries.push({ kind: 'route', at: e.at, title: `Rule fired: ${p.ruleId}`, detail: `class ${p.classUsed}`, data: p });
146
- break;
147
- }
148
- // A failover is the most consequential thing that can happen mid-turn, and the
149
- // reason is what makes it readable rather than alarming.
150
- entries.push({
151
- kind: 'route', at: e.at,
152
- title: `Failed over to ${p.to}`,
153
- detail: `${p.from} declined (${p.reason})`,
154
- data: p,
155
- });
156
- break;
157
- }
158
-
159
- case 'capability.invoked': {
160
- const entry = {
161
- kind: 'tool', at: e.at, title: displayName({ name: p.capability, args: p.args }),
162
- detail: short(JSON.stringify(p.args || {}), 90),
163
- key: p.idempotencyKey || e.id, ok: null, ms: null,
164
- // The facts a reader actually asks for about a call, in one place: when it
165
- // started in absolute time, what it was given, who asked for it, and whether it
166
- // could be replayed. Split across two events, they are a join the reader should
167
- // not have to do.
168
- data: {
169
- capability: p.capability,
170
- args: p.args || {},
171
- actor: p.actor,
172
- scope: p.scope,
173
- effects: p.effects,
174
- started: new Date(e.at).toISOString(),
175
- },
176
- };
177
- calls.set(entry.key, entry);
178
- entries.push(entry);
179
- break;
180
- }
181
-
182
- case 'capability.resulted': {
183
- const call = calls.get(p.idempotencyKey);
184
- if (call) { call.ok = p.ok; call.ms = p.cost?.ms ?? null; }
185
- entries.push({
186
- kind: 'result', at: e.at, title: `${p.capability} → ${p.ok ? 'ok' : 'failed'}`,
187
- detail: short(p.summary, 120), ok: !!p.ok, ms: p.cost?.ms ?? null,
188
- data: {
189
- status: p.ok ? 'completed' : 'failed',
190
- durationMs: p.cost?.ms ?? null,
191
- classUsed: p.classUsed,
192
- finished: new Date(e.at).toISOString(),
193
- result: p.summary,
194
- },
195
- });
196
- break;
197
- }
198
-
199
- case 'assistant.reasoning':
200
- entries.push({ kind: 'reasoning', at: e.at, title: 'Reasoning', detail: `${p.chars || 0} chars`, ref: p.ref });
201
- break;
202
-
203
- case 'assistant.message':
204
- entries.push({
205
- kind: 'assistant', at: e.at, title: 'Answer',
206
- // What it was BASED ON, on the answer row. The first thing anyone checks when an
207
- // answer looks wrong is what stood behind it.
208
- detail: [`${p.chars || 0} chars`, p.citations?.length ? `${p.citations.length} citation${p.citations.length === 1 ? '' : 's'}` : ''].filter(Boolean).join(' · '),
209
- ref: p.ref,
210
- data: p.citations?.length ? { citations: p.citations } : null,
211
- });
212
- break;
213
-
214
- default:
215
- break;
216
- }
217
- }
218
-
219
- for (const entry of entries) entry.offsetMs = startedAt == null ? null : entry.at - startedAt;
220
- return entries;
221
- }
222
-
223
- /**
224
- * The three phases of a turn, as fractions that sum to 1 — the waterfall.
225
- *
226
- * Deliberately three and not more: setup, waiting for the first token, and everything
227
- * after. Each has a different cause and a different fix, which is the only reason to
228
- * split a bar at all. `null` when the turn recorded no duration, because a bar drawn from
229
- * missing numbers is a confident lie.
230
- */
231
- export function phasesOf(run) {
232
- const total = run?.ms;
233
- if (!Number.isFinite(total) || total <= 0) return null;
234
- const setup = Math.max(0, Math.min(run.prepMs || 0, total));
235
- const wait = Math.max(0, Math.min(run.ttftMs || 0, total - setup));
236
- const work = Math.max(0, total - setup - wait);
237
- const pct = (v) => (v / total) * 100;
238
- return {
239
- total,
240
- parts: [
241
- { key: 'setup', ms: setup, pct: pct(setup), label: 'Setup — tools and MCP servers' },
242
- { key: 'wait', ms: wait, pct: pct(wait), label: 'Waiting for the first word' },
243
- { key: 'work', ms: work, pct: pct(work), label: 'Tools and writing' },
244
- ].filter((p) => p.ms > 0),
245
- };
246
- }
247
-
248
- /**
249
- * The three LANES — input, model, tools — as spans across the turn.
250
- *
251
- * A single stacked bar says how the time divided; lanes say what was ACTIVE and when, and
252
- * those are different questions. Two tool calls with model thinking between them is a
253
- * different shape from one long call, and a stacked bar draws them identically.
254
- *
255
- * Spans are positioned as percentages of the turn, so the rendering needs no width.
256
- */
257
- export function lanesOf(entries, run) {
258
- const total = run?.ms;
259
- if (!Number.isFinite(total) || total <= 0) return null;
260
- const pct = (ms) => Math.max(0, Math.min(100, (ms / total) * 100));
261
- const lanes = { input: [], model: [], tools: [] };
262
-
263
- for (const e of entries) {
264
- if (e.offsetMs == null) continue;
265
- const at = pct(e.offsetMs);
266
- if (e.kind === 'system' || e.kind === 'user' || e.kind === 'context') {
267
- lanes.input.push({ left: at, width: Math.max(0.6, pct(200)), label: e.title });
268
- } else if (e.kind === 'tool') {
269
- // A call's span is its own duration when known — the result carries it.
270
- lanes.tools.push({ left: at, width: Math.max(1, pct(e.ms || 400)), label: e.title });
271
- } else if (e.kind === 'assistant' || e.kind === 'reasoning') {
272
- lanes.model.push({ left: at, width: Math.max(1, pct(400)), label: e.title });
273
- }
274
- }
275
- // Waiting for the first word is model time even though nothing was emitted during it —
276
- // otherwise the lane looks idle for the part of the turn the user most felt.
277
- if (run.ttftMs > 0) lanes.model.unshift({ left: pct(run.prepMs || 0), width: pct(run.ttftMs), label: 'Waiting for the first word' });
278
- if (run.prepMs > 0) lanes.input.unshift({ left: 0, width: pct(run.prepMs), label: 'Setup — tools and MCP servers' });
279
- return lanes;
280
- }
281
-
282
- /**
283
- * Derived per-request metrics.
284
- *
285
- * A turn is not one model call. In a tool loop the model is asked, answers with a call, is
286
- * given the result, and is asked again — DSH calls these Request #1, #2, #3, and that
287
- * numbering is the thing that makes a trajectory readable: "the second request is where it
288
- * went wrong" is a sentence you can act on, while "the turn went wrong" is not. Our own log
289
- * already showed this without naming it — 36 turns carried two `context.assembled` events,
290
- * one per round-trip.
291
- *
292
- * Everything here is DERIVED, never stored. Throughput computed at read time cannot
293
- * disagree with the tokens it came from; a stored copy can, and eventually does.
294
- */
295
- export function requestMetrics(req) {
296
- const out = Number(req?.tokensOut) || 0;
297
- const ttft = Number.isFinite(req?.ttftMs) ? req.ttftMs : null;
298
- const total = Number.isFinite(req?.ms) ? req.ms : null;
299
- // Generation is the part AFTER the first token: total minus the wait. Dividing tokens by
300
- // the total instead would blame a slow first token on the model's writing speed.
301
- const generationMs = total != null && ttft != null ? Math.max(0, total - ttft) : null;
302
- return {
303
- tokensIn: Number(req?.tokensIn) || 0,
304
- tokensOut: out,
305
- tokensReasoning: Number(req?.tokensReasoning) || 0,
306
- tokensTotal: (Number(req?.tokensIn) || 0) + out,
307
- ttftMs: ttft,
308
- generationMs,
309
- totalMs: total,
310
- // Only meaningful with both numbers and real generation time; a throughput computed
311
- // from a 0ms window is a very large lie.
312
- throughput: generationMs > 0 && out > 0 ? +(out / (generationMs / 1000)).toFixed(1) : null,
313
- model: req?.model || null,
314
- status: req?.status || 'completed',
315
- };
316
- }
317
-
318
- /**
319
- * Group entries into REQUESTS — one per model round-trip — so the view can show the
320
- * hierarchy DSH shows: a request, the calls it made, and the result that came back.
321
- *
322
- * A new request begins at each prompt (the model being asked again). Tool calls and their
323
- * results belong to the request that asked for them, which is what makes "hierarchy" a real
324
- * relationship rather than a label.
325
- */
326
- export function groupRequests(entries) {
327
- const requests = [];
328
- let current = null;
329
- const open = () => {
330
- current = { index: requests.length + 1, entries: [], calls: [], answer: null, at: null };
331
- requests.push(current);
332
- return current;
333
- };
334
- for (const e of entries) {
335
- if (e.kind === 'system' || (!current && e.kind !== 'context')) open();
336
- if (!current) open();
337
- if (current.at == null) current.at = e.at;
338
- current.entries.push(e);
339
- e.requestIndex = current.index;
340
- if (e.kind === 'tool') current.calls.push(e);
341
- if (e.kind === 'assistant') current.answer = e;
342
- }
343
- return requests;
344
- }
345
-
346
- /** Filter entries by a search string, matching title and detail. */
347
- export function filterEntries(entries, query) {
348
- const q = String(query || '').trim().toLowerCase();
349
- if (!q) return entries;
350
- return entries.filter((e) => `${e.title} ${e.detail || ''}`.toLowerCase().includes(q));
351
- }
352
-
353
- /**
354
- * Group runs into THREADS, because a run on its own is not the unit anyone reasons about.
355
- *
356
- * The activity log listed 1,205 independent rows. But nobody asks "what did run 847 do" —
357
- * they ask what happened in a conversation, a meeting, a note. And those are not one run
358
- * each: a meeting holds its live monitors and its summaries, a note holds every pass over
359
- * it including a swarm of agents, a chat holds every message. Flattening that loses the only
360
- * structure the data has.
361
- *
362
- * Three levels, matching what the runs already record:
363
- * thread (surface + sourceId) → turn (one run) → entries (user, context, tools, assistant)
364
- *
365
- * A run with no sourceId is its OWN thread rather than being pooled with other orphans:
366
- * without an id there is no evidence two runs are related, and inventing a shared parent
367
- * would group unrelated work under one heading — the opposite of the problem being fixed.
368
- */
369
- export function threadsOf(runs = []) {
370
- const byKey = new Map();
371
- for (const run of runs) {
372
- if (!run) continue;
373
- // `kind` is the fallback that makes this work on runs recorded before surface existed —
374
- // and on an export of 1,215 turns, 1,203 had no surface while every one had a kind. A
375
- // grouping that only works on data recorded after the fix groups nothing anyone has.
376
- const surface = run.surface || run.turn?.surface || run.turn?.kind || run.kind || 'other';
377
- const sourceId = run.sourceId || run.turn?.sourceId || null;
378
- // THE PARENT ID IS THE KEY, ON ITS OWN. Keying on surface+id split a conversation from
379
- // the autocomplete done inside it — same thread, different surface — which is the exact
380
- // grouping this exists to produce. Ids are generated and unique, so the surface adds no
381
- // identity, only a way to break one thread into several.
382
- const key = sourceId ? `src:${sourceId}` : `run:${run.turnId || run.id}`;
383
- if (!byKey.has(key)) {
384
- byKey.set(key, { key, surface, sourceId, runs: [], startedAt: Infinity, endedAt: -Infinity, ms: 0, tokensIn: 0, tokensOut: 0, calls: 0, errors: 0 });
385
- }
386
- const t = byKey.get(key);
387
- t.runs.push(run);
388
- const at = run.at ?? run.turn?.startedAt ?? 0;
389
- t.startedAt = Math.min(t.startedAt, at);
390
- t.endedAt = Math.max(t.endedAt, run.turn?.endedAt ?? at);
391
- t.ms += run.turn?.ms || 0;
392
- t.tokensIn += run.turn?.tokensIn || 0;
393
- t.tokensOut += run.turn?.tokensOut || 0;
394
- t.calls += run.calls?.length || 0;
395
- if (run.turn?.reason && run.turn.reason !== 'ok') t.errors += 1;
396
- }
397
- const threads = [...byKey.values()].map((t) => ({
398
- ...t,
399
- // A thread is named after the work it exists FOR, not after the background jobs attached
400
- // to it: a conversation with six autocomplete turns is still a conversation.
401
- surface: (t.runs.find((r) => !r.background)?.surface) || t.surface,
402
- startedAt: Number.isFinite(t.startedAt) ? t.startedAt : 0,
403
- endedAt: Number.isFinite(t.endedAt) ? t.endedAt : 0,
404
- // Chronological WITHIN the thread — a conversation read bottom-up is not a conversation.
405
- runs: [...t.runs].sort((a, b) => (a.at ?? 0) - (b.at ?? 0)),
406
- turns: t.runs.length,
407
- }));
408
- // Most recently active first, which is the one question a log answers on open.
409
- threads.sort((a, b) => b.endedAt - a.endedAt);
410
- return threads;
411
- }
412
-
413
- /** A readable heading for a thread, from whatever it recorded. */
414
- export function threadTitle(thread, titleFor = () => null) {
415
- const named = thread.sourceId ? titleFor(thread.surface, thread.sourceId) : null;
416
- if (named) return named;
417
- const first = thread.runs?.[0];
418
- // Falling back to the surface plus a short id beats "Untitled": it still distinguishes two
419
- // threads from each other, which is the minimum a heading has to do.
420
- const short = thread.sourceId ? String(thread.sourceId).slice(-6) : String(first?.turnId || '').slice(-6);
421
- const label = { chat: 'Chat', note: 'Note', meeting: 'Meeting' }[thread.surface] || (thread.surface || 'Run');
422
- return short ? `${label} · ${short}` : label;
423
- }
424
-
425
- /**
426
- * Split a recorded prompt into the rows a reader needs to tell things apart.
427
- *
428
- * The prompt was one row called "Prompt". But "why did it answer that" is nearly always a
429
- * question about WHICH input said something — the person, the page attached to their
430
- * message, or the instructions we added — and one row cannot answer it. So the same four
431
- * things that are recorded separately are shown separately:
432
- *
433
- * SYSTEM ours, with the tool preamble as its own row: it is the largest single thing we
434
- * inject and it was hiding inside a total nobody could attribute
435
- * USER what the person typed, and nothing else
436
- * CONTEXT what was attached, one row each, named rather than pasted — including whether
437
- * the model was handed it or had to ask
438
- * ASSISTANT / TOOL rows come from the events, not from here
439
- */
440
- export function promptEntries(prompt, at = 0) {
441
- const out = [];
442
- if (!prompt || typeof prompt !== 'object') return out;
443
- if (prompt.system) {
444
- out.push({ kind: 'system', at, title: 'System prompt', detail: `${approxChars(prompt.system)} chars`, text: prompt.system });
445
- }
446
- if (prompt.toolSystem) {
447
- out.push({ kind: 'system', at, title: 'Tool instructions', detail: `${approxChars(prompt.toolSystem)} chars`, text: prompt.toolSystem });
448
- }
449
- for (const m of prompt.messages || []) {
450
- if (!m || !String(m.content || '').trim()) continue;
451
- out.push({
452
- kind: m.role === 'assistant' ? 'assistant' : 'user',
453
- at, title: m.role === 'assistant' ? 'Assistant' : 'User',
454
- detail: '', text: String(m.content),
455
- });
456
- }
457
- for (const c of prompt.context || []) {
458
- out.push({
459
- kind: 'context', at,
460
- title: c.title || c.kind || 'Attached',
461
- // "read on demand" vs "included" are two very different turns that otherwise look
462
- // identical in a log, so the distinction is on the row rather than buried in the data.
463
- detail: [c.url, c.chars ? `${c.chars} chars` : '', c.deferred ? 'read on demand' : 'included'].filter(Boolean).join(' · '),
464
- data: c,
465
- });
466
- }
467
- return out;
468
- }
469
-
470
- const approxChars = (s) => String(s || '').length;
471
-
472
- /**
473
- * Events → TURNS, each holding everything that happened inside it.
474
- *
475
- * The middle level the log was missing. A run was a row with a duration and a token count;
476
- * what it actually DID — what the person asked, what context came with it, which tools ran,
477
- * what came back — was spread across events that only shared an id. So "open the turn" had
478
- * nothing to open.
479
- *
480
- * A turn is the unit with a beginning and an end. Its parent is whatever it was done FOR —
481
- * a conversation, a note, a meeting — carried as `sourceId`, which is the identity the
482
- * grouping is built on. `kind` is only a label: every chat shares the kind 'chat' and they
483
- * are emphatically not one thread.
484
- */
485
- export function turnsOf(events = []) {
486
- const byTurn = new Map();
487
- for (const e of events) {
488
- const id = e?.turnId || e?.payload?.turnId;
489
- if (!id) continue;
490
- if (!byTurn.has(id)) byTurn.set(id, []);
491
- byTurn.get(id).push(e);
492
- }
493
- const turns = [];
494
- for (const [turnId, evs] of byTurn) {
495
- const start = evs.find((e) => e.type === 'turn.started');
496
- const end = evs.find((e) => e.type === 'turn.ended');
497
- const sp = start?.payload || {};
498
- const ep = end?.payload || {};
499
- const at = start?.at ?? evs[0]?.at ?? 0;
500
- turns.push({
501
- turnId,
502
- at,
503
- // The parent. Without it a turn cannot be filed anywhere, which is a real defect and
504
- // not a cosmetic one — 264 of 1,215 turns in a real export were suggestions done for a
505
- // conversation and recorded under nothing.
506
- sourceId: sp.sourceId || null,
507
- surface: sp.surface || sp.kind || null,
508
- kind: sp.kind || 'chat',
509
- agentId: sp.agentId || null,
510
- background: !!sp.background,
511
- // Everything that happened inside, already typed: user, context, route, tool, result,
512
- // reasoning, assistant.
513
- entries: buildTrajectory(evs),
514
- turn: {
515
- startedAt: at, endedAt: end?.at ?? null, ms: ep.ms ?? null, reason: ep.reason || (end ? 'ok' : 'open'),
516
- kind: sp.kind || 'chat', surface: sp.surface || null, sourceId: sp.sourceId || null,
517
- model: ep.model || null, provider: ep.provider || null,
518
- tokensIn: ep.tokensIn ?? null, tokensOut: ep.tokensOut ?? null,
519
- },
520
- calls: evs.filter((e) => e.type === 'capability.invoked'),
521
- });
522
- }
523
- turns.sort((a, b) => a.at - b.at);
524
- return turns;
525
- }
526
-
527
- /** The whole shape in one call: threads → turns → entries. */
528
- export function threadTree(events = []) {
529
- return threadsOf(turnsOf(events));
530
- }
1
+ import{linearize as m}from"./order.js";function k(n){const o=n?.args?.action;return typeof o!="string"||!o?n?.name||"tool":o==="describe"&&n.args.tool?`${n.name}.describe(${n.args.tool})`:`${n.name}.${o}`}const x=Object.freeze(["system","user","context","route","privacy","tool","result","reasoning","assistant"]),f=(n,o=120)=>{const s=String(n??"").replace(/\s+/g," ").trim();return s.length>o?`${s.slice(0,o-1)}…`:s};function h(n){const o=m(n||[]),s=[],e=new Map;let a=null;for(const r of o){const t=r.payload||{};switch(r.type){case"turn.started":a=r.at;break;case"context.assembled":s.push({kind:"context",at:r.at,title:"Context assembled",detail:`${(t.tools||[]).length} tool${(t.tools||[]).length===1?"":"s"} · ${t.used||0} tokens`,data:{tools:t.tools||[],tokens:t.used||0,redaction:!!t.redaction,surface:t.surface}});break;case"context.retrieved":s.push({kind:"context",at:r.at,title:t.count?`Retrieved ${t.count} source${t.count===1?"":"s"}`:"Retrieved material",detail:[t.tool,t.chars?`${t.chars} chars`:""].filter(Boolean).join(" · "),data:{tool:t.tool,count:t.count,sources:t.sources||[]}});break;case"privacy.redacted":{const i=t.counts||{},l=Object.values(i).reduce((u,c)=>u+(Number(c)||0),0);if(!l)break;const d=Object.entries(i).sort((u,c)=>c[1]-u[1]).map(([u,c])=>`${c} ${u.toLowerCase().replace(/_/g," ")}`).join(" · ");s.push({kind:"privacy",at:r.at,title:`Redacted ${l} value${l===1?"":"s"} before sending`,detail:d,data:{counts:i}});break}case"assistant.prompted":s.push({kind:"system",at:r.at,title:"Prompt",detail:`${t.chars||0} chars`,ref:t.ref,expandsToMessages:!0});break;case"policy.changed":{if(!String(t.dial||"").startsWith("route."))break;const i=t.dial==="route.applied";s.push({kind:"route",at:r.at,title:i?`Routed to ${t.to}`:`Would route to ${t.to}`,detail:(t.reasons||[])[0]||"",data:{from:t.from,to:t.to,applied:i,agrees:t.agrees,strategy:t.strategy,reasons:t.reasons,eligible:t.eligible,rejected:t.rejected,graph:t.graph||null}});break}case"automation.fired":{if(t.ruleId!=="router:failover"){s.push({kind:"route",at:r.at,title:`Rule fired: ${t.ruleId}`,detail:`class ${t.classUsed}`,data:t});break}s.push({kind:"route",at:r.at,title:`Failed over to ${t.to}`,detail:`${t.from} declined (${t.reason})`,data:t});break}case"capability.invoked":{const i={kind:"tool",at:r.at,title:k({name:t.capability,args:t.args}),detail:f(JSON.stringify(t.args||{}),90),key:t.idempotencyKey||r.id,ok:null,ms:null,data:{capability:t.capability,args:t.args||{},actor:t.actor,scope:t.scope,effects:t.effects,started:new Date(r.at).toISOString()}};e.set(i.key,i),s.push(i);break}case"capability.resulted":{const i=e.get(t.idempotencyKey);i&&(i.ok=t.ok,i.ms=t.cost?.ms??null),s.push({kind:"result",at:r.at,title:`${t.capability} → ${t.ok?"ok":"failed"}`,detail:f(t.summary,120),ok:!!t.ok,ms:t.cost?.ms??null,data:{status:t.ok?"completed":"failed",durationMs:t.cost?.ms??null,classUsed:t.classUsed,finished:new Date(r.at).toISOString(),result:t.summary}});break}case"assistant.reasoning":s.push({kind:"reasoning",at:r.at,title:"Reasoning",detail:`${t.chars||0} chars`,ref:t.ref});break;case"assistant.message":s.push({kind:"assistant",at:r.at,title:"Answer",detail:[`${t.chars||0} chars`,t.citations?.length?`${t.citations.length} citation${t.citations.length===1?"":"s"}`:""].filter(Boolean).join(" · "),ref:t.ref,data:t.citations?.length?{citations:t.citations}:null});break;default:break}}for(const r of s)r.offsetMs=a==null?null:r.at-a;return s}function $(n){const o=n?.ms;if(!Number.isFinite(o)||o<=0)return null;const s=Math.max(0,Math.min(n.prepMs||0,o)),e=Math.max(0,Math.min(n.ttftMs||0,o-s)),a=Math.max(0,o-s-e),r=t=>t/o*100;return{total:o,parts:[{key:"setup",ms:s,pct:r(s),label:"Setup — tools and MCP servers"},{key:"wait",ms:e,pct:r(e),label:"Waiting for the first word"},{key:"work",ms:a,pct:r(a),label:"Tools and writing"}].filter(t=>t.ms>0)}}function M(n,o){const s=o?.ms;if(!Number.isFinite(s)||s<=0)return null;const e=r=>Math.max(0,Math.min(100,r/s*100)),a={input:[],model:[],tools:[]};for(const r of n){if(r.offsetMs==null)continue;const t=e(r.offsetMs);r.kind==="system"||r.kind==="user"||r.kind==="context"?a.input.push({left:t,width:Math.max(.6,e(200)),label:r.title}):r.kind==="tool"?a.tools.push({left:t,width:Math.max(1,e(r.ms||400)),label:r.title}):(r.kind==="assistant"||r.kind==="reasoning")&&a.model.push({left:t,width:Math.max(1,e(400)),label:r.title})}return o.ttftMs>0&&a.model.unshift({left:e(o.prepMs||0),width:e(o.ttftMs),label:"Waiting for the first word"}),o.prepMs>0&&a.input.unshift({left:0,width:e(o.prepMs),label:"Setup — tools and MCP servers"}),a}function I(n){const o=Number(n?.tokensOut)||0,s=Number.isFinite(n?.ttftMs)?n.ttftMs:null,e=Number.isFinite(n?.ms)?n.ms:null,a=e!=null&&s!=null?Math.max(0,e-s):null;return{tokensIn:Number(n?.tokensIn)||0,tokensOut:o,tokensReasoning:Number(n?.tokensReasoning)||0,tokensTotal:(Number(n?.tokensIn)||0)+o,ttftMs:s,generationMs:a,totalMs:e,throughput:a>0&&o>0?+(o/(a/1e3)).toFixed(1):null,model:n?.model||null,status:n?.status||"completed"}}function w(n){const o=[];let s=null;const e=()=>(s={index:o.length+1,entries:[],calls:[],answer:null,at:null},o.push(s),s);for(const a of n)(a.kind==="system"||!s&&a.kind!=="context")&&e(),s||e(),s.at==null&&(s.at=a.at),s.entries.push(a),a.requestIndex=s.index,a.kind==="tool"&&s.calls.push(a),a.kind==="assistant"&&(s.answer=a);return o}function A(n,o){const s=String(o||"").trim().toLowerCase();return s?n.filter(e=>`${e.title} ${e.detail||""}`.toLowerCase().includes(s)):n}function g(n=[]){const o=new Map;for(const e of n){if(!e)continue;const a=e.surface||e.turn?.surface||e.turn?.kind||e.kind||"other",r=e.sourceId||e.turn?.sourceId||null,t=r?`src:${r}`:`run:${e.turnId||e.id}`;o.has(t)||o.set(t,{key:t,surface:a,sourceId:r,runs:[],startedAt:1/0,endedAt:-1/0,ms:0,tokensIn:0,tokensOut:0,calls:0,errors:0});const i=o.get(t);i.runs.push(e);const l=e.at??e.turn?.startedAt??0;i.startedAt=Math.min(i.startedAt,l),i.endedAt=Math.max(i.endedAt,e.turn?.endedAt??l),i.ms+=e.turn?.ms||0,i.tokensIn+=e.turn?.tokensIn||0,i.tokensOut+=e.turn?.tokensOut||0,i.calls+=e.calls?.length||0,e.turn?.reason&&e.turn.reason!=="ok"&&(i.errors+=1)}const s=[...o.values()].map(e=>({...e,surface:e.runs.find(a=>!a.background)?.surface||e.surface,startedAt:Number.isFinite(e.startedAt)?e.startedAt:0,endedAt:Number.isFinite(e.endedAt)?e.endedAt:0,runs:[...e.runs].sort((a,r)=>(a.at??0)-(r.at??0)),turns:e.runs.length}));return s.sort((e,a)=>a.endedAt-e.endedAt),s}function S(n,o=()=>null){const s=n.sourceId?o(n.surface,n.sourceId):null;if(s)return s;const e=n.runs?.[0],a=n.sourceId?String(n.sourceId).slice(-6):String(e?.turnId||"").slice(-6),r={chat:"Chat",note:"Note",meeting:"Meeting"}[n.surface]||n.surface||"Run";return a?`${r} · ${a}`:r}function v(n,o=0){const s=[];if(!n||typeof n!="object")return s;n.system&&s.push({kind:"system",at:o,title:"System prompt",detail:`${p(n.system)} chars`,text:n.system}),n.toolSystem&&s.push({kind:"system",at:o,title:"Tool instructions",detail:`${p(n.toolSystem)} chars`,text:n.toolSystem});for(const e of n.messages||[])!e||!String(e.content||"").trim()||s.push({kind:e.role==="assistant"?"assistant":"user",at:o,title:e.role==="assistant"?"Assistant":"User",detail:"",text:String(e.content)});for(const e of n.context||[])s.push({kind:"context",at:o,title:e.title||e.kind||"Attached",detail:[e.url,e.chars?`${e.chars} chars`:"",e.deferred?"read on demand":"included"].filter(Boolean).join(" · "),data:e});return s}const p=n=>String(n||"").length;function b(n=[]){const o=new Map;for(const e of n){const a=e?.turnId||e?.payload?.turnId;a&&(o.has(a)||o.set(a,[]),o.get(a).push(e))}const s=[];for(const[e,a]of o){const r=a.find(u=>u.type==="turn.started"),t=a.find(u=>u.type==="turn.ended"),i=r?.payload||{},l=t?.payload||{},d=r?.at??a[0]?.at??0;s.push({turnId:e,at:d,sourceId:i.sourceId||null,surface:i.surface||i.kind||null,kind:i.kind||"chat",agentId:i.agentId||null,background:!!i.background,entries:h(a),turn:{startedAt:d,endedAt:t?.at??null,ms:l.ms??null,reason:l.reason||(t?"ok":"open"),kind:i.kind||"chat",surface:i.surface||null,sourceId:i.sourceId||null,model:l.model||null,provider:l.provider||null,tokensIn:l.tokensIn??null,tokensOut:l.tokensOut??null},calls:a.filter(u=>u.type==="capability.invoked")})}return s.sort((e,a)=>e.at-a.at),s}function O(n=[]){return g(b(n))}export{x as ENTRY_KINDS,h as buildTrajectory,k as displayName,A as filterEntries,w as groupRequests,M as lanesOf,$ as phasesOf,v as promptEntries,I as requestMetrics,S as threadTitle,O as threadTree,g as threadsOf,b as turnsOf};