@opengeni/sdk 0.36.0 → 0.37.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.
@@ -0,0 +1,575 @@
1
+ import {
2
+ encodeCodexRealtimeV3DelegationContextAppend,
3
+ encodeCodexRealtimeV3SessionContextAppend,
4
+ parseCodexRealtimeV3Event,
5
+ } from "./codex-realtime-v3-wire";
6
+ import type { CodexRealtimeV3Event } from "./codex-realtime-v3-wire";
7
+ import type {
8
+ SessionRealtimeInboundEntry,
9
+ SessionRealtimeLedgerEntry,
10
+ SyncSessionRealtimeLedgerRequest,
11
+ SyncSessionRealtimeLedgerResponse,
12
+ } from "./types";
13
+
14
+ export {
15
+ CODEX_REALTIME_CONTEXT_APPEND_MAX_BYTES,
16
+ CODEX_REALTIME_V3_MAX_EVENT_BYTES,
17
+ CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES,
18
+ CODEX_REALTIME_V3_MAX_TEXT_BYTES,
19
+ contextAppendChunks,
20
+ encodeCodexRealtimeV3DelegationContextAppend,
21
+ encodeCodexRealtimeV3SessionContextAppend,
22
+ parseCodexRealtimeV3Event,
23
+ } from "./codex-realtime-v3-wire";
24
+ export type {
25
+ CodexRealtimeV3ContextAppendChannel,
26
+ CodexRealtimeV3DelegationContextAppend,
27
+ CodexRealtimeV3Event,
28
+ CodexRealtimeV3ParseFailure,
29
+ CodexRealtimeV3ParseResult,
30
+ CodexRealtimeV3SessionContextAppend,
31
+ } from "./codex-realtime-v3-wire";
32
+
33
+ export const CODEX_REALTIME_V3_SYNC_MAX_ENTRIES = 64;
34
+ export const CODEX_REALTIME_V3_PENDING_MAX_ENTRIES = 256;
35
+ export const CODEX_REALTIME_V3_PENDING_MAX_BYTES = 16 * 1024 * 1024;
36
+ const REALTIME_DELEGATION_TRANSCRIPT_MAX_BYTES = 65_536;
37
+ const REALTIME_DELEGATION_INPUT_MAX_BYTES = 65_536;
38
+
39
+ export type CodexRealtimeV3BridgeFatal = {
40
+ code: "pending_overflow";
41
+ message: string;
42
+ };
43
+
44
+ export type CodexRealtimeV3BridgeSnapshot = {
45
+ connectionId: string;
46
+ connectionEpoch: number;
47
+ startupFenceSequence: number;
48
+ modeVersion: number;
49
+ speaking: boolean;
50
+ activeDelegationId: string | null;
51
+ lastError: string | null;
52
+ ignoredEventCount: number;
53
+ lastIgnoredEventType: string | null;
54
+ pendingInbound: number;
55
+ pendingInboundBytes: number;
56
+ clientAckThroughSequence: number | null;
57
+ /** Pinned V3 exposes no provider receipt, so this list is always empty. */
58
+ providerAckSequences: number[];
59
+ providerStarted: boolean;
60
+ fatal: CodexRealtimeV3BridgeFatal | null;
61
+ };
62
+
63
+ export type CodexRealtimeV3BridgeOptions = {
64
+ events: RTCDataChannel;
65
+ connectionId: string;
66
+ connectionEpoch: number;
67
+ startupFenceSequence: number;
68
+ modeVersion: number;
69
+ owner: Pick<
70
+ SyncSessionRealtimeLedgerRequest,
71
+ "browserInstanceId" | "ownerKey" | "expectedVersion"
72
+ >;
73
+ sync(request: SyncSessionRealtimeLedgerRequest): Promise<SyncSessionRealtimeLedgerResponse>;
74
+ randomUUID?: (() => string) | undefined;
75
+ /** The controller installs its activation FIFO first, then enables this listener synchronously. */
76
+ listen?: boolean | undefined;
77
+ onSnapshot?: ((snapshot: CodexRealtimeV3BridgeSnapshot) => void) | undefined;
78
+ onFatal?: ((fatal: CodexRealtimeV3BridgeFatal) => void) | undefined;
79
+ };
80
+
81
+ export type CodexRealtimeV3Bridge = {
82
+ snapshot(): CodexRealtimeV3BridgeSnapshot;
83
+ ingest(payload: string): Promise<void>;
84
+ flush(): Promise<void>;
85
+ /** Stop accepting provider events, then durably drain everything already parsed. */
86
+ sealAndFlush(): Promise<void>;
87
+ listen(): void;
88
+ close(): void;
89
+ };
90
+
91
+ type PendingInbound = {
92
+ entry: SessionRealtimeInboundEntry;
93
+ bytes: number;
94
+ };
95
+
96
+ type FinalizedTranscript = {
97
+ role: "user" | "assistant";
98
+ text: string;
99
+ turnId: string;
100
+ };
101
+
102
+ export function createCodexRealtimeV3Bridge(
103
+ options: CodexRealtimeV3BridgeOptions,
104
+ ): CodexRealtimeV3Bridge {
105
+ let closed = false;
106
+ let sealed = false;
107
+ let listening = false;
108
+ let speaking = false;
109
+ let activeDelegationId: string | null = null;
110
+ let lastError: string | null = null;
111
+ let ignoredEventCount = 0;
112
+ let lastIgnoredEventType: string | null = null;
113
+ let fatal: CodexRealtimeV3BridgeFatal | null = null;
114
+ let providerStarted:
115
+ | { providerSessionId: string; providerEventId?: string | null | undefined }
116
+ | undefined;
117
+ let providerStartedAccepted = false;
118
+ let clientAckThroughSequence: number | null = null;
119
+ let pendingInbound: PendingInbound[] = [];
120
+ let pendingInboundCount = 0;
121
+ let pendingInboundBytes = 0;
122
+ let flushing: Promise<void> | null = null;
123
+ let flushRequestedWhileRunning = false;
124
+ let forceSync = false;
125
+ const clientReceivedSequences = new Set<number>();
126
+ const sentSequences = new Set<number>();
127
+ const finalizedTurnIds = new Set<string>();
128
+ let transcriptSinceDelegation: FinalizedTranscript[] = [];
129
+ let pendingDelegationUserTranscript: { delegationItemId: string; text: string } | null = null;
130
+ const randomUUID = options.randomUUID ?? defaultRandomUUID;
131
+
132
+ const snapshot = (): CodexRealtimeV3BridgeSnapshot => ({
133
+ connectionId: options.connectionId,
134
+ connectionEpoch: options.connectionEpoch,
135
+ startupFenceSequence: options.startupFenceSequence,
136
+ modeVersion: options.modeVersion,
137
+ speaking,
138
+ activeDelegationId,
139
+ lastError,
140
+ ignoredEventCount,
141
+ lastIgnoredEventType,
142
+ pendingInbound: pendingInboundCount,
143
+ pendingInboundBytes,
144
+ clientAckThroughSequence,
145
+ providerAckSequences: [],
146
+ providerStarted: providerStartedAccepted,
147
+ fatal,
148
+ });
149
+ const publish = (): void => options.onSnapshot?.(snapshot());
150
+
151
+ const triggerFatal = (message: string): void => {
152
+ if (closed || fatal) return;
153
+ fatal = { code: "pending_overflow", message };
154
+ lastError = message;
155
+ publish();
156
+ try {
157
+ options.onFatal?.({ ...fatal });
158
+ } catch {
159
+ // A consumer callback cannot turn a controlled bridge failure into an
160
+ // unhandled provider-message exception.
161
+ }
162
+ };
163
+
164
+ const enqueue = (entry: SessionRealtimeInboundEntry): boolean => {
165
+ if (closed || sealed || fatal) return false;
166
+ const bytes = utf8ByteLength(JSON.stringify(entry));
167
+ if (
168
+ pendingInboundCount + 1 > CODEX_REALTIME_V3_PENDING_MAX_ENTRIES ||
169
+ pendingInboundBytes + bytes > CODEX_REALTIME_V3_PENDING_MAX_BYTES
170
+ ) {
171
+ triggerFatal("Codex realtime durable event buffer exceeded its hard limit");
172
+ return false;
173
+ }
174
+ pendingInbound.push({ entry, bytes });
175
+ pendingInboundCount += 1;
176
+ pendingInboundBytes += bytes;
177
+ return true;
178
+ };
179
+
180
+ const hasWork = (): boolean =>
181
+ pendingInbound.length > 0 ||
182
+ (!providerStartedAccepted && providerStarted !== undefined) ||
183
+ clientAckThroughSequence !== null ||
184
+ forceSync;
185
+
186
+ const runFlush = async (): Promise<void> => {
187
+ while (true) {
188
+ if (closed || fatal) break;
189
+ const batch = pendingInbound.splice(0, CODEX_REALTIME_V3_SYNC_MAX_ENTRIES);
190
+ const startup = providerStartedAccepted ? undefined : providerStarted;
191
+ const acknowledgedByClient = clientAckThroughSequence;
192
+ const poll = forceSync;
193
+ forceSync = false;
194
+ flushRequestedWhileRunning = false;
195
+ if (batch.length === 0 && !startup && acknowledgedByClient === null && !poll) break;
196
+
197
+ let result: SyncSessionRealtimeLedgerResponse;
198
+ try {
199
+ result = await options.sync({
200
+ ...options.owner,
201
+ connectionId: options.connectionId,
202
+ connectionEpoch: options.connectionEpoch,
203
+ ...(batch.length === 0 ? {} : { entries: batch.map(({ entry }) => entry) }),
204
+ ...(startup ? { providerStarted: startup } : {}),
205
+ ...(acknowledgedByClient === null
206
+ ? {}
207
+ : { clientAckThroughSequence: acknowledgedByClient }),
208
+ });
209
+ } catch (error) {
210
+ // Entries that were in flight retain their accounting and return ahead
211
+ // of every arrival accepted while the request was pending.
212
+ pendingInbound = [...batch, ...pendingInbound];
213
+ throw error;
214
+ }
215
+
216
+ for (const item of batch) {
217
+ pendingInboundCount -= 1;
218
+ pendingInboundBytes -= item.bytes;
219
+ }
220
+ if (startup && providerStarted === startup) providerStartedAccepted = true;
221
+ if (
222
+ acknowledgedByClient !== null &&
223
+ clientAckThroughSequence !== null &&
224
+ clientAckThroughSequence <= acknowledgedByClient
225
+ ) {
226
+ clientAckThroughSequence = null;
227
+ }
228
+ if (closed || fatal) break;
229
+
230
+ for (const entry of result.outbound) {
231
+ // This is OpenGeni's durable browser-delivery acknowledgment. The
232
+ // provider send below remains at-least-once because pinned V3 exposes
233
+ // no provider receipt and providerAckSequences is never populated.
234
+ if (entry.clientAckedAt === null && !clientReceivedSequences.has(entry.sequence)) {
235
+ clientReceivedSequences.add(entry.sequence);
236
+ clientAckThroughSequence = Math.max(clientAckThroughSequence ?? 0, entry.sequence);
237
+ }
238
+ }
239
+ for (let index = 0; index < result.outbound.length;) {
240
+ const entry = result.outbound[index]!;
241
+ if (sentSequences.has(entry.sequence)) {
242
+ index += 1;
243
+ continue;
244
+ }
245
+ if (entry.kind === "delegation_progress" && entry.delegationItemId) {
246
+ const progress: SessionRealtimeLedgerEntry[] = [];
247
+ while (index < result.outbound.length) {
248
+ const candidate = result.outbound[index]!;
249
+ if (
250
+ candidate.kind !== "delegation_progress" ||
251
+ candidate.delegationItemId !== entry.delegationItemId
252
+ ) {
253
+ break;
254
+ }
255
+ if (!sentSequences.has(candidate.sequence)) progress.push(candidate);
256
+ index += 1;
257
+ }
258
+ sendDelegationProgress(options.events, entry.delegationItemId, progress);
259
+ for (const candidate of progress) sentSequences.add(candidate.sequence);
260
+ continue;
261
+ }
262
+ sendOutbound(options.events, entry);
263
+ sentSequences.add(entry.sequence);
264
+ if (
265
+ entry.kind === "session_update" &&
266
+ entry.payload.source === "human_input" &&
267
+ entry.payload.delivery === "steer"
268
+ ) {
269
+ // The session continues under the human's new direction. The typed
270
+ // session update above is all the provider needs; this only keeps the
271
+ // local diagnostic from claiming the prior delegation is still live.
272
+ activeDelegationId = null;
273
+ }
274
+ if (
275
+ (entry.kind === "delegation_result" || entry.kind === "error") &&
276
+ entry.delegationItemId === activeDelegationId
277
+ ) {
278
+ activeDelegationId = null;
279
+ }
280
+ index += 1;
281
+ }
282
+ publish();
283
+ }
284
+ };
285
+
286
+ const requestFlush = (poll: boolean): Promise<void> => {
287
+ if (closed || fatal) return Promise.resolve();
288
+ if (poll) forceSync = true;
289
+ if (flushing) {
290
+ flushRequestedWhileRunning = true;
291
+ return flushing;
292
+ }
293
+ const task = Promise.resolve()
294
+ .then(runFlush)
295
+ .catch((error: unknown) => {
296
+ lastError = safeError(error);
297
+ publish();
298
+ throw error;
299
+ });
300
+ flushing = task;
301
+ void task.then(
302
+ () => {
303
+ if (flushing !== task) return;
304
+ flushing = null;
305
+ if (!closed && !fatal && (flushRequestedWhileRunning || hasWork())) {
306
+ flushRequestedWhileRunning = false;
307
+ void requestFlush(false).catch(() => undefined);
308
+ }
309
+ },
310
+ () => {
311
+ if (flushing === task) flushing = null;
312
+ },
313
+ );
314
+ return task;
315
+ };
316
+
317
+ const ingest = (payload: string): Promise<void> => {
318
+ if (closed || sealed || fatal) return Promise.resolve();
319
+ const parsed = parseCodexRealtimeV3Event(payload);
320
+ if (!parsed.ok) {
321
+ if (parsed.reason === "unsupported_type") {
322
+ ignoredEventCount += 1;
323
+ lastIgnoredEventType = parsed.eventType;
324
+ publish();
325
+ return Promise.resolve();
326
+ }
327
+ lastError = `Rejected Codex realtime V3 event: ${parsed.reason}`;
328
+ publish();
329
+ return Promise.resolve();
330
+ }
331
+ const event = parsed.event;
332
+ let durable = false;
333
+ if (event.type === "session.started") {
334
+ if (!providerStartedAccepted && providerStarted === undefined) {
335
+ providerStarted = {
336
+ providerSessionId: event.sessionId,
337
+ providerEventId: event.providerEventId,
338
+ };
339
+ durable = true;
340
+ }
341
+ } else if (
342
+ event.type === "input_transcript.added" ||
343
+ event.type === "output_transcript.added"
344
+ ) {
345
+ // These events are provider UI deltas. `turn.done` is the single
346
+ // authoritative finalized transcript persisted below.
347
+ } else if (event.type === "delegation.created") {
348
+ activeDelegationId = event.delegationItemId;
349
+ const transcript = delegationTranscript(transcriptSinceDelegation, event.inputTranscript);
350
+ const coveredTurnIds = transcriptSinceDelegation.map((entry) => entry.turnId);
351
+ durable = enqueue({
352
+ operationId: randomUUID(),
353
+ kind: "delegation_call",
354
+ providerEventId: event.providerEventId,
355
+ delegationItemId: event.delegationItemId,
356
+ text: renderRealtimeDelegationInput(event.inputTranscript, transcript),
357
+ payload: {
358
+ offsetMs: event.offsetMs,
359
+ inputTranscript: event.inputTranscript,
360
+ transcriptFenceTurnIds: coveredTurnIds,
361
+ },
362
+ });
363
+ if (durable) {
364
+ const alreadyFinalized = transcriptSinceDelegation.some(
365
+ (entry) =>
366
+ entry.role === "user" &&
367
+ normalizedTranscript(entry.text) === normalizedTranscript(event.inputTranscript),
368
+ );
369
+ pendingDelegationUserTranscript = alreadyFinalized
370
+ ? null
371
+ : { delegationItemId: event.delegationItemId, text: event.inputTranscript };
372
+ transcriptSinceDelegation = [];
373
+ }
374
+ } else if (event.type === "output_audio.delta") {
375
+ speaking = true;
376
+ } else if (event.type === "turn.done") {
377
+ speaking = false;
378
+ if (event.transcript.length > 0 && !finalizedTurnIds.has(event.turnId)) {
379
+ const coveredByDelegationItemId =
380
+ event.role === "user" &&
381
+ pendingDelegationUserTranscript !== null &&
382
+ normalizedTranscript(event.transcript) ===
383
+ normalizedTranscript(pendingDelegationUserTranscript.text)
384
+ ? pendingDelegationUserTranscript.delegationItemId
385
+ : null;
386
+ durable = enqueue(finalTranscript(randomUUID, event, coveredByDelegationItemId));
387
+ if (durable) {
388
+ finalizedTurnIds.add(event.turnId);
389
+ if (coveredByDelegationItemId) {
390
+ pendingDelegationUserTranscript = null;
391
+ } else {
392
+ transcriptSinceDelegation.push({
393
+ role: event.role,
394
+ text: event.transcript,
395
+ turnId: event.turnId,
396
+ });
397
+ }
398
+ }
399
+ }
400
+ } else if (event.type === "error") {
401
+ lastError = event.message;
402
+ durable = enqueue({
403
+ operationId: randomUUID(),
404
+ kind: "error",
405
+ providerEventId: event.providerEventId,
406
+ text: event.message,
407
+ });
408
+ }
409
+ publish();
410
+ return durable ? requestFlush(false) : Promise.resolve();
411
+ };
412
+
413
+ const onMessage = (message: MessageEvent): void => {
414
+ if (typeof message.data !== "string") return;
415
+ void ingest(message.data).catch(() => undefined);
416
+ };
417
+ const listen = (): void => {
418
+ if (closed || sealed || listening) return;
419
+ listening = true;
420
+ options.events.addEventListener("message", onMessage);
421
+ };
422
+ if (options.listen !== false) listen();
423
+ publish();
424
+ return {
425
+ snapshot,
426
+ ingest,
427
+ flush: () => requestFlush(true),
428
+ sealAndFlush: async () => {
429
+ if (closed) return;
430
+ sealed = true;
431
+ if (listening) options.events.removeEventListener("message", onMessage);
432
+ listening = false;
433
+ await requestFlush(true);
434
+ },
435
+ listen,
436
+ close: () => {
437
+ if (closed) return;
438
+ closed = true;
439
+ if (listening) options.events.removeEventListener("message", onMessage);
440
+ listening = false;
441
+ },
442
+ };
443
+ }
444
+
445
+ function finalTranscript(
446
+ randomUUID: () => string,
447
+ event: Extract<CodexRealtimeV3Event, { type: "turn.done" }>,
448
+ coveredByDelegationItemId: string | null,
449
+ ): SessionRealtimeInboundEntry {
450
+ return {
451
+ operationId: randomUUID(),
452
+ kind: event.role === "user" ? "user_transcript" : "assistant_transcript",
453
+ providerEventId: event.providerEventId,
454
+ text: event.transcript,
455
+ payload: {
456
+ turnId: event.turnId,
457
+ ...(coveredByDelegationItemId ? { coveredByDelegationItemId } : {}),
458
+ },
459
+ };
460
+ }
461
+
462
+ function delegationTranscript(
463
+ entries: readonly FinalizedTranscript[],
464
+ inputTranscript: string,
465
+ ): FinalizedTranscript[] {
466
+ const selected = [...entries];
467
+ const normalizedInput = normalizedTranscript(inputTranscript);
468
+ for (let index = selected.length - 1; index >= 0; index -= 1) {
469
+ const entry = selected[index];
470
+ if (
471
+ entry?.role === "user" &&
472
+ normalizedInput.length > 0 &&
473
+ normalizedTranscript(entry.text) === normalizedInput
474
+ ) {
475
+ selected.splice(index, 1);
476
+ break;
477
+ }
478
+ }
479
+ const bounded: FinalizedTranscript[] = [];
480
+ let bytes = 0;
481
+ for (let index = selected.length - 1; index >= 0; index -= 1) {
482
+ const entry = selected[index]!;
483
+ const line = `${entry.role}: ${entry.text}`;
484
+ const lineBytes = utf8ByteLength(line) + (bounded.length > 0 ? 1 : 0);
485
+ if (bytes + lineBytes > REALTIME_DELEGATION_TRANSCRIPT_MAX_BYTES) break;
486
+ bounded.unshift(entry);
487
+ bytes += lineBytes;
488
+ }
489
+ return bounded;
490
+ }
491
+
492
+ function renderRealtimeDelegationInput(
493
+ inputTranscript: string,
494
+ transcript: readonly FinalizedTranscript[],
495
+ ): string {
496
+ const input = takeUtf8Head(inputTranscript, REALTIME_DELEGATION_INPUT_MAX_BYTES);
497
+ const transcriptDelta = transcript.map((entry) => `${entry.role}: ${entry.text}`).join("\n");
498
+ return [
499
+ "<realtime_delegation>",
500
+ ` <input>${escapeXmlText(input)}</input>`,
501
+ ...(transcriptDelta
502
+ ? [` <transcript_delta>${escapeXmlText(transcriptDelta)}</transcript_delta>`]
503
+ : []),
504
+ "</realtime_delegation>",
505
+ ].join("\n");
506
+ }
507
+
508
+ function normalizedTranscript(value: string): string {
509
+ return value.trim().replaceAll(/\s+/g, " ");
510
+ }
511
+
512
+ function takeUtf8Head(value: string, maximumBytes: number): string {
513
+ const bytes = new TextEncoder().encode(value);
514
+ if (bytes.length <= maximumBytes) return value;
515
+ let end = maximumBytes;
516
+ while (end > 0 && (bytes[end]! & 0xc0) === 0x80) end -= 1;
517
+ return new TextDecoder().decode(bytes.subarray(0, end));
518
+ }
519
+
520
+ function escapeXmlText(value: string): string {
521
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
522
+ }
523
+
524
+ function sendOutbound(events: RTCDataChannel, entry: SessionRealtimeLedgerEntry): void {
525
+ if (events.readyState !== "open") throw new Error("Codex realtime data channel is not open");
526
+ const text = entry.text ?? JSON.stringify(entry.payload);
527
+ const payloadChannel =
528
+ entry.payload.channel === "speakable" || entry.payload.channel === "commentary"
529
+ ? entry.payload.channel
530
+ : entry.payload.channel === null
531
+ ? undefined
532
+ : entry.kind === "session_update"
533
+ ? "commentary"
534
+ : undefined;
535
+ const messages =
536
+ (entry.kind === "delegation_result" || entry.kind === "error") && entry.delegationItemId
537
+ ? encodeCodexRealtimeV3DelegationContextAppend({
538
+ delegationItemId: entry.delegationItemId,
539
+ text,
540
+ channel: payloadChannel ?? "speakable",
541
+ })
542
+ : encodeCodexRealtimeV3SessionContextAppend({ text, channel: payloadChannel });
543
+ for (const message of messages) events.send(JSON.stringify(message));
544
+ }
545
+
546
+ function sendDelegationProgress(
547
+ events: RTCDataChannel,
548
+ delegationItemId: string,
549
+ entries: SessionRealtimeLedgerEntry[],
550
+ ): void {
551
+ if (entries.length === 0) return;
552
+ if (events.readyState !== "open") throw new Error("Codex realtime data channel is not open");
553
+ const text = entries.map((entry) => entry.text ?? "").join("");
554
+ if (text.length === 0) return;
555
+ for (const message of encodeCodexRealtimeV3DelegationContextAppend({
556
+ delegationItemId,
557
+ text,
558
+ channel: "commentary",
559
+ })) {
560
+ events.send(JSON.stringify(message));
561
+ }
562
+ }
563
+
564
+ function utf8ByteLength(value: string): number {
565
+ return new TextEncoder().encode(value).byteLength;
566
+ }
567
+
568
+ function defaultRandomUUID(): string {
569
+ if (!globalThis.crypto?.randomUUID) throw new Error("crypto.randomUUID is unavailable");
570
+ return globalThis.crypto.randomUUID();
571
+ }
572
+
573
+ function safeError(error: unknown): string {
574
+ return error instanceof Error ? error.message : "Codex realtime bridge failed";
575
+ }