From 4bc02b0102e45dc8cce97b7fc8499e0ab23d862f Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Fri, 12 Jun 2026 01:01:32 +0900 Subject: feat(metrics): consume contextSize — current context-usage readout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend context-size handoff: re-pin wire@0.5.0 / transport-contract@0.6.0 (+ re-mirror .dispatch reference snapshots). Thread the optional contextSize through core/metrics (done fold + durable + selectCurrentContextSize: latest turn's defined value, undefined=>unknown never 0, durable-wins-over-live). Chat store exposes currentContextSize; ContextSizeBadge renders "N tokens in context" / "context size unknown" above the composer. GLOSSARY: add context size / context window. 533 tests green. --- src/core/metrics/reducer.test.ts | 76 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) (limited to 'src/core/metrics/reducer.test.ts') diff --git a/src/core/metrics/reducer.test.ts b/src/core/metrics/reducer.test.ts index 16c88b3..cd9f673 100644 --- a/src/core/metrics/reducer.test.ts +++ b/src/core/metrics/reducer.test.ts @@ -4,6 +4,7 @@ import { applyDurableMetrics, foldMetricsEvent, initialMetricsState, + selectCurrentContextSize, selectOrderedTurnMetrics, } from "./reducer"; @@ -39,7 +40,11 @@ const stepCompleteEvent = ( const doneEvent = ( turnId: string, - extra: { durationMs?: number; usage?: { inputTokens: number; outputTokens: number } } = {}, + extra: { + durationMs?: number; + usage?: { inputTokens: number; outputTokens: number }; + contextSize?: number; + } = {}, ): TurnDoneEvent => ({ type: "done", conversationId: "c1", @@ -366,3 +371,72 @@ describe("applyDurableMetrics", () => { expect(s.durable.get("t1")?.usage.inputTokens).toBe(99); }); }); + +describe("contextSize / selectCurrentContextSize", () => { + it("live done carries contextSize onto the turn total", () => { + let s = initialMetricsState(); + s = foldMetricsEvent(s, usageEvent("t1", 100, 50, "s1")); + s = foldMetricsEvent(s, stepCompleteEvent("t1", "s1")); + s = foldMetricsEvent(s, doneEvent("t1", { contextSize: 1234 })); + + const ordered = selectOrderedTurnMetrics(s); + expect(ordered[0]?.total?.contextSize).toBe(1234); + expect(selectCurrentContextSize(s)).toBe(1234); + }); + + it("contextSize is NOT the aggregate usage sum (multi-step turn)", () => { + let s = initialMetricsState(); + // Two steps: usage sums to 300 in / 130 out = 430, but contextSize is the + // backend-stamped final-step occupancy, independent of the sum. + s = foldMetricsEvent(s, usageEvent("t1", 100, 50, "s1")); + s = foldMetricsEvent(s, stepCompleteEvent("t1", "s1")); + s = foldMetricsEvent(s, usageEvent("t1", 200, 80, "s2")); + s = foldMetricsEvent(s, stepCompleteEvent("t1", "s2")); + s = foldMetricsEvent(s, doneEvent("t1", { contextSize: 250 })); + + const ordered = selectOrderedTurnMetrics(s); + expect(ordered[0]?.total?.usage).toEqual({ inputTokens: 300, outputTokens: 130 }); + expect(ordered[0]?.total?.contextSize).toBe(250); + expect(selectCurrentContextSize(s)).toBe(250); + }); + + it("persisted (durable) contextSize is preserved and selected", () => { + let s = initialMetricsState(); + s = applyDurableMetrics(s, [ + { turnId: "t1", usage: { inputTokens: 10, outputTokens: 5 }, steps: [], contextSize: 4096 }, + ]); + expect(s.durable.get("t1")?.contextSize).toBe(4096); + expect(selectCurrentContextSize(s)).toBe(4096); + }); + + it("selectCurrentContextSize returns the LATEST turn's value", () => { + let s = initialMetricsState(); + s = foldMetricsEvent(s, doneEvent("t1", { contextSize: 100 })); + s = foldMetricsEvent(s, doneEvent("t2", { contextSize: 900 })); + expect(selectCurrentContextSize(s)).toBe(900); + }); + + it("selectCurrentContextSize skips a later turn that lacks contextSize", () => { + let s = initialMetricsState(); + s = foldMetricsEvent(s, doneEvent("t1", { contextSize: 700 })); + // t2 finishes but the provider reported no per-step usage → no contextSize. + s = foldMetricsEvent(s, doneEvent("t2")); + expect(selectCurrentContextSize(s)).toBe(700); + }); + + it("selectCurrentContextSize is undefined (not 0) when nothing reported", () => { + let s = initialMetricsState(); + expect(selectCurrentContextSize(s)).toBeUndefined(); + s = foldMetricsEvent(s, doneEvent("t1")); + expect(selectCurrentContextSize(s)).toBeUndefined(); + }); + + it("durable contextSize wins over live for a shared turnId", () => { + let s = initialMetricsState(); + s = foldMetricsEvent(s, doneEvent("t1", { contextSize: 111 })); + s = applyDurableMetrics(s, [ + { turnId: "t1", usage: { inputTokens: 1, outputTokens: 1 }, steps: [], contextSize: 222 }, + ]); + expect(selectCurrentContextSize(s)).toBe(222); + }); +}); -- cgit v1.2.3