@tangle-network/sandbox-ui 2.0.20 → 2.0.22
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/README.md +18 -1
- package/dist/{record-Cwyo2oHb.js → AgentTree-KO1TJXBq.js} +203 -313
- package/dist/VirtualList-DLh2JE0F.js +205 -0
- package/dist/{WorkflowGraph-CADReIDl.js → WorkflowGraph-B8cRtVaw.js} +2 -2
- package/dist/event-stream-DNEbvx4s.d.ts +2 -0
- package/dist/globals.css +5910 -240
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +2 -2
- package/dist/{index-ByNsP4VC.d.ts → index-BoKSR0s9.d.ts} +62 -4
- package/dist/{index-BYkigpIM.d.ts → index-gXcuwPOX.d.ts} +13 -7
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -6
- package/dist/record-BGcR48YY.js +47 -0
- package/dist/record.d.ts +66 -0
- package/dist/record.js +2329 -0
- package/dist/report-viewer.js +554 -0
- package/dist/run/live.d.ts +1 -1
- package/dist/run/live.js +26 -28
- package/dist/run/record.d.ts +2 -2
- package/dist/run/record.js +4 -1
- package/dist/run.d.ts +4 -4
- package/dist/run.js +5 -3
- package/dist/runtime-viewer/{AgentGraphCanvas-B74GN1o6-DC3EH_ss.js → AgentGraphCanvas-DDvaU9Rg-DlNZxJ20.js} +187 -168
- package/dist/runtime-viewer/{latest-events-iiWwpIck-BTK0vSlR.js → latest-events-CabhXynX-Dp_vs0Px.js} +705 -691
- package/dist/runtime-viewer/sandbox-ui-CUj4Q1P_.css +2 -0
- package/dist/styles.css +5911 -241
- package/dist/tailwind.css +5677 -5
- package/dist/tokens.css +7 -1
- package/dist/{trace-workspace-C8dHRBDV.js → trace-workspace-DkakKRLV.js} +64 -78
- package/dist/{tree-data-C8CNt4WE.js → tree-data-B_IXAIgg.js} +171 -121
- package/dist/use-follow-tail-Ckx5ZSV2.js +1 -0
- package/dist/vault-tree.js +1 -1
- package/dist/workflows.js +1 -1
- package/dist/{workspace-CzikYt_y.js → workspace--GvnZNpH.js} +1 -1
- package/dist/{workspace-files-pane-DB9Nvwh4.js → workspace-files-pane-C0PeY6rY.js} +12 -12
- package/dist/workspace-viewer.js +9693 -9462
- package/dist/workspace.js +2 -2
- package/package.json +27 -13
- package/dist/agent-stream-CBZ2fXJ2.d.ts +0 -66
- package/dist/runtime-viewer/sandbox-ui-z5RGk327.css +0 -2
package/README.md
CHANGED
|
@@ -530,4 +530,21 @@ Retheming is absolutely supported, but the documentation was thinner than it sho
|
|
|
530
530
|
|
|
531
531
|
Apache-2.0
|
|
532
532
|
|
|
533
|
-
The native Runtime viewer is `LiveRun` from `@tangle-network/sandbox-ui/run/live`. It composes the same trace workspace, selected-agent conversation, retained files and capture coverage. Hosts may supply the existing fetch/EventSource transport for their authenticated session; an API or authenticated transport change clears the previous view, while frames within one stable transport preserve selection and the camera. Keep the transport object stable for that account. `@tangle-network/sandbox-ui/workspace-viewer.js` is the self-contained browser entry for Runtime's existing asset-directory contract, with `styles.css` beside it.
|
|
533
|
+
The native Runtime viewer is `LiveRun` from `@tangle-network/sandbox-ui/run/live`. It composes the same trace workspace, selected-agent conversation, retained files and capture coverage. Hosts may supply the existing fetch/EventSource transport for their authenticated session; an API or authenticated transport change clears the previous view, while frames within one stable transport preserve selection and the camera. Keep the transport object stable for that account. `@tangle-network/sandbox-ui/workspace-viewer.js` is the self-contained browser entry for Runtime's existing asset-directory contract, with `styles.css` beside it. `useDocument` owns same-origin ETag reads. `useEventStream` from `/run` owns native reconnect, terminal end and stale callback cleanup; `useAgentStream` resumes retained rows by generation and sequence. Authenticated openers keep their session and reconnect policy. Producers own validation and accounting.
|
|
534
|
+
|
|
535
|
+
## Retained records and reports
|
|
536
|
+
|
|
537
|
+
`@tangle-network/sandbox-ui/record` owns `AgentRecord`, `ResearchReport` and `RecordWorkGraph`. Install the optional `@drewstone/agent-record` peer for this feature; its public `/record` and `/report` entries own validation, models, provenance relationships and report exports. The generic `/run/record` transcript remains usable without that parser package.
|
|
538
|
+
|
|
539
|
+
```tsx
|
|
540
|
+
import { parseRecord } from '@drewstone/agent-record/record'
|
|
541
|
+
import { AgentRecord } from '@tangle-network/sandbox-ui/record'
|
|
542
|
+
import '@tangle-network/sandbox-ui/styles'
|
|
543
|
+
|
|
544
|
+
const record = parseRecord(input)
|
|
545
|
+
<AgentRecord records={[record]} />
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Selection keeps original run, actor and event IDs, a recorded timestamp, and Messages, Event details or Usage. Charts owns the waterfall clock, playback and zoom. Usage distinguishes missing counters from zero and metered windows from individual responses. Capture gaps and original source references stay visible. The evidence graph keeps recorded entity kinds and relationships and uses the maintained graph camera.
|
|
549
|
+
|
|
550
|
+
Research reports retain source documents, exact citations, Markdown/math, print and JSON/LaTeX exports. Optional source search accepts only a same-origin path and displays only the selected play's retained bytes. `agent-record-report` continues to write a single offline HTML file using this package's report viewer and stylesheet.
|
|
@@ -1,217 +1,14 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
|
|
2
|
+
import { t as VirtualList } from "./VirtualList-DLh2JE0F.js";
|
|
3
|
+
import { t as useFollowTail } from "./use-follow-tail-Ckx5ZSV2.js";
|
|
2
4
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@tangle-network/ui/primitives";
|
|
3
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
6
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
5
7
|
import { Brain, Settings, Timer } from "lucide-react";
|
|
6
|
-
import {
|
|
8
|
+
import { getToolCategory, getToolDisplayMetadata } from "@tangle-network/ui/utils";
|
|
7
9
|
import { AgentTimelineColumn, AgentTimelineStep, SpeakerHeader, agentTimelineStepKind } from "@tangle-network/ui/chat";
|
|
8
|
-
import { ExpandedToolDetail, InlineThinkingItem, RunRowShell, RunStatusIcon, runStatus } from "@tangle-network/ui/run";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Renders only the rows near the viewport of `scroller`. Row heights are measured as they render; unmeasured
|
|
12
|
-
* rows use `estimate`. Spacers keep the scroll height, so the browser's scroll anchoring holds the reading place.
|
|
13
|
-
*/
|
|
14
|
-
function VirtualList({ items, keyOf, render, scroller, estimate = 96, overscan = 900, target }) {
|
|
15
|
-
const heights = useRef(/* @__PURE__ */ new Map());
|
|
16
|
-
const [version, setVersion] = useState(0);
|
|
17
|
-
const [viewport, setViewport] = useState({
|
|
18
|
-
top: 0,
|
|
19
|
-
height: 900
|
|
20
|
-
});
|
|
21
|
-
const observer = useRef(null);
|
|
22
|
-
const nodes = useRef(/* @__PURE__ */ new Map());
|
|
23
|
-
const frame = useRef(0);
|
|
24
|
-
const observe = (element) => {
|
|
25
|
-
if (!observer.current && typeof ResizeObserver !== "undefined") observer.current = new ResizeObserver((entries) => {
|
|
26
|
-
let changed = false;
|
|
27
|
-
for (const entry of entries) {
|
|
28
|
-
const key = nodes.current.get(entry.target);
|
|
29
|
-
if (!key) continue;
|
|
30
|
-
const height = Math.ceil(entry.borderBoxSize?.[0]?.blockSize ?? entry.target.offsetHeight);
|
|
31
|
-
if (height > 0 && heights.current.get(key) !== height) {
|
|
32
|
-
heights.current.set(key, height);
|
|
33
|
-
changed = true;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (changed && !frame.current) frame.current = requestAnimationFrame(() => {
|
|
37
|
-
frame.current = 0;
|
|
38
|
-
setVersion((value) => value + 1);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
observer.current?.observe(element);
|
|
42
|
-
};
|
|
43
|
-
useEffect(() => () => {
|
|
44
|
-
cancelAnimationFrame(frame.current);
|
|
45
|
-
observer.current?.disconnect();
|
|
46
|
-
observer.current = null;
|
|
47
|
-
nodes.current.clear();
|
|
48
|
-
}, []);
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const element = scroller.current;
|
|
51
|
-
if (!element) return;
|
|
52
|
-
let frame = 0;
|
|
53
|
-
const update = () => {
|
|
54
|
-
frame = 0;
|
|
55
|
-
setViewport((current) => current.top === element.scrollTop && current.height === element.clientHeight ? current : {
|
|
56
|
-
top: element.scrollTop,
|
|
57
|
-
height: element.clientHeight
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
const schedule = () => {
|
|
61
|
-
if (!frame) frame = requestAnimationFrame(update);
|
|
62
|
-
};
|
|
63
|
-
update();
|
|
64
|
-
element.addEventListener("scroll", schedule, { passive: true });
|
|
65
|
-
const resize = new ResizeObserver(schedule);
|
|
66
|
-
resize.observe(element);
|
|
67
|
-
return () => {
|
|
68
|
-
cancelAnimationFrame(frame);
|
|
69
|
-
element.removeEventListener("scroll", schedule);
|
|
70
|
-
resize.disconnect();
|
|
71
|
-
};
|
|
72
|
-
}, [scroller]);
|
|
73
|
-
const offsets = useMemo(() => {
|
|
74
|
-
const result = new Float64Array(items.length + 1);
|
|
75
|
-
const guess = typeof estimate === "function" ? estimate : () => estimate;
|
|
76
|
-
for (let i = 0; i < items.length; i++) result[i + 1] = result[i] + (heights.current.get(keyOf(items[i])) ?? guess(items[i]));
|
|
77
|
-
return result;
|
|
78
|
-
}, [
|
|
79
|
-
items,
|
|
80
|
-
keyOf,
|
|
81
|
-
estimate,
|
|
82
|
-
version
|
|
83
|
-
]);
|
|
84
|
-
const find = (y) => {
|
|
85
|
-
let low = 0;
|
|
86
|
-
let high = items.length;
|
|
87
|
-
while (low < high) {
|
|
88
|
-
const mid = low + high >> 1;
|
|
89
|
-
if (offsets[mid + 1] <= y) low = mid + 1;
|
|
90
|
-
else high = mid;
|
|
91
|
-
}
|
|
92
|
-
return low;
|
|
93
|
-
};
|
|
94
|
-
const start = Math.max(0, find(viewport.top - overscan));
|
|
95
|
-
const end = Math.min(items.length, find(viewport.top + viewport.height + overscan) + 1);
|
|
96
|
-
const scrollTo = useCallback((key, align) => {
|
|
97
|
-
const element = scroller.current;
|
|
98
|
-
const index = items.findIndex((item) => keyOf(item) === key);
|
|
99
|
-
if (!element || index < 0) return false;
|
|
100
|
-
const top = offsets[index];
|
|
101
|
-
const bottom = offsets[index + 1];
|
|
102
|
-
if (align || top < element.scrollTop || bottom > element.scrollTop + element.clientHeight) element.scrollTop = Math.max(0, top - 8);
|
|
103
|
-
return true;
|
|
104
|
-
}, [
|
|
105
|
-
items,
|
|
106
|
-
keyOf,
|
|
107
|
-
offsets,
|
|
108
|
-
scroller
|
|
109
|
-
]);
|
|
110
|
-
const pending = useRef(null);
|
|
111
|
-
const rows = useRef(/* @__PURE__ */ new Map());
|
|
112
|
-
const settle = () => {
|
|
113
|
-
const goal = pending.current;
|
|
114
|
-
if (!goal) return;
|
|
115
|
-
if (performance.now() > goal.until) {
|
|
116
|
-
pending.current = null;
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (!scrollTo(goal.key, true)) return;
|
|
120
|
-
const row = rows.current.get(goal.key);
|
|
121
|
-
if (row?.isConnected) {
|
|
122
|
-
const box = row.getBoundingClientRect();
|
|
123
|
-
if (box.top < 72 || box.top > window.innerHeight - 96) window.scrollBy({ top: box.top - 120 });
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
useLayoutEffect(() => {
|
|
127
|
-
pending.current = target ? {
|
|
128
|
-
key: target,
|
|
129
|
-
until: performance.now() + 2e3
|
|
130
|
-
} : null;
|
|
131
|
-
settle();
|
|
132
|
-
}, [target]);
|
|
133
|
-
useLayoutEffect(settle, [
|
|
134
|
-
version,
|
|
135
|
-
viewport,
|
|
136
|
-
items
|
|
137
|
-
]);
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
const element = scroller.current;
|
|
140
|
-
if (!element) return;
|
|
141
|
-
const release = (event) => {
|
|
142
|
-
const key = event instanceof KeyboardEvent && event.target === element && !event.defaultPrevented ? event.key : "";
|
|
143
|
-
pending.current = items.length && (key === "Home" || key === "End") ? {
|
|
144
|
-
key: keyOf(items[key === "Home" ? 0 : items.length - 1]),
|
|
145
|
-
until: performance.now() + 2e3
|
|
146
|
-
} : null;
|
|
147
|
-
if (pending.current) {
|
|
148
|
-
event.preventDefault();
|
|
149
|
-
settle();
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
element.addEventListener("wheel", release, { passive: true });
|
|
153
|
-
element.addEventListener("touchstart", release, { passive: true });
|
|
154
|
-
element.addEventListener("pointerdown", release);
|
|
155
|
-
element.addEventListener("keydown", release);
|
|
156
|
-
return () => {
|
|
157
|
-
element.removeEventListener("wheel", release);
|
|
158
|
-
element.removeEventListener("touchstart", release);
|
|
159
|
-
element.removeEventListener("pointerdown", release);
|
|
160
|
-
element.removeEventListener("keydown", release);
|
|
161
|
-
};
|
|
162
|
-
}, [
|
|
163
|
-
scroller,
|
|
164
|
-
items,
|
|
165
|
-
keyOf,
|
|
166
|
-
offsets
|
|
167
|
-
]);
|
|
168
|
-
const bind = (key) => (element) => {
|
|
169
|
-
if (!element) {
|
|
170
|
-
rows.current.delete(key);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
rows.current.set(key, element);
|
|
174
|
-
for (const [node, value] of nodes.current) if (value === key && node !== element) {
|
|
175
|
-
observer.current?.unobserve(node);
|
|
176
|
-
nodes.current.delete(node);
|
|
177
|
-
}
|
|
178
|
-
if (!nodes.current.has(element)) {
|
|
179
|
-
nodes.current.set(element, key);
|
|
180
|
-
observe(element);
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
useEffect(() => {
|
|
184
|
-
for (const node of [...nodes.current.keys()]) if (!node.isConnected) {
|
|
185
|
-
observer.current?.unobserve(node);
|
|
186
|
-
nodes.current.delete(node);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
190
|
-
/* @__PURE__ */ jsx("div", {
|
|
191
|
-
style: {
|
|
192
|
-
height: offsets[start],
|
|
193
|
-
transitionProperty: "none"
|
|
194
|
-
},
|
|
195
|
-
"aria-hidden": "true"
|
|
196
|
-
}),
|
|
197
|
-
items.slice(start, end).map((item, i) => {
|
|
198
|
-
const key = keyOf(item);
|
|
199
|
-
return /* @__PURE__ */ jsx("div", {
|
|
200
|
-
ref: bind(key),
|
|
201
|
-
className: "virtual-row",
|
|
202
|
-
style: { display: "flow-root" },
|
|
203
|
-
children: render(item, start + i)
|
|
204
|
-
}, key);
|
|
205
|
-
}),
|
|
206
|
-
/* @__PURE__ */ jsx("div", {
|
|
207
|
-
style: {
|
|
208
|
-
height: offsets[items.length] - offsets[end],
|
|
209
|
-
transitionProperty: "none"
|
|
210
|
-
},
|
|
211
|
-
"aria-hidden": "true"
|
|
212
|
-
})
|
|
213
|
-
] });
|
|
214
|
-
}
|
|
10
|
+
import { ExpandedToolDetail, InlineThinkingItem, RunRowShell, RunStatusIcon, runStatus, statusMeta } from "@tangle-network/ui/run";
|
|
11
|
+
import { formatCompact, formatCompact as fmt, formatCost, formatSpanDuration, formatSpanDuration as interval, formatUtcTime as utcTime, reportedCount, sumReportedCounts } from "@tangle-network/charts";
|
|
215
12
|
// src/run/record/model.ts
|
|
216
13
|
var model_exports = /* @__PURE__ */ __exportAll({
|
|
217
14
|
agentState: () => agentState,
|
|
@@ -228,6 +25,7 @@ var model_exports = /* @__PURE__ */ __exportAll({
|
|
|
228
25
|
modelIdentity: () => modelIdentity,
|
|
229
26
|
ms: () => ms,
|
|
230
27
|
preview: () => preview,
|
|
28
|
+
recordFlowTrace: () => recordFlowTrace,
|
|
231
29
|
roleOf: () => roleOf,
|
|
232
30
|
textOf: () => textOf,
|
|
233
31
|
toolKey: () => toolKey,
|
|
@@ -235,13 +33,6 @@ var model_exports = /* @__PURE__ */ __exportAll({
|
|
|
235
33
|
utcTime: () => utcTime
|
|
236
34
|
});
|
|
237
35
|
const ms = (value) => Date.parse(value);
|
|
238
|
-
/** Normalize labels to UTC without rewriting the source timestamp. */
|
|
239
|
-
function utcTime(value, includeDate = false) {
|
|
240
|
-
const date = new Date(value);
|
|
241
|
-
if (!Number.isFinite(date.valueOf())) return "Unknown time";
|
|
242
|
-
const iso = date.toISOString();
|
|
243
|
-
return `${includeDate ? iso.slice(0, 19).replace("T", " ") : iso.slice(11, 19)} UTC`;
|
|
244
|
-
}
|
|
245
36
|
const compact = (value) => value.replace(/\s+/g, " ").trim();
|
|
246
37
|
/**
|
|
247
38
|
* Recorded text names the host folders an agent ran in (`/home/<user>/.cache/disco/host/<run>/sessions/<session>/inputs/x.md`).
|
|
@@ -276,11 +67,6 @@ function withoutHostPaths(event) {
|
|
|
276
67
|
}
|
|
277
68
|
};
|
|
278
69
|
}
|
|
279
|
-
const fmt = (value) => new Intl.NumberFormat("en-US", {
|
|
280
|
-
notation: "compact",
|
|
281
|
-
maximumFractionDigits: 1
|
|
282
|
-
}).format(value);
|
|
283
|
-
const interval = (value) => value < 1e3 ? `${Math.round(value)} ms` : value < 6e4 ? `${(value / 1e3).toFixed(1)} s` : `${(value / 6e4).toFixed(1)} min`;
|
|
284
70
|
const roleOf = (node) => node?.role ?? (node?.kind === "session" ? "Session" : node?.kind === "finding" ? "Finding" : "Agent");
|
|
285
71
|
const textOf = (event) => event.detail.publicText ?? event.detail.recordedClaim ?? "";
|
|
286
72
|
const toolKey = (node, id) => JSON.stringify([node, id]);
|
|
@@ -361,8 +147,9 @@ function indexRecord(record) {
|
|
|
361
147
|
results.set(key, matching);
|
|
362
148
|
}
|
|
363
149
|
}
|
|
364
|
-
const times = events.map((event) => ms(event.at));
|
|
365
|
-
const start = times
|
|
150
|
+
const times = [...events.map((event) => ms(event.at)), ...actors.flatMap((node) => node.start ? [ms(node.start)] : [])].filter(Number.isFinite);
|
|
151
|
+
const start = times.length ? times.reduce((first, at) => Math.min(first, at)) : 0;
|
|
152
|
+
const observedEnd = events.reduce((latest, event) => Math.max(latest, event.end ? ms(event.end) : ms(event.at)), start);
|
|
366
153
|
return {
|
|
367
154
|
nodes,
|
|
368
155
|
canonical,
|
|
@@ -374,7 +161,7 @@ function indexRecord(record) {
|
|
|
374
161
|
calls,
|
|
375
162
|
results,
|
|
376
163
|
start,
|
|
377
|
-
end:
|
|
164
|
+
end: actors.reduce((latest, node) => typeof node.end === "string" && Number.isFinite(ms(node.end)) ? Math.max(latest, ms(node.end)) : latest, observedEnd)
|
|
378
165
|
};
|
|
379
166
|
}
|
|
380
167
|
/** Whether any event of this agent recorded token usage or a list price: a status alone measures nothing. */
|
|
@@ -486,7 +273,7 @@ function tooltipSections(event, index, cutoff) {
|
|
|
486
273
|
const duration = event.detail.durationMs;
|
|
487
274
|
if (typeof duration === "number") sections.push({
|
|
488
275
|
kind: "status",
|
|
489
|
-
value: `⏱ ${
|
|
276
|
+
value: `⏱ ${interval(duration)}`,
|
|
490
277
|
tone: "muted"
|
|
491
278
|
});
|
|
492
279
|
return {
|
|
@@ -496,6 +283,181 @@ function tooltipSections(event, index, cutoff) {
|
|
|
496
283
|
sections
|
|
497
284
|
};
|
|
498
285
|
}
|
|
286
|
+
const time = (value) => typeof value === "string" ? ms(value) : NaN;
|
|
287
|
+
/** Project retained identities and measured intervals; a message without a duration remains a point. */
|
|
288
|
+
function recordFlowTrace(record, live = false, observedAt = Date.now()) {
|
|
289
|
+
const index = indexRecord(record);
|
|
290
|
+
const first = index.events.length || index.actors.some((node) => Number.isFinite(time(node.start))) ? index.start : observedAt;
|
|
291
|
+
const last = Math.max(first, index.end, live ? observedAt : first);
|
|
292
|
+
const spans = [];
|
|
293
|
+
const actors = new Set(index.actors.map((node) => node.id));
|
|
294
|
+
const runningNodes = new Set(index.actors.filter((node) => live && !node.end && [
|
|
295
|
+
"running",
|
|
296
|
+
"waiting",
|
|
297
|
+
"needs-you"
|
|
298
|
+
].includes(runStatus(node.status ?? "unknown"))).map((node) => node.id));
|
|
299
|
+
const offset = (at) => Math.max(0, at - first);
|
|
300
|
+
for (const node of index.actors) {
|
|
301
|
+
if (node.kind === "finding") continue;
|
|
302
|
+
const events = index.byActor.get(node.id) ?? [];
|
|
303
|
+
const start = Number.isFinite(time(node.start)) ? time(node.start) : events[0] ? ms(events[0].at) : NaN;
|
|
304
|
+
if (!Number.isFinite(start)) continue;
|
|
305
|
+
const end = runningNodes.has(node.id) ? last : Number.isFinite(time(node.end)) ? time(node.end) : events.at(-1) ? ms(events.at(-1).at) : start;
|
|
306
|
+
const parent = node.parent ? index.canonical(node.parent) : void 0;
|
|
307
|
+
spans.push({
|
|
308
|
+
id: node.id,
|
|
309
|
+
...parent && actors.has(parent) ? { parentId: parent } : {},
|
|
310
|
+
kind: "pipeline",
|
|
311
|
+
name: node.label,
|
|
312
|
+
startMs: offset(start),
|
|
313
|
+
endMs: offset(Math.max(start, end)),
|
|
314
|
+
status: runStatus(node.status ?? "unknown"),
|
|
315
|
+
approx: !node.start || !node.end
|
|
316
|
+
});
|
|
317
|
+
if (parent && actors.has(parent)) spans.push({
|
|
318
|
+
id: `${node.id}:handoff`,
|
|
319
|
+
parentId: parent,
|
|
320
|
+
kind: "handoff",
|
|
321
|
+
name: `Handoff to ${node.label}`,
|
|
322
|
+
startMs: offset(start),
|
|
323
|
+
endMs: offset(start),
|
|
324
|
+
approx: true,
|
|
325
|
+
meta: { timing: "Recorded child start; handoff duration was not measured" }
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const promptTokens = [];
|
|
329
|
+
const completionTokens = [];
|
|
330
|
+
let cost = 0;
|
|
331
|
+
let costKnown = false;
|
|
332
|
+
let costMissing = false;
|
|
333
|
+
const explicitTools = /* @__PURE__ */ new Set();
|
|
334
|
+
let toolCalls = 0;
|
|
335
|
+
for (const event of index.events) {
|
|
336
|
+
const detail = event.detail;
|
|
337
|
+
const response = detail.usageScope === void 0 && detail.role === "assistant";
|
|
338
|
+
const counters = [
|
|
339
|
+
detail.usage?.input,
|
|
340
|
+
detail.usage?.cacheRead,
|
|
341
|
+
detail.usage?.cacheWrite,
|
|
342
|
+
detail.usage?.output
|
|
343
|
+
].map(reportedCount);
|
|
344
|
+
const prompt = sumReportedCounts(counters.slice(0, 3)), completion = counters[3];
|
|
345
|
+
const measured = counters.filter((value) => value !== void 0);
|
|
346
|
+
const facts = measured.length && measured.length < counters.length ? [{
|
|
347
|
+
label: "Tokens",
|
|
348
|
+
value: `≥ ${fmt(measured.reduce((sum, value) => sum + value, 0))} measured; incomplete counters`
|
|
349
|
+
}] : void 0;
|
|
350
|
+
if (detail.usageScope === void 0) {
|
|
351
|
+
if (response || detail.usage) {
|
|
352
|
+
promptTokens.push(prompt);
|
|
353
|
+
completionTokens.push(completion);
|
|
354
|
+
}
|
|
355
|
+
if (detail.usdKnown === false) costMissing = true;
|
|
356
|
+
else if (typeof detail.costListUsd === "number") {
|
|
357
|
+
cost += detail.costListUsd;
|
|
358
|
+
costKnown = true;
|
|
359
|
+
} else if (detail.usage && Object.values(detail.usage).some((value) => typeof value === "number" && value > 0)) costMissing = true;
|
|
360
|
+
}
|
|
361
|
+
const start = ms(event.at);
|
|
362
|
+
const parentId = index.canonical(event.node);
|
|
363
|
+
if (typeof detail.spanId === "string") {
|
|
364
|
+
const kind = event.kind.replace(/^trace-/, "").toLowerCase();
|
|
365
|
+
const end = time(detail.end);
|
|
366
|
+
const state = runStatus(typeof detail.status === "string" ? detail.status : "unknown");
|
|
367
|
+
const running = live && !Number.isFinite(end) && [
|
|
368
|
+
"running",
|
|
369
|
+
"waiting",
|
|
370
|
+
"needs-you"
|
|
371
|
+
].includes(state);
|
|
372
|
+
const spanKind = ["llm", "model"].includes(kind) ? "model" : kind === "tool" ? "tool" : kind === "waiting" ? "waiting" : kind === "handoff" ? "handoff" : "pipeline";
|
|
373
|
+
const traceId = String(detail.traceId ?? event.node);
|
|
374
|
+
spans.push({
|
|
375
|
+
id: `${traceId}:${detail.spanId}`,
|
|
376
|
+
parentId: typeof detail.parentSpanId === "string" ? `${traceId}:${detail.parentSpanId}` : parentId,
|
|
377
|
+
kind: spanKind,
|
|
378
|
+
name: event.label,
|
|
379
|
+
startMs: offset(start),
|
|
380
|
+
endMs: offset(Number.isFinite(end) ? Math.max(start, end) : running ? last : start),
|
|
381
|
+
status: detail.isError ? "failed" : state,
|
|
382
|
+
meta: { eventId: event.id }
|
|
383
|
+
});
|
|
384
|
+
if (spanKind === "tool") {
|
|
385
|
+
toolCalls++;
|
|
386
|
+
if (typeof detail.toolCallId === "string") explicitTools.add(toolKey(event.node, detail.toolCallId));
|
|
387
|
+
}
|
|
388
|
+
} else if (response) spans.push({
|
|
389
|
+
id: event.id,
|
|
390
|
+
parentId,
|
|
391
|
+
kind: "model",
|
|
392
|
+
name: event.label,
|
|
393
|
+
startMs: offset(start),
|
|
394
|
+
endMs: offset(start),
|
|
395
|
+
approx: true,
|
|
396
|
+
status: runStatus(typeof detail.responseStatus === "string" ? detail.responseStatus : "unknown"),
|
|
397
|
+
promptTokens: prompt,
|
|
398
|
+
completionTokens: completion,
|
|
399
|
+
facts,
|
|
400
|
+
meta: {
|
|
401
|
+
eventId: event.id,
|
|
402
|
+
timing: "Response timestamp; generation duration was not recorded"
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
for (const key of /* @__PURE__ */ new Set([...index.calls.keys(), ...index.results.keys()])) {
|
|
407
|
+
if (explicitTools.has(key)) continue;
|
|
408
|
+
const calls = index.calls.get(key) ?? [], results = index.results.get(key) ?? [];
|
|
409
|
+
const result = calls.length === 1 && results.length === 1 && ms(results[0].at) >= ms(calls[0].event.at) ? results[0] : void 0;
|
|
410
|
+
const timing = calls.length > 1 || results.length > 1 ? "Ambiguous retained call/result identity; no duration measured" : calls.length && results.length ? "Recorded result timestamp precedes call; no interval measured" : "Unmatched retained call or result; no interval measured";
|
|
411
|
+
for (const { event, call } of calls) {
|
|
412
|
+
toolCalls++;
|
|
413
|
+
const start = ms(event.at), parentId = index.canonical(event.node);
|
|
414
|
+
spans.push({
|
|
415
|
+
id: `${event.id}:tool:${call.id}`,
|
|
416
|
+
parentId,
|
|
417
|
+
kind: "tool",
|
|
418
|
+
name: call.name,
|
|
419
|
+
startMs: offset(start),
|
|
420
|
+
endMs: offset(result ? ms(result.at) : start),
|
|
421
|
+
approx: !result || event.detail.atBasis === "carried" || result.detail.atBasis === "carried",
|
|
422
|
+
status: result?.detail.isError ? "failed" : result ? "done" : "unknown",
|
|
423
|
+
facts: result ? void 0 : [{
|
|
424
|
+
label: "Timing",
|
|
425
|
+
value: timing
|
|
426
|
+
}],
|
|
427
|
+
meta: {
|
|
428
|
+
eventId: event.id,
|
|
429
|
+
...!result ? { timing } : {}
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
if (!result) for (const event of results) spans.push({
|
|
434
|
+
id: `${event.id}:result`,
|
|
435
|
+
parentId: index.canonical(event.node),
|
|
436
|
+
kind: "tool",
|
|
437
|
+
name: event.label,
|
|
438
|
+
startMs: offset(ms(event.at)),
|
|
439
|
+
endMs: offset(ms(event.at)),
|
|
440
|
+
approx: true,
|
|
441
|
+
status: event.detail.isError ? "failed" : "unknown",
|
|
442
|
+
facts: [{
|
|
443
|
+
label: "Timing",
|
|
444
|
+
value: timing
|
|
445
|
+
}],
|
|
446
|
+
meta: { eventId: event.id }
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
spans,
|
|
451
|
+
totalMs: last - first,
|
|
452
|
+
promptTokens: sumReportedCounts(promptTokens),
|
|
453
|
+
completionTokens: sumReportedCounts(completionTokens),
|
|
454
|
+
toolCalls,
|
|
455
|
+
...costKnown && !costMissing ? {
|
|
456
|
+
costUsd: cost,
|
|
457
|
+
costBasis: "list"
|
|
458
|
+
} : {}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
499
461
|
// src/run/record/conversation-rows.ts
|
|
500
462
|
/**
|
|
501
463
|
* A key that survives a live record growing. Event ids hash the whole source file, so every append renames every
|
|
@@ -720,11 +682,7 @@ function pollSummary(events) {
|
|
|
720
682
|
measured++;
|
|
721
683
|
tokens += parts.reduce((sum, value) => sum + value, 0);
|
|
722
684
|
}
|
|
723
|
-
const
|
|
724
|
-
notation: "compact",
|
|
725
|
-
maximumFractionDigits: 1
|
|
726
|
-
});
|
|
727
|
-
const tokenText = !measured ? "tokens unknown" : measured < events.length ? `~${count.format(tokens)} tokens (${measured} of ${events.length} measured)` : `~${count.format(tokens)} tokens`;
|
|
685
|
+
const tokenText = !measured ? "tokens unknown" : measured < events.length ? `~${formatCompact(tokens)} tokens (${measured} of ${events.length} measured)` : `~${formatCompact(tokens)} tokens`;
|
|
728
686
|
return {
|
|
729
687
|
polls: events.length,
|
|
730
688
|
spanMs: span,
|
|
@@ -755,11 +713,6 @@ function promptText(text) {
|
|
|
755
713
|
// src/run/record/ConversationRowView.tsx
|
|
756
714
|
/** A run-lifecycle event (start, stop, restart), drawn as a thin marker row. */
|
|
757
715
|
const isLifecycle = (event) => event.category === "lifecycle" || event.detail.lifecycle !== void 0;
|
|
758
|
-
const tokenLabel = (value) => new Intl.NumberFormat("en-US", {
|
|
759
|
-
notation: "compact",
|
|
760
|
-
maximumFractionDigits: 1
|
|
761
|
-
}).format(value);
|
|
762
|
-
const spanLabel = (value) => value < 1e3 ? `${Math.round(value)} ms` : formatDuration(value);
|
|
763
716
|
function ToolStep({ item, previous }) {
|
|
764
717
|
const part = item.part, cost = part.state.metadata?.costUsd, time = part.state.time, category = getToolCategory(part.tool);
|
|
765
718
|
const tone = part.state.status === "error" ? "var(--surface-danger-text)" : [
|
|
@@ -792,12 +745,12 @@ function ToolStep({ item, previous }) {
|
|
|
792
745
|
/* @__PURE__ */ jsx("dt", { children: "Duration" }),
|
|
793
746
|
/* @__PURE__ */ jsx("dd", {
|
|
794
747
|
className: "m-0 text-right",
|
|
795
|
-
children: time?.end != null && time.start != null ?
|
|
748
|
+
children: time?.end != null && time.start != null ? formatSpanDuration(time.end - time.start) : "Not recorded"
|
|
796
749
|
}),
|
|
797
750
|
/* @__PURE__ */ jsx("dt", { children: "Cost" }),
|
|
798
751
|
/* @__PURE__ */ jsx("dd", {
|
|
799
752
|
className: "m-0 text-right",
|
|
800
|
-
children: typeof cost === "number" ?
|
|
753
|
+
children: typeof cost === "number" ? formatCost(cost) : "Not recorded"
|
|
801
754
|
})
|
|
802
755
|
]
|
|
803
756
|
}),
|
|
@@ -926,7 +879,7 @@ function toolItems(id, call, result, state, durationMs, live, opener) {
|
|
|
926
879
|
}
|
|
927
880
|
const toolNote = (part) => {
|
|
928
881
|
const note = part.state.metadata?.note, time = part.state.time;
|
|
929
|
-
const duration = time?.end != null && time.start != null ?
|
|
882
|
+
const duration = time?.end != null && time.start != null ? formatSpanDuration(time.end - time.start) : "Not recorded";
|
|
930
883
|
const result = typeof note === "string" ? note : part.state.status === "error" ? "Failed" : part.state.status === "running" ? "Running" : "Returned";
|
|
931
884
|
return /* @__PURE__ */ jsxs("span", {
|
|
932
885
|
"data-record-tool-summary": "",
|
|
@@ -1074,7 +1027,7 @@ function rowItems(row, context) {
|
|
|
1074
1027
|
if (withheld.length) {
|
|
1075
1028
|
const output = outputTokens(event.detail);
|
|
1076
1029
|
const span = thinkingSpan.get(event.id);
|
|
1077
|
-
const label = output !== null ? `${span !== void 0 && span >= 1e3 ? `for ${
|
|
1030
|
+
const label = output !== null ? `${span !== void 0 && span >= 1e3 ? `for ${formatSpanDuration(span)} · ` : ""}${formatCompact(output)} output tokens · ${withheld.join(" · ")}` : withheld.join(" · ");
|
|
1078
1031
|
items.push({
|
|
1079
1032
|
id: `${event.id}:thinking`,
|
|
1080
1033
|
kind: "custom",
|
|
@@ -1330,27 +1283,12 @@ function Conversation({ index, actor, cutoff, query, category, selected, openSou
|
|
|
1330
1283
|
return map;
|
|
1331
1284
|
}, [rows]);
|
|
1332
1285
|
const retained = (index.byActor.get(actor) ?? []).some((item) => !isLifecycle(item) && (item.detail.role || textOf(item) || callsOf(item).length));
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
following.current = follow && !targetRef.current;
|
|
1340
|
-
if (scroll.current && !targetRef.current) scroll.current.scrollTop = follow ? scroll.current.scrollHeight : 0;
|
|
1341
|
-
}, [actor]);
|
|
1342
|
-
useLayoutEffect(() => {
|
|
1343
|
-
if (!follow) following.current = false;
|
|
1344
|
-
}, [follow]);
|
|
1345
|
-
useEffect(() => {
|
|
1346
|
-
const element = scroll.current;
|
|
1347
|
-
if (!element) return;
|
|
1348
|
-
const track = () => {
|
|
1349
|
-
following.current = followRef.current && element.scrollHeight - element.scrollTop - element.clientHeight < 24;
|
|
1350
|
-
};
|
|
1351
|
-
element.addEventListener("scroll", track, { passive: true });
|
|
1352
|
-
return () => element.removeEventListener("scroll", track);
|
|
1353
|
-
}, []);
|
|
1286
|
+
const { following } = useFollowTail(scroll, {
|
|
1287
|
+
identity: actor,
|
|
1288
|
+
content: rows,
|
|
1289
|
+
follow,
|
|
1290
|
+
target
|
|
1291
|
+
});
|
|
1354
1292
|
const arrival = useRef({ actor });
|
|
1355
1293
|
useLayoutEffect(() => {
|
|
1356
1294
|
const tail = rows.at(-1), event = tail?.kind === "event" ? tail.event : tail?.events.at(-1), entry = tail?.kind === "event" ? tail.event.id : tail?.events[0]?.id;
|
|
@@ -1361,11 +1299,8 @@ function Conversation({ index, actor, cutoff, query, category, selected, openSou
|
|
|
1361
1299
|
};
|
|
1362
1300
|
const element = scroll.current;
|
|
1363
1301
|
if (!element || !following.current) return;
|
|
1364
|
-
|
|
1365
|
-
let frames = 0;
|
|
1366
|
-
let frame = requestAnimationFrame(function pin() {
|
|
1302
|
+
const frame = requestAnimationFrame(() => {
|
|
1367
1303
|
if (!following.current) return;
|
|
1368
|
-
element.scrollTop = element.scrollHeight;
|
|
1369
1304
|
const row = entry && element.querySelector(`[data-entry="${CSS.escape(entry)}"]`);
|
|
1370
1305
|
if (animate && row) {
|
|
1371
1306
|
animate = false;
|
|
@@ -1375,7 +1310,6 @@ function Conversation({ index, actor, cutoff, query, category, selected, openSou
|
|
|
1375
1310
|
easing: style.getPropertyValue("--ease-expo").trim() || "linear"
|
|
1376
1311
|
});
|
|
1377
1312
|
}
|
|
1378
|
-
if (++frames < 20) frame = requestAnimationFrame(pin);
|
|
1379
1313
|
});
|
|
1380
1314
|
return () => cancelAnimationFrame(frame);
|
|
1381
1315
|
}, [rows]);
|
|
@@ -1472,50 +1406,6 @@ function Conversation({ index, actor, cutoff, query, category, selected, openSou
|
|
|
1472
1406
|
})]
|
|
1473
1407
|
}) });
|
|
1474
1408
|
}
|
|
1475
|
-
// src/run/record/prompts.ts
|
|
1476
|
-
/** The conversation needs an opening prompt even when the provider did not retain its first user turn. */
|
|
1477
|
-
function withPrompts(record) {
|
|
1478
|
-
const events = record.events.map((event) => ({
|
|
1479
|
-
...event,
|
|
1480
|
-
detail: { ...event.detail }
|
|
1481
|
-
}));
|
|
1482
|
-
const owners = new Map(record.nodes.map((node) => [node.id, node.agentId ?? node.id]));
|
|
1483
|
-
for (const node of record.nodes.filter((item) => item.kind === "agent")) {
|
|
1484
|
-
const own = events.filter((event) => (owners.get(event.node) ?? event.node) === node.id);
|
|
1485
|
-
const firstReply = own.find((event) => event.detail.role === "assistant")?.at;
|
|
1486
|
-
const userTurns = own.filter((event) => event.detail.role === "user" && typeof event.detail.publicText === "string" && !event.detail.publicText.trimStart().startsWith("<task-notification>"));
|
|
1487
|
-
const initial = userTurns.find((event) => event.detail.promptKind !== "steering" && (!firstReply || event.at <= firstReply));
|
|
1488
|
-
if (initial) initial.detail.promptKind = "initial";
|
|
1489
|
-
else if (node.assignment?.trim()) {
|
|
1490
|
-
const at = node.start ?? own[0]?.at;
|
|
1491
|
-
if (at) events.push({
|
|
1492
|
-
id: `prompt:${node.id}`,
|
|
1493
|
-
node: node.id,
|
|
1494
|
-
at,
|
|
1495
|
-
kind: "message",
|
|
1496
|
-
category: "other",
|
|
1497
|
-
label: "Prompt",
|
|
1498
|
-
source: null,
|
|
1499
|
-
detail: {
|
|
1500
|
-
role: "user",
|
|
1501
|
-
publicText: node.assignment,
|
|
1502
|
-
promptKind: "initial",
|
|
1503
|
-
promptReconstructed: true,
|
|
1504
|
-
promptSource: "node.assignment"
|
|
1505
|
-
}
|
|
1506
|
-
});
|
|
1507
|
-
}
|
|
1508
|
-
for (const event of userTurns) {
|
|
1509
|
-
if (event === initial || event.detail.promptKind === "initial") continue;
|
|
1510
|
-
event.detail.promptKind ??= "steering";
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
events.sort((a, b) => a.at.localeCompare(b.at) || Number(b.detail.promptKind === "initial") - Number(a.detail.promptKind === "initial"));
|
|
1514
|
-
return {
|
|
1515
|
-
...record,
|
|
1516
|
-
events
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
1409
|
// src/run/record/AgentTree.tsx
|
|
1520
1410
|
/** Retained identity and recorded time define the hierarchy; labels never join sessions or invent parents. */
|
|
1521
1411
|
function AgentTree({ index, actor, cutoff, select }) {
|
|
@@ -1547,9 +1437,9 @@ function AgentTree({ index, actor, cutoff, select }) {
|
|
|
1547
1437
|
children: entries.map(({ node, children }) => {
|
|
1548
1438
|
const firstEvent = index.byActor.get(node.id)?.[0];
|
|
1549
1439
|
const begin = node.start ? ms(node.start) : firstEvent ? ms(firstEvent.at) : void 0;
|
|
1550
|
-
const
|
|
1551
|
-
const
|
|
1552
|
-
const state = future ? "Not yet started" :
|
|
1440
|
+
const future = begin !== void 0 && begin > cutoff, pending = typeof node.end === "string" && ms(node.end) > cutoff;
|
|
1441
|
+
const status = runStatus(future || pending ? "unknown" : node.status ?? "unknown");
|
|
1442
|
+
const state = future ? "Not yet started" : pending ? "Not yet settled" : statusMeta(status).label;
|
|
1553
1443
|
return /* @__PURE__ */ jsxs("li", {
|
|
1554
1444
|
className: "min-w-0",
|
|
1555
1445
|
children: [/* @__PURE__ */ jsxs("button", {
|
|
@@ -1562,7 +1452,7 @@ function AgentTree({ index, actor, cutoff, select }) {
|
|
|
1562
1452
|
title: node.label + (node.assignment ? " · " + node.assignment : "") + " · " + state,
|
|
1563
1453
|
onClick: () => select(node.id),
|
|
1564
1454
|
children: [/* @__PURE__ */ jsx(RunStatusIcon, {
|
|
1565
|
-
status
|
|
1455
|
+
status,
|
|
1566
1456
|
className: "mt-0.5 size-3.5 shrink-0",
|
|
1567
1457
|
"aria-hidden": true
|
|
1568
1458
|
}), /* @__PURE__ */ jsxs("span", {
|
|
@@ -1594,4 +1484,4 @@ function AgentTree({ index, actor, cutoff, select }) {
|
|
|
1594
1484
|
})
|
|
1595
1485
|
});
|
|
1596
1486
|
}
|
|
1597
|
-
export {
|
|
1487
|
+
export { fmt as a, modelIdentity as c, recordFlowTrace as d, roleOf as f, utcTime as h, eventSource as i, model_exports as l, toolKey as m, Conversation as n, indexRecord as o, textOf as p, eventMatches as r, interval as s, AgentTree as t, ms as u };
|