@omercnet/paseo-omp 0.4.0-next.123.1 → 0.4.0-next.125.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ import type { ProviderEvent, ProviderUsage } from "@getpaseo/plugin/server/provider";
2
+ import type { OmpRuntimeSession } from "./omp-rpc";
3
+ import type { OmpSessionState, OmpSessionStats } from "./omp-rpc-protocol";
4
+ import type { ActiveTurn } from "./session-terminal";
5
+ import type { OmpTimelineScheduler } from "./timeline-projector";
6
+
7
+ const USAGE_POLL_MS = 1_000;
8
+
9
+ interface UsageContext {
10
+ closed: boolean;
11
+ runtimeDead: boolean;
12
+ generation: number;
13
+ runtime: OmpRuntimeSession;
14
+ activeTurn: ActiveTurn | null;
15
+ }
16
+
17
+ type Emit = (event: ProviderEvent) => void;
18
+ type ReadContext = () => UsageContext;
19
+ type IsPollable = (turn: ActiveTurn) => boolean;
20
+
21
+ interface UsageSample {
22
+ turn: ActiveTurn;
23
+ generation: number;
24
+ runtime: OmpRuntimeSession;
25
+ epoch: number;
26
+ sequence: number;
27
+ promise: Promise<OmpSessionState | undefined>;
28
+ }
29
+
30
+ function usageFrom(
31
+ state: OmpSessionState | undefined,
32
+ stats: OmpSessionStats | undefined,
33
+ ): ProviderUsage | undefined {
34
+ if (!state?.contextUsage && !stats) return undefined;
35
+ const stateContext = state?.contextUsage;
36
+ const statsContext = stats?.contextUsage;
37
+ const modelCapacity = state?.model?.contextWindow;
38
+ const inputTokens = stats?.tokens?.input;
39
+ const cachedInputTokens = stats?.tokens?.cacheRead;
40
+ const outputTokens = stats?.tokens?.output;
41
+ const totalCostUsd = stats?.cost;
42
+ const contextTokens =
43
+ typeof stateContext?.tokens === "number"
44
+ ? stateContext.tokens
45
+ : typeof statsContext?.tokens === "number"
46
+ ? statsContext.tokens
47
+ : undefined;
48
+ const contextWindow =
49
+ typeof stateContext?.contextWindow === "number" && stateContext.contextWindow > 0
50
+ ? stateContext.contextWindow
51
+ : typeof statsContext?.contextWindow === "number" && statsContext.contextWindow > 0
52
+ ? statsContext.contextWindow
53
+ : typeof modelCapacity === "number" && modelCapacity > 0
54
+ ? modelCapacity
55
+ : undefined;
56
+ const usage: ProviderUsage = {
57
+ ...(typeof inputTokens === "number" ? { inputTokens } : {}),
58
+ ...(typeof cachedInputTokens === "number" ? { cachedInputTokens } : {}),
59
+ ...(typeof outputTokens === "number" ? { outputTokens } : {}),
60
+ ...(typeof totalCostUsd === "number" ? { totalCostUsd } : {}),
61
+ ...(contextTokens !== undefined ? { contextWindowUsedTokens: contextTokens } : {}),
62
+ ...(contextWindow !== undefined ? { contextWindowMaxTokens: contextWindow } : {}),
63
+ };
64
+ return Object.keys(usage).length > 0 ? usage : undefined;
65
+ }
66
+
67
+ export class OmpSessionUsage {
68
+ private epoch = 0;
69
+ private sequence = 0;
70
+ private sample: UsageSample | null = null;
71
+ private lastUsage: ProviderUsage | null = null;
72
+
73
+ constructor(
74
+ private readonly sessionId: string,
75
+ private readonly emit: Emit,
76
+ private readonly scheduler: OmpTimelineScheduler,
77
+ private readonly readContext: ReadContext,
78
+ private readonly isPollable: IsPollable,
79
+ ) {}
80
+
81
+ get latest(): ProviderUsage | null {
82
+ return this.lastUsage;
83
+ }
84
+
85
+ private ownsSample(turn: ActiveTurn, generation: number, runtime: OmpRuntimeSession): boolean {
86
+ const context = this.readContext();
87
+ return (
88
+ !context.closed &&
89
+ !context.runtimeDead &&
90
+ !turn.terminal &&
91
+ generation === context.generation &&
92
+ runtime === context.runtime &&
93
+ context.activeTurn === turn
94
+ );
95
+ }
96
+
97
+ publishSnapshot(
98
+ turn: ActiveTurn,
99
+ minimumEpoch = this.epoch,
100
+ minimumSequence = turn.usageSampleFloor,
101
+ ): Promise<OmpSessionState | undefined> {
102
+ const { generation, runtime } = this.readContext();
103
+ if (!this.ownsSample(turn, generation, runtime)) return Promise.resolve(undefined);
104
+ const current = this.sample;
105
+ if (current) {
106
+ if (
107
+ current.turn === turn &&
108
+ current.generation === generation &&
109
+ current.runtime === runtime &&
110
+ current.epoch >= minimumEpoch &&
111
+ current.sequence >= minimumSequence
112
+ ) {
113
+ return current.promise;
114
+ }
115
+ return current.promise.then(() => {
116
+ if (!this.ownsSample(turn, generation, runtime)) return undefined;
117
+ return this.publishSnapshot(turn, minimumEpoch, minimumSequence);
118
+ });
119
+ }
120
+ const epoch = this.epoch;
121
+ const sequence = ++this.sequence;
122
+ const promise = Promise.allSettled([runtime.getState(), runtime.getSessionStats()]).then(
123
+ ([stateResult, statsResult]) => {
124
+ if (
125
+ !this.ownsSample(turn, generation, runtime) ||
126
+ epoch !== this.epoch ||
127
+ epoch < minimumEpoch ||
128
+ sequence < turn.usageSampleFloor ||
129
+ sequence < minimumSequence
130
+ ) {
131
+ return undefined;
132
+ }
133
+ const state = stateResult.status === "fulfilled" ? stateResult.value : undefined;
134
+ const stats = statsResult.status === "fulfilled" ? statsResult.value : undefined;
135
+ const usage = usageFrom(state, stats);
136
+ if (usage) {
137
+ this.lastUsage = usage;
138
+ this.emit({
139
+ type: "session.usage",
140
+ sessionId: this.sessionId,
141
+ turnId: turn.turnId,
142
+ usage,
143
+ });
144
+ }
145
+ return state;
146
+ },
147
+ );
148
+ this.sample = { turn, generation, runtime, epoch, sequence, promise };
149
+ void promise.finally(() => {
150
+ if (this.sample?.promise === promise) this.sample = null;
151
+ });
152
+ return promise;
153
+ }
154
+
155
+ async boundedSnapshot(turn: ActiveTurn, timeoutMs: number): Promise<OmpSessionState | undefined> {
156
+ const timeout = Promise.withResolvers<undefined>();
157
+ const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
158
+ try {
159
+ return await Promise.race([this.publishSnapshot(turn), timeout.promise]);
160
+ } finally {
161
+ this.scheduler.clear(timer);
162
+ }
163
+ }
164
+
165
+ async boundedTerminalState(
166
+ turn: ActiveTurn,
167
+ timeoutMs: number,
168
+ ): Promise<OmpSessionState | undefined> {
169
+ const { generation, runtime } = this.readContext();
170
+ const timeout = Promise.withResolvers<undefined>();
171
+ const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
172
+ try {
173
+ const state = await Promise.race([runtime.getState(), timeout.promise]);
174
+ return this.ownsSample(turn, generation, runtime) ? state : undefined;
175
+ } catch {
176
+ return undefined;
177
+ } finally {
178
+ this.scheduler.clear(timer);
179
+ }
180
+ }
181
+
182
+ schedulePoll(turn: ActiveTurn, delayMs = USAGE_POLL_MS): void {
183
+ if (!this.isPollable(turn) || turn.usagePollTimer !== undefined) return;
184
+ turn.usagePollTimer = this.scheduler.set(() => {
185
+ turn.usagePollTimer = undefined;
186
+ this.poll(turn);
187
+ }, delayMs);
188
+ }
189
+
190
+ poll(turn: ActiveTurn): void {
191
+ if (!this.isPollable(turn) || turn.usagePoll) return;
192
+ const poll = this.publishSnapshot(turn).then(() => undefined);
193
+ turn.usagePoll = poll;
194
+ void poll.finally(() => {
195
+ if (turn.usagePoll === poll) turn.usagePoll = undefined;
196
+ this.schedulePoll(turn);
197
+ });
198
+ }
199
+
200
+ stopPoll(turn: ActiveTurn): void {
201
+ if (turn.usagePollTimer === undefined) return;
202
+ this.scheduler.clear(turn.usagePollTimer);
203
+ turn.usagePollTimer = undefined;
204
+ }
205
+
206
+ invalidateBeforeNextSample(turn: ActiveTurn): void {
207
+ turn.usageSampleFloor = this.sequence + 1;
208
+ if (this.sample?.turn === turn && this.sample.sequence < turn.usageSampleFloor) {
209
+ this.sample = null;
210
+ }
211
+ }
212
+
213
+ completeCompaction(): void {
214
+ this.epoch += 1;
215
+ const staleSample = this.sample;
216
+ if (staleSample && staleSample.epoch < this.epoch) {
217
+ this.sample = null;
218
+ staleSample.turn.usagePoll = undefined;
219
+ }
220
+ this.lastUsage = null;
221
+ }
222
+ clearLatest(): void {
223
+ this.lastUsage = null;
224
+ }
225
+
226
+ clearSample(turn: ActiveTurn): void {
227
+ if (this.sample?.turn === turn) this.sample = null;
228
+ }
229
+
230
+ reset(): void {
231
+ this.sample = null;
232
+ this.lastUsage = null;
233
+ }
234
+ }