@omercnet/paseo-omp 0.4.0 → 1.0.0
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.
- package/README.md +8 -8
- package/client/external-url.ts +2 -11
- package/client/omp-config-styles.ts +284 -0
- package/client/omp-config-surface.tsx +16 -286
- package/client/omp-routing-editor.tsx +1 -3
- package/client/provider-launch-settings.tsx +118 -0
- package/index.client.tsx +21 -2
- package/index.server.ts +10 -0
- package/package.json +14 -9
- package/paseo-plugin.json +2 -1
- package/scripts/prepare-dependencies.mjs +44 -11
- package/server/omp-models.ts +2 -6
- package/server/package-version.ts +1 -1
- package/server/protocol-violation-diagnostics.ts +1 -1
- package/server/provider/catalog.ts +2 -1
- package/server/provider/config-normalization.ts +19 -3
- package/server/provider/connection.ts +30 -0
- package/server/provider/host-tools.ts +2 -2
- package/server/provider/mcp-transport.ts +9 -2
- package/server/provider/omp-rpc-environment.ts +368 -0
- package/server/provider/omp-rpc-process.ts +119 -0
- package/server/provider/omp-rpc-protocol.ts +1949 -0
- package/server/provider/omp-rpc-transport.ts +1373 -0
- package/server/provider/omp-rpc-values.ts +31 -0
- package/server/provider/omp-rpc.ts +267 -3311
- package/server/provider/profile-providers.ts +2 -1
- package/server/provider/prompt-payload.ts +253 -0
- package/server/provider/registration.ts +10 -2
- package/server/provider/session-compaction.ts +187 -0
- package/server/provider/session-descriptors.ts +52 -18
- package/server/provider/session-events.ts +42 -0
- package/server/provider/session-permissions.ts +987 -0
- package/server/provider/session-terminal.ts +307 -0
- package/server/provider/session-usage.ts +234 -0
- package/server/provider/session.ts +145 -1853
- package/server/provider/settings.ts +8 -5
- package/server/provider/subsessions.ts +30 -9
- package/server/provider/timeline-projector.ts +140 -29
- package/shared/external-url.ts +0 -9
- package/shared/provider-launch-settings.ts +18 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ProviderError } from "@getpaseo/plugin/server/provider";
|
|
3
|
+
import type { OmpMessage, OmpRpcEvent } from "./omp-rpc-protocol";
|
|
4
|
+
|
|
5
|
+
export type PendingUser = {
|
|
6
|
+
clientMessageId: string;
|
|
7
|
+
text: string;
|
|
8
|
+
accepted: boolean;
|
|
9
|
+
fallbackOnFinish: boolean;
|
|
10
|
+
bufferedEchoes: OmpMessage[];
|
|
11
|
+
};
|
|
12
|
+
type TerminalCorrelationEvidence = "fresh-native-user" | "current-assistant";
|
|
13
|
+
|
|
14
|
+
type TerminalCorrelation = {
|
|
15
|
+
policy: "initial-turn" | "ordered-legacy" | "native-command";
|
|
16
|
+
evidence: Map<TerminalCorrelationEvidence, number>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type TerminalCandidate = {
|
|
20
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>;
|
|
21
|
+
arrivalSequence: number;
|
|
22
|
+
confidence:
|
|
23
|
+
| "keyed"
|
|
24
|
+
| "initial-turn"
|
|
25
|
+
| "ordered-legacy"
|
|
26
|
+
| "native-command"
|
|
27
|
+
| "interrupted"
|
|
28
|
+
| "ambiguous";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ActiveTurn = {
|
|
32
|
+
turnId: string;
|
|
33
|
+
clientMessageId: string;
|
|
34
|
+
generation: number;
|
|
35
|
+
promptResultEmitted: boolean;
|
|
36
|
+
started: boolean;
|
|
37
|
+
terminal: boolean;
|
|
38
|
+
interrupted: boolean;
|
|
39
|
+
starting: boolean;
|
|
40
|
+
nativeActivity: boolean;
|
|
41
|
+
userEchoObserved: boolean;
|
|
42
|
+
localOnlyDisabled: boolean;
|
|
43
|
+
localOnlyEligible: boolean;
|
|
44
|
+
awaitingPermissionEvidence: boolean;
|
|
45
|
+
activitySequence: number;
|
|
46
|
+
acknowledged: boolean;
|
|
47
|
+
terminalCorrelation: TerminalCorrelation;
|
|
48
|
+
replayingBufferedEvents: boolean;
|
|
49
|
+
agentInvoked?: boolean;
|
|
50
|
+
nativeRequestId?: string;
|
|
51
|
+
promptAcceptedEventIndex?: number;
|
|
52
|
+
localOnlyTimer?: unknown;
|
|
53
|
+
usagePollTimer?: unknown;
|
|
54
|
+
usagePoll?: Promise<void>;
|
|
55
|
+
usageSampleFloor: number;
|
|
56
|
+
manualCompaction: boolean;
|
|
57
|
+
manualCompactionPending: boolean;
|
|
58
|
+
manualCompactionDeadlineTimer?: unknown;
|
|
59
|
+
agentEndPending: boolean;
|
|
60
|
+
agentEndRetryTimer?: unknown;
|
|
61
|
+
agentEndDeadlineTimer?: unknown;
|
|
62
|
+
agentEndCheck?: Promise<void>;
|
|
63
|
+
ambiguousTerminalTimer?: unknown;
|
|
64
|
+
terminalizing: boolean;
|
|
65
|
+
terminalization?: Promise<void>;
|
|
66
|
+
terminalOutcome?: TurnOutcome;
|
|
67
|
+
operationalTerminalStage?: "failed" | "unresolved";
|
|
68
|
+
terminalWake?: VoidDeferred;
|
|
69
|
+
steerReady: VoidDeferred;
|
|
70
|
+
steersInFlight: number;
|
|
71
|
+
deferredAgentEnd?: TerminalCandidate;
|
|
72
|
+
activeToolCallIds: Set<string>;
|
|
73
|
+
bufferedEvents: OmpRpcEvent[];
|
|
74
|
+
pendingUsers: PendingUser[];
|
|
75
|
+
userEchoes: OmpMessage[];
|
|
76
|
+
userCorrelationActive: boolean;
|
|
77
|
+
userLookups: Set<Promise<void>>;
|
|
78
|
+
completedMessageCount: number;
|
|
79
|
+
streamedMessageEntryIds: string[];
|
|
80
|
+
streamedMessageIdentityComplete: boolean;
|
|
81
|
+
lastCompletedAssistantOutcome?: AgentEndOutcome;
|
|
82
|
+
lastCompletedAssistantEntryId?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type VoidDeferred = {
|
|
86
|
+
promise: Promise<void>;
|
|
87
|
+
resolve(value?: void | PromiseLike<void>): void;
|
|
88
|
+
reject(reason?: unknown): void;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type TurnOutcome = {
|
|
92
|
+
state: "completed" | "failed" | "canceled";
|
|
93
|
+
error?: ProviderError;
|
|
94
|
+
usageSampled: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function nativeEntryId(message: OmpMessage): string | undefined {
|
|
98
|
+
return message.entryId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type AgentEndOutcome = "completed" | "failed" | "canceled";
|
|
102
|
+
type AssistantTerminalStatus = AgentEndOutcome | "unavailable";
|
|
103
|
+
|
|
104
|
+
export function assistantTerminalOutcome(
|
|
105
|
+
message: Extract<OmpMessage, { role: "assistant" }>,
|
|
106
|
+
): AgentEndOutcome {
|
|
107
|
+
const stopReason = message.stopReason?.toLowerCase();
|
|
108
|
+
if (stopReason === "aborted" || stopReason === "canceled" || stopReason === "cancelled") {
|
|
109
|
+
return "canceled";
|
|
110
|
+
}
|
|
111
|
+
return stopReason === "error" || message.errorMessage ? "failed" : "completed";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function lastAssistantStatus(
|
|
115
|
+
messages: readonly OmpMessage[],
|
|
116
|
+
startIndex = 0,
|
|
117
|
+
): AssistantTerminalStatus {
|
|
118
|
+
for (let index = messages.length - 1; index >= startIndex; index -= 1) {
|
|
119
|
+
const message = messages[index];
|
|
120
|
+
if (message?.role !== "assistant") continue;
|
|
121
|
+
return assistantTerminalOutcome(message);
|
|
122
|
+
}
|
|
123
|
+
return "unavailable";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function terminalOutcome(
|
|
127
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
128
|
+
turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
|
|
129
|
+
): AgentEndOutcome | undefined {
|
|
130
|
+
const messages = event.messages;
|
|
131
|
+
if (
|
|
132
|
+
messages !== undefined &&
|
|
133
|
+
(event.messageCount === undefined || messages.length >= event.messageCount)
|
|
134
|
+
) {
|
|
135
|
+
const status = lastAssistantStatus(messages);
|
|
136
|
+
return status === "unavailable" ? "completed" : status;
|
|
137
|
+
}
|
|
138
|
+
if (event.messageCount === 0) return "completed";
|
|
139
|
+
if (
|
|
140
|
+
turn.lastCompletedAssistantOutcome !== undefined &&
|
|
141
|
+
(event.messageCount === undefined || turn.completedMessageCount >= event.messageCount)
|
|
142
|
+
) {
|
|
143
|
+
return turn.lastCompletedAssistantOutcome;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function historyTerminalOutcome(
|
|
149
|
+
messages: readonly OmpMessage[],
|
|
150
|
+
declaredCount: number,
|
|
151
|
+
turn: Pick<
|
|
152
|
+
ActiveTurn,
|
|
153
|
+
| "streamedMessageEntryIds"
|
|
154
|
+
| "streamedMessageIdentityComplete"
|
|
155
|
+
| "lastCompletedAssistantOutcome"
|
|
156
|
+
| "lastCompletedAssistantEntryId"
|
|
157
|
+
>,
|
|
158
|
+
retainedMessages: readonly OmpMessage[],
|
|
159
|
+
): AgentEndOutcome | undefined {
|
|
160
|
+
if (
|
|
161
|
+
messages.length < declaredCount ||
|
|
162
|
+
!turn.streamedMessageIdentityComplete ||
|
|
163
|
+
turn.streamedMessageEntryIds.length === 0
|
|
164
|
+
) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const startIndex = messages.length - declaredCount;
|
|
168
|
+
let historyIndex = startIndex;
|
|
169
|
+
for (const entryId of turn.streamedMessageEntryIds) {
|
|
170
|
+
while (historyIndex < messages.length) {
|
|
171
|
+
const historyMessage = messages[historyIndex];
|
|
172
|
+
if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
|
|
173
|
+
historyIndex += 1;
|
|
174
|
+
}
|
|
175
|
+
if (historyIndex >= messages.length) return undefined;
|
|
176
|
+
const correlated = messages[historyIndex];
|
|
177
|
+
if (
|
|
178
|
+
entryId === turn.lastCompletedAssistantEntryId &&
|
|
179
|
+
(correlated?.role !== "assistant" ||
|
|
180
|
+
assistantTerminalOutcome(correlated) !== turn.lastCompletedAssistantOutcome)
|
|
181
|
+
)
|
|
182
|
+
return undefined;
|
|
183
|
+
historyIndex += 1;
|
|
184
|
+
}
|
|
185
|
+
historyIndex = startIndex;
|
|
186
|
+
for (const retained of retainedMessages) {
|
|
187
|
+
const entryId = nativeEntryId(retained);
|
|
188
|
+
if (!entryId) return undefined;
|
|
189
|
+
while (historyIndex < messages.length) {
|
|
190
|
+
const historyMessage = messages[historyIndex];
|
|
191
|
+
if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
|
|
192
|
+
historyIndex += 1;
|
|
193
|
+
}
|
|
194
|
+
if (historyIndex >= messages.length) return undefined;
|
|
195
|
+
const correlated = messages[historyIndex];
|
|
196
|
+
if (!correlated || correlated.role !== retained.role) return undefined;
|
|
197
|
+
if (
|
|
198
|
+
retained.role === "assistant" &&
|
|
199
|
+
correlated.role === "assistant" &&
|
|
200
|
+
assistantTerminalOutcome(retained) !== assistantTerminalOutcome(correlated)
|
|
201
|
+
)
|
|
202
|
+
return undefined;
|
|
203
|
+
historyIndex += 1;
|
|
204
|
+
}
|
|
205
|
+
const status = lastAssistantStatus(messages, startIndex);
|
|
206
|
+
return status === "unavailable" ? undefined : status;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function unknownTerminalOutcomeError(
|
|
210
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
211
|
+
turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
|
|
212
|
+
): string {
|
|
213
|
+
const retainedMessages = event.messages?.length ?? 0;
|
|
214
|
+
const retainedStatus = event.messages ? lastAssistantStatus(event.messages) : "unavailable";
|
|
215
|
+
const lastStatus =
|
|
216
|
+
retainedStatus === "unavailable"
|
|
217
|
+
? (turn.lastCompletedAssistantOutcome ?? "unavailable")
|
|
218
|
+
: retainedStatus;
|
|
219
|
+
return (
|
|
220
|
+
"OMP agent_end omitted terminal messages; outcome is unknown " +
|
|
221
|
+
`(declaredCount=${event.messageCount ?? "unavailable"}, ` +
|
|
222
|
+
`observedCount=${turn.completedMessageCount}, ` +
|
|
223
|
+
`retainedTerminalMessages=${retainedMessages}, lastAssistantStatus=${lastStatus})`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function createActiveTurn(
|
|
228
|
+
clientMessageId: string,
|
|
229
|
+
text: string,
|
|
230
|
+
generation: number,
|
|
231
|
+
terminalPolicy: TerminalCorrelation["policy"],
|
|
232
|
+
manualCompaction = false,
|
|
233
|
+
): ActiveTurn {
|
|
234
|
+
return {
|
|
235
|
+
turnId: randomUUID(),
|
|
236
|
+
clientMessageId,
|
|
237
|
+
agentInvoked: undefined,
|
|
238
|
+
generation,
|
|
239
|
+
promptResultEmitted: false,
|
|
240
|
+
started: false,
|
|
241
|
+
terminal: false,
|
|
242
|
+
awaitingPermissionEvidence: false,
|
|
243
|
+
interrupted: false,
|
|
244
|
+
starting: true,
|
|
245
|
+
nativeActivity: false,
|
|
246
|
+
userEchoObserved: false,
|
|
247
|
+
localOnlyDisabled: false,
|
|
248
|
+
localOnlyEligible: false,
|
|
249
|
+
usageSampleFloor: 0,
|
|
250
|
+
agentEndPending: false,
|
|
251
|
+
terminalizing: false,
|
|
252
|
+
manualCompactionPending: manualCompaction,
|
|
253
|
+
manualCompaction,
|
|
254
|
+
activitySequence: 0,
|
|
255
|
+
acknowledged: false,
|
|
256
|
+
terminalCorrelation: { policy: terminalPolicy, evidence: new Map() },
|
|
257
|
+
replayingBufferedEvents: false,
|
|
258
|
+
steersInFlight: 0,
|
|
259
|
+
activeToolCallIds: new Set(),
|
|
260
|
+
steerReady: Promise.withResolvers<void>(),
|
|
261
|
+
userCorrelationActive: false,
|
|
262
|
+
userLookups: new Set(),
|
|
263
|
+
userEchoes: [],
|
|
264
|
+
completedMessageCount: 0,
|
|
265
|
+
bufferedEvents: [],
|
|
266
|
+
streamedMessageEntryIds: [],
|
|
267
|
+
streamedMessageIdentityComplete: true,
|
|
268
|
+
pendingUsers: [
|
|
269
|
+
{
|
|
270
|
+
clientMessageId,
|
|
271
|
+
text,
|
|
272
|
+
accepted: true,
|
|
273
|
+
fallbackOnFinish: true,
|
|
274
|
+
bufferedEchoes: [],
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export function classifyTerminalCandidate(
|
|
280
|
+
turn: ActiveTurn,
|
|
281
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
282
|
+
arrivalSequence = turn.activitySequence,
|
|
283
|
+
): TerminalCandidate {
|
|
284
|
+
if (event.requestId !== undefined) return { event, confidence: "keyed", arrivalSequence };
|
|
285
|
+
if (turn.agentInvoked === false) return { event, confidence: "ambiguous", arrivalSequence };
|
|
286
|
+
if (turn.terminalCorrelation.policy === "initial-turn") {
|
|
287
|
+
return { event, confidence: "initial-turn", arrivalSequence };
|
|
288
|
+
}
|
|
289
|
+
const assistantSequence = turn.terminalCorrelation.evidence.get("current-assistant");
|
|
290
|
+
if (
|
|
291
|
+
turn.terminalCorrelation.policy === "native-command" &&
|
|
292
|
+
assistantSequence !== undefined &&
|
|
293
|
+
assistantSequence <= arrivalSequence
|
|
294
|
+
) {
|
|
295
|
+
return { event, confidence: "native-command", arrivalSequence };
|
|
296
|
+
}
|
|
297
|
+
const userSequence = turn.terminalCorrelation.evidence.get("fresh-native-user");
|
|
298
|
+
if (
|
|
299
|
+
userSequence !== undefined &&
|
|
300
|
+
assistantSequence !== undefined &&
|
|
301
|
+
userSequence < assistantSequence &&
|
|
302
|
+
assistantSequence <= arrivalSequence
|
|
303
|
+
) {
|
|
304
|
+
return { event, confidence: "ordered-legacy", arrivalSequence };
|
|
305
|
+
}
|
|
306
|
+
return { event, confidence: "ambiguous", arrivalSequence };
|
|
307
|
+
}
|
|
@@ -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
|
+
}
|