@craftedxp/voice-js 0.5.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONSUMING.md +6 -2
- package/README.md +31 -4
- package/dist/assistant.d.mts +32 -0
- package/dist/assistant.d.ts +32 -0
- package/dist/assistant.js +1241 -0
- package/dist/assistant.js.map +1 -0
- package/dist/assistant.mjs +23 -0
- package/dist/assistant.mjs.map +1 -0
- package/dist/browser.d.mts +12 -509
- package/dist/browser.d.ts +12 -608
- package/dist/browser.js +1020 -896
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs +25 -1283
- package/dist/browser.mjs.map +1 -1
- package/dist/chunk-LV7JGPYW.mjs +200 -0
- package/dist/chunk-LV7JGPYW.mjs.map +1 -0
- package/dist/chunk-ZW22Y67M.mjs +1208 -0
- package/dist/chunk-ZW22Y67M.mjs.map +1 -0
- package/dist/config-D2TbvIqT.d.mts +297 -0
- package/dist/config-D2TbvIqT.d.ts +297 -0
- package/dist/embed.iife.js +30 -23358
- package/dist/incomingCall-CfRRzj2P.d.mts +103 -0
- package/dist/incomingCall-CfRRzj2P.d.ts +103 -0
- package/dist/node.d.mts +69 -139
- package/dist/node.d.ts +342 -496
- package/dist/node.js +472 -467
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +19 -0
- package/dist/node.mjs.map +1 -1
- package/dist/room.d.mts +156 -0
- package/dist/room.d.ts +156 -0
- package/dist/room.js +236 -0
- package/dist/room.js.map +1 -0
- package/dist/room.mjs +7 -0
- package/dist/room.mjs.map +1 -0
- package/dist/transcribe.d.mts +14 -0
- package/dist/transcribe.d.ts +14 -0
- package/dist/transcribe.js +1213 -0
- package/dist/transcribe.js.map +1 -0
- package/dist/transcribe.mjs +18 -0
- package/dist/transcribe.mjs.map +1 -0
- package/package.json +22 -4
package/dist/node.js
CHANGED
|
@@ -1,733 +1,738 @@
|
|
|
1
|
-
|
|
2
|
-
var __create = Object.create
|
|
3
|
-
var __defProp = Object.defineProperty
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
8
|
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
}
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
11
12
|
var __copyProps = (to, from, except, desc) => {
|
|
12
|
-
if (
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
14
|
for (let key of __getOwnPropNames(from))
|
|
14
15
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
-
__defProp(to, key, {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
-
isNodeMode || !mod || !mod.__esModule
|
|
30
|
-
? __defProp(target, 'default', { value: mod, enumerable: true })
|
|
31
|
-
: target,
|
|
32
|
-
mod,
|
|
33
|
-
)
|
|
34
|
-
)
|
|
35
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, '__esModule', { value: true }), mod)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
36
29
|
|
|
37
30
|
// src/node.ts
|
|
38
|
-
var node_exports = {}
|
|
31
|
+
var node_exports = {};
|
|
39
32
|
__export(node_exports, {
|
|
40
33
|
buildWsUrl: () => buildWsUrl,
|
|
41
34
|
configureVoiceClient: () => configureVoiceClient,
|
|
42
35
|
createProtocolState: () => createProtocolState,
|
|
43
36
|
createReconnectingWebSocket: () => createReconnectingWebSocket,
|
|
44
37
|
handleServerMessage: () => handleServerMessage,
|
|
45
|
-
parseIncomingCall: () => parseIncomingCall
|
|
46
|
-
})
|
|
47
|
-
module.exports = __toCommonJS(node_exports)
|
|
38
|
+
parseIncomingCall: () => parseIncomingCall
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(node_exports);
|
|
48
41
|
|
|
49
42
|
// src/config.ts
|
|
50
43
|
function normalizeConfig(config) {
|
|
51
|
-
if (!config) throw new Error(
|
|
52
|
-
if (
|
|
44
|
+
if (!config) throw new Error("configureVoiceClient: config is required");
|
|
45
|
+
if ("apiKey" in config) {
|
|
53
46
|
throw new Error(
|
|
54
|
-
|
|
55
|
-
)
|
|
47
|
+
"configureVoiceClient: `apiKey` is no longer supported. Embedding sk_ in JS code ships server-grade credentials to every client. Pass `fetchToken: async ({ agentId }) => { /* call YOUR backend mint */ }` instead \u2014 see the @craftedxp/voice-js README for the migration recipe."
|
|
48
|
+
);
|
|
56
49
|
}
|
|
57
50
|
if (!config.apiBase) {
|
|
58
|
-
throw new Error(
|
|
51
|
+
throw new Error("configureVoiceClient: apiBase is required");
|
|
59
52
|
}
|
|
60
|
-
if (typeof config.fetchToken !==
|
|
61
|
-
throw new Error(
|
|
53
|
+
if (typeof config.fetchToken !== "function") {
|
|
54
|
+
throw new Error("configureVoiceClient: fetchToken must be a function");
|
|
62
55
|
}
|
|
63
56
|
return {
|
|
64
57
|
...config,
|
|
65
|
-
apiBase: config.apiBase.replace(/\/+$/,
|
|
66
|
-
}
|
|
58
|
+
apiBase: config.apiBase.replace(/\/+$/, "")
|
|
59
|
+
};
|
|
67
60
|
}
|
|
68
61
|
function mergeStartCallContext(factory, call) {
|
|
69
|
-
const context =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
: void 0
|
|
73
|
-
const metadata =
|
|
74
|
-
factory.defaultMetadata || call.metadata
|
|
75
|
-
? { ...(factory.defaultMetadata ?? {}), ...(call.metadata ?? {}) }
|
|
76
|
-
: void 0
|
|
77
|
-
return { context, metadata }
|
|
62
|
+
const context = factory.defaultContext || call.context ? { ...factory.defaultContext ?? {}, ...call.context ?? {} } : void 0;
|
|
63
|
+
const metadata = factory.defaultMetadata || call.metadata ? { ...factory.defaultMetadata ?? {}, ...call.metadata ?? {} } : void 0;
|
|
64
|
+
return { context, metadata };
|
|
78
65
|
}
|
|
79
66
|
|
|
80
67
|
// src/ReconnectingWebSocket.ts
|
|
81
|
-
var READYSTATE_OPEN = 1
|
|
82
|
-
var READYSTATE_CLOSED = 3
|
|
68
|
+
var READYSTATE_OPEN = 1;
|
|
69
|
+
var READYSTATE_CLOSED = 3;
|
|
83
70
|
var createReconnectingWebSocket = (options, onEvent) => {
|
|
84
|
-
const maxRetries = options.maxRetries ?? 3
|
|
85
|
-
const initialBackoff = options.initialBackoffMs ?? 500
|
|
86
|
-
const maxBackoff = options.maxBackoffMs ?? 8e3
|
|
87
|
-
let ws = null
|
|
88
|
-
let intentionalClose = false
|
|
89
|
-
let retries = 0
|
|
90
|
-
let backoff = initialBackoff
|
|
91
|
-
let reconnectTimer = null
|
|
71
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
72
|
+
const initialBackoff = options.initialBackoffMs ?? 500;
|
|
73
|
+
const maxBackoff = options.maxBackoffMs ?? 8e3;
|
|
74
|
+
let ws = null;
|
|
75
|
+
let intentionalClose = false;
|
|
76
|
+
let retries = 0;
|
|
77
|
+
let backoff = initialBackoff;
|
|
78
|
+
let reconnectTimer = null;
|
|
92
79
|
const openOnce = () => {
|
|
93
|
-
ws = options.wsFactory(options.url)
|
|
94
|
-
ws.binaryType =
|
|
80
|
+
ws = options.wsFactory(options.url);
|
|
81
|
+
ws.binaryType = "arraybuffer";
|
|
95
82
|
ws.onopen = () => {
|
|
96
|
-
if (retries === 0) onEvent({ type:
|
|
97
|
-
else onEvent({ type:
|
|
98
|
-
retries = 0
|
|
99
|
-
backoff = initialBackoff
|
|
100
|
-
}
|
|
83
|
+
if (retries === 0) onEvent({ type: "open" });
|
|
84
|
+
else onEvent({ type: "reconnected" });
|
|
85
|
+
retries = 0;
|
|
86
|
+
backoff = initialBackoff;
|
|
87
|
+
};
|
|
101
88
|
ws.onmessage = (ev) => {
|
|
102
|
-
onEvent({ type:
|
|
103
|
-
}
|
|
89
|
+
onEvent({ type: "message", data: ev.data });
|
|
90
|
+
};
|
|
104
91
|
ws.onerror = () => {
|
|
105
|
-
onEvent({ type:
|
|
106
|
-
}
|
|
92
|
+
onEvent({ type: "error", error: new Error("WebSocket error") });
|
|
93
|
+
};
|
|
107
94
|
ws.onclose = (ev) => {
|
|
108
|
-
ws = null
|
|
109
|
-
const shouldRetry = !intentionalClose && retries < maxRetries
|
|
95
|
+
ws = null;
|
|
96
|
+
const shouldRetry = !intentionalClose && retries < maxRetries;
|
|
110
97
|
if (!shouldRetry) {
|
|
111
98
|
onEvent({
|
|
112
|
-
type:
|
|
99
|
+
type: "close",
|
|
113
100
|
code: ev.code,
|
|
114
101
|
reason: ev.reason,
|
|
115
|
-
permanent: true
|
|
116
|
-
})
|
|
117
|
-
return
|
|
102
|
+
permanent: true
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
118
105
|
}
|
|
119
106
|
onEvent({
|
|
120
|
-
type:
|
|
107
|
+
type: "close",
|
|
121
108
|
code: ev.code,
|
|
122
109
|
reason: ev.reason,
|
|
123
|
-
permanent: false
|
|
124
|
-
})
|
|
125
|
-
retries
|
|
126
|
-
const delay = Math.min(backoff, maxBackoff)
|
|
127
|
-
backoff = Math.min(backoff * 2, maxBackoff)
|
|
128
|
-
reconnectTimer = setTimeout(openOnce, delay)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
openOnce()
|
|
110
|
+
permanent: false
|
|
111
|
+
});
|
|
112
|
+
retries++;
|
|
113
|
+
const delay = Math.min(backoff, maxBackoff);
|
|
114
|
+
backoff = Math.min(backoff * 2, maxBackoff);
|
|
115
|
+
reconnectTimer = setTimeout(openOnce, delay);
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
openOnce();
|
|
132
119
|
return {
|
|
133
120
|
send: (data) => {
|
|
134
|
-
if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data)
|
|
121
|
+
if (ws && ws.readyState === READYSTATE_OPEN) ws.send(data);
|
|
135
122
|
},
|
|
136
|
-
close: (code = 1e3, reason =
|
|
137
|
-
intentionalClose = true
|
|
123
|
+
close: (code = 1e3, reason = "client-requested") => {
|
|
124
|
+
intentionalClose = true;
|
|
138
125
|
if (reconnectTimer) {
|
|
139
|
-
clearTimeout(reconnectTimer)
|
|
140
|
-
reconnectTimer = null
|
|
126
|
+
clearTimeout(reconnectTimer);
|
|
127
|
+
reconnectTimer = null;
|
|
141
128
|
}
|
|
142
129
|
try {
|
|
143
|
-
ws?.close(code, reason)
|
|
144
|
-
} catch {
|
|
130
|
+
ws?.close(code, reason);
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
145
133
|
},
|
|
146
|
-
readyState: () => ws?.readyState ?? READYSTATE_CLOSED
|
|
147
|
-
}
|
|
148
|
-
}
|
|
134
|
+
readyState: () => ws?.readyState ?? READYSTATE_CLOSED
|
|
135
|
+
};
|
|
136
|
+
};
|
|
149
137
|
|
|
150
138
|
// src/protocol.ts
|
|
151
139
|
var createProtocolState = () => ({
|
|
152
|
-
state:
|
|
140
|
+
state: "idle",
|
|
153
141
|
transcript: [],
|
|
154
142
|
agentBubbleId: null,
|
|
155
143
|
idCounter: 0,
|
|
156
|
-
endReason: null
|
|
157
|
-
})
|
|
144
|
+
endReason: null
|
|
145
|
+
});
|
|
158
146
|
var mapEndReason = (raw) => {
|
|
159
|
-
if (raw ===
|
|
160
|
-
if (raw ===
|
|
161
|
-
if (raw ===
|
|
162
|
-
return
|
|
163
|
-
}
|
|
147
|
+
if (raw === "agent_ended") return "agent_ended";
|
|
148
|
+
if (raw === "caller_hung_up") return "user_hangup";
|
|
149
|
+
if (raw === "silence_timeout" || raw === "max_duration") return "timeout";
|
|
150
|
+
return "error";
|
|
151
|
+
};
|
|
164
152
|
function handleServerMessage(raw, state, cb) {
|
|
165
|
-
let msg
|
|
153
|
+
let msg;
|
|
166
154
|
try {
|
|
167
|
-
msg = JSON.parse(raw)
|
|
155
|
+
msg = JSON.parse(raw);
|
|
168
156
|
} catch {
|
|
169
|
-
return
|
|
157
|
+
return;
|
|
170
158
|
}
|
|
171
159
|
switch (msg.type) {
|
|
172
|
-
case
|
|
173
|
-
cb.onConnected()
|
|
174
|
-
setState(state,
|
|
175
|
-
return
|
|
176
|
-
case
|
|
177
|
-
const text = msg.text ??
|
|
178
|
-
if (!text) return
|
|
179
|
-
const isFinal = !!msg.isFinal
|
|
180
|
-
if (!isFinal) setState(state,
|
|
181
|
-
upsertUserPartial(state, text, isFinal)
|
|
182
|
-
cb.onTranscript(state.transcript)
|
|
183
|
-
return
|
|
184
|
-
}
|
|
185
|
-
case
|
|
186
|
-
const id = `m${state.idCounter++}
|
|
187
|
-
state.agentBubbleId = id
|
|
188
|
-
state.transcript = [...state.transcript, { id, role:
|
|
189
|
-
cb.onTranscript(state.transcript)
|
|
190
|
-
const seq = typeof msg.seq ===
|
|
191
|
-
cb.onAgentTurnStart(seq)
|
|
192
|
-
setState(state,
|
|
193
|
-
return
|
|
194
|
-
}
|
|
195
|
-
case
|
|
196
|
-
const delta = msg.text ??
|
|
197
|
-
if (!delta || !state.agentBubbleId) return
|
|
198
|
-
const id = state.agentBubbleId
|
|
199
|
-
state.transcript = state.transcript.map(
|
|
200
|
-
e.id === id && e.role ===
|
|
201
|
-
)
|
|
202
|
-
cb.onTranscript(state.transcript)
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
case
|
|
206
|
-
state.agentBubbleId = null
|
|
207
|
-
const seq = typeof msg.seq ===
|
|
208
|
-
cb.onAgentTurnEnd(seq)
|
|
209
|
-
setState(state,
|
|
210
|
-
return
|
|
211
|
-
}
|
|
212
|
-
case
|
|
213
|
-
cb.onInterrupt()
|
|
214
|
-
return
|
|
215
|
-
case
|
|
216
|
-
const committed = (msg.committedText ??
|
|
160
|
+
case "connected":
|
|
161
|
+
cb.onConnected();
|
|
162
|
+
setState(state, "listening", cb);
|
|
163
|
+
return;
|
|
164
|
+
case "transcript": {
|
|
165
|
+
const text = msg.text ?? "";
|
|
166
|
+
if (!text) return;
|
|
167
|
+
const isFinal = !!msg.isFinal;
|
|
168
|
+
if (!isFinal) setState(state, "user_speaking", cb);
|
|
169
|
+
upsertUserPartial(state, text, isFinal);
|
|
170
|
+
cb.onTranscript(state.transcript);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
case "agent_turn_start": {
|
|
174
|
+
const id = `m${state.idCounter++}`;
|
|
175
|
+
state.agentBubbleId = id;
|
|
176
|
+
state.transcript = [...state.transcript, { id, role: "agent", text: "" }];
|
|
177
|
+
cb.onTranscript(state.transcript);
|
|
178
|
+
const seq = typeof msg.seq === "number" ? msg.seq : void 0;
|
|
179
|
+
cb.onAgentTurnStart(seq);
|
|
180
|
+
setState(state, "agent_speaking", cb);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
case "agent_text": {
|
|
184
|
+
const delta = msg.text ?? "";
|
|
185
|
+
if (!delta || !state.agentBubbleId) return;
|
|
186
|
+
const id = state.agentBubbleId;
|
|
187
|
+
state.transcript = state.transcript.map(
|
|
188
|
+
(e) => e.id === id && e.role === "agent" ? { ...e, text: e.text + delta } : e
|
|
189
|
+
);
|
|
190
|
+
cb.onTranscript(state.transcript);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
case "agent_turn_end": {
|
|
194
|
+
state.agentBubbleId = null;
|
|
195
|
+
const seq = typeof msg.seq === "number" ? msg.seq : void 0;
|
|
196
|
+
cb.onAgentTurnEnd(seq);
|
|
197
|
+
setState(state, "listening", cb);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
case "interrupt":
|
|
201
|
+
cb.onInterrupt();
|
|
202
|
+
return;
|
|
203
|
+
case "agent_turn_abort": {
|
|
204
|
+
const committed = (msg.committedText ?? "").trim();
|
|
217
205
|
if (state.agentBubbleId) {
|
|
218
|
-
const id = state.agentBubbleId
|
|
206
|
+
const id = state.agentBubbleId;
|
|
219
207
|
if (committed) {
|
|
220
|
-
state.transcript = state.transcript.map(
|
|
221
|
-
e.id === id && e.role ===
|
|
222
|
-
)
|
|
208
|
+
state.transcript = state.transcript.map(
|
|
209
|
+
(e) => e.id === id && e.role === "agent" ? { ...e, text: committed, interrupted: true } : e
|
|
210
|
+
);
|
|
223
211
|
} else {
|
|
224
|
-
state.transcript = state.transcript.filter((e) => e.id !== id)
|
|
212
|
+
state.transcript = state.transcript.filter((e) => e.id !== id);
|
|
225
213
|
}
|
|
226
|
-
cb.onTranscript(state.transcript)
|
|
214
|
+
cb.onTranscript(state.transcript);
|
|
227
215
|
}
|
|
228
|
-
state.agentBubbleId = null
|
|
229
|
-
return
|
|
216
|
+
state.agentBubbleId = null;
|
|
217
|
+
return;
|
|
230
218
|
}
|
|
231
|
-
case
|
|
219
|
+
case "tool_call":
|
|
232
220
|
state.transcript = [
|
|
233
221
|
...state.transcript,
|
|
234
222
|
{
|
|
235
223
|
id: `m${state.idCounter++}`,
|
|
236
|
-
role:
|
|
237
|
-
text: `\u2192 ${String(msg.tool ??
|
|
238
|
-
}
|
|
239
|
-
]
|
|
240
|
-
cb.onTranscript(state.transcript)
|
|
241
|
-
return
|
|
242
|
-
case
|
|
224
|
+
role: "tool",
|
|
225
|
+
text: `\u2192 ${String(msg.tool ?? "?")}(${msg.args ? JSON.stringify(msg.args) : ""})`
|
|
226
|
+
}
|
|
227
|
+
];
|
|
228
|
+
cb.onTranscript(state.transcript);
|
|
229
|
+
return;
|
|
230
|
+
case "tool_result":
|
|
243
231
|
state.transcript = [
|
|
244
232
|
...state.transcript,
|
|
245
233
|
{
|
|
246
234
|
id: `m${state.idCounter++}`,
|
|
247
|
-
role:
|
|
248
|
-
text: `${msg.ok ?
|
|
249
|
-
}
|
|
250
|
-
]
|
|
251
|
-
cb.onTranscript(state.transcript)
|
|
252
|
-
return
|
|
253
|
-
case
|
|
254
|
-
const toolCallId = String(msg.toolCallId ??
|
|
255
|
-
const name = String(msg.name ??
|
|
256
|
-
const args = msg.args ?? {}
|
|
257
|
-
if (!toolCallId || !name) return
|
|
258
|
-
cb.onClientToolCall({ toolCallId, name, args })
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
case
|
|
262
|
-
const reasonRaw = String(msg.reason ??
|
|
263
|
-
const reason = mapEndReason(reasonRaw)
|
|
264
|
-
state.endReason = reason
|
|
235
|
+
role: "tool",
|
|
236
|
+
text: `${msg.ok ? "\u2713" : "\u2717"} ${String(msg.tool ?? "?")}`
|
|
237
|
+
}
|
|
238
|
+
];
|
|
239
|
+
cb.onTranscript(state.transcript);
|
|
240
|
+
return;
|
|
241
|
+
case "client_tool_call": {
|
|
242
|
+
const toolCallId = String(msg.toolCallId ?? "");
|
|
243
|
+
const name = String(msg.name ?? "");
|
|
244
|
+
const args = msg.args ?? {};
|
|
245
|
+
if (!toolCallId || !name) return;
|
|
246
|
+
cb.onClientToolCall({ toolCallId, name, args });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
case "call_end": {
|
|
250
|
+
const reasonRaw = String(msg.reason ?? "");
|
|
251
|
+
const reason = mapEndReason(reasonRaw);
|
|
252
|
+
state.endReason = reason;
|
|
265
253
|
state.transcript = [
|
|
266
254
|
...state.transcript,
|
|
267
255
|
{
|
|
268
256
|
id: `m${state.idCounter++}`,
|
|
269
|
-
role:
|
|
270
|
-
text: `call ended${reasonRaw ? ` (${reasonRaw})` :
|
|
271
|
-
}
|
|
272
|
-
]
|
|
273
|
-
cb.onTranscript(state.transcript)
|
|
274
|
-
cb.onCallEnd(reason)
|
|
275
|
-
return
|
|
257
|
+
role: "system",
|
|
258
|
+
text: `call ended${reasonRaw ? ` (${reasonRaw})` : ""}`
|
|
259
|
+
}
|
|
260
|
+
];
|
|
261
|
+
cb.onTranscript(state.transcript);
|
|
262
|
+
cb.onCallEnd(reason);
|
|
263
|
+
return;
|
|
276
264
|
}
|
|
277
|
-
case
|
|
278
|
-
const code = msg.code ??
|
|
279
|
-
const message = msg.message ??
|
|
280
|
-
cb.onError({ code, message })
|
|
281
|
-
return
|
|
265
|
+
case "error": {
|
|
266
|
+
const code = msg.code ?? "server_error";
|
|
267
|
+
const message = msg.message ?? "server error";
|
|
268
|
+
cb.onError({ code, message });
|
|
269
|
+
return;
|
|
282
270
|
}
|
|
283
271
|
}
|
|
284
272
|
}
|
|
285
273
|
var setState = (state, next, cb) => {
|
|
286
|
-
if (state.state === next) return
|
|
287
|
-
cb.onState(next)
|
|
288
|
-
}
|
|
274
|
+
if (state.state === next) return;
|
|
275
|
+
cb.onState(next);
|
|
276
|
+
};
|
|
289
277
|
var upsertUserPartial = (state, text, isFinal) => {
|
|
290
|
-
let idx = -1
|
|
278
|
+
let idx = -1;
|
|
291
279
|
for (let i = state.transcript.length - 1; i >= 0; i--) {
|
|
292
|
-
const e = state.transcript[i]
|
|
293
|
-
if (e.role ===
|
|
294
|
-
idx = i
|
|
295
|
-
break
|
|
280
|
+
const e = state.transcript[i];
|
|
281
|
+
if (e.role === "user" && e.committed === false) {
|
|
282
|
+
idx = i;
|
|
283
|
+
break;
|
|
296
284
|
}
|
|
297
285
|
}
|
|
298
286
|
if (idx === -1) {
|
|
299
287
|
state.transcript = [
|
|
300
288
|
...state.transcript,
|
|
301
|
-
{ id: `m${state.idCounter++}`, role:
|
|
302
|
-
]
|
|
303
|
-
return
|
|
304
|
-
}
|
|
305
|
-
const target = state.transcript[idx]
|
|
306
|
-
const next = [...state.transcript]
|
|
307
|
-
next[idx] = { ...target, text, committed: isFinal }
|
|
308
|
-
state.transcript = next
|
|
309
|
-
}
|
|
289
|
+
{ id: `m${state.idCounter++}`, role: "user", text, committed: isFinal }
|
|
290
|
+
];
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const target = state.transcript[idx];
|
|
294
|
+
const next = [...state.transcript];
|
|
295
|
+
next[idx] = { ...target, text, committed: isFinal };
|
|
296
|
+
state.transcript = next;
|
|
297
|
+
};
|
|
310
298
|
function buildWsUrl(args) {
|
|
311
|
-
const base = new URL(args.apiBase)
|
|
312
|
-
const proto = base.protocol ===
|
|
313
|
-
const bargeQS = args.bargeIn === false ?
|
|
314
|
-
return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}
|
|
299
|
+
const base = new URL(args.apiBase);
|
|
300
|
+
const proto = base.protocol === "https:" ? "wss:" : "ws:";
|
|
301
|
+
const bargeQS = args.bargeIn === false ? "&barge=off" : "";
|
|
302
|
+
return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`;
|
|
315
303
|
}
|
|
316
304
|
|
|
317
305
|
// src/clientTools.ts
|
|
318
|
-
var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]
|
|
319
|
-
var MAX_TOOLS = 64
|
|
320
|
-
var MAX_USAGE = 500
|
|
321
|
-
var MAX_TIMEOUT_MS = 3e4
|
|
306
|
+
var NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
307
|
+
var MAX_TOOLS = 64;
|
|
308
|
+
var MAX_USAGE = 500;
|
|
309
|
+
var MAX_TIMEOUT_MS = 3e4;
|
|
322
310
|
var validateClientToolMap = (tools) => {
|
|
323
|
-
if (tools === void 0) return
|
|
324
|
-
if (typeof tools !==
|
|
325
|
-
throw new Error(
|
|
311
|
+
if (tools === void 0) return;
|
|
312
|
+
if (typeof tools !== "object" || tools === null || Array.isArray(tools)) {
|
|
313
|
+
throw new Error("clientTools must be an object keyed by tool name");
|
|
326
314
|
}
|
|
327
|
-
const entries = Object.entries(tools)
|
|
315
|
+
const entries = Object.entries(tools);
|
|
328
316
|
if (entries.length > MAX_TOOLS) {
|
|
329
|
-
throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`)
|
|
317
|
+
throw new Error(`clientTools may declare at most 64 tools (got ${entries.length})`);
|
|
330
318
|
}
|
|
331
319
|
for (const [name, def] of entries) {
|
|
332
320
|
if (!NAME_RE.test(name)) {
|
|
333
321
|
throw new Error(
|
|
334
|
-
`clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)
|
|
335
|
-
)
|
|
322
|
+
`clientTools["${name}"]: name must be a valid identifier (^[a-zA-Z_][a-zA-Z0-9_]*$)`
|
|
323
|
+
);
|
|
336
324
|
}
|
|
337
|
-
if (!def || typeof def !==
|
|
338
|
-
throw new Error(`clientTools["${name}"]: must be an object`)
|
|
325
|
+
if (!def || typeof def !== "object") {
|
|
326
|
+
throw new Error(`clientTools["${name}"]: must be an object`);
|
|
339
327
|
}
|
|
340
|
-
if (typeof def.description !==
|
|
341
|
-
throw new Error(`clientTools["${name}"]: must have a description`)
|
|
328
|
+
if (typeof def.description !== "string" || def.description.length === 0) {
|
|
329
|
+
throw new Error(`clientTools["${name}"]: must have a description`);
|
|
342
330
|
}
|
|
343
|
-
if (typeof def.handler !==
|
|
344
|
-
throw new Error(`clientTools["${name}"]: must have a handler function`)
|
|
331
|
+
if (typeof def.handler !== "function") {
|
|
332
|
+
throw new Error(`clientTools["${name}"]: must have a handler function`);
|
|
345
333
|
}
|
|
346
334
|
if (def.usage !== void 0 && def.usage.length > MAX_USAGE) {
|
|
347
|
-
throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`)
|
|
335
|
+
throw new Error(`clientTools["${name}"]: usage must be \u2264500 chars`);
|
|
348
336
|
}
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
(!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)
|
|
352
|
-
) {
|
|
353
|
-
throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`)
|
|
337
|
+
if (def.timeoutMs !== void 0 && (!Number.isFinite(def.timeoutMs) || def.timeoutMs <= 0 || def.timeoutMs > MAX_TIMEOUT_MS)) {
|
|
338
|
+
throw new Error(`clientTools["${name}"]: timeoutMs must be in (0, 30000]`);
|
|
354
339
|
}
|
|
355
340
|
}
|
|
356
|
-
}
|
|
341
|
+
};
|
|
357
342
|
var buildRegisterFrame = (tools) => ({
|
|
358
|
-
type:
|
|
343
|
+
type: "client_tools_register",
|
|
359
344
|
tools: Object.entries(tools).map(([name, def]) => ({
|
|
360
345
|
name,
|
|
361
346
|
description: def.description,
|
|
362
347
|
parameters: def.parameters,
|
|
363
|
-
...
|
|
364
|
-
...
|
|
365
|
-
}))
|
|
366
|
-
})
|
|
348
|
+
...def.usage !== void 0 ? { usage: def.usage } : {},
|
|
349
|
+
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
350
|
+
}))
|
|
351
|
+
});
|
|
367
352
|
var dispatchClientToolCall = (send, tools, frame) => {
|
|
368
353
|
const safeSend = (payload) => {
|
|
369
354
|
try {
|
|
370
|
-
send(payload)
|
|
371
|
-
} catch {
|
|
372
|
-
|
|
373
|
-
|
|
355
|
+
send(payload);
|
|
356
|
+
} catch {
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const tool = tools[frame.name];
|
|
374
360
|
if (!tool) {
|
|
375
361
|
safeSend({
|
|
376
|
-
type:
|
|
362
|
+
type: "client_tool_result",
|
|
377
363
|
toolCallId: frame.toolCallId,
|
|
378
|
-
error: `No handler for ${frame.name}
|
|
379
|
-
})
|
|
380
|
-
return
|
|
364
|
+
error: `No handler for ${frame.name}`
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
381
367
|
}
|
|
382
368
|
void (async () => {
|
|
383
369
|
try {
|
|
384
|
-
const out = await tool.handler(frame.args)
|
|
370
|
+
const out = await tool.handler(frame.args);
|
|
385
371
|
safeSend({
|
|
386
|
-
type:
|
|
372
|
+
type: "client_tool_result",
|
|
387
373
|
toolCallId: frame.toolCallId,
|
|
388
|
-
result: typeof out ===
|
|
389
|
-
})
|
|
374
|
+
result: typeof out === "string" ? out : JSON.stringify(out)
|
|
375
|
+
});
|
|
390
376
|
} catch (err) {
|
|
391
377
|
safeSend({
|
|
392
|
-
type:
|
|
378
|
+
type: "client_tool_result",
|
|
393
379
|
toolCallId: frame.toolCallId,
|
|
394
|
-
error: err instanceof Error ? err.message : String(err)
|
|
395
|
-
})
|
|
380
|
+
error: err instanceof Error ? err.message : String(err)
|
|
381
|
+
});
|
|
396
382
|
}
|
|
397
|
-
})()
|
|
398
|
-
}
|
|
383
|
+
})();
|
|
384
|
+
};
|
|
399
385
|
|
|
400
386
|
// src/ClientMarksBuffer.ts
|
|
401
387
|
var createClientMarksBuffer = (args) => {
|
|
402
|
-
const now = args.now ?? (() => performance.now())
|
|
403
|
-
let pendingFirstOutboundAt = null
|
|
404
|
-
const inFlight = /* @__PURE__ */ new Map()
|
|
388
|
+
const now = args.now ?? (() => performance.now());
|
|
389
|
+
let pendingFirstOutboundAt = null;
|
|
390
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
405
391
|
const tryEmit = (seq) => {
|
|
406
|
-
const slot = inFlight.get(seq)
|
|
407
|
-
if (!slot) return
|
|
408
|
-
if (!slot.ended) return
|
|
409
|
-
const marks = {}
|
|
392
|
+
const slot = inFlight.get(seq);
|
|
393
|
+
if (!slot) return;
|
|
394
|
+
if (!slot.ended) return;
|
|
395
|
+
const marks = {};
|
|
410
396
|
if (slot.firstOutboundAt !== null && slot.firstAudibleAt !== null) {
|
|
411
|
-
marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt
|
|
397
|
+
marks.client_mic_to_first_audible_ms = slot.firstAudibleAt - slot.firstOutboundAt;
|
|
412
398
|
}
|
|
413
399
|
args.send({
|
|
414
|
-
type:
|
|
400
|
+
type: "client_marks",
|
|
415
401
|
seq,
|
|
416
402
|
marks,
|
|
417
|
-
clientNow: Date.now()
|
|
418
|
-
})
|
|
419
|
-
inFlight.delete(seq)
|
|
420
|
-
}
|
|
403
|
+
clientNow: Date.now()
|
|
404
|
+
});
|
|
405
|
+
inFlight.delete(seq);
|
|
406
|
+
};
|
|
421
407
|
const markFirstOutboundAudio = () => {
|
|
422
|
-
if (pendingFirstOutboundAt !== null) return
|
|
423
|
-
pendingFirstOutboundAt = now()
|
|
424
|
-
}
|
|
408
|
+
if (pendingFirstOutboundAt !== null) return;
|
|
409
|
+
pendingFirstOutboundAt = now();
|
|
410
|
+
};
|
|
425
411
|
const markFirstAudibleOutput = () => {
|
|
426
|
-
let target
|
|
412
|
+
let target;
|
|
427
413
|
for (const slot of inFlight.values()) {
|
|
428
414
|
if (!slot.ended) {
|
|
429
|
-
target = slot
|
|
415
|
+
target = slot;
|
|
430
416
|
}
|
|
431
417
|
}
|
|
432
|
-
if (!target) return
|
|
433
|
-
if (target.firstAudibleAt !== null) return
|
|
434
|
-
target.firstAudibleAt = now()
|
|
435
|
-
}
|
|
418
|
+
if (!target) return;
|
|
419
|
+
if (target.firstAudibleAt !== null) return;
|
|
420
|
+
target.firstAudibleAt = now();
|
|
421
|
+
};
|
|
436
422
|
const onAgentTurnStart = (seq) => {
|
|
437
423
|
inFlight.set(seq, {
|
|
438
424
|
firstOutboundAt: pendingFirstOutboundAt,
|
|
439
425
|
firstAudibleAt: null,
|
|
440
|
-
ended: false
|
|
441
|
-
})
|
|
442
|
-
pendingFirstOutboundAt = null
|
|
443
|
-
}
|
|
426
|
+
ended: false
|
|
427
|
+
});
|
|
428
|
+
pendingFirstOutboundAt = null;
|
|
429
|
+
};
|
|
444
430
|
const onAgentTurnEnd = (seq) => {
|
|
445
|
-
const slot = inFlight.get(seq)
|
|
431
|
+
const slot = inFlight.get(seq);
|
|
446
432
|
if (!slot) {
|
|
447
|
-
args.send({ type:
|
|
448
|
-
return
|
|
433
|
+
args.send({ type: "client_marks", seq, marks: {}, clientNow: Date.now() });
|
|
434
|
+
return;
|
|
449
435
|
}
|
|
450
|
-
slot.ended = true
|
|
451
|
-
tryEmit(seq)
|
|
452
|
-
}
|
|
436
|
+
slot.ended = true;
|
|
437
|
+
tryEmit(seq);
|
|
438
|
+
};
|
|
453
439
|
const flush = () => {
|
|
454
440
|
for (const seq of [...inFlight.keys()]) {
|
|
455
|
-
const slot = inFlight.get(seq)
|
|
456
|
-
slot.ended = true
|
|
457
|
-
tryEmit(seq)
|
|
441
|
+
const slot = inFlight.get(seq);
|
|
442
|
+
slot.ended = true;
|
|
443
|
+
tryEmit(seq);
|
|
458
444
|
}
|
|
459
|
-
pendingFirstOutboundAt = null
|
|
460
|
-
}
|
|
445
|
+
pendingFirstOutboundAt = null;
|
|
446
|
+
};
|
|
461
447
|
return {
|
|
462
448
|
markFirstOutboundAudio,
|
|
463
449
|
markFirstAudibleOutput,
|
|
464
450
|
onAgentTurnStart,
|
|
465
451
|
onAgentTurnEnd,
|
|
466
|
-
flush
|
|
467
|
-
}
|
|
468
|
-
}
|
|
452
|
+
flush
|
|
453
|
+
};
|
|
454
|
+
};
|
|
469
455
|
|
|
470
456
|
// src/NodeVoiceClient.ts
|
|
457
|
+
var READYSTATE_OPEN2 = 1;
|
|
471
458
|
var NodeVoiceClient = class {
|
|
472
459
|
constructor(args) {
|
|
473
|
-
this.rws = null
|
|
474
|
-
this.muted = false
|
|
475
|
-
this.startedAt = null
|
|
476
|
-
this.endedFired = false
|
|
477
|
-
this.lastError = null
|
|
460
|
+
this.rws = null;
|
|
461
|
+
this.muted = false;
|
|
462
|
+
this.startedAt = null;
|
|
463
|
+
this.endedFired = false;
|
|
464
|
+
this.lastError = null;
|
|
478
465
|
this.end = () => {
|
|
479
|
-
this.teardown(
|
|
480
|
-
}
|
|
466
|
+
this.teardown("user_hangup");
|
|
467
|
+
};
|
|
481
468
|
this.mute = () => {
|
|
482
|
-
this.muted = true
|
|
483
|
-
}
|
|
469
|
+
this.muted = true;
|
|
470
|
+
};
|
|
484
471
|
this.unmute = () => {
|
|
485
|
-
this.muted = false
|
|
486
|
-
}
|
|
472
|
+
this.muted = false;
|
|
473
|
+
};
|
|
487
474
|
// ---------------------------------------------------------------
|
|
488
475
|
// Node-only raw audio surface
|
|
489
476
|
// ---------------------------------------------------------------
|
|
490
477
|
this.sendAudioChunk = (pcm) => {
|
|
491
|
-
if (!this.rws) return false
|
|
492
|
-
this.marks.markFirstOutboundAudio()
|
|
478
|
+
if (!this.rws) return false;
|
|
479
|
+
this.marks.markFirstOutboundAudio();
|
|
493
480
|
if (this.muted) {
|
|
494
|
-
const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength
|
|
495
|
-
this.rws.send(new ArrayBuffer(len))
|
|
496
|
-
return true
|
|
481
|
+
const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength;
|
|
482
|
+
this.rws.send(new ArrayBuffer(len));
|
|
483
|
+
return true;
|
|
497
484
|
}
|
|
498
|
-
this.rws.send(pcm)
|
|
499
|
-
return true
|
|
500
|
-
}
|
|
485
|
+
this.rws.send(pcm);
|
|
486
|
+
return true;
|
|
487
|
+
};
|
|
488
|
+
this.sendClientEvent = (text) => {
|
|
489
|
+
if (!this.rws) return false;
|
|
490
|
+
const t = text.trim().slice(0, 400);
|
|
491
|
+
if (!t) return false;
|
|
492
|
+
this.rws.send(JSON.stringify({ type: "client_event", text: t }));
|
|
493
|
+
return true;
|
|
494
|
+
};
|
|
495
|
+
// Typed user turn (text/multimodal sessions — token minted with
|
|
496
|
+
// channel:'text'). The server accepts user_text only on text-channel
|
|
497
|
+
// sessions and rejects it on voice (anti-injection), so this is a
|
|
498
|
+
// no-op in practice on voice calls beyond the frame being ignored
|
|
499
|
+
// server-side.
|
|
500
|
+
this.sendText = (text) => {
|
|
501
|
+
const t = text.trim();
|
|
502
|
+
if (!t || !this.rws || this.rws.readyState() !== READYSTATE_OPEN2) return false;
|
|
503
|
+
this.rws.send(JSON.stringify({ type: "user_text", text: t }));
|
|
504
|
+
return true;
|
|
505
|
+
};
|
|
501
506
|
// ---------------------------------------------------------------
|
|
502
507
|
// Internal
|
|
503
508
|
// ---------------------------------------------------------------
|
|
504
509
|
this.setState = (next) => {
|
|
505
|
-
if (this.proto.state === next) return
|
|
506
|
-
this.proto.state = next
|
|
507
|
-
this.args.options.onStateChange?.(next)
|
|
508
|
-
}
|
|
510
|
+
if (this.proto.state === next) return;
|
|
511
|
+
this.proto.state = next;
|
|
512
|
+
this.args.options.onStateChange?.(next);
|
|
513
|
+
};
|
|
509
514
|
this.sendClientToolsRegister = () => {
|
|
510
|
-
const frame = buildRegisterFrame(this.args.options.clientTools ?? {})
|
|
511
|
-
this.rws?.send(JSON.stringify(frame))
|
|
512
|
-
}
|
|
515
|
+
const frame = buildRegisterFrame(this.args.options.clientTools ?? {});
|
|
516
|
+
this.rws?.send(JSON.stringify(frame));
|
|
517
|
+
};
|
|
513
518
|
this.emitError = (err) => {
|
|
514
|
-
this.lastError = err
|
|
515
|
-
this.args.options.onError?.(err)
|
|
516
|
-
}
|
|
519
|
+
this.lastError = err;
|
|
520
|
+
this.args.options.onError?.(err);
|
|
521
|
+
};
|
|
517
522
|
this.handleSocketEvent = (ev) => {
|
|
518
523
|
switch (ev.type) {
|
|
519
|
-
case
|
|
520
|
-
break
|
|
521
|
-
case
|
|
522
|
-
this.proto.transcript = []
|
|
523
|
-
this.proto.agentBubbleId = null
|
|
524
|
-
this.args.options.onTranscript?.(this.proto.transcript)
|
|
525
|
-
this.setState(
|
|
526
|
-
break
|
|
527
|
-
case
|
|
528
|
-
if (typeof ev.data ===
|
|
524
|
+
case "open":
|
|
525
|
+
break;
|
|
526
|
+
case "reconnected":
|
|
527
|
+
this.proto.transcript = [];
|
|
528
|
+
this.proto.agentBubbleId = null;
|
|
529
|
+
this.args.options.onTranscript?.(this.proto.transcript);
|
|
530
|
+
this.setState("listening");
|
|
531
|
+
break;
|
|
532
|
+
case "message":
|
|
533
|
+
if (typeof ev.data === "string") {
|
|
529
534
|
handleServerMessage(ev.data, this.proto, {
|
|
530
535
|
onState: this.setState,
|
|
531
536
|
onTranscript: (entries) => this.args.options.onTranscript?.(entries),
|
|
532
537
|
onError: this.emitError,
|
|
533
538
|
onInterrupt: () => this.args.options.onInterrupt?.(),
|
|
534
539
|
onAgentTurnStart: (seq) => {
|
|
535
|
-
if (typeof seq ===
|
|
536
|
-
this.args.options.onAgentTurnStart?.()
|
|
540
|
+
if (typeof seq === "number") this.marks.onAgentTurnStart(seq);
|
|
541
|
+
this.args.options.onAgentTurnStart?.();
|
|
537
542
|
},
|
|
538
543
|
onAgentTurnEnd: (seq) => {
|
|
539
|
-
if (typeof seq ===
|
|
544
|
+
if (typeof seq === "number") this.marks.onAgentTurnEnd(seq);
|
|
540
545
|
},
|
|
541
546
|
onCallEnd: (reason) => this.teardown(reason),
|
|
542
547
|
onConnected: () => this.sendClientToolsRegister(),
|
|
543
|
-
onClientToolCall: (frame) =>
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
})
|
|
548
|
+
onClientToolCall: (frame) => dispatchClientToolCall(
|
|
549
|
+
(f) => this.rws?.send(JSON.stringify(f)),
|
|
550
|
+
this.args.options.clientTools ?? {},
|
|
551
|
+
frame
|
|
552
|
+
)
|
|
553
|
+
});
|
|
550
554
|
} else {
|
|
551
|
-
this.marks.markFirstAudibleOutput()
|
|
552
|
-
this.args.options.onAudioChunk?.(ev.data)
|
|
555
|
+
this.marks.markFirstAudibleOutput();
|
|
556
|
+
this.args.options.onAudioChunk?.(ev.data);
|
|
553
557
|
}
|
|
554
|
-
break
|
|
555
|
-
case
|
|
558
|
+
break;
|
|
559
|
+
case "close":
|
|
556
560
|
if (ev.permanent) {
|
|
557
|
-
const reason = this.proto.endReason ?? (this.lastError ?
|
|
558
|
-
this.teardown(reason)
|
|
561
|
+
const reason = this.proto.endReason ?? (this.lastError ? "error" : "user_hangup");
|
|
562
|
+
this.teardown(reason);
|
|
559
563
|
}
|
|
560
|
-
break
|
|
561
|
-
case
|
|
562
|
-
this.emitError({ code:
|
|
563
|
-
break
|
|
564
|
+
break;
|
|
565
|
+
case "error":
|
|
566
|
+
this.emitError({ code: "socket_error", message: ev.error.message });
|
|
567
|
+
break;
|
|
564
568
|
}
|
|
565
|
-
}
|
|
569
|
+
};
|
|
566
570
|
this.teardown = (reason) => {
|
|
567
571
|
try {
|
|
568
|
-
this.marks.flush()
|
|
569
|
-
} catch {
|
|
572
|
+
this.marks.flush();
|
|
573
|
+
} catch {
|
|
574
|
+
}
|
|
570
575
|
try {
|
|
571
|
-
this.rws?.close(1e3, reason)
|
|
572
|
-
} catch {
|
|
573
|
-
|
|
574
|
-
this.
|
|
575
|
-
this.
|
|
576
|
-
|
|
576
|
+
this.rws?.close(1e3, reason);
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
this.rws = null;
|
|
580
|
+
this.setState("ended");
|
|
581
|
+
this.fireEndOnce(reason);
|
|
582
|
+
};
|
|
577
583
|
this.fireEndOnce = (reason) => {
|
|
578
|
-
if (this.endedFired) return
|
|
579
|
-
this.endedFired = true
|
|
580
|
-
const startedAt = this.startedAt ?? Date.now()
|
|
584
|
+
if (this.endedFired) return;
|
|
585
|
+
this.endedFired = true;
|
|
586
|
+
const startedAt = this.startedAt ?? Date.now();
|
|
581
587
|
this.args.options.onEnd?.({
|
|
582
588
|
reason,
|
|
583
|
-
errorCode: reason ===
|
|
584
|
-
durationMs: Date.now() - startedAt
|
|
585
|
-
})
|
|
586
|
-
}
|
|
587
|
-
this.args = args
|
|
588
|
-
this.proto = createProtocolState()
|
|
589
|
-
validateClientToolMap(args.options.clientTools)
|
|
589
|
+
errorCode: reason === "error" ? this.lastError?.code : void 0,
|
|
590
|
+
durationMs: Date.now() - startedAt
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
this.args = args;
|
|
594
|
+
this.proto = createProtocolState();
|
|
595
|
+
validateClientToolMap(args.options.clientTools);
|
|
590
596
|
this.marks = createClientMarksBuffer({
|
|
591
597
|
send: (frame) => {
|
|
592
598
|
try {
|
|
593
|
-
this.rws?.send(JSON.stringify(frame))
|
|
594
|
-
} catch {
|
|
595
|
-
|
|
596
|
-
|
|
599
|
+
this.rws?.send(JSON.stringify(frame));
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
});
|
|
597
604
|
}
|
|
598
605
|
// ---------------------------------------------------------------
|
|
599
606
|
// Call interface
|
|
600
607
|
// ---------------------------------------------------------------
|
|
601
608
|
get state() {
|
|
602
|
-
return this.proto.state
|
|
609
|
+
return this.proto.state;
|
|
603
610
|
}
|
|
604
611
|
get transcript() {
|
|
605
|
-
return this.proto.transcript.slice()
|
|
612
|
+
return this.proto.transcript.slice();
|
|
606
613
|
}
|
|
607
614
|
get isMuted() {
|
|
608
|
-
return this.muted
|
|
615
|
+
return this.muted;
|
|
609
616
|
}
|
|
610
617
|
// ---------------------------------------------------------------
|
|
611
618
|
// Lifecycle
|
|
612
619
|
// ---------------------------------------------------------------
|
|
613
620
|
async start() {
|
|
614
|
-
this.setState(
|
|
615
|
-
this.startedAt = Date.now()
|
|
621
|
+
this.setState("connecting");
|
|
622
|
+
this.startedAt = Date.now();
|
|
616
623
|
const url = buildWsUrl({
|
|
617
624
|
apiBase: this.args.config.apiBase,
|
|
618
625
|
agentId: this.args.options.agentId,
|
|
619
626
|
token: this.args.token,
|
|
620
|
-
bargeIn: this.args.options.bargeIn
|
|
621
|
-
})
|
|
627
|
+
bargeIn: this.args.options.bargeIn
|
|
628
|
+
});
|
|
622
629
|
this.rws = createReconnectingWebSocket(
|
|
623
630
|
{
|
|
624
631
|
url,
|
|
625
632
|
wsFactory: this.args.wsFactory,
|
|
626
|
-
maxRetries: 3
|
|
633
|
+
maxRetries: 3
|
|
627
634
|
},
|
|
628
|
-
(ev) => this.handleSocketEvent(ev)
|
|
629
|
-
)
|
|
635
|
+
(ev) => this.handleSocketEvent(ev)
|
|
636
|
+
);
|
|
630
637
|
}
|
|
631
|
-
}
|
|
638
|
+
};
|
|
632
639
|
|
|
633
640
|
// src/incomingCall.ts
|
|
634
641
|
var parseIncomingCall = (raw) => {
|
|
635
|
-
if (typeof raw !==
|
|
636
|
-
throw new Error(
|
|
637
|
-
}
|
|
638
|
-
const p = raw
|
|
639
|
-
if (typeof p.token !==
|
|
640
|
-
throw new Error(
|
|
641
|
-
}
|
|
642
|
-
if (typeof p.agentId !==
|
|
643
|
-
throw new Error(
|
|
644
|
-
}
|
|
645
|
-
const transport = p.transport ===
|
|
646
|
-
const out = { token: p.token, agentId: p.agentId, transport }
|
|
647
|
-
if (transport ===
|
|
648
|
-
out.webrtcGatewayBase = p.webrtcGatewayBase
|
|
649
|
-
}
|
|
650
|
-
if (typeof p.expiresAt ===
|
|
651
|
-
if (typeof p.agentName ===
|
|
652
|
-
if (typeof p.agentAvatarUrl ===
|
|
653
|
-
return out
|
|
654
|
-
}
|
|
642
|
+
if (typeof raw !== "object" || raw === null) {
|
|
643
|
+
throw new Error("parseIncomingCall: payload must be an object");
|
|
644
|
+
}
|
|
645
|
+
const p = raw;
|
|
646
|
+
if (typeof p.token !== "string" || !p.token.startsWith("ct_")) {
|
|
647
|
+
throw new Error("parseIncomingCall: missing or invalid `token` (expected a ct_ string)");
|
|
648
|
+
}
|
|
649
|
+
if (typeof p.agentId !== "string" || p.agentId.length === 0) {
|
|
650
|
+
throw new Error("parseIncomingCall: missing `agentId`");
|
|
651
|
+
}
|
|
652
|
+
const transport = p.transport === "webrtc" ? "webrtc" : "ws";
|
|
653
|
+
const out = { token: p.token, agentId: p.agentId, transport };
|
|
654
|
+
if (transport === "webrtc" && typeof p.webrtcGatewayBase === "string") {
|
|
655
|
+
out.webrtcGatewayBase = p.webrtcGatewayBase;
|
|
656
|
+
}
|
|
657
|
+
if (typeof p.expiresAt === "number") out.expiresAt = p.expiresAt;
|
|
658
|
+
if (typeof p.agentName === "string") out.agentName = p.agentName;
|
|
659
|
+
if (typeof p.agentAvatarUrl === "string") out.agentAvatarUrl = p.agentAvatarUrl;
|
|
660
|
+
return out;
|
|
661
|
+
};
|
|
655
662
|
|
|
656
663
|
// src/node.ts
|
|
657
|
-
var cachedWsCtor = null
|
|
664
|
+
var cachedWsCtor = null;
|
|
658
665
|
var loadWsCtor = async () => {
|
|
659
|
-
if (cachedWsCtor) return cachedWsCtor
|
|
666
|
+
if (cachedWsCtor) return cachedWsCtor;
|
|
660
667
|
try {
|
|
661
|
-
const mod = await import(
|
|
662
|
-
const ctor = mod.WebSocket ?? mod.default
|
|
668
|
+
const mod = await import("ws");
|
|
669
|
+
const ctor = mod.WebSocket ?? mod.default;
|
|
663
670
|
if (!ctor) {
|
|
664
|
-
throw new Error(
|
|
671
|
+
throw new Error("imported `ws` but neither default nor named WebSocket export was found");
|
|
665
672
|
}
|
|
666
|
-
cachedWsCtor = ctor
|
|
667
|
-
return ctor
|
|
673
|
+
cachedWsCtor = ctor;
|
|
674
|
+
return ctor;
|
|
668
675
|
} catch (err) {
|
|
669
676
|
throw new Error(
|
|
670
|
-
"@craftedxp/voice-js (node): missing optional peer `ws`. Install it with `npm install ws` (ws is declared as `peerDependenciesMeta.optional` so npm doesn't install it automatically). Original: " +
|
|
671
|
-
|
|
672
|
-
)
|
|
677
|
+
"@craftedxp/voice-js (node): missing optional peer `ws`. Install it with `npm install ws` (ws is declared as `peerDependenciesMeta.optional` so npm doesn't install it automatically). Original: " + (err instanceof Error ? err.message : String(err))
|
|
678
|
+
);
|
|
673
679
|
}
|
|
674
|
-
}
|
|
680
|
+
};
|
|
675
681
|
var NodeVoiceFactory = class {
|
|
676
682
|
constructor(config) {
|
|
677
683
|
this.startCall = async (options) => {
|
|
678
684
|
if (!options.agentId) {
|
|
679
|
-
throw new Error(
|
|
685
|
+
throw new Error("startCall: agentId is required");
|
|
680
686
|
}
|
|
681
|
-
const WsCtor = await loadWsCtor()
|
|
682
|
-
const wsFactory = (url) => new WsCtor(url)
|
|
683
|
-
const { context, metadata } = mergeStartCallContext(this.config, options)
|
|
687
|
+
const WsCtor = await loadWsCtor();
|
|
688
|
+
const wsFactory = (url) => new WsCtor(url);
|
|
689
|
+
const { context, metadata } = mergeStartCallContext(this.config, options);
|
|
684
690
|
const fetchArgs = {
|
|
685
691
|
agentId: options.agentId,
|
|
686
692
|
userId: options.userId,
|
|
687
693
|
context,
|
|
688
|
-
metadata
|
|
689
|
-
}
|
|
690
|
-
let token
|
|
694
|
+
metadata
|
|
695
|
+
};
|
|
696
|
+
let token;
|
|
691
697
|
if (options.token) {
|
|
692
|
-
token = options.token
|
|
698
|
+
token = options.token;
|
|
693
699
|
} else {
|
|
694
|
-
const r = await this.config.fetchToken(fetchArgs)
|
|
700
|
+
const r = await this.config.fetchToken(fetchArgs);
|
|
695
701
|
if (!r) {
|
|
696
|
-
throw new Error(
|
|
702
|
+
throw new Error("configureVoiceClient.fetchToken returned empty token");
|
|
697
703
|
}
|
|
698
|
-
token = typeof r ===
|
|
704
|
+
token = typeof r === "string" ? r : r.token;
|
|
699
705
|
if (!token) {
|
|
700
|
-
throw new Error(
|
|
706
|
+
throw new Error("configureVoiceClient.fetchToken returned an object without `token`");
|
|
701
707
|
}
|
|
702
|
-
if (typeof r !==
|
|
708
|
+
if (typeof r !== "string" && r.transport === "webrtc") {
|
|
703
709
|
console.warn(
|
|
704
|
-
|
|
705
|
-
)
|
|
710
|
+
"@craftedxp/voice-js (node): agent is configured for WebRTC but the Node bundle only supports WebSocket \u2014 falling back to WS. Use the browser bundle for WebRTC transport."
|
|
711
|
+
);
|
|
706
712
|
}
|
|
707
713
|
}
|
|
708
714
|
const client = new NodeVoiceClient({
|
|
709
715
|
config: this.config,
|
|
710
716
|
options: { ...options, context, metadata },
|
|
711
717
|
token,
|
|
712
|
-
wsFactory
|
|
713
|
-
})
|
|
714
|
-
await client.start()
|
|
715
|
-
return client
|
|
716
|
-
}
|
|
717
|
-
this.config = config
|
|
718
|
-
}
|
|
719
|
-
}
|
|
718
|
+
wsFactory
|
|
719
|
+
});
|
|
720
|
+
await client.start();
|
|
721
|
+
return client;
|
|
722
|
+
};
|
|
723
|
+
this.config = config;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
720
726
|
function configureVoiceClient(config) {
|
|
721
|
-
return new NodeVoiceFactory(normalizeConfig(config))
|
|
727
|
+
return new NodeVoiceFactory(normalizeConfig(config));
|
|
722
728
|
}
|
|
723
729
|
// Annotate the CommonJS export names for ESM import in node:
|
|
724
|
-
0 &&
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
//# sourceMappingURL=node.js.map
|
|
730
|
+
0 && (module.exports = {
|
|
731
|
+
buildWsUrl,
|
|
732
|
+
configureVoiceClient,
|
|
733
|
+
createProtocolState,
|
|
734
|
+
createReconnectingWebSocket,
|
|
735
|
+
handleServerMessage,
|
|
736
|
+
parseIncomingCall
|
|
737
|
+
});
|
|
738
|
+
//# sourceMappingURL=node.js.map
|