@craftedxp/voice-js 0.5.4 → 0.6.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/dist/browser.d.mts +65 -1
- package/dist/browser.d.ts +467 -502
- package/dist/browser.js +921 -836
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs +93 -1
- package/dist/browser.mjs.map +1 -1
- package/dist/embed.iife.js +67 -23358
- package/dist/node.d.mts +63 -0
- package/dist/node.d.ts +462 -483
- package/dist/node.js +453 -467
- package/dist/node.js.map +1 -1
- package/dist/node.mjs.map +1 -1
- package/package.json +2 -1
package/dist/node.js
CHANGED
|
@@ -1,733 +1,719 @@
|
|
|
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
|
|
471
457
|
var NodeVoiceClient = class {
|
|
472
458
|
constructor(args) {
|
|
473
|
-
this.rws = null
|
|
474
|
-
this.muted = false
|
|
475
|
-
this.startedAt = null
|
|
476
|
-
this.endedFired = false
|
|
477
|
-
this.lastError = null
|
|
459
|
+
this.rws = null;
|
|
460
|
+
this.muted = false;
|
|
461
|
+
this.startedAt = null;
|
|
462
|
+
this.endedFired = false;
|
|
463
|
+
this.lastError = null;
|
|
478
464
|
this.end = () => {
|
|
479
|
-
this.teardown(
|
|
480
|
-
}
|
|
465
|
+
this.teardown("user_hangup");
|
|
466
|
+
};
|
|
481
467
|
this.mute = () => {
|
|
482
|
-
this.muted = true
|
|
483
|
-
}
|
|
468
|
+
this.muted = true;
|
|
469
|
+
};
|
|
484
470
|
this.unmute = () => {
|
|
485
|
-
this.muted = false
|
|
486
|
-
}
|
|
471
|
+
this.muted = false;
|
|
472
|
+
};
|
|
487
473
|
// ---------------------------------------------------------------
|
|
488
474
|
// Node-only raw audio surface
|
|
489
475
|
// ---------------------------------------------------------------
|
|
490
476
|
this.sendAudioChunk = (pcm) => {
|
|
491
|
-
if (!this.rws) return false
|
|
492
|
-
this.marks.markFirstOutboundAudio()
|
|
477
|
+
if (!this.rws) return false;
|
|
478
|
+
this.marks.markFirstOutboundAudio();
|
|
493
479
|
if (this.muted) {
|
|
494
|
-
const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength
|
|
495
|
-
this.rws.send(new ArrayBuffer(len))
|
|
496
|
-
return true
|
|
480
|
+
const len = ArrayBuffer.isView(pcm) ? pcm.byteLength : pcm.byteLength;
|
|
481
|
+
this.rws.send(new ArrayBuffer(len));
|
|
482
|
+
return true;
|
|
497
483
|
}
|
|
498
|
-
this.rws.send(pcm)
|
|
499
|
-
return true
|
|
500
|
-
}
|
|
484
|
+
this.rws.send(pcm);
|
|
485
|
+
return true;
|
|
486
|
+
};
|
|
501
487
|
// ---------------------------------------------------------------
|
|
502
488
|
// Internal
|
|
503
489
|
// ---------------------------------------------------------------
|
|
504
490
|
this.setState = (next) => {
|
|
505
|
-
if (this.proto.state === next) return
|
|
506
|
-
this.proto.state = next
|
|
507
|
-
this.args.options.onStateChange?.(next)
|
|
508
|
-
}
|
|
491
|
+
if (this.proto.state === next) return;
|
|
492
|
+
this.proto.state = next;
|
|
493
|
+
this.args.options.onStateChange?.(next);
|
|
494
|
+
};
|
|
509
495
|
this.sendClientToolsRegister = () => {
|
|
510
|
-
const frame = buildRegisterFrame(this.args.options.clientTools ?? {})
|
|
511
|
-
this.rws?.send(JSON.stringify(frame))
|
|
512
|
-
}
|
|
496
|
+
const frame = buildRegisterFrame(this.args.options.clientTools ?? {});
|
|
497
|
+
this.rws?.send(JSON.stringify(frame));
|
|
498
|
+
};
|
|
513
499
|
this.emitError = (err) => {
|
|
514
|
-
this.lastError = err
|
|
515
|
-
this.args.options.onError?.(err)
|
|
516
|
-
}
|
|
500
|
+
this.lastError = err;
|
|
501
|
+
this.args.options.onError?.(err);
|
|
502
|
+
};
|
|
517
503
|
this.handleSocketEvent = (ev) => {
|
|
518
504
|
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 ===
|
|
505
|
+
case "open":
|
|
506
|
+
break;
|
|
507
|
+
case "reconnected":
|
|
508
|
+
this.proto.transcript = [];
|
|
509
|
+
this.proto.agentBubbleId = null;
|
|
510
|
+
this.args.options.onTranscript?.(this.proto.transcript);
|
|
511
|
+
this.setState("listening");
|
|
512
|
+
break;
|
|
513
|
+
case "message":
|
|
514
|
+
if (typeof ev.data === "string") {
|
|
529
515
|
handleServerMessage(ev.data, this.proto, {
|
|
530
516
|
onState: this.setState,
|
|
531
517
|
onTranscript: (entries) => this.args.options.onTranscript?.(entries),
|
|
532
518
|
onError: this.emitError,
|
|
533
519
|
onInterrupt: () => this.args.options.onInterrupt?.(),
|
|
534
520
|
onAgentTurnStart: (seq) => {
|
|
535
|
-
if (typeof seq ===
|
|
536
|
-
this.args.options.onAgentTurnStart?.()
|
|
521
|
+
if (typeof seq === "number") this.marks.onAgentTurnStart(seq);
|
|
522
|
+
this.args.options.onAgentTurnStart?.();
|
|
537
523
|
},
|
|
538
524
|
onAgentTurnEnd: (seq) => {
|
|
539
|
-
if (typeof seq ===
|
|
525
|
+
if (typeof seq === "number") this.marks.onAgentTurnEnd(seq);
|
|
540
526
|
},
|
|
541
527
|
onCallEnd: (reason) => this.teardown(reason),
|
|
542
528
|
onConnected: () => this.sendClientToolsRegister(),
|
|
543
|
-
onClientToolCall: (frame) =>
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
})
|
|
529
|
+
onClientToolCall: (frame) => dispatchClientToolCall(
|
|
530
|
+
(f) => this.rws?.send(JSON.stringify(f)),
|
|
531
|
+
this.args.options.clientTools ?? {},
|
|
532
|
+
frame
|
|
533
|
+
)
|
|
534
|
+
});
|
|
550
535
|
} else {
|
|
551
|
-
this.marks.markFirstAudibleOutput()
|
|
552
|
-
this.args.options.onAudioChunk?.(ev.data)
|
|
536
|
+
this.marks.markFirstAudibleOutput();
|
|
537
|
+
this.args.options.onAudioChunk?.(ev.data);
|
|
553
538
|
}
|
|
554
|
-
break
|
|
555
|
-
case
|
|
539
|
+
break;
|
|
540
|
+
case "close":
|
|
556
541
|
if (ev.permanent) {
|
|
557
|
-
const reason = this.proto.endReason ?? (this.lastError ?
|
|
558
|
-
this.teardown(reason)
|
|
542
|
+
const reason = this.proto.endReason ?? (this.lastError ? "error" : "user_hangup");
|
|
543
|
+
this.teardown(reason);
|
|
559
544
|
}
|
|
560
|
-
break
|
|
561
|
-
case
|
|
562
|
-
this.emitError({ code:
|
|
563
|
-
break
|
|
545
|
+
break;
|
|
546
|
+
case "error":
|
|
547
|
+
this.emitError({ code: "socket_error", message: ev.error.message });
|
|
548
|
+
break;
|
|
564
549
|
}
|
|
565
|
-
}
|
|
550
|
+
};
|
|
566
551
|
this.teardown = (reason) => {
|
|
567
552
|
try {
|
|
568
|
-
this.marks.flush()
|
|
569
|
-
} catch {
|
|
553
|
+
this.marks.flush();
|
|
554
|
+
} catch {
|
|
555
|
+
}
|
|
570
556
|
try {
|
|
571
|
-
this.rws?.close(1e3, reason)
|
|
572
|
-
} catch {
|
|
573
|
-
|
|
574
|
-
this.
|
|
575
|
-
this.
|
|
576
|
-
|
|
557
|
+
this.rws?.close(1e3, reason);
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
this.rws = null;
|
|
561
|
+
this.setState("ended");
|
|
562
|
+
this.fireEndOnce(reason);
|
|
563
|
+
};
|
|
577
564
|
this.fireEndOnce = (reason) => {
|
|
578
|
-
if (this.endedFired) return
|
|
579
|
-
this.endedFired = true
|
|
580
|
-
const startedAt = this.startedAt ?? Date.now()
|
|
565
|
+
if (this.endedFired) return;
|
|
566
|
+
this.endedFired = true;
|
|
567
|
+
const startedAt = this.startedAt ?? Date.now();
|
|
581
568
|
this.args.options.onEnd?.({
|
|
582
569
|
reason,
|
|
583
|
-
errorCode: reason ===
|
|
584
|
-
durationMs: Date.now() - startedAt
|
|
585
|
-
})
|
|
586
|
-
}
|
|
587
|
-
this.args = args
|
|
588
|
-
this.proto = createProtocolState()
|
|
589
|
-
validateClientToolMap(args.options.clientTools)
|
|
570
|
+
errorCode: reason === "error" ? this.lastError?.code : void 0,
|
|
571
|
+
durationMs: Date.now() - startedAt
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
this.args = args;
|
|
575
|
+
this.proto = createProtocolState();
|
|
576
|
+
validateClientToolMap(args.options.clientTools);
|
|
590
577
|
this.marks = createClientMarksBuffer({
|
|
591
578
|
send: (frame) => {
|
|
592
579
|
try {
|
|
593
|
-
this.rws?.send(JSON.stringify(frame))
|
|
594
|
-
} catch {
|
|
595
|
-
|
|
596
|
-
|
|
580
|
+
this.rws?.send(JSON.stringify(frame));
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
});
|
|
597
585
|
}
|
|
598
586
|
// ---------------------------------------------------------------
|
|
599
587
|
// Call interface
|
|
600
588
|
// ---------------------------------------------------------------
|
|
601
589
|
get state() {
|
|
602
|
-
return this.proto.state
|
|
590
|
+
return this.proto.state;
|
|
603
591
|
}
|
|
604
592
|
get transcript() {
|
|
605
|
-
return this.proto.transcript.slice()
|
|
593
|
+
return this.proto.transcript.slice();
|
|
606
594
|
}
|
|
607
595
|
get isMuted() {
|
|
608
|
-
return this.muted
|
|
596
|
+
return this.muted;
|
|
609
597
|
}
|
|
610
598
|
// ---------------------------------------------------------------
|
|
611
599
|
// Lifecycle
|
|
612
600
|
// ---------------------------------------------------------------
|
|
613
601
|
async start() {
|
|
614
|
-
this.setState(
|
|
615
|
-
this.startedAt = Date.now()
|
|
602
|
+
this.setState("connecting");
|
|
603
|
+
this.startedAt = Date.now();
|
|
616
604
|
const url = buildWsUrl({
|
|
617
605
|
apiBase: this.args.config.apiBase,
|
|
618
606
|
agentId: this.args.options.agentId,
|
|
619
607
|
token: this.args.token,
|
|
620
|
-
bargeIn: this.args.options.bargeIn
|
|
621
|
-
})
|
|
608
|
+
bargeIn: this.args.options.bargeIn
|
|
609
|
+
});
|
|
622
610
|
this.rws = createReconnectingWebSocket(
|
|
623
611
|
{
|
|
624
612
|
url,
|
|
625
613
|
wsFactory: this.args.wsFactory,
|
|
626
|
-
maxRetries: 3
|
|
614
|
+
maxRetries: 3
|
|
627
615
|
},
|
|
628
|
-
(ev) => this.handleSocketEvent(ev)
|
|
629
|
-
)
|
|
616
|
+
(ev) => this.handleSocketEvent(ev)
|
|
617
|
+
);
|
|
630
618
|
}
|
|
631
|
-
}
|
|
619
|
+
};
|
|
632
620
|
|
|
633
621
|
// src/incomingCall.ts
|
|
634
622
|
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
|
-
}
|
|
623
|
+
if (typeof raw !== "object" || raw === null) {
|
|
624
|
+
throw new Error("parseIncomingCall: payload must be an object");
|
|
625
|
+
}
|
|
626
|
+
const p = raw;
|
|
627
|
+
if (typeof p.token !== "string" || !p.token.startsWith("ct_")) {
|
|
628
|
+
throw new Error("parseIncomingCall: missing or invalid `token` (expected a ct_ string)");
|
|
629
|
+
}
|
|
630
|
+
if (typeof p.agentId !== "string" || p.agentId.length === 0) {
|
|
631
|
+
throw new Error("parseIncomingCall: missing `agentId`");
|
|
632
|
+
}
|
|
633
|
+
const transport = p.transport === "webrtc" ? "webrtc" : "ws";
|
|
634
|
+
const out = { token: p.token, agentId: p.agentId, transport };
|
|
635
|
+
if (transport === "webrtc" && typeof p.webrtcGatewayBase === "string") {
|
|
636
|
+
out.webrtcGatewayBase = p.webrtcGatewayBase;
|
|
637
|
+
}
|
|
638
|
+
if (typeof p.expiresAt === "number") out.expiresAt = p.expiresAt;
|
|
639
|
+
if (typeof p.agentName === "string") out.agentName = p.agentName;
|
|
640
|
+
if (typeof p.agentAvatarUrl === "string") out.agentAvatarUrl = p.agentAvatarUrl;
|
|
641
|
+
return out;
|
|
642
|
+
};
|
|
655
643
|
|
|
656
644
|
// src/node.ts
|
|
657
|
-
var cachedWsCtor = null
|
|
645
|
+
var cachedWsCtor = null;
|
|
658
646
|
var loadWsCtor = async () => {
|
|
659
|
-
if (cachedWsCtor) return cachedWsCtor
|
|
647
|
+
if (cachedWsCtor) return cachedWsCtor;
|
|
660
648
|
try {
|
|
661
|
-
const mod = await import(
|
|
662
|
-
const ctor = mod.WebSocket ?? mod.default
|
|
649
|
+
const mod = await import("ws");
|
|
650
|
+
const ctor = mod.WebSocket ?? mod.default;
|
|
663
651
|
if (!ctor) {
|
|
664
|
-
throw new Error(
|
|
652
|
+
throw new Error("imported `ws` but neither default nor named WebSocket export was found");
|
|
665
653
|
}
|
|
666
|
-
cachedWsCtor = ctor
|
|
667
|
-
return ctor
|
|
654
|
+
cachedWsCtor = ctor;
|
|
655
|
+
return ctor;
|
|
668
656
|
} catch (err) {
|
|
669
657
|
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
|
-
)
|
|
658
|
+
"@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))
|
|
659
|
+
);
|
|
673
660
|
}
|
|
674
|
-
}
|
|
661
|
+
};
|
|
675
662
|
var NodeVoiceFactory = class {
|
|
676
663
|
constructor(config) {
|
|
677
664
|
this.startCall = async (options) => {
|
|
678
665
|
if (!options.agentId) {
|
|
679
|
-
throw new Error(
|
|
666
|
+
throw new Error("startCall: agentId is required");
|
|
680
667
|
}
|
|
681
|
-
const WsCtor = await loadWsCtor()
|
|
682
|
-
const wsFactory = (url) => new WsCtor(url)
|
|
683
|
-
const { context, metadata } = mergeStartCallContext(this.config, options)
|
|
668
|
+
const WsCtor = await loadWsCtor();
|
|
669
|
+
const wsFactory = (url) => new WsCtor(url);
|
|
670
|
+
const { context, metadata } = mergeStartCallContext(this.config, options);
|
|
684
671
|
const fetchArgs = {
|
|
685
672
|
agentId: options.agentId,
|
|
686
673
|
userId: options.userId,
|
|
687
674
|
context,
|
|
688
|
-
metadata
|
|
689
|
-
}
|
|
690
|
-
let token
|
|
675
|
+
metadata
|
|
676
|
+
};
|
|
677
|
+
let token;
|
|
691
678
|
if (options.token) {
|
|
692
|
-
token = options.token
|
|
679
|
+
token = options.token;
|
|
693
680
|
} else {
|
|
694
|
-
const r = await this.config.fetchToken(fetchArgs)
|
|
681
|
+
const r = await this.config.fetchToken(fetchArgs);
|
|
695
682
|
if (!r) {
|
|
696
|
-
throw new Error(
|
|
683
|
+
throw new Error("configureVoiceClient.fetchToken returned empty token");
|
|
697
684
|
}
|
|
698
|
-
token = typeof r ===
|
|
685
|
+
token = typeof r === "string" ? r : r.token;
|
|
699
686
|
if (!token) {
|
|
700
|
-
throw new Error(
|
|
687
|
+
throw new Error("configureVoiceClient.fetchToken returned an object without `token`");
|
|
701
688
|
}
|
|
702
|
-
if (typeof r !==
|
|
689
|
+
if (typeof r !== "string" && r.transport === "webrtc") {
|
|
703
690
|
console.warn(
|
|
704
|
-
|
|
705
|
-
)
|
|
691
|
+
"@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."
|
|
692
|
+
);
|
|
706
693
|
}
|
|
707
694
|
}
|
|
708
695
|
const client = new NodeVoiceClient({
|
|
709
696
|
config: this.config,
|
|
710
697
|
options: { ...options, context, metadata },
|
|
711
698
|
token,
|
|
712
|
-
wsFactory
|
|
713
|
-
})
|
|
714
|
-
await client.start()
|
|
715
|
-
return client
|
|
716
|
-
}
|
|
717
|
-
this.config = config
|
|
718
|
-
}
|
|
719
|
-
}
|
|
699
|
+
wsFactory
|
|
700
|
+
});
|
|
701
|
+
await client.start();
|
|
702
|
+
return client;
|
|
703
|
+
};
|
|
704
|
+
this.config = config;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
720
707
|
function configureVoiceClient(config) {
|
|
721
|
-
return new NodeVoiceFactory(normalizeConfig(config))
|
|
708
|
+
return new NodeVoiceFactory(normalizeConfig(config));
|
|
722
709
|
}
|
|
723
710
|
// 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
|
|
711
|
+
0 && (module.exports = {
|
|
712
|
+
buildWsUrl,
|
|
713
|
+
configureVoiceClient,
|
|
714
|
+
createProtocolState,
|
|
715
|
+
createReconnectingWebSocket,
|
|
716
|
+
handleServerMessage,
|
|
717
|
+
parseIncomingCall
|
|
718
|
+
});
|
|
719
|
+
//# sourceMappingURL=node.js.map
|