@tangle-network/charts 0.15.1 → 0.16.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 +15 -6
- package/dist/{flow-trace-B5dlYNsD.d.ts → flow-trace-DTyBgPp8.d.ts} +17 -0
- package/dist/index.d.ts +56 -2
- package/dist/index.js +234 -1
- package/dist/react/flow-waterfall.css +292 -68
- package/dist/react/index.d.ts +13 -3
- package/dist/react/index.js +1730 -368
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Benchmark figures as static SVG and HTML tables, plus optional React timing prim
|
|
|
4
4
|
|
|
5
5
|
The React entry also exports `DayBars`, `HBars`, `Histogram`, `Lines`, `Meter`, `Spark`, and `Gantt` for live operational pages. Import `@tangle-network/charts/react/live.css` for their layout and set `--chart-live-*` theme tokens on an ancestor. Data, state colors, destination links, labels, and interpretation remain with the caller. `DayBars` accepts an `onOpen(index)` action with keyboard activation.
|
|
6
6
|
|
|
7
|
-
`FlowWaterfall` from `@tangle-network/charts/react` is the shared live trace inspector. It owns hierarchy layout, folding, search, playback and running-span growth. Producers own identity, `parentId`, explicit `status` and observed totals. Import `@tangle-network/charts/react/flow-waterfall.css`; its `fw-*` classes and six-field summary support hosts without a Tailwind build. The React-free root exports the `FlowSpan` and `FlowTrace` types.
|
|
7
|
+
`FlowWaterfall` from `@tangle-network/charts/react` is the shared live trace inspector: an agent timeline (one row per agent, elbow handoffs, hatched waiting) and a virtualized span waterfall over one clock. It owns hierarchy layout, folding, search, playback, zoom, span cards and running-span growth. Producers own identity, `parentId`, explicit `status` and observed totals. Import `@tangle-network/charts/react/flow-waterfall.css`; its `fw-*` classes and six-field summary support hosts without a Tailwind build. The React-free root exports the `FlowSpan` and `FlowTrace` types.
|
|
8
8
|
|
|
9
9
|
`WaterfallHeader`, `WaterfallTimeAxis`, `WaterfallBar` and `WaterfallRow` remain available for existing consumers that own their own hierarchy and selection. All use the same `waterfallSpanGeometry` and `waterfallTicks`. Existing fixed-width cells, tones, formatting, attribution slots and unordered-time `—` marks are preserved. Optional fluid tracks, inner-bar colour/class, overlay and clipping fields support the shared inspector without changing existing calls. The primitives were extracted from Intelligence under [brand #211](https://github.com/tangle-network/brand/pull/211); the shared inspector extends that presentation ownership while leaving failure interpretation with producers.
|
|
10
10
|
|
|
@@ -87,6 +87,8 @@ Identity is carried by direct labels, never by hue.
|
|
|
87
87
|
|
|
88
88
|
## Develop
|
|
89
89
|
|
|
90
|
+
Local Beelink gate (set `TOUCHED_INTEGRATION_TEST` to the touched integration test file; the gate installs with npm ci from package-lock.json): `beelink-gate auto https://github.com/tangle-network/charts.git "$(git rev-parse HEAD)" -- sh -c 'npm run typecheck && npm exec -- vitest run "$1"' _ "${TOUCHED_INTEGRATION_TEST:?set touched integration test path}"`.
|
|
91
|
+
|
|
90
92
|
```sh
|
|
91
93
|
npm ci
|
|
92
94
|
npm run gen:css # after editing src/figure.css or Brand tokens
|
|
@@ -323,16 +325,23 @@ The component tests cover gap/zero/one/equal/negative cases, controlled identity
|
|
|
323
325
|
|
|
324
326
|
```tsx
|
|
325
327
|
import { FlowWaterfall } from '@tangle-network/charts/react';
|
|
326
|
-
import
|
|
328
|
+
import { catalogPrice, otlpFlowTrace, parseOtlpJsonl } from '@tangle-network/charts';
|
|
327
329
|
import '@tangle-network/charts/react/flow-waterfall.css';
|
|
328
330
|
|
|
329
|
-
// The
|
|
330
|
-
|
|
331
|
+
// The one input contract: OTLP-JSONL from `traces convert <ref> --out spans.otlp.jsonl`.
|
|
332
|
+
const { spans } = parseOtlpJsonl(text);
|
|
333
|
+
// List prices come from the price owner at runtime (the Router's /v1/models), never a copied table.
|
|
334
|
+
const trace = otlpFlowTrace(spans, { price: catalogPrice(await (await fetch(catalogUrl)).json()) });
|
|
335
|
+
<FlowWaterfall trace={trace} live={running} defaultView="agents" renderIcon={brandMark} />;
|
|
331
336
|
```
|
|
332
337
|
|
|
333
|
-
|
|
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. 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
|
+
|
|
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 (CSS variables carry the clock). 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.
|
|
341
|
+
|
|
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.
|
|
334
343
|
|
|
335
|
-
**Provenance.** FlowWaterfall and its recorded Discovery fixture move from agent-app #1267/#1273 at `78a3e8f7fd9bf93b23afa8290db6d721db544237`; the independent implementation copied no paid reference assets. [Fixture provenance](docs/fixtures/discovery-waterfall-provenance.md) records measured timing, observed token totals and unknowns. The historical [baseline](docs/fixtures/live-playback-rates-baseline.json) and [fixed](docs/fixtures/live-playback-rates-fixed.json) rate measurements are retained. The packed public consumer proof measures all five rates against actual browser wall time during live growth, with ±15% acceptance, using React 18 and 19 at 1100px and 390px.
|
|
344
|
+
**Provenance.** FlowWaterfall and its recorded Discovery fixture move from agent-app #1267/#1273 at `78a3e8f7fd9bf93b23afa8290db6d721db544237`; the independent implementation copied no paid reference assets. [Fixture provenance](docs/fixtures/discovery-waterfall-provenance.md) records measured timing, observed token totals and unknowns. `trace-release.json` and `trace-incident.json` are invented fixtures shaped like the design references (agent counts, durations and states), with original text. The historical [baseline](docs/fixtures/live-playback-rates-baseline.json) and [fixed](docs/fixtures/live-playback-rates-fixed.json) rate measurements are retained. The packed public consumer proof measures all five rates against actual browser wall time during live growth, with ±15% acceptance, using React 18 and 19 at 1100px and 390px.
|
|
336
345
|
|
|
337
346
|
```sh
|
|
338
347
|
npm run build
|
|
@@ -317,6 +317,21 @@ interface FlowSpan {
|
|
|
317
317
|
startMs: number;
|
|
318
318
|
endMs: number;
|
|
319
319
|
approx?: boolean;
|
|
320
|
+
/** Muted text after the name: the agent's model or the tool's provider. */
|
|
321
|
+
detail?: string;
|
|
322
|
+
/** Host icon key, passed to `renderIcon`; the inspector draws a kind glyph without one. */
|
|
323
|
+
icon?: string;
|
|
324
|
+
/** Recorded values only. Absent means unknown and is shown as Unknown, never as zero. */
|
|
325
|
+
costUsd?: number;
|
|
326
|
+
promptTokens?: number;
|
|
327
|
+
completionTokens?: number;
|
|
328
|
+
/** Short producer summaries shown in the span card. */
|
|
329
|
+
input?: string;
|
|
330
|
+
output?: string;
|
|
331
|
+
/** The failed span this one retries; the bar carries a retry glyph. */
|
|
332
|
+
retryOf?: string;
|
|
333
|
+
/** For a handoff, the span that received the work; a connector joins the two rows. */
|
|
334
|
+
handoffTo?: string;
|
|
320
335
|
meta?: Record<string, unknown>;
|
|
321
336
|
}
|
|
322
337
|
/** Describe the structure of a flow trace including spans, timing, tokens, cost, and tool calls */
|
|
@@ -327,6 +342,8 @@ interface FlowTrace {
|
|
|
327
342
|
completionTokens: number;
|
|
328
343
|
/** Computed when per-token pricing is supplied. */
|
|
329
344
|
costUsd?: number;
|
|
345
|
+
/** `list`: priced from a model catalog's list prices, not a bill; the summary says so. */
|
|
346
|
+
costBasis?: 'billed' | 'list';
|
|
330
347
|
toolCalls: number;
|
|
331
348
|
/** Authoritative LLM request count when supplied; absent means unknown. */
|
|
332
349
|
modelCalls?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as CostBasis, B as Tier, C as MarkerShape, D as YTick, E as YTarget, F as IntervalMethod, H as isRefusal, I as MatrixCell, L as RateRow, M as Exclusion, N as Figure, O as BreakdownInput, P as Interval, R as Refusal, S as MarkVerdict, T as YScale, V as TierCounts, _ as PointGlyph, a as TimelineSpan, b as ChartMark, c as CHECKER_HUES, d as OUTCOME_COLORS, f as OUTCOME_ORDER, g as PointFields, h as checkerColor, i as TimelineOptions, j as Estimate, k as ComparisonRow, l as ENDING_COLORS, m as TickOutcome, n as FlowTrace, o as TimelineTick, p as RunEnding, r as TimelineLane, s as timeline, t as FlowSpan, u as ENDING_ORDER, v as BandKind, w as XCategory, x as MarkKind, y as ChartBand, z as Setup } from "./flow-trace-
|
|
1
|
+
import { A as CostBasis, B as Tier, C as MarkerShape, D as YTick, E as YTarget, F as IntervalMethod, H as isRefusal, I as MatrixCell, L as RateRow, M as Exclusion, N as Figure, O as BreakdownInput, P as Interval, R as Refusal, S as MarkVerdict, T as YScale, V as TierCounts, _ as PointGlyph, a as TimelineSpan, b as ChartMark, c as CHECKER_HUES, d as OUTCOME_COLORS, f as OUTCOME_ORDER, g as PointFields, h as checkerColor, i as TimelineOptions, j as Estimate, k as ComparisonRow, l as ENDING_COLORS, m as TickOutcome, n as FlowTrace, o as TimelineTick, p as RunEnding, r as TimelineLane, s as timeline, t as FlowSpan, u as ENDING_ORDER, v as BandKind, w as XCategory, x as MarkKind, y as ChartBand, z as Setup } from "./flow-trace-DTyBgPp8.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. */
|
|
@@ -344,4 +344,58 @@ interface UnitDotOptions {
|
|
|
344
344
|
*/
|
|
345
345
|
export declare function unitDotLayout(groups: DotGroup[], width: number, height: number, o?: UnitDotOptions): UnitDotLayout;
|
|
346
346
|
//#endregion
|
|
347
|
-
|
|
347
|
+
//#region src/otlp-trace.d.ts
|
|
348
|
+
/** One span of the OTLP-JSONL contract written by `traces convert` (tangle-network/traces). */
|
|
349
|
+
interface OtlpSpan {
|
|
350
|
+
trace_id: string;
|
|
351
|
+
span_id: string;
|
|
352
|
+
parent_span_id?: string;
|
|
353
|
+
name: string;
|
|
354
|
+
kind: 'AGENT' | 'LLM' | 'TOOL' | 'CHAIN' | string;
|
|
355
|
+
start_time: string;
|
|
356
|
+
end_time: string;
|
|
357
|
+
status?: {
|
|
358
|
+
code?: string;
|
|
359
|
+
message?: string;
|
|
360
|
+
};
|
|
361
|
+
attributes?: Record<string, unknown>;
|
|
362
|
+
resource?: {
|
|
363
|
+
attributes?: Record<string, unknown>;
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
interface OtlpTraceOptions {
|
|
367
|
+
/** The stream is still being written: open agent spans run to the observation time. */
|
|
368
|
+
live?: boolean;
|
|
369
|
+
/** Attributes that name an agent, in order. Spans that carry one and have no agent ancestor are grouped under it. */
|
|
370
|
+
agentKeys?: string[];
|
|
371
|
+
/** List price of one model call; undefined leaves it unknown. Build it from the price owner with `catalogPrice`. */
|
|
372
|
+
price?: (model: string, inputTokens: number, outputTokens: number) => number | undefined;
|
|
373
|
+
}
|
|
374
|
+
/** Price model calls from an OpenAI-compatible model catalog (`data[].pricing.prompt|completion`, USD per token), such as
|
|
375
|
+
* the Router's `/v1/models`. Read the catalog at runtime; never copy its numbers. */
|
|
376
|
+
export declare function catalogPrice(catalog: {
|
|
377
|
+
data?: Array<{
|
|
378
|
+
id?: string;
|
|
379
|
+
pricing?: {
|
|
380
|
+
prompt?: string | number;
|
|
381
|
+
completion?: string | number;
|
|
382
|
+
};
|
|
383
|
+
}>;
|
|
384
|
+
}): (model: string, inputTokens: number, outputTokens: number) => number | undefined;
|
|
385
|
+
/** Parse OTLP-JSONL text; blank and malformed lines are counted, never thrown. */
|
|
386
|
+
export declare function parseOtlpJsonl(text: string): {
|
|
387
|
+
spans: OtlpSpan[];
|
|
388
|
+
malformed: number;
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Project OTLP spans onto the inspector's trace. Kinds map AGENT→agent row, LLM→model call, TOOL→tool call; a TOOL
|
|
392
|
+
* marked `traces.agent.spawn` becomes a handoff to the agent it parents. CHAIN spans become rows only when they have
|
|
393
|
+
* children; leaf CHAIN markers (prompts, message boundaries) are counted in `omitted`. Tokens are
|
|
394
|
+
* `gen_ai.usage.input_tokens` + `output_tokens`; the contract carries no cost, so cost stays unknown.
|
|
395
|
+
* A later span with the same span_id replaces an earlier one, so live deltas can be appended as they arrive.
|
|
396
|
+
*/
|
|
397
|
+
export declare function otlpFlowTrace(input: OtlpSpan[], options?: OtlpTraceOptions): FlowTrace & {
|
|
398
|
+
omitted: number;
|
|
399
|
+
};
|
|
400
|
+
//#endregion
|
|
401
|
+
export { type BandKind, type BreakdownInput, type BreakdownOptions, CHECKER_HUES, type ChartBand, type ChartMark, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type DotGroup, type DotGroupRect, ENDING_COLORS, ENDING_ORDER, type Estimate, type Exclusion, type Figure, type FlowSpan, type FlowTrace, type HeatmapCell, type HeatmapColumn, type HeatmapOptions, type HeatmapRow, type HeatmapStatus, type Interval, type IntervalMethod, type LatencyRangeOptions, type LatencyRangeRow, type MarkKind, type MarkVerdict, type MatrixCell, type MetricBarsOptions, type MetricRow, type MultiplePanel, OUTCOME_COLORS, OUTCOME_ORDER, type Observations, type OtlpSpan, type OtlpTraceOptions, type PhaseWaterfallOptions, type PointFields, type PointGlyph, type RankedRatesOptions, type RateIdentity, type RateRow, type Refusal, type RenderFigureOptions, type RunEnding, type Setup, type SmallMultiplesOptions, type TaskMatrixOptions, type TickOutcome, type Tier, type TierCounts, type TimePoint, type TimeSeriesOptions, type TimeSeriesRow, type TimelineLane, type TimelineOptions, type TimelineSpan, type TimelineTick, type UnitDotLayout, type UnitDotOptions, type WaterfallPhase, type XCategory, type YScale, type YTarget, type YTick, checkerColor, isRefusal, timeline };
|
package/dist/index.js
CHANGED
|
@@ -3336,4 +3336,237 @@ function unitDotLayout(groups, width, height, o = {}) {
|
|
|
3336
3336
|
};
|
|
3337
3337
|
}
|
|
3338
3338
|
//#endregion
|
|
3339
|
-
|
|
3339
|
+
//#region src/otlp-trace.ts
|
|
3340
|
+
/** Price model calls from an OpenAI-compatible model catalog (`data[].pricing.prompt|completion`, USD per token), such as
|
|
3341
|
+
* the Router's `/v1/models`. Read the catalog at runtime; never copy its numbers. */
|
|
3342
|
+
function catalogPrice(catalog) {
|
|
3343
|
+
const rates = /* @__PURE__ */ new Map();
|
|
3344
|
+
for (const model of catalog.data ?? []) {
|
|
3345
|
+
const input = Number(model.pricing?.prompt), output = Number(model.pricing?.completion);
|
|
3346
|
+
if (model.id && Number.isFinite(input) && Number.isFinite(output)) rates.set(model.id, {
|
|
3347
|
+
input,
|
|
3348
|
+
output
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
return (model, inputTokens, outputTokens) => {
|
|
3352
|
+
const rate = rates.get(model) ?? rates.get(shortModel(model));
|
|
3353
|
+
return rate ? rate.input * inputTokens + rate.output * outputTokens : void 0;
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
/** The OpenTelemetry GenAI name first, then the agent identities current producers write. */
|
|
3357
|
+
const AGENT_KEYS = [
|
|
3358
|
+
"gen_ai.agent.name",
|
|
3359
|
+
"agent.name",
|
|
3360
|
+
"cli_bridge.profile.name"
|
|
3361
|
+
];
|
|
3362
|
+
const MODEL_KEYS = [
|
|
3363
|
+
"gen_ai.response.model",
|
|
3364
|
+
"gen_ai.request.model",
|
|
3365
|
+
"llm.model_name"
|
|
3366
|
+
];
|
|
3367
|
+
/** Parse OTLP-JSONL text; blank and malformed lines are counted, never thrown. */
|
|
3368
|
+
function parseOtlpJsonl(text) {
|
|
3369
|
+
const spans = [];
|
|
3370
|
+
let malformed = 0;
|
|
3371
|
+
for (const line of text.split("\n")) {
|
|
3372
|
+
if (!line.trim()) continue;
|
|
3373
|
+
try {
|
|
3374
|
+
const span = JSON.parse(line);
|
|
3375
|
+
if (typeof span.span_id === "string" && typeof span.start_time === "string") spans.push(span);
|
|
3376
|
+
else malformed++;
|
|
3377
|
+
} catch {
|
|
3378
|
+
malformed++;
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
return {
|
|
3382
|
+
spans,
|
|
3383
|
+
malformed
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
const str = (value) => typeof value === "string" && value ? value : void 0;
|
|
3387
|
+
const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3388
|
+
const pick = (attributes, keys) => {
|
|
3389
|
+
for (const key of keys) {
|
|
3390
|
+
const value = str(attributes[key]);
|
|
3391
|
+
if (value) return value;
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3394
|
+
/** `opencode/zai-coding-plan/glm-5.3` reads as `glm-5.3`; the full id stays in the span card input. */
|
|
3395
|
+
const shortModel = (model) => model.split("/").pop() ?? model;
|
|
3396
|
+
/**
|
|
3397
|
+
* Project OTLP spans onto the inspector's trace. Kinds map AGENT→agent row, LLM→model call, TOOL→tool call; a TOOL
|
|
3398
|
+
* marked `traces.agent.spawn` becomes a handoff to the agent it parents. CHAIN spans become rows only when they have
|
|
3399
|
+
* children; leaf CHAIN markers (prompts, message boundaries) are counted in `omitted`. Tokens are
|
|
3400
|
+
* `gen_ai.usage.input_tokens` + `output_tokens`; the contract carries no cost, so cost stays unknown.
|
|
3401
|
+
* A later span with the same span_id replaces an earlier one, so live deltas can be appended as they arrive.
|
|
3402
|
+
*/
|
|
3403
|
+
function otlpFlowTrace(input, options = {}) {
|
|
3404
|
+
const agentKeys = options.agentKeys ?? AGENT_KEYS;
|
|
3405
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3406
|
+
for (const span of input) byId.set(span.span_id, span);
|
|
3407
|
+
const spans = [...byId.values()];
|
|
3408
|
+
const time = (iso) => Date.parse(iso);
|
|
3409
|
+
const origin = spans.reduce((min, span) => Math.min(min, time(span.start_time)), Infinity);
|
|
3410
|
+
const last = spans.reduce((max, span) => Math.max(max, time(span.end_time), time(span.start_time)), -Infinity);
|
|
3411
|
+
const parents = new Set(spans.flatMap((span) => span.parent_span_id ? [span.parent_span_id] : []));
|
|
3412
|
+
const attrs = (span) => span.attributes ?? {};
|
|
3413
|
+
const isAgent = (span) => span.kind === "AGENT";
|
|
3414
|
+
const agentAncestor = (span) => {
|
|
3415
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3416
|
+
for (let cursor = span.parent_span_id ? byId.get(span.parent_span_id) : void 0; cursor && !seen.has(cursor.span_id); cursor = cursor.parent_span_id ? byId.get(cursor.parent_span_id) : void 0) {
|
|
3417
|
+
seen.add(cursor.span_id);
|
|
3418
|
+
if (isAgent(cursor)) return cursor;
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3422
|
+
const groupOf = /* @__PURE__ */ new Map();
|
|
3423
|
+
for (const span of spans) {
|
|
3424
|
+
if (isAgent(span) || span.parent_span_id && byId.has(span.parent_span_id)) continue;
|
|
3425
|
+
const name = pick(attrs(span), agentKeys);
|
|
3426
|
+
if (!name) continue;
|
|
3427
|
+
groupOf.set(span.span_id, name);
|
|
3428
|
+
const group = grouped.get(name) ?? {
|
|
3429
|
+
start: Infinity,
|
|
3430
|
+
end: -Infinity,
|
|
3431
|
+
models: /* @__PURE__ */ new Map(),
|
|
3432
|
+
lastEnd: -Infinity,
|
|
3433
|
+
failed: false
|
|
3434
|
+
};
|
|
3435
|
+
group.start = Math.min(group.start, time(span.start_time));
|
|
3436
|
+
group.end = Math.max(group.end, time(span.end_time));
|
|
3437
|
+
if (time(span.end_time) >= group.lastEnd) {
|
|
3438
|
+
group.lastEnd = time(span.end_time);
|
|
3439
|
+
group.failed = span.status?.code === "STATUS_CODE_ERROR";
|
|
3440
|
+
}
|
|
3441
|
+
grouped.set(name, group);
|
|
3442
|
+
}
|
|
3443
|
+
const models = /* @__PURE__ */ new Map();
|
|
3444
|
+
const tally = (key, model) => {
|
|
3445
|
+
const counts = models.get(key) ?? /* @__PURE__ */ new Map();
|
|
3446
|
+
counts.set(model, (counts.get(model) ?? 0) + 1);
|
|
3447
|
+
models.set(key, counts);
|
|
3448
|
+
};
|
|
3449
|
+
const rootOf = (span) => {
|
|
3450
|
+
let cursor = span;
|
|
3451
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3452
|
+
while (cursor.parent_span_id && byId.has(cursor.parent_span_id) && !seen.has(cursor.span_id)) {
|
|
3453
|
+
seen.add(cursor.span_id);
|
|
3454
|
+
cursor = byId.get(cursor.parent_span_id);
|
|
3455
|
+
}
|
|
3456
|
+
return cursor;
|
|
3457
|
+
};
|
|
3458
|
+
for (const span of spans) {
|
|
3459
|
+
if (span.kind !== "LLM") continue;
|
|
3460
|
+
const model = pick(attrs(span), MODEL_KEYS);
|
|
3461
|
+
if (!model) continue;
|
|
3462
|
+
const agent = agentAncestor(span);
|
|
3463
|
+
const group = agent ? void 0 : groupOf.get(rootOf(span).span_id);
|
|
3464
|
+
if (agent) tally(agent.span_id, shortModel(model));
|
|
3465
|
+
else if (group) tally(`group:${group}`, shortModel(model));
|
|
3466
|
+
}
|
|
3467
|
+
const topModel = (key) => [...models.get(key)?.entries() ?? []].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
3468
|
+
const out = [];
|
|
3469
|
+
let omitted = 0;
|
|
3470
|
+
let promptTokens = 0;
|
|
3471
|
+
let completionTokens = 0;
|
|
3472
|
+
let toolCalls = 0;
|
|
3473
|
+
let modelCalls = 0;
|
|
3474
|
+
let errors = 0;
|
|
3475
|
+
let cost = 0;
|
|
3476
|
+
let unpriced = 0;
|
|
3477
|
+
const offset = (iso) => time(iso) - origin;
|
|
3478
|
+
const isSpawn = (span) => span.kind === "TOOL" && (attrs(span)["traces.agent.spawn"] === true || /spawn/i.test(str(attrs(span)["gen_ai.tool.name"]) ?? span.name));
|
|
3479
|
+
const groupOfSpan = (span) => groupOf.get(rootOf(span).span_id);
|
|
3480
|
+
const spawns = spans.filter((span) => isSpawn(span) && groupOfSpan(span)).sort((a, b) => time(a.start_time) - time(b.start_time));
|
|
3481
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
3482
|
+
const spawnedBy = /* @__PURE__ */ new Map();
|
|
3483
|
+
for (const [name, group] of [...grouped].sort((a, b) => a[1].start - b[1].start)) {
|
|
3484
|
+
let pick;
|
|
3485
|
+
for (const spawn of spawns) {
|
|
3486
|
+
if (time(spawn.start_time) > group.start) break;
|
|
3487
|
+
if (!claimed.has(spawn.span_id) && groupOfSpan(spawn) !== name) pick = spawn;
|
|
3488
|
+
}
|
|
3489
|
+
if (pick) {
|
|
3490
|
+
claimed.add(pick.span_id);
|
|
3491
|
+
spawnedBy.set(name, pick);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
const handoffTo = new Map([...spawnedBy].map(([name, spawn]) => [spawn.span_id, `group:${name}`]));
|
|
3495
|
+
for (const [name, group] of grouped) {
|
|
3496
|
+
const spawn = spawnedBy.get(name);
|
|
3497
|
+
out.push({
|
|
3498
|
+
id: `group:${name}`,
|
|
3499
|
+
kind: "pipeline",
|
|
3500
|
+
name,
|
|
3501
|
+
detail: topModel(`group:${name}`),
|
|
3502
|
+
startMs: group.start - origin,
|
|
3503
|
+
endMs: group.end - origin,
|
|
3504
|
+
...spawn ? { parentId: `group:${groupOfSpan(spawn)}` } : {},
|
|
3505
|
+
status: options.live ? "running" : group.failed ? "failed" : "done"
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
const subagentNumber = new Map(spans.filter((span) => isAgent(span) && span.parent_span_id && byId.has(span.parent_span_id) && !pick(attrs(span), agentKeys)).sort((a, b) => time(a.start_time) - time(b.start_time)).map((span, i) => [span.span_id, i + 1]));
|
|
3509
|
+
const spawnTarget = /* @__PURE__ */ new Map();
|
|
3510
|
+
for (const span of spans) if (isAgent(span) && span.parent_span_id) spawnTarget.set(span.parent_span_id, span.span_id);
|
|
3511
|
+
for (const span of spans) {
|
|
3512
|
+
const a = attrs(span);
|
|
3513
|
+
if (span.kind === "CHAIN" && !parents.has(span.span_id)) {
|
|
3514
|
+
omitted++;
|
|
3515
|
+
continue;
|
|
3516
|
+
}
|
|
3517
|
+
const failed = span.status?.code === "STATUS_CODE_ERROR";
|
|
3518
|
+
const startMs = offset(span.start_time);
|
|
3519
|
+
let endMs = Math.max(startMs, offset(span.end_time));
|
|
3520
|
+
const open = options.live === true && isAgent(span) && span.status?.code !== "STATUS_CODE_OK" && !failed;
|
|
3521
|
+
if (open) endMs = Math.max(endMs, last - origin);
|
|
3522
|
+
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;
|
|
3523
|
+
const model = pick(a, MODEL_KEYS);
|
|
3524
|
+
const prompt = num(a["gen_ai.usage.input_tokens"]);
|
|
3525
|
+
const completion = num(a["gen_ai.usage.output_tokens"]);
|
|
3526
|
+
let kind = span.kind === "LLM" ? "model" : span.kind === "TOOL" ? "tool" : "pipeline";
|
|
3527
|
+
if (kind === "tool" && (a["traces.agent.spawn"] === true || spawnTarget.has(span.span_id) || handoffTo.has(span.span_id))) kind = "handoff";
|
|
3528
|
+
let name = str(a["gen_ai.tool.name"]) ?? span.name;
|
|
3529
|
+
if (isAgent(span)) name = pick(a, agentKeys) ?? (subagentNumber.has(span.span_id) ? `Subagent ${subagentNumber.get(span.span_id)}` : str(a["service.name"]) ?? str(span.resource?.attributes?.["service.name"]) ?? span.name);
|
|
3530
|
+
else if (kind === "model" && (span.name === "llm.call" || span.name.startsWith("chat "))) name = "Model call";
|
|
3531
|
+
if (kind === "model") modelCalls++;
|
|
3532
|
+
if (kind === "tool" || kind === "handoff") toolCalls++;
|
|
3533
|
+
if (failed) errors++;
|
|
3534
|
+
promptTokens += prompt ?? 0;
|
|
3535
|
+
completionTokens += completion ?? 0;
|
|
3536
|
+
const listPrice = kind === "model" && model && options.price && (prompt !== void 0 || completion !== void 0) ? options.price(model, prompt ?? 0, completion ?? 0) : void 0;
|
|
3537
|
+
if (listPrice !== void 0) cost += listPrice;
|
|
3538
|
+
else if (kind === "model" && (prompt || completion)) unpriced++;
|
|
3539
|
+
out.push({
|
|
3540
|
+
id: span.span_id,
|
|
3541
|
+
...parentId ? { parentId } : {},
|
|
3542
|
+
kind,
|
|
3543
|
+
name,
|
|
3544
|
+
startMs,
|
|
3545
|
+
endMs,
|
|
3546
|
+
status: open ? "running" : failed ? "failed" : "done",
|
|
3547
|
+
...endMs === startMs && kind !== "pipeline" ? { approx: true } : {},
|
|
3548
|
+
...isAgent(span) ? { detail: topModel(span.span_id) } : kind === "model" && model ? { detail: shortModel(model) } : {},
|
|
3549
|
+
...listPrice !== void 0 ? { costUsd: listPrice } : {},
|
|
3550
|
+
...prompt !== void 0 ? { promptTokens: prompt } : {},
|
|
3551
|
+
...completion !== void 0 ? { completionTokens: completion } : {},
|
|
3552
|
+
...span.status?.message ? { output: span.status.message } : {},
|
|
3553
|
+
...spawnTarget.has(span.span_id) ? { handoffTo: spawnTarget.get(span.span_id) } : handoffTo.has(span.span_id) ? { handoffTo: handoffTo.get(span.span_id) } : {}
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3556
|
+
return {
|
|
3557
|
+
spans: out,
|
|
3558
|
+
totalMs: Number.isFinite(last - origin) ? last - origin : 0,
|
|
3559
|
+
promptTokens,
|
|
3560
|
+
completionTokens,
|
|
3561
|
+
toolCalls,
|
|
3562
|
+
modelCalls,
|
|
3563
|
+
errors,
|
|
3564
|
+
omitted,
|
|
3565
|
+
...options.price && unpriced === 0 && cost > 0 ? {
|
|
3566
|
+
costUsd: cost,
|
|
3567
|
+
costBasis: "list"
|
|
3568
|
+
} : {}
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
//#endregion
|
|
3572
|
+
export { CHECKER_HUES, ENDING_COLORS, ENDING_ORDER, OUTCOME_COLORS, OUTCOME_ORDER, breakdown, catalogPrice, chartTokensCss, checkerColor, comparisonIntervals, costFrontier, figureCss, heatmap, isRefusal, latencyRange, metricBars, otlpFlowTrace, parseOtlpJsonl, phaseWaterfall, rankedRates, rateOrder, renderFigure, renderNotes, renderRefusals, smallMultiples, taskMatrix, timeSeries, timeline, unitDotLayout };
|