@tangle-network/charts 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -337,7 +337,7 @@ const trace = otlpFlowTrace(spans, { price: catalogPrice(await (await fetch(cata
337
337
 
338
338
  `defaultView="agents"` opens the agent timeline: one row per agent with its model, thinking and tool marks, hatched waiting while it delegates, elbow connectors to each agent it spawned and back, retry and failure glyphs, and a headline with the measured speedup over sequential (agent busy time over wall time). Expanding an agent opens the span view with that agent unfolded. The span view virtualizes rows (5,000 spans scroll at 58+ fps in headless Chromium), pins ancestors while their children scroll, and labels each bar with its duration and recorded cost. Hover shows a span card; click pins it and zooms the axis to a span too narrow to read. `onSelect(span)` hears each opened row and pinned bar, so a host can open its own drill-down; `markers={[{ at, label, tone }]}` draws labelled instants, such as releases, across the rows. Give an agent span an `outcome` (`{ tone, label }`) and its lane takes the outcome color and the header a legend. `detail` becomes the agent's subtitle. The card is a table of what the row recorded, every value with its unit: model, duration, tokens in, out and cached (the span's `promptTokens`, `completionTokens` and `cachedTokens`, else the sum of its children's), LLM latency (median and p95 of its model calls), cost, LLM calls, tool calls by tool (the name before " · "), failed steps, and retries and restarts (`retryOf`, `marks` of kind `restart`); a value nobody recorded is left out. `facts` (`[{ label, value }]`) adds the host's rows after it. The card opens at the pointer, above or below its row, whichever side fits the viewport. `onTimeChange` reports where the reader scrubbed (null when live), and markers double as an event track (click, N or P to jump). A partly priced trace shows its floor and coverage (`costCoverage`), and `billedUsd` shows the real charge beside the list price. Ctrl/Cmd+wheel or `+`/`-`/`0` zoom, Shift+wheel pans, the scrub track under the controls shows the whole trace, and arrow keys walk the tree.
339
339
 
340
- Live views append spans as they arrive: `traces watch <id|run-dir> --format jsonl --spans` emits a `traces.stream.span` event per new or changed span, and `otlpFlowTrace` keeps the last span per `span_id`. Running agent spans grow to the clock between deltas without React renders. `activityAt` supplies trace-relative event times and `medianEventGapMs` preserves producer cadence before tail truncation; without it, child-step activity supplies the samples. Silence beyond five minutes and three median event gaps hatches the tail and names the quiet duration. The Errors filter expands matching agents and steps; its rug locates each error, marking unreported times separately. Containers below 600px use stacked 44px rows and a Key disclosure. Search supports Cmd/Ctrl+F and keeps ancestor context. Missing cost and authoritative LLM counts stay Unknown; a cost priced from a catalog is labelled list price. Invalid intervals retain a per-row `—` timing mark. The static phase waterfall remains a separate chart for disjoint phases.
340
+ Live views append spans as they arrive: `traces watch <id|run-dir> --format jsonl --spans` emits a `traces.stream.span` event per new or changed span, and `otlpFlowTrace` keeps the last span per `span_id`. Running agent spans grow to the clock between deltas without React renders. `activityAt` supplies trace-relative event times and `medianEventGapMs` preserves producer cadence before tail truncation; without it, child-step activity supplies the samples. Silence beyond five minutes and three median event gaps hatches the tail and names the quiet duration. The Errors filter expands matching agents and steps; its rug locates each error, marking unreported times separately. Containers below 600px use stacked 44px rows and a Key disclosure. Search supports Cmd/Ctrl+F and keeps ancestor context. Token totals are optional: omit unknown counts, retain measured zero, and guard optional token fields when reading a trace. Missing, partial and nonfinite usage displays a dash, including during playback. Missing cost and authoritative LLM counts stay Unknown; a cost priced from a catalog is labelled list price. Invalid intervals retain a per-row `—` timing mark. The static phase waterfall remains a separate chart for disjoint phases.
341
341
 
342
342
  `scripts/flow-waterfall-demo.mjs` serves the demo routes (reference-shaped fixtures, recorded runs, a synthetic load trace and OTLP files from `FW_DATA_DIR`); `scripts/flow-waterfall-capture.mjs` captures frames, hover cards, fps, first paint and recordings, and with `FW_LIVE_RELAY` records a live session through `scripts/flow-waterfall-live-relay.mjs` with every span's event-to-pixel time.
343
343
 
@@ -391,8 +391,9 @@ interface FlowMarker {
391
391
  interface FlowTrace {
392
392
  spans: FlowSpan[];
393
393
  totalMs: number;
394
- promptTokens: number;
395
- completionTokens: number;
394
+ /** Recorded totals only; absent or invalid values remain unknown, including during playback. */
395
+ promptTokens?: number;
396
+ completionTokens?: number;
396
397
  /** Computed when per-token pricing is supplied. */
397
398
  costUsd?: number;
398
399
  /** `list`: priced from a model catalog's list prices, not a bill; the summary says so. */
@@ -1198,4 +1198,13 @@ function axisTicks(t0, t1, px, left, right, label) {
1198
1198
  return fit([t0, t1]) ? [t0, t1] : [t0];
1199
1199
  }
1200
1200
  //#endregion
1201
- export { titled as $, pct as A, usdTick as B, costText as C, intervalGap as D, estimateNote as E, rateOrder as F, hatchPath as G, MARKER_SHAPES as H, tierAtLeast as I, markerPath as J, hbar as K, tierSummary as L, pts as M, rateDataGap as N, intervalName as O, rateIntervalGap as P, text as Q, tierTotal as R, TIERS_BEST_FIRST as S, duration as T, circle as U, table as V, hatch as W, rect as X, polyline as Y, svgRoot as Z, groupRowHeight as _, OUTCOME_COLORS as a, plural as at, isRefusal as b, checkerColor as c, wrap as ct, MARK_VERDICTS as d, wideWidth as et, drawBands as f, eventsTable as g, eventsGap as h, ENDING_ORDER as i, listJoin as it, perPass as j, kn as k, EVENTS_READ as l, drawMarks as m, CHECKER_HUES as n, esc as nt, OUTCOME_ORDER as o, textBlock as ot, drawGroups as p, line as q, ENDING_COLORS as r, fitLines as rt, checkerClass as s, textWidth as st, timeline as t, capitalize as tt, MARK_HEADS as u, markLabelRows as v, drawable as w, METHOD_NAME as x, markName as y, usd as z };
1201
+ //#region src/flow-trace.ts
1202
+ /** Internal quantity boundaries shared by construction, aggregation and presentation. */
1203
+ const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
1204
+ const reportedCount = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
1205
+ function sumReportedCounts(values) {
1206
+ if (!values.length || values.some((value) => reportedCount(value) === void 0)) return void 0;
1207
+ return reportedCount(values.reduce((sum, value) => sum + value, 0));
1208
+ }
1209
+ //#endregion
1210
+ export { rect as $, intervalGap as A, tierSummary as B, isRefusal as C, drawable as D, costText as E, pts as F, MARKER_SHAPES as G, usd as H, rateDataGap as I, hatchPath as J, circle as K, rateIntervalGap as L, kn as M, pct as N, duration as O, perPass as P, polyline as Q, rateOrder as R, markName as S, TIERS_BEST_FIRST as T, usdTick as U, tierTotal as V, table as W, line as X, hbar as Y, markerPath as Z, drawMarks as _, CHECKER_HUES as a, esc as at, groupRowHeight as b, OUTCOME_COLORS as c, plural as ct, checkerColor as d, wrap as dt, svgRoot as et, EVENTS_READ as f, drawGroups as g, drawBands as h, timeline as i, capitalize as it, intervalName as j, estimateNote as k, OUTCOME_ORDER as l, textBlock as lt, MARK_VERDICTS as m, reportedCount as n, titled as nt, ENDING_COLORS as o, fitLines as ot, MARK_HEADS as p, hatch as q, sumReportedCounts as r, wideWidth as rt, ENDING_ORDER as s, listJoin as st, finiteNumber as t, text as tt, checkerClass as u, textWidth as ut, eventsGap as v, METHOD_NAME as w, markLabelRows as x, eventsTable as y, tierAtLeast as z };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as BreakdownInput, B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, H as Tier, I as Interval, L as IntervalMethod, M as CostBasis, N as Estimate, O as YTarget, P as Exclusion, R as MatrixCell, S as ChartMark, T as MarkerShape, U as TierCounts, V as Setup, W as isRefusal, _ as checkerColor, a as TimelineLane, b as BandKind, c as TimelineTick, d as ENDING_COLORS, f as ENDING_ORDER, g as TickOutcome, h as RunEnding, i as FlowTrace, j as ComparisonRow, k as YTick, l as timeline, m as OUTCOME_ORDER, n as FlowOutcome, o as TimelineOptions, p as OUTCOME_COLORS, r as FlowSpan, s as TimelineSpan, t as FlowMarker, u as CHECKER_HUES, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph, z as RateRow } from "./flow-trace-CKk85dAV.js";
1
+ import { A as BreakdownInput, B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, H as Tier, I as Interval, L as IntervalMethod, M as CostBasis, N as Estimate, O as YTarget, P as Exclusion, R as MatrixCell, S as ChartMark, T as MarkerShape, U as TierCounts, V as Setup, W as isRefusal, _ as checkerColor, a as TimelineLane, b as BandKind, c as TimelineTick, d as ENDING_COLORS, f as ENDING_ORDER, g as TickOutcome, h as RunEnding, i as FlowTrace, j as ComparisonRow, k as YTick, l as timeline, m as OUTCOME_ORDER, n as FlowOutcome, o as TimelineOptions, p as OUTCOME_COLORS, r as FlowSpan, s as TimelineSpan, t as FlowMarker, u as CHECKER_HUES, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph, z as RateRow } from "./flow-trace-CF4Q07cA.js";
2
2
  //#region src/block.d.ts
3
3
  interface RenderFigureOptions {
4
4
  /** Chart uses a closed data disclosure; plot keeps only the SVG and accessible data. */
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as titled, A as pct, B as usdTick, C as costText, D as intervalGap, E as estimateNote, F as rateOrder, H as MARKER_SHAPES, I as tierAtLeast, J as markerPath, K as hbar, L as tierSummary, M as pts, N as rateDataGap, O as intervalName, P as rateIntervalGap, Q as text, R as tierTotal, S as TIERS_BEST_FIRST, T as duration, U as circle, V as table, W as hatch, X as rect, Y as polyline, Z as svgRoot, _ as groupRowHeight, a as OUTCOME_COLORS, at as plural, b as isRefusal, c as checkerColor, ct as wrap, d as MARK_VERDICTS, et as wideWidth, f as drawBands, g as eventsTable, h as eventsGap, i as ENDING_ORDER, it as listJoin, j as perPass, k as kn, l as EVENTS_READ, m as drawMarks, n as CHECKER_HUES, nt as esc, o as OUTCOME_ORDER, ot as textBlock, p as drawGroups, q as line, r as ENDING_COLORS, rt as fitLines, s as checkerClass, st as textWidth, t as timeline, tt as capitalize, v as markLabelRows, w as drawable, x as METHOD_NAME, z as usd } from "./timeline-B7c-cede.js";
1
+ import { $ as rect, A as intervalGap, B as tierSummary, C as isRefusal, D as drawable, E as costText, F as pts, G as MARKER_SHAPES, H as usd, I as rateDataGap, K as circle, L as rateIntervalGap, M as kn, N as pct, O as duration, P as perPass, Q as polyline, R as rateOrder, T as TIERS_BEST_FIRST, U as usdTick, V as tierTotal, W as table, X as line, Y as hbar, Z as markerPath, _ as drawMarks, a as CHECKER_HUES, at as esc, b as groupRowHeight, c as OUTCOME_COLORS, ct as plural, d as checkerColor, dt as wrap, et as svgRoot, f as EVENTS_READ, g as drawGroups, h as drawBands, i as timeline, it as capitalize, j as intervalName, k as estimateNote, l as OUTCOME_ORDER, lt as textBlock, m as MARK_VERDICTS, n as reportedCount, nt as titled, o as ENDING_COLORS, ot as fitLines, q as hatch, r as sumReportedCounts, rt as wideWidth, s as ENDING_ORDER, st as listJoin, t as finiteNumber, tt as text, u as checkerClass, ut as textWidth, v as eventsGap, w as METHOD_NAME, x as markLabelRows, y as eventsTable, z as tierAtLeast } from "./flow-trace-k097Ukvs.js";
2
2
  //#region src/block.ts
3
3
  /**
4
4
  * The figure block: one markup for every figure on every surface.
@@ -3349,8 +3349,9 @@ const CACHE_KEYS = [...CACHE_READ_KEYS, "gen_ai.usage.cache_creation_input_token
3349
3349
  function catalogPrice(catalog) {
3350
3350
  const rates = /* @__PURE__ */ new Map();
3351
3351
  for (const model of catalog.data ?? []) {
3352
- const input = Number(model.pricing?.prompt), output = Number(model.pricing?.completion);
3353
- if (model.id && Number.isFinite(input) && Number.isFinite(output)) rates.set(model.id, {
3352
+ const rate = (value) => typeof value === "string" ? value.trim() ? finiteNumber(Number(value)) : void 0 : finiteNumber(value);
3353
+ const input = rate(model.pricing?.prompt), output = rate(model.pricing?.completion);
3354
+ if (model.id && input !== void 0 && output !== void 0) rates.set(model.id, {
3354
3355
  input,
3355
3356
  output
3356
3357
  });
@@ -3391,7 +3392,6 @@ function parseOtlpJsonl(text) {
3391
3392
  };
3392
3393
  }
3393
3394
  const str = (value) => typeof value === "string" && value ? value : void 0;
3394
- const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
3395
3395
  const pick = (attributes, keys) => {
3396
3396
  for (const key of keys) {
3397
3397
  const value = str(attributes[key]);
@@ -3474,8 +3474,7 @@ function otlpFlowTrace(input, options = {}) {
3474
3474
  const topModel = (key) => [...models.get(key)?.entries() ?? []].sort((a, b) => b[1] - a[1])[0]?.[0];
3475
3475
  const out = [];
3476
3476
  let omitted = 0;
3477
- let promptTokens = 0;
3478
- let completionTokens = 0;
3477
+ const usage = [];
3479
3478
  let toolCalls = 0;
3480
3479
  let modelCalls = 0;
3481
3480
  let errors = 0;
@@ -3530,9 +3529,9 @@ function otlpFlowTrace(input, options = {}) {
3530
3529
  if (open) endMs = Math.max(endMs, last - origin);
3531
3530
  const parentId = span.parent_span_id && byId.has(span.parent_span_id) ? span.parent_span_id : groupOf.has(span.span_id) ? `group:${groupOf.get(span.span_id)}` : void 0;
3532
3531
  const model = pick(a, MODEL_KEYS);
3533
- const prompt = num(a["gen_ai.usage.input_tokens"]);
3534
- const completion = num(a["gen_ai.usage.output_tokens"]);
3535
- const cached = CACHE_READ_KEYS.map((key) => num(a[key])).find((n) => n !== void 0);
3532
+ const prompt = reportedCount(a["gen_ai.usage.input_tokens"]);
3533
+ const completion = reportedCount(a["gen_ai.usage.output_tokens"]);
3534
+ const cached = CACHE_READ_KEYS.map((key) => reportedCount(a[key])).find((n) => n !== void 0);
3536
3535
  let kind = span.kind === "LLM" ? "model" : span.kind === "TOOL" ? "tool" : "pipeline";
3537
3536
  if (kind === "tool" && (a["traces.agent.spawn"] === true || spawnTarget.has(span.span_id) || handoffTo.has(span.span_id))) kind = "handoff";
3538
3537
  let name = str(a["gen_ai.tool.name"]) ?? span.name;
@@ -3542,16 +3541,13 @@ function otlpFlowTrace(input, options = {}) {
3542
3541
  if (kind === "model" && !aggregate) modelCalls++;
3543
3542
  if (kind === "tool" || kind === "handoff") toolCalls++;
3544
3543
  if (failed) errors++;
3545
- if (!aggregate) {
3546
- promptTokens += prompt ?? 0;
3547
- completionTokens += completion ?? 0;
3548
- }
3549
- const listPrice = kind === "model" && !aggregate && model && options.price && (prompt !== void 0 || completion !== void 0) ? options.price(model, prompt ?? 0, completion ?? 0) : void 0;
3544
+ if (!aggregate && (kind === "model" || prompt !== void 0 || completion !== void 0)) usage.push([prompt, completion]);
3545
+ const listPrice = finiteNumber(kind === "model" && !aggregate && model && options.price && prompt !== void 0 && completion !== void 0 ? options.price(model, prompt, completion) : void 0);
3550
3546
  if (listPrice !== void 0) {
3551
3547
  cost += listPrice;
3552
3548
  priced++;
3553
3549
  if (CACHE_KEYS.some((key) => a[key] !== void 0)) cacheSplit = true;
3554
- } else if (kind === "model" && !aggregate && (prompt || completion)) unpriced++;
3550
+ } else if (kind === "model" && !aggregate) unpriced++;
3555
3551
  out.push({
3556
3552
  id: span.span_id,
3557
3553
  ...parentId ? { parentId } : {},
@@ -3559,6 +3555,7 @@ function otlpFlowTrace(input, options = {}) {
3559
3555
  name,
3560
3556
  startMs,
3561
3557
  endMs,
3558
+ ...aggregate ? { meta: { "cli_bridge.usage.scope": "sum_of_children" } } : {},
3562
3559
  status: open ? "running" : failed ? "failed" : "done",
3563
3560
  ...endMs === startMs && kind !== "pipeline" ? { approx: true } : {},
3564
3561
  ...isAgent(span) ? { detail: topModel(span.span_id) } : kind === "model" && model ? { detail: shortModel(model) } : {},
@@ -3573,8 +3570,8 @@ function otlpFlowTrace(input, options = {}) {
3573
3570
  return {
3574
3571
  spans: out,
3575
3572
  totalMs: Number.isFinite(last - origin) ? last - origin : 0,
3576
- promptTokens,
3577
- completionTokens,
3573
+ promptTokens: sumReportedCounts(usage.map(([value]) => value)),
3574
+ completionTokens: sumReportedCounts(usage.map(([, value]) => value)),
3578
3575
  toolCalls,
3579
3576
  modelCalls,
3580
3577
  errors,
@@ -396,6 +396,8 @@
396
396
  .fw-root .fw-title-line { flex-wrap: wrap; }
397
397
  .fw-root .fw-search-input { width: 120px; }
398
398
  .fw-root .fw-axis-label { border: 0; padding: 0; }
399
+ .fw-root .fw-tick:is(:nth-child(2), :nth-child(3)) { transform: translate(-50%, -50%); }
400
+ .fw-root .fw-tick:nth-child(4) { transform: translate(calc(-100% - 4px), -50%); }
399
401
  .fw-root .fw-row { transform: none; top: var(--row-y); contain: style; }
400
402
  .fw-root .fw-label { position: absolute; z-index: 3; height: 20px; top: 0; left: 0; right: 0; padding: 0 calc(var(--fw-end-w, 0px) + 8px) 0 calc(8px + var(--depth) * 12px); border: 0; gap: 4px; background: transparent; pointer-events: none; }
401
403
  .fw-root .fw-view-agents .fw-names { flex-direction: row; font-size: 14px; line-height: 20px; }
@@ -404,12 +406,11 @@
404
406
  .fw-root .fw-lane { grid-column: 2; }
405
407
  .fw-root .fw-end { top: 0; height: 20px; left: auto; right: 4px; }
406
408
  .fw-root .fw-duration { font-size: 12px; }
407
- .fw-root .fw-time { display: block; position: absolute; top: -12px; left: clamp(0px, var(--fw-progress), calc(100% - 48px)); background: var(--fw-surface); }
409
+ .fw-root .fw-time { display: block; position: absolute; top: -12px; left: var(--fw-progress); transform: translateX(calc(-1 * var(--fw-progress))); width: max-content; max-width: 100%; white-space: nowrap; background: var(--fw-surface); }
408
410
  .fw-root .fw-playback { position: relative; padding: 12px 8px 8px; }
409
411
  .fw-root .fw-overview { margin-top: 12px; }
410
412
  }
411
413
 
412
- @container flow-waterfall (max-width: 599px) { .fw-root .fw-tick:nth-child(4) { transform: translate(calc(-100% - 4px), -50%); } }
413
414
 
414
415
  .fw-root.fw-fill-agent .fw-view-agents .fw-seg-failed[data-model] { background: var(--model-fill); --fw-seg-ink: light-dark(var(--fw-surface), var(--bg-root)); }
415
416
 
@@ -1,4 +1,4 @@
1
- import { B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, O as YTarget, S as ChartMark, T as MarkerShape, a as TimelineLane, b as BandKind, c as TimelineTick, g as TickOutcome, i as FlowTrace, k as YTick, n as FlowOutcome, o as TimelineOptions, r as FlowSpan, s as TimelineSpan, t as FlowMarker, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph } from "../flow-trace-CKk85dAV.js";
1
+ import { B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, O as YTarget, S as ChartMark, T as MarkerShape, a as TimelineLane, b as BandKind, c as TimelineTick, g as TickOutcome, i as FlowTrace, k as YTick, n as FlowOutcome, o as TimelineOptions, r as FlowSpan, s as TimelineSpan, t as FlowMarker, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph } from "../flow-trace-CF4Q07cA.js";
2
2
  import { CSSProperties, ReactElement, ReactNode } from "react";
3
3
  //#region src/react/sparkline.d.ts
4
4
  type SparklineDirection = "rising" | "falling" | "flat";
@@ -1,4 +1,4 @@
1
- import { G as hatchPath, J as markerPath, T as duration, b as isRefusal, h as eventsGap, t as timeline, u as MARK_HEADS, y as markName } from "../timeline-B7c-cede.js";
1
+ import { C as isRefusal, J as hatchPath, O as duration, S as markName, Z as markerPath, i as timeline, n as reportedCount, p as MARK_HEADS, r as sumReportedCounts, t as finiteNumber, v as eventsGap } from "../flow-trace-k097Ukvs.js";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Fragment as Fragment$1, createContext, createElement, forwardRef, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
4
  //#region src/react/sparkline.tsx
@@ -6515,12 +6515,14 @@ function formatSpanDuration(ms) {
6515
6515
  }
6516
6516
  /** $0.12, $0.005, $12.40; never rounds a recorded nonzero cost to $0.00. */
6517
6517
  function formatCost(usd) {
6518
+ if (finiteNumber(usd) === void 0) return "—";
6518
6519
  if (usd === 0) return "$0.00";
6519
6520
  if (usd < .01) return `$${usd.toPrecision(1).replace(/0+$/, "")}`;
6520
6521
  return `$${usd.toFixed(2)}`;
6521
6522
  }
6522
6523
  /** 9.8k, 754.4k, 1.2M with the exact count left to a title. */
6523
6524
  function formatCount(n) {
6525
+ if (reportedCount(n) === void 0) return "—";
6524
6526
  if (n < 1e3) return String(n);
6525
6527
  if (n < 1e6) return `${(n / 1e3).toFixed(1)}K`;
6526
6528
  return `${(n / 1e6).toFixed(1)}M`;
@@ -6646,9 +6648,9 @@ function flowWaterfallRows(trace, resolutionMs) {
6646
6648
  }
6647
6649
  Object.assign(row, stepStats(kids));
6648
6650
  const tokens = own ? {
6649
- prompt: row.span.promptTokens,
6650
- completion: row.span.completionTokens,
6651
- cached: row.span.cachedTokens
6651
+ prompt: reportedCount(row.span.promptTokens),
6652
+ completion: reportedCount(row.span.completionTokens),
6653
+ cached: reportedCount(row.span.cachedTokens)
6652
6654
  } : sumTokens(kids);
6653
6655
  if (tokens) row.tokens = tokens;
6654
6656
  if (row.hasChildren && costKnown) row.rollupCostUsd = cost;
@@ -6721,13 +6723,14 @@ function mergeStats(rows) {
6721
6723
  }
6722
6724
  function sumTokens(rows) {
6723
6725
  let out;
6724
- for (const row of rows) for (const key of [
6726
+ const sources = rows.filter((row) => row.kind === "model" || row.tokens !== void 0);
6727
+ for (const key of [
6725
6728
  "prompt",
6726
6729
  "completion",
6727
6730
  "cached"
6728
6731
  ]) {
6729
- const n = row.tokens?.[key];
6730
- if (n !== void 0) (out ??= {})[key] = (out[key] ?? 0) + n;
6732
+ const total = sumReportedCounts(sources.map((row) => row.tokens?.[key]));
6733
+ if (total !== void 0) (out ??= {})[key] = total;
6731
6734
  }
6732
6735
  return out;
6733
6736
  }
@@ -6851,39 +6854,40 @@ function busyMs(row, clockMs) {
6851
6854
  }
6852
6855
  return total;
6853
6856
  }
6854
- /** Totals as of a playback position, from per-span records. A total the spans do not carry stays the trace's own. */
6857
+ /** Complete per-span usage drives playback; authoritative call/error counts remain trace totals. */
6855
6858
  function totalsAt(trace, clockMs) {
6856
- let cost = 0, tokens = 0, models = 0, tools = 0, errors = 0;
6859
+ let cost = 0;
6857
6860
  let costs = false, counted = false;
6861
+ const tokens = [];
6858
6862
  for (const span of trace.spans) {
6859
6863
  if (span.startMs > clockMs) continue;
6860
- if (span.kind === "model") models++;
6861
- if (span.kind === "tool") tools++;
6864
+ const aggregate = span.meta?.["cli_bridge.usage.scope"] === "sum_of_children";
6862
6865
  if (span.endMs > clockMs) continue;
6863
- if (spanFailed(span)) errors++;
6864
6866
  if (span.costUsd !== void 0) {
6865
6867
  cost += span.costUsd;
6866
6868
  costs = true;
6867
6869
  }
6868
- if (span.promptTokens !== void 0 || span.completionTokens !== void 0) {
6869
- tokens += (span.promptTokens ?? 0) + (span.completionTokens ?? 0);
6870
- counted = true;
6870
+ if (!aggregate && (span.kind === "model" || span.promptTokens !== void 0 || span.completionTokens !== void 0)) {
6871
+ tokens.push(sumReportedCounts([span.promptTokens, span.completionTokens]));
6872
+ counted ||= span.promptTokens !== void 0 || span.completionTokens !== void 0;
6871
6873
  }
6872
6874
  }
6873
6875
  return {
6874
6876
  costUsd: trace.costUsd === void 0 ? void 0 : costs ? cost : trace.costUsd,
6875
- tokens: counted ? tokens : trace.promptTokens + trace.completionTokens,
6876
- modelCalls: trace.modelCalls === void 0 ? void 0 : models,
6877
- toolCalls: trace.toolCalls === void 0 ? void 0 : tools,
6878
- errors
6877
+ tokens: counted ? sumReportedCounts(tokens) : sumReportedCounts([trace.promptTokens, trace.completionTokens]),
6878
+ modelCalls: reportedCount(trace.modelCalls),
6879
+ toolCalls: reportedCount(trace.toolCalls),
6880
+ errors: reportedCount(trace.errors)
6879
6881
  };
6880
6882
  }
6881
6883
  /** The cost a reader can rely on: a lower bound when some calls are unpriced, a dash only when nothing is priced. */
6882
6884
  function costSummary(costUsd, trace) {
6885
+ costUsd = finiteNumber(costUsd);
6886
+ const billedUsd = finiteNumber(trace.billedUsd);
6883
6887
  const list = trace.costBasis === "list";
6884
6888
  const label = list ? "Cost · list price" : "Cost";
6885
- if (costUsd === void 0) return trace.billedUsd !== void 0 ? {
6886
- value: `${formatCost(trace.billedUsd)} billed`,
6889
+ if (costUsd === void 0) return billedUsd !== void 0 ? {
6890
+ value: `${formatCost(billedUsd)} billed`,
6887
6891
  title: "No list price for these calls; billed is the seat charge",
6888
6892
  label
6889
6893
  } : {
@@ -6895,13 +6899,13 @@ function costSummary(costUsd, trace) {
6895
6899
  const partial = coverage !== void 0 && coverage.priced < coverage.total;
6896
6900
  const upper = !partial && trace.costBound === "upper";
6897
6901
  return {
6898
- value: `${partial || trace.costBound === "lower" ? "≥" : upper ? "≤" : list ? "≈" : ""}${formatCost(costUsd)}${trace.billedUsd !== void 0 ? ` · ${formatCost(trace.billedUsd)} billed` : ""}`,
6902
+ value: `${partial || trace.costBound === "lower" ? "≥" : upper ? "≤" : list ? "≈" : ""}${formatCost(costUsd)}${billedUsd !== void 0 ? ` · ${formatCost(billedUsd)} billed` : ""}`,
6899
6903
  title: [
6900
6904
  partial ? `${coverage.priced} of ${coverage.total} model calls priced` : coverage ? `all ${coverage.total} model calls priced` : "",
6901
6905
  list ? "at the model catalog list price; not a bill" : "",
6902
6906
  upper ? "cached input is not split out, so it is priced as fresh input: at most this" : "",
6903
6907
  trace.costBound === "lower" ? "a floor: the producer reports at least this" : "",
6904
- trace.billedUsd !== void 0 ? `billed ${formatCost(trace.billedUsd)} (the seat charge)` : ""
6908
+ billedUsd !== void 0 ? `billed ${formatCost(billedUsd)} (the seat charge)` : ""
6905
6909
  ].filter(Boolean).join("; "),
6906
6910
  label
6907
6911
  };
@@ -7315,9 +7319,12 @@ function metrics(row, clockMs, full) {
7315
7319
  ["completion", "out"],
7316
7320
  ["cached", "cached"]
7317
7321
  ].flatMap(([key, unit]) => tokens[key] === void 0 ? [] : [count(tokens[key], unit)]);
7318
- out.push(["Tokens", /* @__PURE__ */ jsx("span", {
7322
+ out.push(["Tokens", parts.length ? /* @__PURE__ */ jsx("span", {
7319
7323
  className: `fw-card-parts${parts.length > 2 ? " fw-card-stack" : ""}`,
7320
7324
  children: parts.map((part, i) => /* @__PURE__ */ jsx("span", { children: part }, i))
7325
+ }) : /* @__PURE__ */ jsx("span", {
7326
+ title: "Tokens not reported",
7327
+ children: "—"
7321
7328
  })]);
7322
7329
  }
7323
7330
  const latency = row.modelMs;
@@ -8716,36 +8723,42 @@ function usual(values) {
8716
8723
  /** The summary strip: as of the playhead while scrubbing, the producer's totals otherwise. */
8717
8724
  function summaryCells(trace, at, durationMs) {
8718
8725
  const cost = costSummary(at ? at.costUsd : trace.costUsd, trace);
8719
- const tokens = at ? at.tokens : trace.promptTokens + trace.completionTokens;
8720
- const modelCalls = at ? at.modelCalls : trace.modelCalls;
8721
- const errors = at ? at.errors : trace.errors ?? (trace.spans.filter((span) => spanFailed(span) && span.kind !== "pipeline").length || trace.spans.filter(spanFailed).length);
8726
+ const tokens = at ? at.tokens : sumReportedCounts([trace.promptTokens, trace.completionTokens]);
8727
+ const modelCalls = reportedCount(at ? at.modelCalls : trace.modelCalls);
8728
+ const toolCalls = reportedCount(at ? at.toolCalls : trace.toolCalls);
8729
+ const observedErrors = trace.spans.filter((span) => spanFailed(span) && span.kind !== "pipeline").length || trace.spans.filter(spanFailed).length;
8730
+ const errors = at ? at.errors : trace.errors === void 0 ? observedErrors || void 0 : reportedCount(trace.errors);
8722
8731
  return {
8723
8732
  cost,
8724
8733
  tokens,
8725
8734
  errors,
8726
8735
  summary: [
8727
8736
  ["Duration", formatSpanDuration(durationMs)],
8728
- ...cost.value === "—" ? [] : [[
8737
+ [
8729
8738
  cost.label,
8730
8739
  cost.value,
8731
8740
  cost.title
8732
- ]],
8741
+ ],
8733
8742
  [
8734
8743
  "Tokens",
8735
- formatCount(tokens),
8736
- `${tokens.toLocaleString()} tokens`
8744
+ tokens === void 0 ? "—" : formatCount(tokens),
8745
+ tokens === void 0 ? "Tokens not reported" : `${tokens.toLocaleString()} tokens`
8737
8746
  ],
8738
8747
  [
8739
8748
  "Model calls",
8740
8749
  modelCalls === void 0 ? "—" : modelCalls.toLocaleString(),
8741
- modelCalls === void 0 ? "Model output windows do not establish the number of model requests" : void 0
8750
+ modelCalls === void 0 ? "Model calls not reported" : at ? "Reported trace total" : void 0
8742
8751
  ],
8743
8752
  [
8744
8753
  "Tool calls",
8745
- (at ? at.toolCalls : trace.toolCalls)?.toLocaleString() ?? "—",
8746
- (at ? at.toolCalls : trace.toolCalls) === void 0 ? "The producer did not count tool calls" : void 0
8754
+ toolCalls?.toLocaleString() ?? "—",
8755
+ toolCalls === void 0 ? "Tool calls not reported" : at ? "Reported trace total" : void 0
8747
8756
  ],
8748
- ["Errors", errors.toLocaleString()]
8757
+ [
8758
+ "Errors",
8759
+ errors?.toLocaleString() ?? "—",
8760
+ errors === void 0 ? "Errors not reported" : at ? "Reported trace total" : void 0
8761
+ ]
8749
8762
  ]
8750
8763
  };
8751
8764
  }
@@ -9223,7 +9236,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
9223
9236
  const quietAgents = agents.filter((row) => quietActivity(row, clockMs).quiet).length;
9224
9237
  const recordedErrors = trace.spans.filter((span) => spanFailed(span) && span.kind !== "pipeline");
9225
9238
  const errorSpans = recordedErrors.length ? recordedErrors : trace.spans.filter(spanFailed);
9226
- const errorTimes = Array.from({ length: errors }, (_, index) => errorSpans[index]?.endMs);
9239
+ const errorTimes = Array.from({ length: errors ?? 0 }, (_, index) => errorSpans[index]?.endMs);
9227
9240
  const done = !isLive && !trace.spans.some(spanRunning);
9228
9241
  const shown = (row) => row && drawnRow(row, visible);
9229
9242
  const cardRow = shown(card ? byId.get(card.target) : void 0);
@@ -9410,7 +9423,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
9410
9423
  showSummary && /* @__PURE__ */ jsx("dl", {
9411
9424
  className: "fw-summary",
9412
9425
  children: summary.map(([name, value, title]) => /* @__PURE__ */ jsxs("div", {
9413
- className: name === "Errors" && errors > 0 ? "fw-has-errors" : void 0,
9426
+ className: name === "Errors" && errors !== void 0 && errors > 0 ? "fw-has-errors" : void 0,
9414
9427
  children: [/* @__PURE__ */ jsx("dt", { children: name }), /* @__PURE__ */ jsx("dd", {
9415
9428
  className: "fw-value",
9416
9429
  title,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
4
  "description": "Static SVG and HTML figures for benchmarks and operational timing. Draws from producer data and refuses what it cannot draw honestly.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@
52
52
  "check:run-tokens": "node scripts/check-run-tokens.mjs"
53
53
  },
54
54
  "devDependencies": {
55
- "@tangle-network/brand": "1.31.1",
55
+ "@tangle-network/brand": "^1.31.1",
56
56
  "@testing-library/react": "^16.3.3",
57
57
  "@testing-library/user-event": "^14.6.7",
58
58
  "@types/node": "^26.6.3",
@@ -66,7 +66,7 @@
66
66
  "tsdown": "^0.23.0",
67
67
  "typescript": "^7.0.2",
68
68
  "vitest": "^5.0.2",
69
- "@tangle-network/ui": "11.51.1"
69
+ "@tangle-network/ui": "^11.51.1"
70
70
  },
71
71
  "keywords": [
72
72
  "tangle",