summaryrefslogtreecommitdiffhomepage
path: root/scripts/live-probe.ts
diff options
context:
space:
mode:
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();