@memberjunction/realtime-runtime 6.2.0-edge.3 → 6.2.0-edge.5
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 +75 -0
- package/dist/channels/base-realtime-channel-client.d.ts +535 -28
- package/dist/channels/base-realtime-channel-client.d.ts.map +1 -1
- package/dist/channels/base-realtime-channel-client.js +864 -10
- package/dist/channels/base-realtime-channel-client.js.map +1 -1
- package/dist/channels/channel-action-dispatcher.d.ts +70 -0
- package/dist/channels/channel-action-dispatcher.d.ts.map +1 -0
- package/dist/channels/channel-action-dispatcher.js +136 -0
- package/dist/channels/channel-action-dispatcher.js.map +1 -0
- package/dist/channels/channel-catalog-note.d.ts +48 -0
- package/dist/channels/channel-catalog-note.d.ts.map +1 -0
- package/dist/channels/channel-catalog-note.js +119 -0
- package/dist/channels/channel-catalog-note.js.map +1 -0
- package/dist/channels/channel-contract-types.d.ts +107 -0
- package/dist/channels/channel-contract-types.d.ts.map +1 -0
- package/dist/channels/channel-contract-types.js +12 -0
- package/dist/channels/channel-contract-types.js.map +1 -0
- package/dist/channels/channel-descriptor-synthesis.d.ts +56 -0
- package/dist/channels/channel-descriptor-synthesis.d.ts.map +1 -0
- package/dist/channels/channel-descriptor-synthesis.js +90 -0
- package/dist/channels/channel-descriptor-synthesis.js.map +1 -0
- package/dist/channels/channel-perception.d.ts +82 -0
- package/dist/channels/channel-perception.d.ts.map +1 -0
- package/dist/channels/channel-perception.js +120 -0
- package/dist/channels/channel-perception.js.map +1 -0
- package/dist/channels/channel-schema-format.d.ts +17 -0
- package/dist/channels/channel-schema-format.d.ts.map +1 -0
- package/dist/channels/channel-schema-format.js +45 -0
- package/dist/channels/channel-schema-format.js.map +1 -0
- package/dist/channels/channel-state-delta.d.ts +59 -0
- package/dist/channels/channel-state-delta.d.ts.map +1 -0
- package/dist/channels/channel-state-delta.js +116 -0
- package/dist/channels/channel-state-delta.js.map +1 -0
- package/dist/channels/channel-surface-placement.d.ts +25 -0
- package/dist/channels/channel-surface-placement.d.ts.map +1 -0
- package/dist/channels/channel-surface-placement.js +29 -0
- package/dist/channels/channel-surface-placement.js.map +1 -0
- package/dist/channels/channel-visual-pump.d.ts +126 -0
- package/dist/channels/channel-visual-pump.d.ts.map +1 -0
- package/dist/channels/channel-visual-pump.js +174 -0
- package/dist/channels/channel-visual-pump.js.map +1 -0
- package/dist/hosts/IRealtimeMediaHost.d.ts +86 -34
- package/dist/hosts/IRealtimeMediaHost.d.ts.map +1 -1
- package/dist/hosts/IRealtimeMediaHost.js +2 -1
- package/dist/hosts/IRealtimeMediaHost.js.map +1 -1
- package/dist/index.d.ts +26 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/session/RealtimeSessionRuntime.d.ts +984 -111
- package/dist/session/RealtimeSessionRuntime.d.ts.map +1 -1
- package/dist/session/RealtimeSessionRuntime.js +2396 -381
- package/dist/session/RealtimeSessionRuntime.js.map +1 -1
- package/dist/session/RecordingShardDrain.d.ts +59 -0
- package/dist/session/RecordingShardDrain.d.ts.map +1 -0
- package/dist/session/RecordingShardDrain.js +135 -0
- package/dist/session/RecordingShardDrain.js.map +1 -0
- package/dist/session/RecordingShardQueue.d.ts +94 -0
- package/dist/session/RecordingShardQueue.d.ts.map +1 -0
- package/dist/session/RecordingShardQueue.js +176 -0
- package/dist/session/RecordingShardQueue.js.map +1 -0
- package/dist/session/RecordingShardStore.d.ts +57 -0
- package/dist/session/RecordingShardStore.d.ts.map +1 -0
- package/dist/session/RecordingShardStore.js +184 -0
- package/dist/session/RecordingShardStore.js.map +1 -0
- package/dist/session/RecordingShardWindow.d.ts +42 -0
- package/dist/session/RecordingShardWindow.d.ts.map +1 -0
- package/dist/session/RecordingShardWindow.js +88 -0
- package/dist/session/RecordingShardWindow.js.map +1 -0
- package/dist/session/RecordingTransport.d.ts +66 -0
- package/dist/session/RecordingTransport.d.ts.map +1 -0
- package/dist/session/RecordingTransport.js +61 -0
- package/dist/session/RecordingTransport.js.map +1 -0
- package/dist/session/app-client-tool-registry.d.ts +76 -0
- package/dist/session/app-client-tool-registry.d.ts.map +1 -0
- package/dist/session/app-client-tool-registry.js +100 -0
- package/dist/session/app-client-tool-registry.js.map +1 -0
- package/dist/session/avatar-notice.d.ts +43 -0
- package/dist/session/avatar-notice.d.ts.map +1 -0
- package/dist/session/avatar-notice.js +60 -0
- package/dist/session/avatar-notice.js.map +1 -0
- package/dist/session/call-end.d.ts +21 -0
- package/dist/session/call-end.d.ts.map +1 -0
- package/dist/session/call-end.js +19 -0
- package/dist/session/call-end.js.map +1 -0
- package/dist/session/channel-exposure-preferences.d.ts +70 -0
- package/dist/session/channel-exposure-preferences.d.ts.map +1 -0
- package/dist/session/channel-exposure-preferences.js +138 -0
- package/dist/session/channel-exposure-preferences.js.map +1 -0
- package/dist/session/channel-session-scope.d.ts +142 -0
- package/dist/session/channel-session-scope.d.ts.map +1 -0
- package/dist/session/channel-session-scope.js +128 -0
- package/dist/session/channel-session-scope.js.map +1 -0
- package/dist/session/client-session-deadline.d.ts +28 -0
- package/dist/session/client-session-deadline.d.ts.map +1 -0
- package/dist/session/client-session-deadline.js +50 -0
- package/dist/session/client-session-deadline.js.map +1 -0
- package/dist/session/realtime-captures.d.ts +236 -0
- package/dist/session/realtime-captures.d.ts.map +1 -0
- package/dist/session/realtime-captures.js +383 -0
- package/dist/session/realtime-captures.js.map +1 -0
- package/dist/session/realtime-microphone.d.ts +50 -0
- package/dist/session/realtime-microphone.d.ts.map +1 -0
- package/dist/session/realtime-microphone.js +51 -0
- package/dist/session/realtime-microphone.js.map +1 -0
- package/dist/session/session-event-hub.d.ts +72 -0
- package/dist/session/session-event-hub.d.ts.map +1 -0
- package/dist/session/session-event-hub.js +151 -0
- package/dist/session/session-event-hub.js.map +1 -0
- package/dist/session/session-launcher.d.ts +128 -0
- package/dist/session/session-launcher.d.ts.map +1 -0
- package/dist/session/session-launcher.js +160 -0
- package/dist/session/session-launcher.js.map +1 -0
- package/package.json +10 -9
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The microphone of one realtime session, as a host's device menu shows it: the one the call uses, the
|
|
3
|
+
* ones the user can move it to (`RealtimeSessionRuntime.SwitchMicrophone`), and why none works once the call lost it.
|
|
4
|
+
*
|
|
5
|
+
* The call opens the microphone through the host's camera-and-microphone controller, and the controller reports the
|
|
6
|
+
* device in use and the devices the browser lists. This module reads the microphone's part of that report. It shows
|
|
7
|
+
* nothing outside a call and nothing on a host without a controller, which opens the microphone itself
|
|
8
|
+
* (`IRealtimeMediaHost.AcquireMicrophone`) and so has no list to offer.
|
|
9
|
+
*
|
|
10
|
+
* @module @memberjunction/realtime-runtime
|
|
11
|
+
*/
|
|
12
|
+
import type { LocalMediaFailure, LocalMediaState, MediaDevice } from '@memberjunction/ai-realtime-client';
|
|
13
|
+
/** The call's microphone: the one in use, the ones the user can move it to, and why none works once the call lost it. */
|
|
14
|
+
export interface RealtimeMicrophoneState {
|
|
15
|
+
/**
|
|
16
|
+
* The microphone in use. Kept while a switch is under way, until the new one is on. Absent outside a call, on a host
|
|
17
|
+
* without a controller, and when the controller has no microphone open (it stopped, or no device could open).
|
|
18
|
+
*/
|
|
19
|
+
DeviceID?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The microphones the browser lists while the call is connected: what `SwitchMicrophone` can move to. The call has
|
|
22
|
+
* opened the microphone, so the browser gives their names. Empty outside a call and on a host without a controller.
|
|
23
|
+
*/
|
|
24
|
+
Devices: readonly MediaDevice[];
|
|
25
|
+
/**
|
|
26
|
+
* Why no microphone works: the call lost its microphone (the one in use failed and so did the fallback to the default,
|
|
27
|
+
* or the last one went away), so the agent hears nothing until the user picks one with `SwitchMicrophone`. Kept while
|
|
28
|
+
* the picked one opens, and gone once a microphone is on. Absent while the microphone works or is being switched,
|
|
29
|
+
* outside a call, and on a host without a controller.
|
|
30
|
+
*/
|
|
31
|
+
Failure?: LocalMediaFailure;
|
|
32
|
+
}
|
|
33
|
+
/** No microphone to show or switch: what a host gets outside a call and on a host without a controller. */
|
|
34
|
+
export declare const REALTIME_MICROPHONE_NONE: RealtimeMicrophoneState;
|
|
35
|
+
/**
|
|
36
|
+
* The call's microphone as a controller reports it. The device in use is the controller's while its microphone is on.
|
|
37
|
+
* While the controller is starting a microphone (a switch, a lost device replaced by the default, or a pick after the call
|
|
38
|
+
* lost its microphone), the call keeps what it shows now: the microphone in use, so a device menu keeps showing it until
|
|
39
|
+
* the new one is on, or why none works. Failed, there is none, and the failure says why. Stopped, there is none.
|
|
40
|
+
*
|
|
41
|
+
* @param media The controller's state.
|
|
42
|
+
* @param current What the call shows now.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ReadMicrophoneState(media: LocalMediaState, current?: RealtimeMicrophoneState): RealtimeMicrophoneState;
|
|
45
|
+
/**
|
|
46
|
+
* Whether two states name the same microphone in use, list the same microphones (by id and name, in the same order) and
|
|
47
|
+
* give the same failure.
|
|
48
|
+
*/
|
|
49
|
+
export declare function SameMicrophoneState(a: RealtimeMicrophoneState, b: RealtimeMicrophoneState): boolean;
|
|
50
|
+
//# sourceMappingURL=realtime-microphone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-microphone.d.ts","sourceRoot":"","sources":["../../src/session/realtime-microphone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAE1G,yHAAyH;AACzH,MAAM,WAAW,uBAAuB;IACpC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IAChC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC/B;AAED,2GAA2G;AAC3G,eAAO,MAAM,wBAAwB,EAAE,uBAAuE,CAAC;AAE/G;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,EAAE,OAAO,GAAE,uBAAkD,GAAG,uBAAuB,CAiBhJ;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,uBAAuB,EAAE,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAOnG"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The microphone of one realtime session, as a host's device menu shows it: the one the call uses, the
|
|
3
|
+
* ones the user can move it to (`RealtimeSessionRuntime.SwitchMicrophone`), and why none works once the call lost it.
|
|
4
|
+
*
|
|
5
|
+
* The call opens the microphone through the host's camera-and-microphone controller, and the controller reports the
|
|
6
|
+
* device in use and the devices the browser lists. This module reads the microphone's part of that report. It shows
|
|
7
|
+
* nothing outside a call and nothing on a host without a controller, which opens the microphone itself
|
|
8
|
+
* (`IRealtimeMediaHost.AcquireMicrophone`) and so has no list to offer.
|
|
9
|
+
*
|
|
10
|
+
* @module @memberjunction/realtime-runtime
|
|
11
|
+
*/
|
|
12
|
+
/** No microphone to show or switch: what a host gets outside a call and on a host without a controller. */
|
|
13
|
+
export const REALTIME_MICROPHONE_NONE = Object.freeze({ Devices: Object.freeze([]) });
|
|
14
|
+
/**
|
|
15
|
+
* The call's microphone as a controller reports it. The device in use is the controller's while its microphone is on.
|
|
16
|
+
* While the controller is starting a microphone (a switch, a lost device replaced by the default, or a pick after the call
|
|
17
|
+
* lost its microphone), the call keeps what it shows now: the microphone in use, so a device menu keeps showing it until
|
|
18
|
+
* the new one is on, or why none works. Failed, there is none, and the failure says why. Stopped, there is none.
|
|
19
|
+
*
|
|
20
|
+
* @param media The controller's state.
|
|
21
|
+
* @param current What the call shows now.
|
|
22
|
+
*/
|
|
23
|
+
export function ReadMicrophoneState(media, current = REALTIME_MICROPHONE_NONE) {
|
|
24
|
+
const microphone = media.Microphone;
|
|
25
|
+
const devices = media.Devices.filter((d) => d.Kind === 'microphone');
|
|
26
|
+
switch (microphone.Status) {
|
|
27
|
+
case 'on':
|
|
28
|
+
return { ...(microphone.DeviceID ? { DeviceID: microphone.DeviceID } : {}), Devices: devices };
|
|
29
|
+
case 'starting':
|
|
30
|
+
return {
|
|
31
|
+
...(current.DeviceID ? { DeviceID: current.DeviceID } : {}),
|
|
32
|
+
...(current.Failure ? { Failure: current.Failure } : {}),
|
|
33
|
+
Devices: devices,
|
|
34
|
+
};
|
|
35
|
+
case 'failed':
|
|
36
|
+
return { Failure: microphone.Failure ?? 'error', Devices: devices };
|
|
37
|
+
default:
|
|
38
|
+
return { Devices: devices };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether two states name the same microphone in use, list the same microphones (by id and name, in the same order) and
|
|
43
|
+
* give the same failure.
|
|
44
|
+
*/
|
|
45
|
+
export function SameMicrophoneState(a, b) {
|
|
46
|
+
return (a.DeviceID === b.DeviceID &&
|
|
47
|
+
a.Failure === b.Failure &&
|
|
48
|
+
a.Devices.length === b.Devices.length &&
|
|
49
|
+
a.Devices.every((d, i) => d.DeviceID === b.Devices[i].DeviceID && d.Label === b.Devices[i].Label));
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=realtime-microphone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-microphone.js","sourceRoot":"","sources":["../../src/session/realtime-microphone.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAyBH,2GAA2G;AAC3G,MAAM,CAAC,MAAM,wBAAwB,GAA4B,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AAE/G;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAsB,EAAE,UAAmC,wBAAwB;IACnH,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC;IACpC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC;IACrE,QAAQ,UAAU,CAAC,MAAM,EAAE,CAAC;QACxB,KAAK,IAAI;YACL,OAAO,EAAE,GAAG,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QACnG,KAAK,UAAU;YACX,OAAO;gBACH,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3D,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACxD,OAAO,EAAE,OAAO;aACnB,CAAC;QACN,KAAK,QAAQ;YACT,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QACxE;YACI,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IACpC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,CAA0B,EAAE,CAA0B;IACtF,OAAO,CACH,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ;QACzB,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO;QACvB,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM;QACrC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CACpG,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Observable } from 'rxjs';
|
|
2
|
+
import type { RealtimeSessionEvent, UnknownRealtimeSessionEvent } from '@memberjunction/ai-core-plus';
|
|
3
|
+
/** An event a session delivers: a known, typed one, or an app-defined one the runtime does not interpret. */
|
|
4
|
+
export type RealtimeSessionStreamEvent = RealtimeSessionEvent | UnknownRealtimeSessionEvent;
|
|
5
|
+
/** The slice of a verification status read the hub needs to recover a missed `identity.verified`. */
|
|
6
|
+
export interface RealtimeSessionVerificationSnapshot {
|
|
7
|
+
VerificationState: 'unverified' | 'pending' | 'verified';
|
|
8
|
+
VerifiedEmail?: string;
|
|
9
|
+
VerifiedAt?: string;
|
|
10
|
+
MaxSessionDeadlineIso?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Where session events come from — the transport seam. `GraphQLRealtimeSessionClient`
|
|
14
|
+
* (`@memberjunction/graphql-dataprovider`) satisfies this structurally; a host on another transport
|
|
15
|
+
* supplies its own.
|
|
16
|
+
*/
|
|
17
|
+
export interface IRealtimeSessionEventSource {
|
|
18
|
+
/** Opens the session's event stream. It completes when the transport recycles (e.g. a token refresh). */
|
|
19
|
+
SubscribeToSessionEvents(agentSessionId: string): Observable<RealtimeSessionStreamEvent>;
|
|
20
|
+
/** Reads the session's verification state once — the durable backstop for an event missed while away. Never throws. */
|
|
21
|
+
GetVerificationStatus(agentSessionId: string): Promise<RealtimeSessionVerificationSnapshot>;
|
|
22
|
+
}
|
|
23
|
+
/** Tuning for {@link RealtimeSessionEventHub}'s reconnect loop. */
|
|
24
|
+
export interface RealtimeSessionEventHubOptions {
|
|
25
|
+
/** Consecutive failed/short-lived subscriptions tolerated before the hub gives up. Default 6. */
|
|
26
|
+
MaxReconnectAttempts?: number;
|
|
27
|
+
/** First reconnect delay; doubles per consecutive attempt up to {@link MaxBackoffMs}. Default 1000. */
|
|
28
|
+
BaseBackoffMs?: number;
|
|
29
|
+
/** Ceiling of the reconnect delay. Default 15000. */
|
|
30
|
+
MaxBackoffMs?: number;
|
|
31
|
+
/** A subscription that lived at least this long counts as healthy and resets the attempt counter. Default 30000. */
|
|
32
|
+
StableAfterMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Keeps one session's event stream open for the session's life and delivers every event to a sink.
|
|
36
|
+
*
|
|
37
|
+
* The stream is a long-lived subscription that ends when the transport recycles its connection (token
|
|
38
|
+
* refresh, network blip) — and delivery has no replay, so an event published during the gap is gone.
|
|
39
|
+
* The hub therefore, on every re-subscribe: opens the new subscription FIRST (so nothing published
|
|
40
|
+
* from now on is missed), THEN reads the verification status once, and if the person verified while the
|
|
41
|
+
* hub was away, delivers the `identity.verified` event it missed (marked `Recovered`).
|
|
42
|
+
*
|
|
43
|
+
* Reconnects are capped: a stream that keeps failing is given up on (once, loudly) rather than retried
|
|
44
|
+
* forever; a stream that stayed up long enough counts as healthy and resets the cap.
|
|
45
|
+
*
|
|
46
|
+
* Framework-agnostic and clock-driven by `setTimeout`, so tests drive it with fake timers.
|
|
47
|
+
*/
|
|
48
|
+
export declare class RealtimeSessionEventHub {
|
|
49
|
+
private readonly source;
|
|
50
|
+
private readonly deliver;
|
|
51
|
+
private readonly options;
|
|
52
|
+
private sessionId;
|
|
53
|
+
private subscription;
|
|
54
|
+
private reconnectTimer;
|
|
55
|
+
private attempts;
|
|
56
|
+
private verifiedDelivered;
|
|
57
|
+
/** Bumped by every Start/Stop so a late callback from a previous run can tell it is stale. */
|
|
58
|
+
private generation;
|
|
59
|
+
constructor(source: IRealtimeSessionEventSource, deliver: (event: RealtimeSessionStreamEvent) => void, options?: RealtimeSessionEventHubOptions);
|
|
60
|
+
/** Whether the hub currently has (or is about to re-open) a subscription. */
|
|
61
|
+
get IsRunning(): boolean;
|
|
62
|
+
/** Opens the stream for `agentSessionId`. Replaces any run in progress. */
|
|
63
|
+
Start(agentSessionId: string): void;
|
|
64
|
+
/** Closes the stream and cancels any pending reconnect. Safe to call when not running. */
|
|
65
|
+
Stop(): void;
|
|
66
|
+
private connect;
|
|
67
|
+
private onEvent;
|
|
68
|
+
private onStreamEnded;
|
|
69
|
+
/** Reads the verification status once and delivers the `identity.verified` event the hub missed, if any. */
|
|
70
|
+
private recoverMissedVerification;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=session-event-hub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-event-hub.d.ts","sourceRoot":"","sources":["../../src/session/session-event-hub.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,MAAM,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC;AAEtG,6GAA6G;AAC7G,MAAM,MAAM,0BAA0B,GAAG,oBAAoB,GAAG,2BAA2B,CAAC;AAE5F,qGAAqG;AACrG,MAAM,WAAW,mCAAmC;IAClD,iBAAiB,EAAE,YAAY,GAAG,SAAS,GAAG,UAAU,CAAC;IACzD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,yGAAyG;IACzG,wBAAwB,CAAC,cAAc,EAAE,MAAM,GAAG,UAAU,CAAC,0BAA0B,CAAC,CAAC;IACzF,uHAAuH;IACvH,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,mCAAmC,CAAC,CAAC;CAC7F;AAED,mEAAmE;AACnE,MAAM,WAAW,8BAA8B;IAC7C,iGAAiG;IACjG,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,uGAAuG;IACvG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oHAAoH;IACpH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;;;;;;GAaG;AACH,qBAAa,uBAAuB;IAWhC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAX1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;IACnE,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,iBAAiB,CAAS;IAClC,8FAA8F;IAC9F,OAAO,CAAC,UAAU,CAAK;gBAGJ,MAAM,EAAE,2BAA2B,EACnC,OAAO,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,IAAI,EACrE,OAAO,GAAE,8BAAmC;IAK9C,6EAA6E;IAC7E,IAAW,SAAS,IAAI,OAAO,CAE9B;IAED,2EAA2E;IACpE,KAAK,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI;IAQ1C,0FAA0F;IACnF,IAAI,IAAI,IAAI;IAWnB,OAAO,CAAC,OAAO;IA2Bf,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,aAAa;IA0BrB,4GAA4G;YAC9F,yBAAyB;CAgCxC"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
MaxReconnectAttempts: 6,
|
|
3
|
+
BaseBackoffMs: 1000,
|
|
4
|
+
MaxBackoffMs: 15_000,
|
|
5
|
+
StableAfterMs: 30_000
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Keeps one session's event stream open for the session's life and delivers every event to a sink.
|
|
9
|
+
*
|
|
10
|
+
* The stream is a long-lived subscription that ends when the transport recycles its connection (token
|
|
11
|
+
* refresh, network blip) — and delivery has no replay, so an event published during the gap is gone.
|
|
12
|
+
* The hub therefore, on every re-subscribe: opens the new subscription FIRST (so nothing published
|
|
13
|
+
* from now on is missed), THEN reads the verification status once, and if the person verified while the
|
|
14
|
+
* hub was away, delivers the `identity.verified` event it missed (marked `Recovered`).
|
|
15
|
+
*
|
|
16
|
+
* Reconnects are capped: a stream that keeps failing is given up on (once, loudly) rather than retried
|
|
17
|
+
* forever; a stream that stayed up long enough counts as healthy and resets the cap.
|
|
18
|
+
*
|
|
19
|
+
* Framework-agnostic and clock-driven by `setTimeout`, so tests drive it with fake timers.
|
|
20
|
+
*/
|
|
21
|
+
export class RealtimeSessionEventHub {
|
|
22
|
+
constructor(source, deliver, options = {}) {
|
|
23
|
+
this.source = source;
|
|
24
|
+
this.deliver = deliver;
|
|
25
|
+
this.sessionId = null;
|
|
26
|
+
this.subscription = null;
|
|
27
|
+
this.reconnectTimer = null;
|
|
28
|
+
this.attempts = 0;
|
|
29
|
+
this.verifiedDelivered = false;
|
|
30
|
+
/** Bumped by every Start/Stop so a late callback from a previous run can tell it is stale. */
|
|
31
|
+
this.generation = 0;
|
|
32
|
+
this.options = { ...DEFAULTS, ...options };
|
|
33
|
+
}
|
|
34
|
+
/** Whether the hub currently has (or is about to re-open) a subscription. */
|
|
35
|
+
get IsRunning() {
|
|
36
|
+
return this.sessionId !== null;
|
|
37
|
+
}
|
|
38
|
+
/** Opens the stream for `agentSessionId`. Replaces any run in progress. */
|
|
39
|
+
Start(agentSessionId) {
|
|
40
|
+
this.Stop();
|
|
41
|
+
this.sessionId = agentSessionId;
|
|
42
|
+
this.attempts = 0;
|
|
43
|
+
this.verifiedDelivered = false;
|
|
44
|
+
this.connect(false);
|
|
45
|
+
}
|
|
46
|
+
/** Closes the stream and cancels any pending reconnect. Safe to call when not running. */
|
|
47
|
+
Stop() {
|
|
48
|
+
this.generation++;
|
|
49
|
+
this.sessionId = null;
|
|
50
|
+
if (this.reconnectTimer !== null) {
|
|
51
|
+
clearTimeout(this.reconnectTimer);
|
|
52
|
+
this.reconnectTimer = null;
|
|
53
|
+
}
|
|
54
|
+
this.subscription?.unsubscribe();
|
|
55
|
+
this.subscription = null;
|
|
56
|
+
}
|
|
57
|
+
connect(isReconnect) {
|
|
58
|
+
const sessionId = this.sessionId;
|
|
59
|
+
if (sessionId === null) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const generation = this.generation;
|
|
63
|
+
const openedAt = Date.now();
|
|
64
|
+
this.subscription?.unsubscribe();
|
|
65
|
+
try {
|
|
66
|
+
this.subscription = this.source.SubscribeToSessionEvents(sessionId).subscribe({
|
|
67
|
+
next: (event) => this.onEvent(generation, event),
|
|
68
|
+
error: (error) => {
|
|
69
|
+
console.warn('[RealtimeSession] The session event stream failed:', error);
|
|
70
|
+
this.onStreamEnded(generation, openedAt);
|
|
71
|
+
},
|
|
72
|
+
complete: () => this.onStreamEnded(generation, openedAt)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.warn('[RealtimeSession] Could not open the session event stream:', error);
|
|
77
|
+
this.onStreamEnded(generation, openedAt);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (isReconnect) {
|
|
81
|
+
void this.recoverMissedVerification(generation, sessionId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
onEvent(generation, event) {
|
|
85
|
+
if (generation !== this.generation) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (event.Type === 'identity.verified') {
|
|
89
|
+
this.verifiedDelivered = true;
|
|
90
|
+
}
|
|
91
|
+
this.deliver(event);
|
|
92
|
+
}
|
|
93
|
+
onStreamEnded(generation, openedAt) {
|
|
94
|
+
if (generation !== this.generation || this.sessionId === null) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (Date.now() - openedAt >= this.options.StableAfterMs) {
|
|
98
|
+
this.attempts = 0;
|
|
99
|
+
}
|
|
100
|
+
this.attempts++;
|
|
101
|
+
if (this.attempts > this.options.MaxReconnectAttempts) {
|
|
102
|
+
console.error(`[RealtimeSession] The session event stream for ${this.sessionId} kept ending; giving up after ${this.options.MaxReconnectAttempts} attempts. ` +
|
|
103
|
+
'Session events (such as identity verification) will not arrive until the next session.');
|
|
104
|
+
this.sessionId = null;
|
|
105
|
+
this.subscription = null;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const delay = Math.min(this.options.BaseBackoffMs * 2 ** (this.attempts - 1), this.options.MaxBackoffMs);
|
|
109
|
+
this.reconnectTimer = setTimeout(() => {
|
|
110
|
+
this.reconnectTimer = null;
|
|
111
|
+
if (generation === this.generation) {
|
|
112
|
+
this.connect(true);
|
|
113
|
+
}
|
|
114
|
+
}, delay);
|
|
115
|
+
}
|
|
116
|
+
/** Reads the verification status once and delivers the `identity.verified` event the hub missed, if any. */
|
|
117
|
+
async recoverMissedVerification(generation, sessionId) {
|
|
118
|
+
if (this.verifiedDelivered) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let status;
|
|
122
|
+
try {
|
|
123
|
+
status = await this.source.GetVerificationStatus(sessionId);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.warn('[RealtimeSession] Could not read the verification status after reconnecting:', error);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (generation !== this.generation || this.verifiedDelivered) {
|
|
130
|
+
return; // stopped, restarted, or the live event arrived while the status was in flight
|
|
131
|
+
}
|
|
132
|
+
if (status.VerificationState !== 'verified' || !status.VerifiedEmail || !status.VerifiedAt) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.verifiedDelivered = true;
|
|
136
|
+
this.deliver({
|
|
137
|
+
Type: 'identity.verified',
|
|
138
|
+
AgentSessionID: sessionId,
|
|
139
|
+
OccurredAt: status.VerifiedAt,
|
|
140
|
+
Payload: {
|
|
141
|
+
VerifiedEmail: status.VerifiedEmail,
|
|
142
|
+
VerifiedName: '',
|
|
143
|
+
VerifiedAt: status.VerifiedAt,
|
|
144
|
+
Method: 'link',
|
|
145
|
+
...(status.MaxSessionDeadlineIso ? { MaxSessionDeadlineIso: status.MaxSessionDeadlineIso } : {}),
|
|
146
|
+
Recovered: true
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=session-event-hub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-event-hub.js","sourceRoot":"","sources":["../../src/session/session-event-hub.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { IMetadataProvider } from '@memberjunction/core';
|
|
2
|
+
import { type RealtimeToolDefinition } from '@memberjunction/ai';
|
|
3
|
+
import type { AppContextSnapshot } from '@memberjunction/ai-core-plus';
|
|
4
|
+
import type { StartRealtimeClientSessionResult } from './RealtimeSessionRuntime.js';
|
|
5
|
+
/**
|
|
6
|
+
* Everything the runtime knows when it needs a session minted — the inputs of the stock
|
|
7
|
+
* `StartRealtimeClientSession` mutation, as typed values rather than a GraphQL variable bag.
|
|
8
|
+
*
|
|
9
|
+
* A launcher may use all, some or none of it: the default sends it to the stock mutation; a
|
|
10
|
+
* host-specific launcher can map it onto its own mint (a deployment-specific mutation, a REST
|
|
11
|
+
* endpoint, a guest-session exchange) so long as it returns a {@link StartRealtimeClientSessionResult}.
|
|
12
|
+
*/
|
|
13
|
+
export interface RealtimeSessionLaunchRequest {
|
|
14
|
+
/** The agent the session fronts. */
|
|
15
|
+
TargetAgentId: string;
|
|
16
|
+
/** The conversation the session belongs to, when it continues one. */
|
|
17
|
+
ConversationId: string | null;
|
|
18
|
+
/** A prior session whose saved channel state this one resumes. */
|
|
19
|
+
LastSessionId: string | null;
|
|
20
|
+
/** A realtime model the user picked, when the host exposes a model picker. */
|
|
21
|
+
PreferredModelId: string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Client-executed tools to declare to the model at mint: the host's own plus the native tools of the
|
|
24
|
+
* channels mounted with the session. A launcher that mints through its own endpoint must pass them on,
|
|
25
|
+
* or the model never learns it can call them.
|
|
26
|
+
*/
|
|
27
|
+
ClientTools: readonly RealtimeToolDefinition[];
|
|
28
|
+
/** The co-agent override, or `null` to let server metadata choose. */
|
|
29
|
+
CoAgentId: string | null;
|
|
30
|
+
/** Session-config overrides (authorization-gated server side), as JSON. */
|
|
31
|
+
ConfigOverridesJson: string | null;
|
|
32
|
+
/** Whether the user consented to recording this session. */
|
|
33
|
+
RecordingConsent: boolean;
|
|
34
|
+
/** When the recording started (ISO), or `null` when the session is not being recorded. */
|
|
35
|
+
RecordingStartedAt: string | null;
|
|
36
|
+
/** A per-session media-kit collection override. */
|
|
37
|
+
MediaCollectionId: string | null;
|
|
38
|
+
/** The application the session runs in. */
|
|
39
|
+
ApplicationId: string | null;
|
|
40
|
+
/** The live app-context snapshot to inject into the companion prompt. */
|
|
41
|
+
AppContext: AppContextSnapshot | null;
|
|
42
|
+
/**
|
|
43
|
+
* The channels this host can mount (JSON), which the server scopes against the agent's and the app's
|
|
44
|
+
* configuration and answers with the resolved policy. `null` when the host brings none.
|
|
45
|
+
*/
|
|
46
|
+
ChannelCandidatesJson: string | null;
|
|
47
|
+
/** The fork (`MJ: Conversation Branches.ID`) the session starts on; `null` is Main. */
|
|
48
|
+
ConversationBranchId: string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Whether this host can show the agent's video: one of its channels shows it (the Avatar channel). `false` for a host
|
|
51
|
+
* with none (the embeddable widget, the mobile app): the server then asks the model for no avatar, so a video model is
|
|
52
|
+
* not chosen for a face nobody would see, and says why (`host`). A launcher that mints through its own endpoint should
|
|
53
|
+
* pass a `false` on. Absent: not stated, and the server assumes the host may show it.
|
|
54
|
+
*/
|
|
55
|
+
ShowsAgentVideo?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/** What the runtime hands a launcher besides the request: the provider the session runs on. */
|
|
58
|
+
export interface RealtimeSessionLaunchContext {
|
|
59
|
+
/**
|
|
60
|
+
* The MJ provider this session is authenticated on. The default launcher mints through it; a custom
|
|
61
|
+
* launcher that talks to MJ should do the same so the call rides the session's credential.
|
|
62
|
+
*/
|
|
63
|
+
Provider: IMetadataProvider;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* How a realtime session gets MINTED — the seam between "a host wants a session" and "the server has
|
|
67
|
+
* created one and handed back a token".
|
|
68
|
+
*
|
|
69
|
+
* The runtime owns everything either side of it (channel scoping, driver resolution, connecting,
|
|
70
|
+
* transcripts, teardown). A launcher owns only the exchange. That is what lets an app change how a
|
|
71
|
+
* session is minted — a guest-session exchange, a deployment-specific mutation that carries server-side
|
|
72
|
+
* context the stock one cannot — without proxying the runtime's provider or re-implementing the run
|
|
73
|
+
* half. Install one with {@link RealtimeSessionRuntime.Launcher}.
|
|
74
|
+
*
|
|
75
|
+
* A launcher must either resolve a usable result (one carrying an ephemeral token, or a relay session's relay URL) or
|
|
76
|
+
* throw; the runtime turns a throw into a failed start with the error on `LastStartError`.
|
|
77
|
+
*/
|
|
78
|
+
export interface IRealtimeSessionLauncher {
|
|
79
|
+
/**
|
|
80
|
+
* Mints one session.
|
|
81
|
+
*
|
|
82
|
+
* @param request The inputs the runtime resolved for this start.
|
|
83
|
+
* @param context The provider the session runs on.
|
|
84
|
+
* @returns The minted session, including `ClientPolicyJson` when the launcher's mint scoped channels,
|
|
85
|
+
* `AvatarStatusJson` when the server reports the session's avatar status, and `Transport` and `RelayUrl` for a
|
|
86
|
+
* session that runs through MJAPI's realtime relay.
|
|
87
|
+
*/
|
|
88
|
+
Launch(request: RealtimeSessionLaunchRequest, context: RealtimeSessionLaunchContext): Promise<StartRealtimeClientSessionResult>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether a minted session carries what its client driver connects with: an ephemeral token, or, for a relay session
|
|
92
|
+
* (`Transport` `'relay'`), its relay URL.
|
|
93
|
+
*
|
|
94
|
+
* @param result The minted session, or nothing.
|
|
95
|
+
*/
|
|
96
|
+
export declare function HasClientCredential(result: Pick<StartRealtimeClientSessionResult, 'EphemeralToken' | 'Transport' | 'RelayUrl'> | null | undefined): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The stock launcher: mints through the `StartRealtimeClientSession` GraphQL mutation on the session's
|
|
99
|
+
* provider. This is what every host gets unless it installs another.
|
|
100
|
+
*
|
|
101
|
+
* When the session has channel candidates it asks the server to scope them (`channelCandidatesJson`)
|
|
102
|
+
* and to return the resolved policy (`ClientPolicyJson`). It also asks for the session's live-avatar status
|
|
103
|
+
* (`AvatarStatusJson`), which the call reads to say why it shows no avatar, and for how the browser reaches the
|
|
104
|
+
* provider (`Transport` and `RelayUrl`), which a relay session's client driver connects with. A host that shows no agent
|
|
105
|
+
* video ({@link RealtimeSessionLaunchRequest.ShowsAgentVideo} `false`) says so (`showsAgentVideo: false`), so the
|
|
106
|
+
* server asks the model for no avatar. A server that predates an extension rejects it at validation, in which case the
|
|
107
|
+
* launcher mints without that extension alone (the runtime then resolves the channel scope locally, shows no avatar
|
|
108
|
+
* notice, treats every session as direct, or leaves the avatar to the browser's audio-only fallback at connect) — a new
|
|
109
|
+
* client must keep working against an older server, and a failed mint over an optional extension would be the worst way
|
|
110
|
+
* to find out they differ. Each fallback is remembered per launcher instance so a long-lived runtime asks once.
|
|
111
|
+
*/
|
|
112
|
+
export declare class DefaultRealtimeSessionLauncher implements IRealtimeSessionLauncher {
|
|
113
|
+
/** The extensions of the mint the server rejected; each is added once and kept. */
|
|
114
|
+
private readonly serverLacks;
|
|
115
|
+
Launch(request: RealtimeSessionLaunchRequest, context: RealtimeSessionLaunchContext): Promise<StartRealtimeClientSessionResult>;
|
|
116
|
+
/** The mutation's variables for one request. */
|
|
117
|
+
private buildVariables;
|
|
118
|
+
/**
|
|
119
|
+
* Runs the mint with every extension this server has not rejected, dropping one at a time when the server rejects it
|
|
120
|
+
* at validation. Each retry asks for one extension fewer, so the loop ends.
|
|
121
|
+
*/
|
|
122
|
+
private executeMintMutation;
|
|
123
|
+
/** Remembers that the server lacks an extension of the mint, and says so once. */
|
|
124
|
+
private rememberUnsupported;
|
|
125
|
+
/** The `StartRealtimeClientSession` document, with the extensions asked for. */
|
|
126
|
+
private buildMintMutation;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=session-launcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-launcher.d.ts","sourceRoot":"","sources":["../../src/session/session-launcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,EAAgD,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC/G,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,0BAA0B,CAAC;AAEjF;;;;;;;GAOG;AACH,MAAM,WAAW,4BAA4B;IAC3C,oCAAoC;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kEAAkE;IAClE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,WAAW,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC/C,sEAAsE;IACtE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,2EAA2E;IAC3E,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,4DAA4D;IAC5D,gBAAgB,EAAE,OAAO,CAAC;IAC1B,0FAA0F;IAC1F,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,mDAAmD;IACnD,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,2CAA2C;IAC3C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,yEAAyE;IACzE,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACtC;;;OAGG;IACH,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,uFAAuF;IACvF,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,+FAA+F;AAC/F,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,QAAQ,EAAE,iBAAiB,CAAC;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;;;OAQG;IACH,MAAM,CAAC,OAAO,EAAE,4BAA4B,EAAE,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,gCAAgC,CAAC,CAAC;CACjI;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,IAAI,CAAC,gCAAgC,EAAE,gBAAgB,GAAG,WAAW,GAAG,UAAU,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAK3J;AAoDD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,8BAA+B,YAAW,wBAAwB;IAC7E,mFAAmF;IACnF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmC;IAElD,MAAM,CAAC,OAAO,EAAE,4BAA4B,EAAE,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,gCAAgC,CAAC;IAW5I,gDAAgD;IAChD,OAAO,CAAC,cAAc;IAkBtB;;;OAGG;YACW,mBAAmB;IA+BjC,kFAAkF;IAClF,OAAO,CAAC,mBAAmB;IAK3B,gFAAgF;IAChF,OAAO,CAAC,iBAAiB;CA8B1B"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { ParseRealtimeClientTransport } from '@memberjunction/ai';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a minted session carries what its client driver connects with: an ephemeral token, or, for a relay session
|
|
4
|
+
* (`Transport` `'relay'`), its relay URL.
|
|
5
|
+
*
|
|
6
|
+
* @param result The minted session, or nothing.
|
|
7
|
+
*/
|
|
8
|
+
export function HasClientCredential(result) {
|
|
9
|
+
if (!result) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return !!result.EphemeralToken || (ParseRealtimeClientTransport(result.Transport) === 'relay' && !!result.RelayUrl);
|
|
13
|
+
}
|
|
14
|
+
/** What the launcher says, once, when the server rejects an extension of the mint. */
|
|
15
|
+
const UNSUPPORTED_EXTENSION_WARNINGS = {
|
|
16
|
+
ChannelScoping: '[RealtimeSession] The server does not support channel scoping — minting without it and resolving channels locally.',
|
|
17
|
+
AvatarStatus: '[RealtimeSession] The server does not report the avatar status — minting without it; the call shows no avatar notice.',
|
|
18
|
+
RelayTransport: '[RealtimeSession] The server does not report the session transport — minting without it; every session connects directly.',
|
|
19
|
+
NoAgentVideo: "[RealtimeSession] The server does not take showsAgentVideo — minting without it; it may ask for an avatar this app can't show (the call stays audio only).",
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The extension a mint failure rejects, when the failure is a GraphQL validation rejection of one the mint asked for —
|
|
23
|
+
* the signature of a server that predates it ("Unknown argument "channelCandidatesJson"…", "Cannot query field
|
|
24
|
+
* "ClientPolicyJson"…", "Cannot query field "AvatarStatusJson"…", "Cannot query field "Transport"…", "Unknown argument
|
|
25
|
+
* "showsAgentVideo"…"). Only that case is recoverable; any other failure is a real mint failure and must surface. A
|
|
26
|
+
* rejection names one field at a time, so a server that predates several extensions is found out one step at a time.
|
|
27
|
+
*/
|
|
28
|
+
function unsupportedExtension(error, asked) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
if (asked.NoAgentVideo && message.includes('showsAgentVideo')) {
|
|
31
|
+
return 'NoAgentVideo';
|
|
32
|
+
}
|
|
33
|
+
if (asked.AvatarStatus && message.includes('AvatarStatusJson')) {
|
|
34
|
+
return 'AvatarStatus';
|
|
35
|
+
}
|
|
36
|
+
// The quoted field names, not the bare words: "Transport" alone could be any transport error, and "RelayUrl"
|
|
37
|
+
// any message that names the field.
|
|
38
|
+
if (asked.RelayTransport && (message.includes('field "Transport"') || message.includes('field "RelayUrl"'))) {
|
|
39
|
+
return 'RelayTransport';
|
|
40
|
+
}
|
|
41
|
+
if (asked.ChannelScoping && (message.includes('channelCandidatesJson') || message.includes('ClientPolicyJson'))) {
|
|
42
|
+
return 'ChannelScoping';
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The stock launcher: mints through the `StartRealtimeClientSession` GraphQL mutation on the session's
|
|
48
|
+
* provider. This is what every host gets unless it installs another.
|
|
49
|
+
*
|
|
50
|
+
* When the session has channel candidates it asks the server to scope them (`channelCandidatesJson`)
|
|
51
|
+
* and to return the resolved policy (`ClientPolicyJson`). It also asks for the session's live-avatar status
|
|
52
|
+
* (`AvatarStatusJson`), which the call reads to say why it shows no avatar, and for how the browser reaches the
|
|
53
|
+
* provider (`Transport` and `RelayUrl`), which a relay session's client driver connects with. A host that shows no agent
|
|
54
|
+
* video ({@link RealtimeSessionLaunchRequest.ShowsAgentVideo} `false`) says so (`showsAgentVideo: false`), so the
|
|
55
|
+
* server asks the model for no avatar. A server that predates an extension rejects it at validation, in which case the
|
|
56
|
+
* launcher mints without that extension alone (the runtime then resolves the channel scope locally, shows no avatar
|
|
57
|
+
* notice, treats every session as direct, or leaves the avatar to the browser's audio-only fallback at connect) — a new
|
|
58
|
+
* client must keep working against an older server, and a failed mint over an optional extension would be the worst way
|
|
59
|
+
* to find out they differ. Each fallback is remembered per launcher instance so a long-lived runtime asks once.
|
|
60
|
+
*/
|
|
61
|
+
export class DefaultRealtimeSessionLauncher {
|
|
62
|
+
constructor() {
|
|
63
|
+
/** The extensions of the mint the server rejected; each is added once and kept. */
|
|
64
|
+
this.serverLacks = new Set();
|
|
65
|
+
}
|
|
66
|
+
async Launch(request, context) {
|
|
67
|
+
// The session's provider is the GraphQL one in every shipped host; this is the same narrowing the runtime applies for its relay mutations.
|
|
68
|
+
const transport = context.Provider;
|
|
69
|
+
const result = await this.executeMintMutation(transport, this.buildVariables(request), request.ChannelCandidatesJson, request.ShowsAgentVideo === false);
|
|
70
|
+
const payload = result?.StartRealtimeClientSession;
|
|
71
|
+
if (!payload || !HasClientCredential(payload)) {
|
|
72
|
+
throw new Error('StartRealtimeClientSession returned no ephemeral token (or, for a relay session, no relay URL)');
|
|
73
|
+
}
|
|
74
|
+
return payload;
|
|
75
|
+
}
|
|
76
|
+
/** The mutation's variables for one request. */
|
|
77
|
+
buildVariables(request) {
|
|
78
|
+
return {
|
|
79
|
+
targetAgentId: request.TargetAgentId,
|
|
80
|
+
conversationId: request.ConversationId,
|
|
81
|
+
lastSessionId: request.LastSessionId,
|
|
82
|
+
preferredModelId: request.PreferredModelId,
|
|
83
|
+
clientToolsJson: request.ClientTools.length > 0 ? JSON.stringify(request.ClientTools) : null,
|
|
84
|
+
coAgentId: request.CoAgentId,
|
|
85
|
+
configOverridesJson: request.ConfigOverridesJson,
|
|
86
|
+
recordingConsent: request.RecordingConsent,
|
|
87
|
+
recordingStartedAt: request.RecordingStartedAt,
|
|
88
|
+
mediaCollectionId: request.MediaCollectionId,
|
|
89
|
+
applicationId: request.ApplicationId,
|
|
90
|
+
appContextJson: request.AppContext ? JSON.stringify(request.AppContext) : null,
|
|
91
|
+
conversationBranchId: request.ConversationBranchId
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Runs the mint with every extension this server has not rejected, dropping one at a time when the server rejects it
|
|
96
|
+
* at validation. Each retry asks for one extension fewer, so the loop ends.
|
|
97
|
+
*/
|
|
98
|
+
async executeMintMutation(transport, variables, channelCandidatesJson, noAgentVideo) {
|
|
99
|
+
let asked = {
|
|
100
|
+
ChannelScoping: channelCandidatesJson !== null && !this.serverLacks.has('ChannelScoping'),
|
|
101
|
+
AvatarStatus: !this.serverLacks.has('AvatarStatus'),
|
|
102
|
+
RelayTransport: !this.serverLacks.has('RelayTransport'),
|
|
103
|
+
NoAgentVideo: noAgentVideo && !this.serverLacks.has('NoAgentVideo'),
|
|
104
|
+
};
|
|
105
|
+
for (;;) {
|
|
106
|
+
try {
|
|
107
|
+
const mintVariables = {
|
|
108
|
+
...variables,
|
|
109
|
+
...(asked.ChannelScoping ? { channelCandidatesJson } : {}),
|
|
110
|
+
...(asked.NoAgentVideo ? { showsAgentVideo: false } : {}),
|
|
111
|
+
};
|
|
112
|
+
return await transport.ExecuteGQL(this.buildMintMutation(asked), mintVariables);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const unsupported = unsupportedExtension(error, asked);
|
|
116
|
+
if (!unsupported) {
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
this.rememberUnsupported(unsupported);
|
|
120
|
+
asked = { ...asked, [unsupported]: false };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Remembers that the server lacks an extension of the mint, and says so once. */
|
|
125
|
+
rememberUnsupported(extension) {
|
|
126
|
+
console.warn(UNSUPPORTED_EXTENSION_WARNINGS[extension]);
|
|
127
|
+
this.serverLacks.add(extension);
|
|
128
|
+
}
|
|
129
|
+
/** The `StartRealtimeClientSession` document, with the extensions asked for. */
|
|
130
|
+
buildMintMutation(asked) {
|
|
131
|
+
const extraVariable = (asked.ChannelScoping ? ', $channelCandidatesJson: String' : '') + (asked.NoAgentVideo ? ', $showsAgentVideo: Boolean' : '');
|
|
132
|
+
const extraArgument = (asked.ChannelScoping ? ', channelCandidatesJson: $channelCandidatesJson' : '') + (asked.NoAgentVideo ? ', showsAgentVideo: $showsAgentVideo' : '');
|
|
133
|
+
const extraField = [
|
|
134
|
+
asked.ChannelScoping ? 'ClientPolicyJson' : '',
|
|
135
|
+
asked.AvatarStatus ? 'AvatarStatusJson' : '',
|
|
136
|
+
asked.RelayTransport ? 'Transport' : '',
|
|
137
|
+
asked.RelayTransport ? 'RelayUrl' : '',
|
|
138
|
+
]
|
|
139
|
+
.filter((field) => field.length > 0)
|
|
140
|
+
.map((field) => `\n ${field}`)
|
|
141
|
+
.join('');
|
|
142
|
+
return `
|
|
143
|
+
mutation StartRealtimeClientSession($targetAgentId: String!, $conversationId: String, $lastSessionId: String, $preferredModelId: String, $clientToolsJson: String, $coAgentId: String, $configOverridesJson: String, $recordingConsent: Boolean, $recordingStartedAt: String, $mediaCollectionId: String, $applicationId: String, $appContextJson: String, $conversationBranchId: String${extraVariable}) {
|
|
144
|
+
StartRealtimeClientSession(targetAgentId: $targetAgentId, conversationId: $conversationId, lastSessionId: $lastSessionId, preferredModelId: $preferredModelId, clientToolsJson: $clientToolsJson, coAgentId: $coAgentId, configOverridesJson: $configOverridesJson, recordingConsent: $recordingConsent, recordingStartedAt: $recordingStartedAt, mediaCollectionId: $mediaCollectionId, applicationId: $applicationId, appContextJson: $appContextJson, conversationBranchId: $conversationBranchId${extraArgument}) {
|
|
145
|
+
AgentSessionId
|
|
146
|
+
ConversationId
|
|
147
|
+
Provider
|
|
148
|
+
Model
|
|
149
|
+
EphemeralToken
|
|
150
|
+
ExpiresAt
|
|
151
|
+
SessionConfigJson
|
|
152
|
+
ModelName
|
|
153
|
+
NarrationInstructionsTemplate
|
|
154
|
+
PriorChannelStatesJson${extraField}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=session-launcher.js.map
|