summaryrefslogtreecommitdiffhomepage
path: root/scripts/live-probe.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-10 10:06:27 +0900
committerAdam Malczewski <[email protected]>2026-06-10 10:06:27 +0900
commitf8bf715abc8a89ec0c6370b40403c509b1ce2870 (patch)
tree915600a766e042a8491ac57423542cde1dda1eb6 /scripts/live-probe.ts
parentccfd2f4157c1cbbb3d8aeceee94d9e963a82ab03 (diff)
downloaddispatch-web-f8bf715abc8a89ec0c6370b40403c509b1ce2870.tar.gz
dispatch-web-f8bf715abc8a89ec0c6370b40403c509b1ce2870.zip
feat(metrics): per-turn + per-step token/timing metrics bubbles
Consume [email protected] / [email protected] metrics: usage.stepId, step-complete (ttft/decode/genTotal), done.durationMs/usage, and the durable GET /conversations/:id/metrics endpoint. - core/metrics: pure live-fold + durable-merge reducer; decode-rate TPS; head-aligned, stable placement; progressive per-step rows (each shown as its step ends) with the turn-total row gated on the done event. - features/chat: store folds metric events + hydrates durable TurnMetrics; ChatView renders inline step bubbles + a turn-total bubble. - app: MetricsSync HTTP effect (tolerates 404) injected into chat stores. - scripts/live-probe: drives the metrics path; live-verified 17/17 vs bin/up. - docs: regenerate .dispatch wire/transport mirrors to 0.4.0; glossary terms (turn/step metrics, TTFT, decode time, TPS, metrics bubble); trim handoff.
Diffstat (limited to 'scripts/live-probe.ts')
-rw-r--r--scripts/live-probe.ts81
1 files changed, 79 insertions, 2 deletions
diff --git a/scripts/live-probe.ts b/scripts/live-probe.ts
index 2c4dfb9..2b2880b 100644
--- a/scripts/live-probe.ts
+++ b/scripts/live-probe.ts
@@ -30,6 +30,7 @@ import type {
ChatDeltaMessage,
ChatErrorMessage,
ConversationHistoryResponse,
+ ConversationMetricsResponse,
} from "@dispatch/transport-contract";
import type { SurfaceServerMessage } from "@dispatch/ui-contract";
import { createIdbChunkStore } from "../src/adapters/idb/index.ts";
@@ -43,6 +44,13 @@ import {
selectMessages,
type TranscriptState,
} from "../src/core/chunks/index.ts";
+import {
+ applyDurableMetrics,
+ foldMetricsEvent,
+ initialMetricsState,
+ type MetricsState,
+ selectOrderedTurnMetrics,
+} from "../src/core/metrics/index.ts";
import { createConversationCache } from "../src/features/conversation-cache/index.ts";
const WS_URL = process.env.PROBE_WS ?? "ws://localhost:24205";
@@ -74,6 +82,15 @@ async function historySync(id: string, sinceSeq: number): Promise<ConversationHi
return (await res.json()) as ConversationHistoryResponse;
}
+/** Durable metrics fetch — returns the response, or the HTTP status when not OK
+ * (the endpoint is being implemented backend-side; the FE tolerates a 404). */
+async function metricsSync(id: string): Promise<ConversationMetricsResponse | { status: number }> {
+ const url = `${HTTP_BASE}/conversations/${encodeURIComponent(id)}/metrics`;
+ const res = await fetch(url, { headers: { Origin: "http://localhost:24204" } });
+ if (!res.ok) return { status: res.status };
+ return (await res.json()) as ConversationMetricsResponse;
+}
+
type ChatMsg = ChatDeltaMessage | ChatErrorMessage;
type Socket = ReturnType<typeof createSurfaceSocket>;
@@ -87,8 +104,15 @@ async function runTurn(
socket: Socket,
conversationId: string,
prompt: string,
-): Promise<{ state: TranscriptState; deltas: number; sealed: boolean; error: string | null }> {
+): Promise<{
+ state: TranscriptState;
+ metrics: MetricsState;
+ deltas: number;
+ sealed: boolean;
+ error: string | null;
+}> {
let state = initialState();
+ let metrics = initialMetricsState();
let deltas = 0;
let sealed = false;
let error: string | null = null;
@@ -102,6 +126,7 @@ async function runTurn(
}
deltas++;
state = foldEvent(state, msg.event);
+ metrics = foldMetricsEvent(metrics, msg.event);
if (msg.event.type === "turn-sealed") {
sealed = true;
done.resolve();
@@ -113,7 +138,7 @@ async function runTurn(
await done.promise;
clearTimeout(timeout);
handlers.delete(conversationId);
- return { state, deltas, sealed, error };
+ return { state, metrics, deltas, sealed, error };
}
function toolChunksOf(state: TranscriptState) {
@@ -178,6 +203,58 @@ async function main() {
.join("");
record("turn 1 committed transcript has assistant text", committedText.length > 0);
+ // ─── Metrics: LIVE token + timing ([email protected] usage/step-complete/done) ──────
+ const liveTurns = selectOrderedTurnMetrics(t1.metrics);
+ const m1 = liveTurns[0];
+ record(
+ "turn 1 LIVE metrics: a turn with output tokens",
+ m1 !== undefined && m1.usage.outputTokens > 0,
+ m1
+ ? `in=${m1.usage.inputTokens} out=${m1.usage.outputTokens} steps=${m1.steps.length}`
+ : "no turn",
+ );
+ if (m1 !== undefined) {
+ const anyGen = m1.steps.some((s) => s.genTotalMs !== undefined);
+ const anyTtft = m1.steps.some((s) => s.ttftMs !== undefined);
+ note(
+ `live timing: durationMs=${m1.durationMs ?? "—"}, ` +
+ `genTotalMs present=${anyGen}, ttftMs present=${anyTtft}`,
+ );
+ record(
+ "turn 1 LIVE metrics carries timing (durationMs or step genTotalMs)",
+ m1.durationMs !== undefined || anyGen,
+ "requires the backend runtime to have a clock",
+ );
+ }
+
+ // ─── Metrics: DURABLE endpoint (GET /conversations/:id/metrics) ──────────────
+ const dm = await metricsSync(textConv);
+ if ("status" in dm) {
+ note(
+ `durable /metrics not available yet (HTTP ${dm.status}) — FE degrades to live-only, as designed`,
+ );
+ record(
+ "durable /metrics is implemented OR gracefully absent (404)",
+ dm.status === 404 || dm.status === 405,
+ `HTTP ${dm.status}`,
+ );
+ } else {
+ record(
+ "durable /metrics returned TurnMetrics[]",
+ Array.isArray(dm.turns),
+ `${dm.turns.length} turn(s)`,
+ );
+ const durableMerged = selectOrderedTurnMetrics(
+ applyDurableMetrics(initialMetricsState(), dm.turns),
+ );
+ const d1 = durableMerged[0];
+ record(
+ "durable /metrics turn has token usage",
+ d1 !== undefined && d1.usage.outputTokens > 0,
+ d1 ? `out=${d1.usage.outputTokens} steps=${d1.steps.length}` : "no turn",
+ );
+ }
+
// ─── Turn 2: tool-call batching ([email protected] stepId) ─────────────────────────
console.log(`\n[live-probe] TURN 2 (tools): "${TOOL_PROMPT}"`);
const toolConv = crypto.randomUUID();