@lokutor/sdk 1.5.1 → 1.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/index.js CHANGED
@@ -140,7 +140,12 @@ function isRetryableCode(code) {
140
140
  "validation.speed_out_of_range",
141
141
  "validation.steps_out_of_range",
142
142
  "validation.invalid_request_format",
143
- "internal.cancelled"
143
+ "internal.cancelled",
144
+ "agent.not_found",
145
+ "billing.insufficient_balance",
146
+ "billing.topup_required",
147
+ "billing.limit_reached",
148
+ "billing.account_disputed"
144
149
  ];
145
150
  return !fatal.includes(code);
146
151
  }
@@ -715,6 +720,9 @@ function nowMs() {
715
720
  function wsToHttp(url) {
716
721
  return url.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
717
722
  }
723
+ function apiOrigin(base) {
724
+ return new URL(wsToHttp(base || DEFAULT_URLS.VOICE_AGENT)).origin;
725
+ }
718
726
  async function fetchJson(url, timeoutMs = 1e4) {
719
727
  const controller = new AbortController();
720
728
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -827,10 +835,16 @@ var VoiceAgentClient = class {
827
835
  serverPlaybackRate = 44100;
828
836
  serverInputRate = 16e3;
829
837
  serverUrl;
838
+ authProvider;
839
+ voiceExplicit = false;
840
+ languageExplicit = false;
830
841
  constructor(config) {
831
- this.apiKey = config.apiKey;
842
+ this.apiKey = config.apiKey || "";
843
+ this.authProvider = config.auth;
832
844
  this.agentId = config.agentId || "";
833
- this.prompt = config.prompt;
845
+ this.prompt = config.prompt ?? "";
846
+ this.voiceExplicit = config.voice !== void 0;
847
+ this.languageExplicit = config.language !== void 0;
834
848
  this.voice = config.voice || "F1" /* F1 */;
835
849
  this.language = config.language || "en" /* ENGLISH */;
836
850
  this.serverUrl = config.serverUrl || DEFAULT_URLS.VOICE_AGENT;
@@ -850,6 +864,15 @@ var VoiceAgentClient = class {
850
864
  */
851
865
  async connect(customAudioManager) {
852
866
  this.isUserDisconnect = false;
867
+ if (this.authProvider) {
868
+ try {
869
+ this.apiKey = await this.authProvider();
870
+ } catch (err) {
871
+ const error = new LokutorError("auth.missing_key", "The auth callback failed to return a credential", { original: err, retryable: true });
872
+ this.emit("error", error);
873
+ throw error;
874
+ }
875
+ }
853
876
  if (this.enableAudio || customAudioManager) {
854
877
  if (customAudioManager) {
855
878
  this.audioManager = customAudioManager;
@@ -872,11 +895,11 @@ var VoiceAgentClient = class {
872
895
  let url = this.serverUrl;
873
896
  if (this.apiKey) {
874
897
  const separator = url.includes("?") ? "&" : "?";
875
- url += `${separator}api_key=${this.apiKey}`;
898
+ url += `${separator}api_key=${encodeURIComponent(this.apiKey)}`;
876
899
  }
877
900
  if (this.agentId) {
878
901
  const separator = url.includes("?") ? "&" : "?";
879
- url += `${separator}agent_id=${this.agentId}`;
902
+ url += `${separator}agent_id=${encodeURIComponent(this.agentId)}`;
880
903
  }
881
904
  const redactedUrl = url.replace(/api_key=[^&]+/, "api_key=***");
882
905
  sdkTrace("ws.connect", {
@@ -942,7 +965,7 @@ var VoiceAgentClient = class {
942
965
  });
943
966
  console.error("\u274C WebSocket error:", error.message);
944
967
  sdkTrace("ws.error", { code: error.code, message: error.message });
945
- if (this.onError) this.onError(error);
968
+ this.emit("error", error);
946
969
  if (!this.isConnected) {
947
970
  settle(() => reject(error));
948
971
  }
@@ -979,8 +1002,8 @@ var VoiceAgentClient = class {
979
1002
  });
980
1003
  console.log(`Session ended by the server (code ${event.code})`);
981
1004
  this.emit("ended", { code: event.code, reason: event.reason || "" });
982
- if (this.onError) this.onError(ended);
983
- if (this.onStatus) this.onStatus("disconnected");
1005
+ this.emit("error", ended);
1006
+ this.emit("status", "disconnected");
984
1007
  return;
985
1008
  }
986
1009
  if (!this.isUserDisconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
@@ -988,7 +1011,7 @@ var VoiceAgentClient = class {
988
1011
  this.reconnectAttempts++;
989
1012
  const backoffDelay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 1e4);
990
1013
  console.warn(`Connection lost. Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
991
- if (this.onStatus) this.onStatus("reconnecting");
1014
+ this.emit("status", "reconnecting");
992
1015
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
993
1016
  this.reconnectTimer = setTimeout(() => {
994
1017
  this.reconnectTimer = null;
@@ -998,15 +1021,15 @@ var VoiceAgentClient = class {
998
1021
  }, backoffDelay);
999
1022
  } else if (this.isUserDisconnect) {
1000
1023
  console.log("Disconnected");
1001
- if (this.onStatus) this.onStatus("disconnected");
1024
+ this.emit("status", "disconnected");
1002
1025
  } else {
1003
1026
  console.log("Max reconnection attempts reached");
1004
- if (this.onStatus) this.onStatus("disconnected");
1027
+ this.emit("status", "disconnected");
1005
1028
  }
1006
1029
  };
1007
1030
  } catch (err) {
1008
1031
  const error = err instanceof LokutorError ? err : new LokutorError("internal.error", "Failed to create WebSocket connection", { original: err });
1009
- if (this.onError) this.onError(error);
1032
+ this.emit("error", error);
1010
1033
  settle(() => reject(error));
1011
1034
  }
1012
1035
  });
@@ -1034,13 +1057,14 @@ var VoiceAgentClient = class {
1034
1057
  sendConfig() {
1035
1058
  if (!this.ws || !this.isConnected) return;
1036
1059
  this.configConfirmed = false;
1037
- this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
1038
- this.ws.send(JSON.stringify({ type: "language", data: this.language }));
1060
+ const agentSession = Boolean(this.agentId);
1061
+ if (!agentSession || this.voiceExplicit) this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
1062
+ if (!agentSession || this.languageExplicit) this.ws.send(JSON.stringify({ type: "language", data: this.language }));
1039
1063
  if (this.tools && this.tools.length > 0) {
1040
1064
  this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
1041
1065
  }
1042
1066
  this.ws.send(JSON.stringify({ type: "visemes", data: this.wantVisemes }));
1043
- this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
1067
+ if (!agentSession || this.prompt) this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
1044
1068
  this.ws.send(JSON.stringify({ type: "rates", playback: 44100, input: 16e3 }));
1045
1069
  sdkTrace("ws.send.config", {
1046
1070
  promptLen: this.prompt?.length || 0,
@@ -1101,6 +1125,10 @@ var VoiceAgentClient = class {
1101
1125
  sdkTrace("server_rates", { playback: this.serverPlaybackRate, input: this.serverInputRate });
1102
1126
  break;
1103
1127
  case "transcript":
1128
+ if (msg.partial) {
1129
+ this.emit("transcription_partial", msg.data);
1130
+ break;
1131
+ }
1104
1132
  const role = msg.role === "user" ? "user" : "agent";
1105
1133
  this.messages.push({
1106
1134
  role,
@@ -1108,11 +1136,9 @@ var VoiceAgentClient = class {
1108
1136
  timestamp: nowMs()
1109
1137
  });
1110
1138
  if (msg.role === "user") {
1111
- if (this.onTranscription) this.onTranscription(msg.data);
1112
1139
  this.emit("transcription", msg.data);
1113
1140
  console.log(`\u{1F4AC} You: ${msg.data}`);
1114
1141
  } else {
1115
- if (this.onResponse) this.onResponse(msg.data);
1116
1142
  this.emit("response", msg.data);
1117
1143
  console.log(`\u{1F916} Agent: ${msg.data}`);
1118
1144
  }
@@ -1134,7 +1160,6 @@ var VoiceAgentClient = class {
1134
1160
  if (msg.data === "interrupted" && this.audioManager) {
1135
1161
  this.audioManager.stopPlayback();
1136
1162
  }
1137
- if (this.onStatus) this.onStatus(msg.data);
1138
1163
  this.emit("status", msg.data);
1139
1164
  const icons = {
1140
1165
  "interrupted": "\u26A1",
@@ -1184,11 +1209,12 @@ var VoiceAgentClient = class {
1184
1209
  }
1185
1210
  break;
1186
1211
  }
1187
- case "tool_call":
1188
- this.emit("tool_call", { name: msg.name, arguments: msg.arguments });
1212
+ case "tool_call": {
1213
+ const call = { name: msg.name, arguments: msg.arguments, call_id: msg.call_id };
1189
1214
  console.log(`\u{1F6E0}\uFE0F Tool Call: ${msg.name}(${msg.arguments})`);
1190
- this.emit("tool_call", msg);
1215
+ this.emit("tool_call", call);
1191
1216
  break;
1217
+ }
1192
1218
  }
1193
1219
  } catch (e) {
1194
1220
  sdkTrace("ws.recv.parse_error", { preview: text?.slice(0, 120) });
@@ -1302,7 +1328,7 @@ var VoiceAgentClient = class {
1302
1328
  * No authentication required.
1303
1329
  */
1304
1330
  static async fetchVoices(baseUrl) {
1305
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1331
+ const url = apiOrigin(baseUrl);
1306
1332
  const data = await fetchJson(`${url}/voices`);
1307
1333
  return data.voices || [];
1308
1334
  }
@@ -1311,7 +1337,7 @@ var VoiceAgentClient = class {
1311
1337
  * No authentication required.
1312
1338
  */
1313
1339
  static async fetchLanguages(baseUrl) {
1314
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1340
+ const url = apiOrigin(baseUrl);
1315
1341
  const data = await fetchJson(`${url}/languages`);
1316
1342
  return data.languages || [];
1317
1343
  }
@@ -1320,7 +1346,7 @@ var VoiceAgentClient = class {
1320
1346
  * No authentication required.
1321
1347
  */
1322
1348
  static async fetchModels(baseUrl) {
1323
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1349
+ const url = apiOrigin(baseUrl);
1324
1350
  const data = await fetchJson(`${url}/models`);
1325
1351
  return data.models || [];
1326
1352
  }
@@ -1329,7 +1355,7 @@ var VoiceAgentClient = class {
1329
1355
  * No authentication required.
1330
1356
  */
1331
1357
  static async fetchConfig(baseUrl) {
1332
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1358
+ const url = apiOrigin(baseUrl);
1333
1359
  return fetchJson(`${url}/config`);
1334
1360
  }
1335
1361
  /**
@@ -1337,16 +1363,16 @@ var VoiceAgentClient = class {
1337
1363
  * No authentication required.
1338
1364
  */
1339
1365
  static async fetchStatus(baseUrl) {
1340
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1366
+ const url = apiOrigin(baseUrl);
1341
1367
  return fetchJson(`${url}/status`);
1342
1368
  }
1343
1369
  /**
1344
- * Fetch health/liveness status from the server.
1345
- * No authentication required.
1370
+ * Fetch readiness from the server (GET /ready: 200 once it can serve).
1371
+ * No authentication required. (/health is the operator dashboard's and needs its password.)
1346
1372
  */
1347
1373
  static async fetchHealth(baseUrl) {
1348
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1349
- return fetchJson(`${url}/health`);
1374
+ const url = apiOrigin(baseUrl);
1375
+ return fetchJson(`${url}/ready`);
1350
1376
  }
1351
1377
  /**
1352
1378
  * Update the system prompt mid-conversation
@@ -1371,6 +1397,7 @@ var VoiceAgentClient = class {
1371
1397
  */
1372
1398
  updateVoice(voice) {
1373
1399
  this.voice = voice;
1400
+ this.voiceExplicit = true;
1374
1401
  if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
1375
1402
  this.ws.send(JSON.stringify({ type: "voice", data: voice }));
1376
1403
  console.log(`\u2699\uFE0F Updated voice: ${voice}`);
@@ -1381,6 +1408,7 @@ var VoiceAgentClient = class {
1381
1408
  */
1382
1409
  updateLanguage(language) {
1383
1410
  this.language = language;
1411
+ this.languageExplicit = true;
1384
1412
  if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
1385
1413
  this.ws.send(JSON.stringify({ type: "language", data: language }));
1386
1414
  console.log(`\u2699\uFE0F Updated language: ${language}`);
@@ -1440,98 +1468,120 @@ var TTSClient = class {
1440
1468
  */
1441
1469
  synthesize(options) {
1442
1470
  return new Promise((resolve, reject) => {
1443
- let activityTimeout;
1471
+ let timer;
1444
1472
  let ws;
1445
1473
  let startTime;
1446
1474
  let firstByteReceived = false;
1447
- const refreshTimeout = () => {
1448
- if (activityTimeout) clearTimeout(activityTimeout);
1449
- activityTimeout = setTimeout(() => {
1450
- console.log("\u23F1\uFE0F TTS synthesis reached inactivity timeout (2s) - resolving");
1451
- if (ws) ws.close();
1475
+ let finished = false;
1476
+ const finish = (err) => {
1477
+ if (finished) return;
1478
+ finished = true;
1479
+ if (timer) clearTimeout(timer);
1480
+ try {
1481
+ ws?.close();
1482
+ } catch {
1483
+ }
1484
+ if (err) {
1485
+ if (options.onError) options.onError(err);
1486
+ reject(err);
1487
+ } else {
1452
1488
  resolve();
1453
- }, 2e3);
1489
+ }
1490
+ };
1491
+ const arm = () => {
1492
+ if (timer) clearTimeout(timer);
1493
+ timer = setTimeout(
1494
+ () => finish(new LokutorError(
1495
+ "internal.timeout",
1496
+ firstByteReceived ? "TTS stream stalled" : "No audio within 30 s",
1497
+ { retryable: true }
1498
+ )),
1499
+ firstByteReceived ? 15e3 : 3e4
1500
+ );
1454
1501
  };
1455
1502
  try {
1456
1503
  let url = DEFAULT_URLS.TTS;
1457
1504
  if (this.apiKey) {
1458
1505
  const separator = url.includes("?") ? "&" : "?";
1459
- url += `${separator}api_key=${this.apiKey}`;
1506
+ url += `${separator}api_key=${encodeURIComponent(this.apiKey)}`;
1460
1507
  }
1461
1508
  ws = new WebSocket(url);
1462
1509
  ws.binaryType = "arraybuffer";
1463
1510
  ws.onopen = () => {
1464
- refreshTimeout();
1511
+ arm();
1465
1512
  const req = {
1466
1513
  text: options.text,
1467
1514
  voice: options.voice || "F1" /* F1 */,
1468
1515
  language: options.language || "en" /* ENGLISH */,
1469
- speed: options.speed || 1.05,
1470
- steps: options.steps || 24,
1471
1516
  visemes: options.visemes || false
1472
1517
  };
1518
+ if (options.speed !== void 0) req.speed = options.speed;
1519
+ if (options.steps !== void 0) req.steps = options.steps;
1473
1520
  ws.send(JSON.stringify(req));
1474
1521
  startTime = nowMs();
1475
1522
  };
1476
1523
  ws.onmessage = async (event) => {
1477
- refreshTimeout();
1478
1524
  if (event.data instanceof ArrayBuffer) {
1479
1525
  if (!firstByteReceived) {
1480
1526
  const ttfb = nowMs() - startTime;
1481
1527
  if (options.onTTFB) options.onTTFB(ttfb);
1482
1528
  firstByteReceived = true;
1483
1529
  }
1530
+ arm();
1484
1531
  if (options.onAudio) options.onAudio(new Uint8Array(event.data));
1485
1532
  return;
1486
1533
  }
1487
1534
  const text = event.data.toString();
1488
1535
  if (text === "EOS") {
1489
- if (activityTimeout) clearTimeout(activityTimeout);
1490
- ws.close();
1491
- resolve();
1536
+ finish();
1492
1537
  return;
1493
1538
  }
1539
+ let msg;
1494
1540
  try {
1495
- const msg = JSON.parse(text);
1496
- if (msg.type === "audio" && msg.data) {
1497
- const audioBuffer = base64ToUint8Array(msg.data);
1498
- if (!firstByteReceived) {
1499
- const ttfb = nowMs() - startTime;
1500
- if (options.onTTFB) options.onTTFB(ttfb);
1501
- firstByteReceived = true;
1502
- }
1503
- if (options.onAudio) options.onAudio(audioBuffer);
1504
- return;
1505
- }
1506
- if (msg.type === "visemes" && Array.isArray(msg.data) && options.onVisemes) {
1507
- options.onVisemes(normalizeVisemes(msg.data));
1508
- return;
1509
- }
1510
- if (Array.isArray(msg) && options.onVisemes) {
1511
- options.onVisemes(normalizeVisemes(msg));
1512
- return;
1513
- }
1514
- if (msg.type === "eos") {
1515
- if (activityTimeout) clearTimeout(activityTimeout);
1516
- ws.close();
1517
- resolve();
1541
+ msg = JSON.parse(text);
1542
+ } catch {
1543
+ return;
1544
+ }
1545
+ if (msg.type === "error") {
1546
+ const data = typeof msg.data === "object" && msg.data ? msg.data : { message: msg.data };
1547
+ finish(new LokutorError(data.code ?? "internal.error", data.message ?? "TTS failed", {
1548
+ detail: data.detail,
1549
+ retryable: data.retryable ?? false
1550
+ }));
1551
+ return;
1552
+ }
1553
+ if (msg.type === "audio" && msg.data) {
1554
+ const audioBuffer = base64ToUint8Array(msg.data);
1555
+ if (!firstByteReceived) {
1556
+ const ttfb = nowMs() - startTime;
1557
+ if (options.onTTFB) options.onTTFB(ttfb);
1558
+ firstByteReceived = true;
1518
1559
  }
1519
- } catch (e) {
1560
+ arm();
1561
+ if (options.onAudio) options.onAudio(audioBuffer);
1562
+ return;
1563
+ }
1564
+ if (msg.type === "visemes" && Array.isArray(msg.data) && options.onVisemes) {
1565
+ options.onVisemes(normalizeVisemes(msg.data));
1566
+ return;
1567
+ }
1568
+ if (Array.isArray(msg) && options.onVisemes) {
1569
+ options.onVisemes(normalizeVisemes(msg));
1570
+ return;
1571
+ }
1572
+ if (msg.type === "eos") {
1573
+ finish();
1520
1574
  }
1521
1575
  };
1522
- ws.onerror = (err) => {
1523
- if (activityTimeout) clearTimeout(activityTimeout);
1524
- if (options.onError) options.onError(err);
1525
- reject(err);
1526
- };
1527
- ws.onclose = () => {
1528
- if (activityTimeout) clearTimeout(activityTimeout);
1529
- resolve();
1576
+ ws.onerror = (err) => finish(err);
1577
+ ws.onclose = (ev) => {
1578
+ if (!finished) {
1579
+ if (firstByteReceived && ev.code === 1e3) finish();
1580
+ else finish(new LokutorError("internal.error", `TTS socket closed (${ev.code}${ev.reason ? `: ${ev.reason}` : ""})`, { retryable: true }));
1581
+ }
1530
1582
  };
1531
1583
  } catch (err) {
1532
- if (activityTimeout) clearTimeout(activityTimeout);
1533
- if (options.onError) options.onError(err);
1534
- reject(err);
1584
+ finish(err);
1535
1585
  }
1536
1586
  });
1537
1587
  }
@@ -1637,17 +1687,28 @@ var SpeechToTextClient = class {
1637
1687
  try {
1638
1688
  let url = this.serverUrl;
1639
1689
  const separator = url.includes("?") ? "&" : "?";
1640
- url += `${separator}api_key=${this.apiKey}`;
1690
+ url += `${separator}api_key=${encodeURIComponent(this.apiKey)}`;
1641
1691
  this.ws = new WebSocket(url);
1642
1692
  this.ws.onopen = async () => {
1643
1693
  this.isConnected = true;
1644
1694
  this.onStatusChange?.("connected");
1645
1695
  this.ws.send(JSON.stringify({ language: this.language || "en" /* ENGLISH */, vad: this.vad }));
1646
- await this.audioManager.startMicrophone((data) => {
1647
- if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
1648
- this.ws.send(data);
1696
+ try {
1697
+ await this.audioManager.startMicrophone((data) => {
1698
+ if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
1699
+ this.ws.send(data);
1700
+ }
1701
+ });
1702
+ } catch (err) {
1703
+ const error = err instanceof LokutorError ? err : new LokutorError("audio.microphone_unavailable", "Could not start the microphone", { original: err });
1704
+ this.onError?.(error);
1705
+ try {
1706
+ this.ws?.close();
1707
+ } catch {
1649
1708
  }
1650
- });
1709
+ settle(() => reject(error));
1710
+ return;
1711
+ }
1651
1712
  settle(() => resolve(true));
1652
1713
  };
1653
1714
  this.ws.onmessage = (event) => {
@@ -1661,7 +1722,11 @@ var SpeechToTextClient = class {
1661
1722
  this.onPartialTranscript?.(msg.data ?? "");
1662
1723
  }
1663
1724
  } else if (msg.type === "error") {
1664
- this.onError?.(new LokutorError("internal.error", msg.data ?? "STT error"));
1725
+ const data = typeof msg.data === "object" && msg.data ? msg.data : { message: msg.data };
1726
+ this.onError?.(new LokutorError(data.code ?? "internal.error", data.message ?? "STT error", {
1727
+ detail: data.detail,
1728
+ retryable: data.retryable ?? false
1729
+ }));
1665
1730
  }
1666
1731
  } catch {
1667
1732
  }