feat: harden workflow gates and expose token usage

This commit is contained in:
2026-07-21 12:14:26 +02:00
parent 8aa1676811
commit 2ff63d371f
20 changed files with 604 additions and 53 deletions
+20
View File
@@ -38,6 +38,26 @@ test("text_delta accumulates into transcript", () => {
expect(useSessionStore.getState().transcript.at(-1)?.text).toBe("Analisi");
});
test("usage events accumulate billed token categories but keep the latest context occupancy", () => {
const store = useSessionStore.getState();
store.applyEvent({
type: "usage",
usage: { input: 60_000, cacheRead: 400_000, output: 20_000, totalTokens: 80_000, contextWindow: 200_000 },
});
store.applyEvent({
type: "usage",
usage: { input: 50_348, cacheRead: 233_344, output: 13_652, totalTokens: 55_935, contextWindow: 200_000 },
});
expect(useSessionStore.getState().tokenUsage).toEqual({
input: 110_348,
cacheRead: 633_344,
output: 33_652,
totalTokens: 55_935,
contextWindow: 200_000,
});
});
test("folds prompt, lifecycle, streams, tool updates, and gates in chronological order", () => {
const store = useSessionStore.getState();
store.setPhase("F1");
+14 -1
View File
@@ -1,5 +1,5 @@
import { create } from "zustand";
import type { ActivityEntry, StreamEvent, WidgetDescriptor } from "../api/types";
import type { ActivityEntry, StreamEvent, TokenUsage, WidgetDescriptor } from "../api/types";
interface Entry {
role: "assistant";
@@ -21,6 +21,7 @@ interface SessionState {
// system event — the only end-of-turn signal on the final workflow step, where no
// follow-up gate arrives to release the spinner.
agentActive: boolean;
tokenUsage: TokenUsage | null;
applyEvent: (e: StreamEvent) => void;
clearPending: () => void;
resetSession: () => void;
@@ -65,6 +66,7 @@ const empty = {
phaseError: null as string | null,
seenGateIds: new Set<string>(),
agentActive: false,
tokenUsage: null as TokenUsage | null,
};
export const useSessionStore = create<SessionState>((set) => ({
@@ -145,6 +147,17 @@ export const useSessionStore = create<SessionState>((set) => ({
}
return { activityLog, agentActive: true };
}
if (e.type === "usage") {
return {
tokenUsage: {
input: (st.tokenUsage?.input ?? 0) + e.usage.input,
cacheRead: (st.tokenUsage?.cacheRead ?? 0) + e.usage.cacheRead,
output: (st.tokenUsage?.output ?? 0) + e.usage.output,
totalTokens: e.usage.totalTokens,
contextWindow: e.usage.contextWindow,
},
};
}
if (e.type === "info") {
const level = e.level ?? "info";
const stepMessages = [...st.stepMessages, { level, text: e.text }];