summaryrefslogtreecommitdiffhomepage
path: root/src/core/telemetry/selectors.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-07 18:41:27 +0900
committerAdam Malczewski <[email protected]>2026-06-07 18:41:27 +0900
commit9a4fc0aa0b56e7bb083ecc615647ed8f6b2ef4c2 (patch)
tree1fd5b4081736cf48c7b916abcdbeab267b4322d6 /src/core/telemetry/selectors.ts
parentdcf5a2993c9cbeb266f2629cf33fbc43f52c7ca8 (diff)
downloaddispatch-web-9a4fc0aa0b56e7bb083ecc615647ed8f6b2ef4c2.tar.gz
dispatch-web-9a4fc0aa0b56e7bb083ecc615647ed8f6b2ef4c2.zip
feat(chat): live turn metrics — telemetry reducer + rendering
Consume wire/transport-contract 0.3.0 (step-complete event + timing fields on usage/tool-result/done). Pure core/telemetry module: foldMetricEvent (reducer) + derived selectors (stepTps, turnTps, etc). TelemetryState is pure data, no active-turn tracking — consumers pass turnId to selectors. ChatStore wires foldMetricEvent into handleDelta and exposes telemetry + currentTurnId. ChatView shows step-metrics footer (time/TPS/tokens) on assistant text bubbles and durationMs badge on tool cards. New TurnSummary component renders turn-level stats (wall-clock, tokens, steps, TPS) in a DaisyUI stats block. Extended live-probe to verify telemetry events against bin/up (pending backend restart). 336 tests, typecheck 0, biome clean, build ok.
Diffstat (limited to 'src/core/telemetry/selectors.ts')
-rw-r--r--src/core/telemetry/selectors.ts95
1 files changed, 95 insertions, 0 deletions
diff --git a/src/core/telemetry/selectors.ts b/src/core/telemetry/selectors.ts
new file mode 100644
index 0000000..ecf1794
--- /dev/null
+++ b/src/core/telemetry/selectors.ts
@@ -0,0 +1,95 @@
+import type { Usage } from "@dispatch/wire";
+import type { StepMetrics, TelemetryState, TurnMetrics } from "./types";
+
+/** Get the metrics for a specific step within a turn. */
+export function stepMetrics(
+ state: TelemetryState,
+ turnId: string,
+ stepIndex: number,
+): StepMetrics | undefined {
+ return state.turns.get(turnId)?.steps[stepIndex];
+}
+
+/** Get the metrics for a turn. */
+export function turnMetrics(state: TelemetryState, turnId: string): TurnMetrics | undefined {
+ return state.turns.get(turnId);
+}
+
+/** The number of steps in a turn. */
+export function stepCount(state: TelemetryState, turnId: string): number {
+ return state.turns.get(turnId)?.steps.length ?? 0;
+}
+
+/** TTFT of the first step in a turn (the turn-visible first-token latency). */
+export function turnTtft(state: TelemetryState, turnId: string): number | undefined {
+ return state.turns.get(turnId)?.steps[0]?.ttftMs;
+}
+
+/** Sum of all steps' decode times in a turn. */
+export function totalDecodeMs(state: TelemetryState, turnId: string): number | undefined {
+ const steps = state.turns.get(turnId)?.steps;
+ if (steps === undefined || steps.length === 0) return undefined;
+ let total = 0;
+ let found = false;
+ for (const s of steps) {
+ if (s.decodeMs !== undefined) {
+ total += s.decodeMs;
+ found = true;
+ }
+ }
+ return found ? total : undefined;
+}
+
+/** Aggregate output tokens across all steps in a turn. */
+export function totalOutputTokens(state: TelemetryState, turnId: string): number | undefined {
+ const turn = state.turns.get(turnId);
+ if (turn === undefined) return undefined;
+ if (turn.doneUsage !== undefined) return turn.doneUsage.outputTokens;
+ let total = 0;
+ let found = false;
+ for (const s of turn.steps) {
+ if (s.usage?.outputTokens !== undefined) {
+ total += s.usage.outputTokens;
+ found = true;
+ }
+ }
+ return found ? total : undefined;
+}
+
+/** Aggregate input tokens across all steps in a turn. */
+export function totalInputTokens(state: TelemetryState, turnId: string): number | undefined {
+ const turn = state.turns.get(turnId);
+ if (turn === undefined) return undefined;
+ if (turn.doneUsage !== undefined) return turn.doneUsage.inputTokens;
+ let total = 0;
+ let found = false;
+ for (const s of turn.steps) {
+ if (s.usage?.inputTokens !== undefined) {
+ total += s.usage.inputTokens;
+ found = true;
+ }
+ }
+ return found ? total : undefined;
+}
+
+/** Derived TPS for a step: outputTokens / (decodeMs / 1000). */
+export function stepTps(step: StepMetrics): number | undefined {
+ if (step.usage?.outputTokens === undefined || step.decodeMs === undefined) return undefined;
+ if (step.decodeMs === 0) return undefined;
+ return step.usage.outputTokens / (step.decodeMs / 1000);
+}
+
+/** Derived aggregate TPS for a turn. */
+export function turnTps(state: TelemetryState, turnId: string): number | undefined {
+ const outTokens = totalOutputTokens(state, turnId);
+ const decode = totalDecodeMs(state, turnId);
+ if (outTokens === undefined || decode === undefined || decode === 0) return undefined;
+ return outTokens / (decode / 1000);
+}
+
+/** Sum of tool execution durations within a step. */
+export function stepToolDuration(step: StepMetrics): number | undefined {
+ return step.toolDurationMs !== undefined && step.toolDurationMs > 0
+ ? step.toolDurationMs
+ : undefined;
+}