@memberjunction/realtime-widget 0.0.0 → 5.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -43
- package/dist/embed.d.ts +18 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +33 -0
- package/dist/embed.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +32 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +163 -0
- package/dist/loader.js.map +1 -0
- package/dist/session/visitor-key-cookie.d.ts +20 -0
- package/dist/session/visitor-key-cookie.d.ts.map +1 -0
- package/dist/session/visitor-key-cookie.js +60 -0
- package/dist/session/visitor-key-cookie.js.map +1 -0
- package/dist/session/widget-session-client.d.ts +74 -0
- package/dist/session/widget-session-client.d.ts.map +1 -0
- package/dist/session/widget-session-client.js +147 -0
- package/dist/session/widget-session-client.js.map +1 -0
- package/dist/transport/mock-widget-transport.d.ts +26 -0
- package/dist/transport/mock-widget-transport.d.ts.map +1 -0
- package/dist/transport/mock-widget-transport.js +42 -0
- package/dist/transport/mock-widget-transport.js.map +1 -0
- package/dist/transport/runtime-widget-transport.d.ts +54 -0
- package/dist/transport/runtime-widget-transport.d.ts.map +1 -0
- package/dist/transport/runtime-widget-transport.js +203 -0
- package/dist/transport/runtime-widget-transport.js.map +1 -0
- package/dist/transport/widget-transport.d.ts +41 -0
- package/dist/transport/widget-transport.d.ts.map +1 -0
- package/dist/transport/widget-transport.js +13 -0
- package/dist/transport/widget-transport.js.map +1 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/browser-capabilities.d.ts +36 -0
- package/dist/ui/browser-capabilities.d.ts.map +1 -0
- package/dist/ui/browser-capabilities.js +30 -0
- package/dist/ui/browser-capabilities.js.map +1 -0
- package/dist/ui/support-widget-element.d.ts +149 -0
- package/dist/ui/support-widget-element.d.ts.map +1 -0
- package/dist/ui/support-widget-element.js +540 -0
- package/dist/ui/support-widget-element.js.map +1 -0
- package/dist/ui/tokens.d.ts +16 -0
- package/dist/ui/tokens.d.ts.map +1 -0
- package/dist/ui/tokens.js +135 -0
- package/dist/ui/tokens.js.map +1 -0
- package/dist/voice/channels/base-widget-channel.d.ts +63 -0
- package/dist/voice/channels/base-widget-channel.d.ts.map +1 -0
- package/dist/voice/channels/base-widget-channel.js +36 -0
- package/dist/voice/channels/base-widget-channel.js.map +1 -0
- package/dist/voice/channels/whiteboard-channel.d.ts +38 -0
- package/dist/voice/channels/whiteboard-channel.d.ts.map +1 -0
- package/dist/voice/channels/whiteboard-channel.js +214 -0
- package/dist/voice/channels/whiteboard-channel.js.map +1 -0
- package/dist/voice/channels/widget-channel-host.d.ts +68 -0
- package/dist/voice/channels/widget-channel-host.d.ts.map +1 -0
- package/dist/voice/channels/widget-channel-host.js +111 -0
- package/dist/voice/channels/widget-channel-host.js.map +1 -0
- package/dist/voice/guest-tool-relay.d.ts +14 -0
- package/dist/voice/guest-tool-relay.d.ts.map +1 -0
- package/dist/voice/guest-tool-relay.js +28 -0
- package/dist/voice/guest-tool-relay.js.map +1 -0
- package/dist/voice/guest-voice-mint.d.ts +23 -0
- package/dist/voice/guest-voice-mint.d.ts.map +1 -0
- package/dist/voice/guest-voice-mint.js +61 -0
- package/dist/voice/guest-voice-mint.js.map +1 -0
- package/dist/voice/mock-voice-controller.d.ts +22 -0
- package/dist/voice/mock-voice-controller.d.ts.map +1 -0
- package/dist/voice/mock-voice-controller.js +42 -0
- package/dist/voice/mock-voice-controller.js.map +1 -0
- package/dist/voice/realtime-voice-controller.d.ts +75 -0
- package/dist/voice/realtime-voice-controller.d.ts.map +1 -0
- package/dist/voice/realtime-voice-controller.js +182 -0
- package/dist/voice/realtime-voice-controller.js.map +1 -0
- package/dist/voice/voice-abuse-guard.d.ts +39 -0
- package/dist/voice/voice-abuse-guard.d.ts.map +1 -0
- package/dist/voice/voice-abuse-guard.js +57 -0
- package/dist/voice/voice-abuse-guard.js.map +1 -0
- package/dist/voice/voice-controller.d.ts +39 -0
- package/dist/voice/voice-controller.d.ts.map +1 -0
- package/dist/voice/voice-controller.js +10 -0
- package/dist/voice/voice-controller.js.map +1 -0
- package/package.json +36 -7
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Resolves + coordinates the interactive channels a widget voice session may attach.
|
|
3
|
+
*
|
|
4
|
+
* Built from the minted session's `enabledChannels` (which mirrors the widget instance's
|
|
5
|
+
* `EnabledChannels`), it resolves each named channel to a framework-free {@link BaseWidgetChannel}
|
|
6
|
+
* via `MJGlobal.ClassFactory` — exactly how the voice controller resolves the realtime driver — and
|
|
7
|
+
* then: aggregates their client-tool definitions (declared to the model at mint), routes the model's
|
|
8
|
+
* channel-prefixed tool calls to the owning channel (the rest are relayed to the server by the
|
|
9
|
+
* controller), and binds each channel's surface into the widget's demonstration slot on first use.
|
|
10
|
+
*
|
|
11
|
+
* An empty `enabledChannels` (the default for every widget) yields an inert host: no tools, no surface,
|
|
12
|
+
* no behavior change — so channels are strictly opt-in per widget instance.
|
|
13
|
+
*
|
|
14
|
+
* @module @memberjunction/realtime-widget
|
|
15
|
+
*/
|
|
16
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
17
|
+
import { BaseWidgetChannel } from './base-widget-channel.js';
|
|
18
|
+
/** Coordinates the set of interactive channels enabled for a voice session. */
|
|
19
|
+
export class WidgetChannelHost {
|
|
20
|
+
/**
|
|
21
|
+
* @param enabledChannels Channel names the widget instance allows (from the minted session).
|
|
22
|
+
* @param context The perception seam channels use to feed the model (channel → agent notes).
|
|
23
|
+
*/
|
|
24
|
+
constructor(enabledChannels, context) {
|
|
25
|
+
this.context = context;
|
|
26
|
+
this.channels = [];
|
|
27
|
+
this.boundChannels = new Set();
|
|
28
|
+
for (const name of enabledChannels) {
|
|
29
|
+
const channel = this.resolveChannel(name);
|
|
30
|
+
if (channel) {
|
|
31
|
+
channel.Initialize(context);
|
|
32
|
+
this.channels.push(channel);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** True when at least one enabled channel resolved (so the controller declares tools + shows the surface). */
|
|
37
|
+
get HasChannels() {
|
|
38
|
+
return this.channels.length > 0;
|
|
39
|
+
}
|
|
40
|
+
/** The resolved channels (read-only) — used to build/reveal their surfaces. */
|
|
41
|
+
get Channels() {
|
|
42
|
+
return this.channels;
|
|
43
|
+
}
|
|
44
|
+
/** Every enabled channel's client-tool definitions, flattened — declared to the model at session mint. */
|
|
45
|
+
GetToolDefinitions() {
|
|
46
|
+
return this.channels.flatMap((c) => c.GetToolDefinitions());
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The channel that owns a tool name (matched by prefix), or `undefined` when none does (the caller
|
|
50
|
+
* then relays the tool to the server). Pure lookup — does NOT apply the tool, so the caller can bind
|
|
51
|
+
* the channel's surface before applying.
|
|
52
|
+
*/
|
|
53
|
+
FindChannel(toolName) {
|
|
54
|
+
return this.channels.find((c) => toolName.startsWith(c.ToolNamePrefix));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Routes a model tool call to the owning channel and APPLIES it. Returns `{ handled: false }` when
|
|
58
|
+
* no channel owns it. Convenience for callers that have already bound surfaces (and for tests).
|
|
59
|
+
*/
|
|
60
|
+
RouteTool(toolName, argsJson) {
|
|
61
|
+
const channel = this.FindChannel(toolName);
|
|
62
|
+
if (!channel) {
|
|
63
|
+
return { handled: false };
|
|
64
|
+
}
|
|
65
|
+
return { handled: true, resultJson: channel.ApplyAgentTool(toolName, argsJson), channel };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Binds a channel's surface into a host element, once. The caller supplies a factory that creates/
|
|
69
|
+
* returns the host element for a given channel name (the widget's demonstration slot). Idempotent.
|
|
70
|
+
*/
|
|
71
|
+
BindSurface(channel, host) {
|
|
72
|
+
if (this.boundChannels.has(channel.ChannelName)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
channel.BindSurface(host);
|
|
76
|
+
this.boundChannels.add(channel.ChannelName);
|
|
77
|
+
}
|
|
78
|
+
/** Disposes every channel (clears surfaces + perception subscriptions). */
|
|
79
|
+
Dispose() {
|
|
80
|
+
for (const channel of this.channels) {
|
|
81
|
+
channel.Dispose();
|
|
82
|
+
}
|
|
83
|
+
this.channels.length = 0;
|
|
84
|
+
this.boundChannels.clear();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolves one channel name to an instance via ClassFactory; returns null (logged) when no channel
|
|
88
|
+
* is registered for the key. The explicit `GetRegistration` check is REQUIRED: ClassFactory falls
|
|
89
|
+
* back to the highest-priority registration for the base class when a key is unknown, so without it
|
|
90
|
+
* an unrecognized channel name would silently resolve to some other channel.
|
|
91
|
+
*/
|
|
92
|
+
resolveChannel(name) {
|
|
93
|
+
const key = name?.trim();
|
|
94
|
+
if (!key) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const registration = MJGlobal.Instance.ClassFactory.GetRegistration(BaseWidgetChannel, key);
|
|
98
|
+
if (!registration) {
|
|
99
|
+
console.warn(`[mj-widget] No interactive channel registered for '${key}' — skipping.`);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const channel = MJGlobal.Instance.ClassFactory.CreateInstance(BaseWidgetChannel, key);
|
|
103
|
+
// Defense-in-depth: ensure the resolved channel actually claims this name (not a fallback).
|
|
104
|
+
if (!channel || channel.ChannelName !== key) {
|
|
105
|
+
console.warn(`[mj-widget] Interactive channel '${key}' did not resolve cleanly — skipping.`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return channel;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=widget-channel-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"widget-channel-host.js","sourceRoot":"","sources":["../../../src/voice/channels/widget-channel-host.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Live relay for NON-channel (server-executed) realtime tool calls — chiefly
|
|
3
|
+
* `invoke-target-agent`. When the widget's voice session has interactive channels enabled, the
|
|
4
|
+
* controller intercepts every tool call; channel-prefixed calls run locally, and everything else is
|
|
5
|
+
* relayed here to MJAPI's `ExecuteRealtimeSessionTool` (the same mutation Explorer's realtime overlay
|
|
6
|
+
* uses) and the result fed back to the model. Reuses the configured guest GraphQLDataProvider; no new
|
|
7
|
+
* endpoint. Live-only — exercised under a running MJAPI, not in unit tests.
|
|
8
|
+
*
|
|
9
|
+
* @module @memberjunction/realtime-widget
|
|
10
|
+
*/
|
|
11
|
+
import type { RelayToolFn } from './realtime-voice-controller.js';
|
|
12
|
+
/** Builds the relay fn the voice controller calls for non-channel tools (returns the tool result JSON). */
|
|
13
|
+
export declare function createGuestToolRelay(): RelayToolFn;
|
|
14
|
+
//# sourceMappingURL=guest-tool-relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guest-tool-relay.d.ts","sourceRoot":"","sources":["../../src/voice/guest-tool-relay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAOlE,2GAA2G;AAC3G,wBAAgB,oBAAoB,IAAI,WAAW,CAUlD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Live relay for NON-channel (server-executed) realtime tool calls — chiefly
|
|
3
|
+
* `invoke-target-agent`. When the widget's voice session has interactive channels enabled, the
|
|
4
|
+
* controller intercepts every tool call; channel-prefixed calls run locally, and everything else is
|
|
5
|
+
* relayed here to MJAPI's `ExecuteRealtimeSessionTool` (the same mutation Explorer's realtime overlay
|
|
6
|
+
* uses) and the result fed back to the model. Reuses the configured guest GraphQLDataProvider; no new
|
|
7
|
+
* endpoint. Live-only — exercised under a running MJAPI, not in unit tests.
|
|
8
|
+
*
|
|
9
|
+
* @module @memberjunction/realtime-widget
|
|
10
|
+
*/
|
|
11
|
+
import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
|
|
12
|
+
const EXECUTE_TOOL_MUTATION = `
|
|
13
|
+
mutation WidgetRelayRealtimeTool($agentSessionId: String!, $callId: String!, $toolName: String!, $argsJson: String!) {
|
|
14
|
+
ExecuteRealtimeSessionTool(agentSessionId: $agentSessionId, callId: $callId, toolName: $toolName, argsJson: $argsJson)
|
|
15
|
+
}`;
|
|
16
|
+
/** Builds the relay fn the voice controller calls for non-channel tools (returns the tool result JSON). */
|
|
17
|
+
export function createGuestToolRelay() {
|
|
18
|
+
return async (agentSessionId, callId, toolName, argsJson) => {
|
|
19
|
+
const data = (await GraphQLDataProvider.Instance.ExecuteGQL(EXECUTE_TOOL_MUTATION, {
|
|
20
|
+
agentSessionId,
|
|
21
|
+
callId,
|
|
22
|
+
toolName,
|
|
23
|
+
argsJson,
|
|
24
|
+
}));
|
|
25
|
+
return data.ExecuteRealtimeSessionTool ?? '{}';
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=guest-tool-relay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guest-tool-relay.js","sourceRoot":"","sources":["../../src/voice/guest-tool-relay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAG3E,MAAM,qBAAqB,GAAG;;;EAG5B,CAAC;AAEH,2GAA2G;AAC3G,MAAM,UAAU,oBAAoB;IAChC,OAAO,KAAK,EAAE,cAAsB,EAAE,MAAc,EAAE,QAAgB,EAAE,QAAgB,EAAmB,EAAE;QACzG,MAAM,IAAI,GAAG,CAAC,MAAM,mBAAmB,CAAC,QAAQ,CAAC,UAAU,CAAC,qBAAqB,EAAE;YAC/E,cAAc;YACd,MAAM;YACN,QAAQ;YACR,QAAQ;SACX,CAAC,CAA2C,CAAC;QAC9C,OAAO,IAAI,CAAC,0BAA0B,IAAI,IAAI,CAAC;IACnD,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The production voice-mint function: calls the `StartRealtimeClientSession`
|
|
3
|
+
* GraphQL mutation (via the already-configured guest GraphQLDataProvider) for the widget's
|
|
4
|
+
* PINNED agent and maps the result to the `ClientRealtimeSessionConfig` the realtime client
|
|
5
|
+
* applies verbatim — exactly mirroring Explorer's `buildClientConfig`. Reuses the shipped
|
|
6
|
+
* mint resolver; no new server endpoint.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: live-only (needs the guest GraphQL provider configured by RuntimeWidgetTransport +
|
|
9
|
+
* a running MJAPI). Unit tests use MockVoiceController, so this is type-checked but not
|
|
10
|
+
* exercised offline.
|
|
11
|
+
*
|
|
12
|
+
* @module @memberjunction/realtime-widget
|
|
13
|
+
*/
|
|
14
|
+
import type { WidgetSession } from '../types.js';
|
|
15
|
+
import type { VoiceMintFn } from './realtime-voice-controller.js';
|
|
16
|
+
/**
|
|
17
|
+
* Builds a VoiceMintFn that mints a realtime session for the widget's pinned agent. The controller
|
|
18
|
+
* passes the enabled channels' client-tool definitions, which are declared to the model at session
|
|
19
|
+
* start (so it can call e.g. `Whiteboard_*`); `RealtimeToolDefinition` is structurally identical to
|
|
20
|
+
* {@link WidgetChannelToolDefinition} (Name / Description / ParametersSchema), so they serialize as-is.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createGuestVoiceMint(session: WidgetSession): VoiceMintFn;
|
|
23
|
+
//# sourceMappingURL=guest-voice-mint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guest-voice-mint.d.ts","sourceRoot":"","sources":["../../src/voice/guest-voice-mint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAmB,MAAM,gCAAgC,CAAC;AA2BnF;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,aAAa,GAAG,WAAW,CAgBxE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The production voice-mint function: calls the `StartRealtimeClientSession`
|
|
3
|
+
* GraphQL mutation (via the already-configured guest GraphQLDataProvider) for the widget's
|
|
4
|
+
* PINNED agent and maps the result to the `ClientRealtimeSessionConfig` the realtime client
|
|
5
|
+
* applies verbatim — exactly mirroring Explorer's `buildClientConfig`. Reuses the shipped
|
|
6
|
+
* mint resolver; no new server endpoint.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: live-only (needs the guest GraphQL provider configured by RuntimeWidgetTransport +
|
|
9
|
+
* a running MJAPI). Unit tests use MockVoiceController, so this is type-checked but not
|
|
10
|
+
* exercised offline.
|
|
11
|
+
*
|
|
12
|
+
* @module @memberjunction/realtime-widget
|
|
13
|
+
*/
|
|
14
|
+
import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
|
|
15
|
+
const START_REALTIME_MUTATION = `
|
|
16
|
+
mutation StartWidgetVoiceSession($targetAgentId: String, $clientToolsJson: String) {
|
|
17
|
+
StartRealtimeClientSession(targetAgentId: $targetAgentId, clientToolsJson: $clientToolsJson) {
|
|
18
|
+
AgentSessionId
|
|
19
|
+
Provider
|
|
20
|
+
Model
|
|
21
|
+
EphemeralToken
|
|
22
|
+
ExpiresAt
|
|
23
|
+
SessionConfigJson
|
|
24
|
+
}
|
|
25
|
+
}`;
|
|
26
|
+
/**
|
|
27
|
+
* Builds a VoiceMintFn that mints a realtime session for the widget's pinned agent. The controller
|
|
28
|
+
* passes the enabled channels' client-tool definitions, which are declared to the model at session
|
|
29
|
+
* start (so it can call e.g. `Whiteboard_*`); `RealtimeToolDefinition` is structurally identical to
|
|
30
|
+
* {@link WidgetChannelToolDefinition} (Name / Description / ParametersSchema), so they serialize as-is.
|
|
31
|
+
*/
|
|
32
|
+
export function createGuestVoiceMint(session) {
|
|
33
|
+
return async (clientTools) => {
|
|
34
|
+
const data = (await GraphQLDataProvider.Instance.ExecuteGQL(START_REALTIME_MUTATION, {
|
|
35
|
+
targetAgentId: session.pinnedAgentId,
|
|
36
|
+
clientToolsJson: clientTools.length > 0 ? JSON.stringify(clientTools) : undefined,
|
|
37
|
+
}));
|
|
38
|
+
const r = data.StartRealtimeClientSession;
|
|
39
|
+
const sessionConfig = {
|
|
40
|
+
Provider: r.Provider,
|
|
41
|
+
Model: r.Model,
|
|
42
|
+
EphemeralToken: r.EphemeralToken,
|
|
43
|
+
ExpiresAt: r.ExpiresAt,
|
|
44
|
+
SessionConfig: parseSessionConfig(r.SessionConfigJson),
|
|
45
|
+
};
|
|
46
|
+
return { provider: r.Provider, sessionConfig, agentSessionId: r.AgentSessionId };
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Parses the server-built session config; an empty object on failure (client applies nothing). */
|
|
50
|
+
function parseSessionConfig(json) {
|
|
51
|
+
if (!json) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(json);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=guest-voice-mint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guest-voice-mint.js","sourceRoot":"","sources":["../../src/voice/guest-voice-mint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAkB3E,MAAM,uBAAuB,GAAG;;;;;;;;;;EAU9B,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAsB;IACvD,OAAO,KAAK,EAAE,WAA0C,EAA4B,EAAE;QAClF,MAAM,IAAI,GAAG,CAAC,MAAM,mBAAmB,CAAC,QAAQ,CAAC,UAAU,CAAC,uBAAuB,EAAE;YACjF,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,eAAe,EAAE,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS;SACpF,CAAC,CAAkC,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,0BAA0B,CAAC;QAC1C,MAAM,aAAa,GAAgC;YAC/C,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,cAAc,EAAE,CAAC,CAAC,cAAc;YAChC,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,aAAa,EAAE,kBAAkB,CAAC,CAAC,CAAC,iBAAiB,CAAC;SACzD,CAAC;QACF,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC;IACrF,CAAC,CAAC;AACN,CAAC;AAED,mGAAmG;AACnG,SAAS,kBAAkB,CAAC,IAAmB;IAC3C,IAAI,CAAC,IAAI,EAAE,CAAC;QACR,OAAO,EAAE,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAiD,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview In-memory voice controller for unit tests + the offline demo. Drives a
|
|
3
|
+
* scripted state/transcript sequence without real audio or a network.
|
|
4
|
+
*
|
|
5
|
+
* @module @memberjunction/realtime-widget
|
|
6
|
+
*/
|
|
7
|
+
import type { IVoiceController, VoiceControllerCallbacks } from './voice-controller.js';
|
|
8
|
+
/** Deterministic, dependency-free voice controller for tests + demos. */
|
|
9
|
+
export declare class MockVoiceController implements IVoiceController {
|
|
10
|
+
StartCount: number;
|
|
11
|
+
StopCount: number;
|
|
12
|
+
private active;
|
|
13
|
+
private callbacks;
|
|
14
|
+
get IsActive(): boolean;
|
|
15
|
+
Start(callbacks: VoiceControllerCallbacks): Promise<void>;
|
|
16
|
+
Stop(): Promise<void>;
|
|
17
|
+
/** Test helper: push a transcript line as if the model produced it. */
|
|
18
|
+
EmitTranscript(role: 'user' | 'agent', text: string, isFinal?: boolean): void;
|
|
19
|
+
/** Test helper: simulate an abuse-ceiling abort. */
|
|
20
|
+
AbortForAbuse(reason: string): void;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=mock-voice-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-voice-controller.d.ts","sourceRoot":"","sources":["../../src/voice/mock-voice-controller.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAExF,yEAAyE;AACzE,qBAAa,mBAAoB,YAAW,gBAAgB;IACjD,UAAU,SAAK;IACf,SAAS,SAAK;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAyC;IAE1D,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAEY,KAAK,CAAC,SAAS,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQzD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAOlC,uEAAuE;IAChE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI;IAIjF,oDAAoD;IAC7C,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;CAK7C"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview In-memory voice controller for unit tests + the offline demo. Drives a
|
|
3
|
+
* scripted state/transcript sequence without real audio or a network.
|
|
4
|
+
*
|
|
5
|
+
* @module @memberjunction/realtime-widget
|
|
6
|
+
*/
|
|
7
|
+
/** Deterministic, dependency-free voice controller for tests + demos. */
|
|
8
|
+
export class MockVoiceController {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.StartCount = 0;
|
|
11
|
+
this.StopCount = 0;
|
|
12
|
+
this.active = false;
|
|
13
|
+
this.callbacks = null;
|
|
14
|
+
}
|
|
15
|
+
get IsActive() {
|
|
16
|
+
return this.active;
|
|
17
|
+
}
|
|
18
|
+
async Start(callbacks) {
|
|
19
|
+
this.StartCount++;
|
|
20
|
+
this.active = true;
|
|
21
|
+
this.callbacks = callbacks;
|
|
22
|
+
callbacks.onState('connecting');
|
|
23
|
+
callbacks.onState('listening');
|
|
24
|
+
}
|
|
25
|
+
async Stop() {
|
|
26
|
+
this.StopCount++;
|
|
27
|
+
this.active = false;
|
|
28
|
+
this.callbacks?.onState('ended');
|
|
29
|
+
this.callbacks?.onEnded();
|
|
30
|
+
}
|
|
31
|
+
/** Test helper: push a transcript line as if the model produced it. */
|
|
32
|
+
EmitTranscript(role, text, isFinal = true) {
|
|
33
|
+
this.callbacks?.onTranscript({ role, text, isFinal });
|
|
34
|
+
}
|
|
35
|
+
/** Test helper: simulate an abuse-ceiling abort. */
|
|
36
|
+
AbortForAbuse(reason) {
|
|
37
|
+
this.active = false;
|
|
38
|
+
this.callbacks?.onState('ended');
|
|
39
|
+
this.callbacks?.onEnded(reason);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=mock-voice-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-voice-controller.js","sourceRoot":"","sources":["../../src/voice/mock-voice-controller.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,yEAAyE;AACzE,MAAM,OAAO,mBAAmB;IAAhC;QACW,eAAU,GAAG,CAAC,CAAC;QACf,cAAS,GAAG,CAAC,CAAC;QACb,WAAM,GAAG,KAAK,CAAC;QACf,cAAS,GAAoC,IAAI,CAAC;IAgC9D,CAAC;IA9BG,IAAW,QAAQ;QACf,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAEM,KAAK,CAAC,KAAK,CAAC,SAAmC;QAClD,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,SAAS,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QAChC,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACnC,CAAC;IAEM,KAAK,CAAC,IAAI;QACb,IAAI,CAAC,SAAS,EAAE,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QACjC,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC;IAC9B,CAAC;IAED,uEAAuE;IAChE,cAAc,CAAC,IAAsB,EAAE,IAAY,EAAE,OAAO,GAAG,IAAI;QACtE,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAC1D,CAAC;IAED,oDAAoD;IAC7C,aAAa,CAAC,MAAc;QAC/B,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;QACpB,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QACjC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC;CACJ"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Production voice controller — reuses `@memberjunction/ai-realtime-client`
|
|
3
|
+
* (client-direct topology) VERBATIM (no new driver). It mints an ephemeral realtime
|
|
4
|
+
* session (via an injected mint fn that calls the StartRealtimeClientSession mutation
|
|
5
|
+
* with the guest token + the PINNED agent), resolves the provider driver through the
|
|
6
|
+
* ClassFactory, acquires the mic, wires transcripts/usage/state, and enforces the
|
|
7
|
+
* client-side `VoiceAbuseGuard` ceilings as defense-in-depth.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: this path requires browser audio + a live MJAPI realtime mint; it is exercised
|
|
10
|
+
* by W4's acceptance (gated on Auth0/MJAPI boot). Unit tests cover the abuse guard and
|
|
11
|
+
* the widget UI via `MockVoiceController`.
|
|
12
|
+
*
|
|
13
|
+
* @module @memberjunction/realtime-widget
|
|
14
|
+
*/
|
|
15
|
+
import type { ClientRealtimeSessionConfig } from '@memberjunction/ai';
|
|
16
|
+
import type { IVoiceController, VoiceControllerCallbacks } from './voice-controller.js';
|
|
17
|
+
import { type VoiceAbuseLimits } from './voice-abuse-guard.js';
|
|
18
|
+
import type { WidgetChannelToolDefinition } from './channels/base-widget-channel.js';
|
|
19
|
+
/** What the realtime mint returns to the client (the provider driver name + its session config). */
|
|
20
|
+
export interface VoiceMintResult {
|
|
21
|
+
/** Driver key for ClassFactory.CreateInstance(BaseRealtimeClient, Provider) — e.g. 'openai'. */
|
|
22
|
+
provider: string;
|
|
23
|
+
/** Provider-specific ephemeral session config (carries the ephemeral credential). */
|
|
24
|
+
sessionConfig: ClientRealtimeSessionConfig;
|
|
25
|
+
/** The durable AIAgentSession id — needed to relay non-channel (server) tool calls back to MJAPI. */
|
|
26
|
+
agentSessionId: string;
|
|
27
|
+
}
|
|
28
|
+
/** Mints a realtime session for the pinned agent using the guest token (live: GraphQL mutation). */
|
|
29
|
+
export type VoiceMintFn = (clientTools: WidgetChannelToolDefinition[]) => Promise<VoiceMintResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Relays a NON-channel (server-executed) tool call — e.g. `invoke-target-agent` — back to MJAPI and
|
|
32
|
+
* returns its result JSON. Injected so the controller stays free of the GraphQL client; the loader
|
|
33
|
+
* supplies the live implementation (ExecuteRealtimeSessionTool). Absent in tests / channel-only setups.
|
|
34
|
+
*/
|
|
35
|
+
export type RelayToolFn = (agentSessionId: string, callId: string, toolName: string, argsJson: string) => Promise<string>;
|
|
36
|
+
/** Reuses the shipped realtime client stack for the widget's voice modality. */
|
|
37
|
+
export declare class RealtimeVoiceController implements IVoiceController {
|
|
38
|
+
private readonly mint;
|
|
39
|
+
private readonly limits?;
|
|
40
|
+
/** Interactive channels (by name) the widget may attach this session (from the minted session). */
|
|
41
|
+
private readonly enabledChannels;
|
|
42
|
+
/** Live relay for non-channel tool calls; omitted when no channels are enabled (no OnToolCall wiring). */
|
|
43
|
+
private readonly relayTool?;
|
|
44
|
+
private client;
|
|
45
|
+
private micStream;
|
|
46
|
+
private guardTimer;
|
|
47
|
+
private active;
|
|
48
|
+
private channelHost;
|
|
49
|
+
private agentSessionId;
|
|
50
|
+
constructor(mint: VoiceMintFn, limits?: VoiceAbuseLimits,
|
|
51
|
+
/** Interactive channels (by name) the widget may attach this session (from the minted session). */
|
|
52
|
+
enabledChannels?: readonly string[],
|
|
53
|
+
/** Live relay for non-channel tool calls; omitted when no channels are enabled (no OnToolCall wiring). */
|
|
54
|
+
relayTool?: RelayToolFn);
|
|
55
|
+
get IsActive(): boolean;
|
|
56
|
+
Start(callbacks: VoiceControllerCallbacks): Promise<void>;
|
|
57
|
+
Stop(): Promise<void>;
|
|
58
|
+
/** Wires the realtime client's events to the widget callbacks + abuse guard. */
|
|
59
|
+
private wireClient;
|
|
60
|
+
/**
|
|
61
|
+
* Routes the model's tool calls (wired only when channels are enabled): a channel-prefixed call is
|
|
62
|
+
* applied LOCALLY to its surface (revealing it on first use) and the result fed straight back; any
|
|
63
|
+
* other tool (e.g. `invoke-target-agent`) is RELAYED to MJAPI via the injected relay. Either way the
|
|
64
|
+
* model always receives a tool result, so it never stalls.
|
|
65
|
+
*/
|
|
66
|
+
private wireToolCalls;
|
|
67
|
+
/** Applies a channel tool locally or relays a server tool; always sends a tool result back. */
|
|
68
|
+
private handleToolCall;
|
|
69
|
+
/** Polls the abuse guard; aborts the session cleanly when a ceiling trips. */
|
|
70
|
+
private startGuardPolling;
|
|
71
|
+
/** Disconnects the client, stops mic tracks, clears the timer, and notifies once. */
|
|
72
|
+
private teardown;
|
|
73
|
+
private mapState;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=realtime-voice-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-voice-controller.d.ts","sourceRoot":"","sources":["../../src/voice/realtime-voice-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AAEtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,wBAAwB,EAAoB,MAAM,uBAAuB,CAAC;AAC1G,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,mCAAmC,CAAC;AAMrF,oGAAoG;AACpG,MAAM,WAAW,eAAe;IAC5B,gGAAgG;IAChG,QAAQ,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,aAAa,EAAE,2BAA2B,CAAC;IAC3C,qGAAqG;IACrG,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED,oGAAoG;AACpG,MAAM,MAAM,WAAW,GAAG,CAAC,WAAW,EAAE,2BAA2B,EAAE,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;AAEnG;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAK1H,gFAAgF;AAChF,qBAAa,uBAAwB,YAAW,gBAAgB;IASxD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IACxB,mGAAmG;IACnG,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,0GAA0G;IAC1G,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAb/B,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,cAAc,CAAM;gBAGP,IAAI,EAAE,WAAW,EACjB,MAAM,CAAC,EAAE,gBAAgB;IAC1C,mGAAmG;IAClF,eAAe,GAAE,SAAS,MAAM,EAAO;IACxD,0GAA0G;IACzF,SAAS,CAAC,EAAE,WAAW;IAG5C,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAEY,KAAK,CAAC,SAAS,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCzD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIlC,gFAAgF;IAChF,OAAO,CAAC,UAAU;IAgBlB;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAMrB,+FAA+F;YACjF,cAAc;IA6B5B,8EAA8E;IAC9E,OAAO,CAAC,iBAAiB;IASzB,qFAAqF;YACvE,QAAQ;IA2BtB,OAAO,CAAC,QAAQ;CAenB"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Production voice controller — reuses `@memberjunction/ai-realtime-client`
|
|
3
|
+
* (client-direct topology) VERBATIM (no new driver). It mints an ephemeral realtime
|
|
4
|
+
* session (via an injected mint fn that calls the StartRealtimeClientSession mutation
|
|
5
|
+
* with the guest token + the PINNED agent), resolves the provider driver through the
|
|
6
|
+
* ClassFactory, acquires the mic, wires transcripts/usage/state, and enforces the
|
|
7
|
+
* client-side `VoiceAbuseGuard` ceilings as defense-in-depth.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: this path requires browser audio + a live MJAPI realtime mint; it is exercised
|
|
10
|
+
* by W4's acceptance (gated on Auth0/MJAPI boot). Unit tests cover the abuse guard and
|
|
11
|
+
* the widget UI via `MockVoiceController`.
|
|
12
|
+
*
|
|
13
|
+
* @module @memberjunction/realtime-widget
|
|
14
|
+
*/
|
|
15
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
16
|
+
import { BaseRealtimeClient } from '@memberjunction/ai-realtime-client';
|
|
17
|
+
import { VoiceAbuseGuard } from './voice-abuse-guard.js';
|
|
18
|
+
import { WidgetChannelHost } from './channels/widget-channel-host.js';
|
|
19
|
+
// Side-effect import: registers the built-in framework-free channels with the ClassFactory. Lives in
|
|
20
|
+
// the (lazily-loaded) voice chunk, so it never weighs down the embed entry. Add new channels here.
|
|
21
|
+
import { LoadWidgetWhiteboardChannel } from './channels/whiteboard-channel.js';
|
|
22
|
+
LoadWidgetWhiteboardChannel();
|
|
23
|
+
/** How often to poll the abuse guard while a voice session is live. */
|
|
24
|
+
const GUARD_POLL_MS = 5_000;
|
|
25
|
+
/** Reuses the shipped realtime client stack for the widget's voice modality. */
|
|
26
|
+
export class RealtimeVoiceController {
|
|
27
|
+
constructor(mint, limits,
|
|
28
|
+
/** Interactive channels (by name) the widget may attach this session (from the minted session). */
|
|
29
|
+
enabledChannels = [],
|
|
30
|
+
/** Live relay for non-channel tool calls; omitted when no channels are enabled (no OnToolCall wiring). */
|
|
31
|
+
relayTool) {
|
|
32
|
+
this.mint = mint;
|
|
33
|
+
this.limits = limits;
|
|
34
|
+
this.enabledChannels = enabledChannels;
|
|
35
|
+
this.relayTool = relayTool;
|
|
36
|
+
this.client = null;
|
|
37
|
+
this.micStream = null;
|
|
38
|
+
this.guardTimer = null;
|
|
39
|
+
this.active = false;
|
|
40
|
+
this.channelHost = null;
|
|
41
|
+
this.agentSessionId = '';
|
|
42
|
+
}
|
|
43
|
+
get IsActive() {
|
|
44
|
+
return this.active;
|
|
45
|
+
}
|
|
46
|
+
async Start(callbacks) {
|
|
47
|
+
callbacks.onState('connecting');
|
|
48
|
+
const guard = new VoiceAbuseGuard(this.limits);
|
|
49
|
+
// Resolve the enabled interactive channels up front so their client tools are declared to the
|
|
50
|
+
// model at mint. The perception seam delegates to the (soon-to-exist) realtime client.
|
|
51
|
+
this.channelHost = new WidgetChannelHost(this.enabledChannels, {
|
|
52
|
+
SendContextNote: (text) => this.client?.SendContextNote(text),
|
|
53
|
+
});
|
|
54
|
+
const channelTools = this.channelHost.HasChannels ? this.channelHost.GetToolDefinitions() : [];
|
|
55
|
+
const minted = await this.mint(channelTools);
|
|
56
|
+
this.agentSessionId = minted.agentSessionId;
|
|
57
|
+
const client = MJGlobal.Instance.ClassFactory.CreateInstance(BaseRealtimeClient, minted.provider);
|
|
58
|
+
if (!client) {
|
|
59
|
+
callbacks.onState('error');
|
|
60
|
+
callbacks.onEnded('No realtime driver available for this provider.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.client = client;
|
|
64
|
+
this.wireClient(client, callbacks, guard);
|
|
65
|
+
// Only intercept tool calls when channels are active — otherwise leave the prior behavior intact.
|
|
66
|
+
if (this.channelHost.HasChannels) {
|
|
67
|
+
this.wireToolCalls(client, callbacks);
|
|
68
|
+
}
|
|
69
|
+
this.micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
70
|
+
await client.Connect(minted.sessionConfig, this.micStream);
|
|
71
|
+
this.active = true;
|
|
72
|
+
guard.Start(Date.now());
|
|
73
|
+
this.startGuardPolling(callbacks, guard);
|
|
74
|
+
}
|
|
75
|
+
async Stop() {
|
|
76
|
+
await this.teardown();
|
|
77
|
+
}
|
|
78
|
+
/** Wires the realtime client's events to the widget callbacks + abuse guard. */
|
|
79
|
+
wireClient(client, callbacks, guard) {
|
|
80
|
+
client.OnStateChange((state) => callbacks.onState(this.mapState(state)));
|
|
81
|
+
client.OnTranscript((t) => callbacks.onTranscript({
|
|
82
|
+
role: t.Role === 'Assistant' ? 'agent' : 'user',
|
|
83
|
+
text: t.Text,
|
|
84
|
+
isFinal: t.IsFinal,
|
|
85
|
+
}));
|
|
86
|
+
client.OnUsage((u) => guard.AddUsage(u.OutputTokens));
|
|
87
|
+
client.OnError((e) => {
|
|
88
|
+
callbacks.onState('error');
|
|
89
|
+
void this.teardown(callbacks, e.Message ?? 'Voice error.');
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Routes the model's tool calls (wired only when channels are enabled): a channel-prefixed call is
|
|
94
|
+
* applied LOCALLY to its surface (revealing it on first use) and the result fed straight back; any
|
|
95
|
+
* other tool (e.g. `invoke-target-agent`) is RELAYED to MJAPI via the injected relay. Either way the
|
|
96
|
+
* model always receives a tool result, so it never stalls.
|
|
97
|
+
*/
|
|
98
|
+
wireToolCalls(client, callbacks) {
|
|
99
|
+
client.OnToolCall((call) => {
|
|
100
|
+
void this.handleToolCall(client, callbacks, call);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Applies a channel tool locally or relays a server tool; always sends a tool result back. */
|
|
104
|
+
async handleToolCall(client, callbacks, call) {
|
|
105
|
+
const channel = this.channelHost?.FindChannel(call.ToolName);
|
|
106
|
+
if (channel) {
|
|
107
|
+
// Reveal + bind the channel's surface the first time it's touched, THEN apply the tool (so the
|
|
108
|
+
// very first draw lands on a mounted surface), and feed the result back to the model.
|
|
109
|
+
const host = callbacks.getChannelSurface?.(channel.ChannelName, channel.SurfaceTitle);
|
|
110
|
+
if (host) {
|
|
111
|
+
this.channelHost.BindSurface(channel, host);
|
|
112
|
+
}
|
|
113
|
+
client.SendToolResult(call.CallID, channel.ApplyAgentTool(call.ToolName, call.ArgumentsJson ?? '{}'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Non-channel tool → relay to the server (or report it can't be run, so the model recovers).
|
|
117
|
+
if (this.relayTool && this.agentSessionId) {
|
|
118
|
+
try {
|
|
119
|
+
const resultJson = await this.relayTool(this.agentSessionId, call.CallID, call.ToolName, call.ArgumentsJson ?? '{}');
|
|
120
|
+
client.SendToolResult(call.CallID, resultJson);
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
client.SendToolResult(call.CallID, JSON.stringify({ error: e instanceof Error ? e.message : 'tool relay failed' }));
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
client.SendToolResult(call.CallID, JSON.stringify({ error: `tool '${call.ToolName}' is not available in this session` }));
|
|
128
|
+
}
|
|
129
|
+
/** Polls the abuse guard; aborts the session cleanly when a ceiling trips. */
|
|
130
|
+
startGuardPolling(callbacks, guard) {
|
|
131
|
+
this.guardTimer = setInterval(() => {
|
|
132
|
+
const reason = guard.ShouldAbort(Date.now());
|
|
133
|
+
if (reason) {
|
|
134
|
+
void this.teardown(callbacks, VoiceAbuseGuard.MessageFor(reason));
|
|
135
|
+
}
|
|
136
|
+
}, GUARD_POLL_MS);
|
|
137
|
+
}
|
|
138
|
+
/** Disconnects the client, stops mic tracks, clears the timer, and notifies once. */
|
|
139
|
+
async teardown(callbacks, endedReason) {
|
|
140
|
+
if (this.guardTimer) {
|
|
141
|
+
clearInterval(this.guardTimer);
|
|
142
|
+
this.guardTimer = null;
|
|
143
|
+
}
|
|
144
|
+
if (this.client) {
|
|
145
|
+
try {
|
|
146
|
+
await this.client.Disconnect();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
/* best-effort */
|
|
150
|
+
}
|
|
151
|
+
this.client = null;
|
|
152
|
+
}
|
|
153
|
+
this.micStream?.getTracks().forEach((t) => t.stop());
|
|
154
|
+
this.micStream = null;
|
|
155
|
+
if (this.channelHost) {
|
|
156
|
+
this.channelHost.Dispose();
|
|
157
|
+
this.channelHost = null;
|
|
158
|
+
}
|
|
159
|
+
const wasActive = this.active;
|
|
160
|
+
this.active = false;
|
|
161
|
+
if (wasActive && callbacks) {
|
|
162
|
+
callbacks.onState('ended');
|
|
163
|
+
callbacks.onEnded(endedReason);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
mapState(state) {
|
|
167
|
+
switch (state) {
|
|
168
|
+
case 'connecting':
|
|
169
|
+
case 'connected':
|
|
170
|
+
return 'connecting';
|
|
171
|
+
case 'listening':
|
|
172
|
+
return 'listening';
|
|
173
|
+
case 'speaking':
|
|
174
|
+
return 'speaking';
|
|
175
|
+
case 'closed':
|
|
176
|
+
return 'ended';
|
|
177
|
+
default:
|
|
178
|
+
return 'error';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=realtime-voice-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-voice-controller.js","sourceRoot":"","sources":["../../src/voice/realtime-voice-controller.ts"],"names":[],"mappings":"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"}
|