@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.mjs CHANGED
@@ -87,7 +87,12 @@ function isRetryableCode(code) {
87
87
  "validation.speed_out_of_range",
88
88
  "validation.steps_out_of_range",
89
89
  "validation.invalid_request_format",
90
- "internal.cancelled"
90
+ "internal.cancelled",
91
+ "agent.not_found",
92
+ "billing.insufficient_balance",
93
+ "billing.topup_required",
94
+ "billing.limit_reached",
95
+ "billing.account_disputed"
91
96
  ];
92
97
  return !fatal.includes(code);
93
98
  }
@@ -662,6 +667,9 @@ function nowMs() {
662
667
  function wsToHttp(url) {
663
668
  return url.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
664
669
  }
670
+ function apiOrigin(base) {
671
+ return new URL(wsToHttp(base || DEFAULT_URLS.VOICE_AGENT)).origin;
672
+ }
665
673
  async function fetchJson(url, timeoutMs = 1e4) {
666
674
  const controller = new AbortController();
667
675
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -774,10 +782,16 @@ var VoiceAgentClient = class {
774
782
  serverPlaybackRate = 44100;
775
783
  serverInputRate = 16e3;
776
784
  serverUrl;
785
+ authProvider;
786
+ voiceExplicit = false;
787
+ languageExplicit = false;
777
788
  constructor(config) {
778
- this.apiKey = config.apiKey;
789
+ this.apiKey = config.apiKey || "";
790
+ this.authProvider = config.auth;
779
791
  this.agentId = config.agentId || "";
780
- this.prompt = config.prompt;
792
+ this.prompt = config.prompt ?? "";
793
+ this.voiceExplicit = config.voice !== void 0;
794
+ this.languageExplicit = config.language !== void 0;
781
795
  this.voice = config.voice || "F1" /* F1 */;
782
796
  this.language = config.language || "en" /* ENGLISH */;
783
797
  this.serverUrl = config.serverUrl || DEFAULT_URLS.VOICE_AGENT;
@@ -797,6 +811,15 @@ var VoiceAgentClient = class {
797
811
  */
798
812
  async connect(customAudioManager) {
799
813
  this.isUserDisconnect = false;
814
+ if (this.authProvider) {
815
+ try {
816
+ this.apiKey = await this.authProvider();
817
+ } catch (err) {
818
+ const error = new LokutorError("auth.missing_key", "The auth callback failed to return a credential", { original: err, retryable: true });
819
+ this.emit("error", error);
820
+ throw error;
821
+ }
822
+ }
800
823
  if (this.enableAudio || customAudioManager) {
801
824
  if (customAudioManager) {
802
825
  this.audioManager = customAudioManager;
@@ -819,11 +842,11 @@ var VoiceAgentClient = class {
819
842
  let url = this.serverUrl;
820
843
  if (this.apiKey) {
821
844
  const separator = url.includes("?") ? "&" : "?";
822
- url += `${separator}api_key=${this.apiKey}`;
845
+ url += `${separator}api_key=${encodeURIComponent(this.apiKey)}`;
823
846
  }
824
847
  if (this.agentId) {
825
848
  const separator = url.includes("?") ? "&" : "?";
826
- url += `${separator}agent_id=${this.agentId}`;
849
+ url += `${separator}agent_id=${encodeURIComponent(this.agentId)}`;
827
850
  }
828
851
  const redactedUrl = url.replace(/api_key=[^&]+/, "api_key=***");
829
852
  sdkTrace("ws.connect", {
@@ -889,7 +912,7 @@ var VoiceAgentClient = class {
889
912
  });
890
913
  console.error("\u274C WebSocket error:", error.message);
891
914
  sdkTrace("ws.error", { code: error.code, message: error.message });
892
- if (this.onError) this.onError(error);
915
+ this.emit("error", error);
893
916
  if (!this.isConnected) {
894
917
  settle(() => reject(error));
895
918
  }
@@ -926,8 +949,8 @@ var VoiceAgentClient = class {
926
949
  });
927
950
  console.log(`Session ended by the server (code ${event.code})`);
928
951
  this.emit("ended", { code: event.code, reason: event.reason || "" });
929
- if (this.onError) this.onError(ended);
930
- if (this.onStatus) this.onStatus("disconnected");
952
+ this.emit("error", ended);
953
+ this.emit("status", "disconnected");
931
954
  return;
932
955
  }
933
956
  if (!this.isUserDisconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
@@ -935,7 +958,7 @@ var VoiceAgentClient = class {
935
958
  this.reconnectAttempts++;
936
959
  const backoffDelay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 1e4);
937
960
  console.warn(`Connection lost. Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
938
- if (this.onStatus) this.onStatus("reconnecting");
961
+ this.emit("status", "reconnecting");
939
962
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
940
963
  this.reconnectTimer = setTimeout(() => {
941
964
  this.reconnectTimer = null;
@@ -945,15 +968,15 @@ var VoiceAgentClient = class {
945
968
  }, backoffDelay);
946
969
  } else if (this.isUserDisconnect) {
947
970
  console.log("Disconnected");
948
- if (this.onStatus) this.onStatus("disconnected");
971
+ this.emit("status", "disconnected");
949
972
  } else {
950
973
  console.log("Max reconnection attempts reached");
951
- if (this.onStatus) this.onStatus("disconnected");
974
+ this.emit("status", "disconnected");
952
975
  }
953
976
  };
954
977
  } catch (err) {
955
978
  const error = err instanceof LokutorError ? err : new LokutorError("internal.error", "Failed to create WebSocket connection", { original: err });
956
- if (this.onError) this.onError(error);
979
+ this.emit("error", error);
957
980
  settle(() => reject(error));
958
981
  }
959
982
  });
@@ -981,13 +1004,14 @@ var VoiceAgentClient = class {
981
1004
  sendConfig() {
982
1005
  if (!this.ws || !this.isConnected) return;
983
1006
  this.configConfirmed = false;
984
- this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
985
- this.ws.send(JSON.stringify({ type: "language", data: this.language }));
1007
+ const agentSession = Boolean(this.agentId);
1008
+ if (!agentSession || this.voiceExplicit) this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
1009
+ if (!agentSession || this.languageExplicit) this.ws.send(JSON.stringify({ type: "language", data: this.language }));
986
1010
  if (this.tools && this.tools.length > 0) {
987
1011
  this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
988
1012
  }
989
1013
  this.ws.send(JSON.stringify({ type: "visemes", data: this.wantVisemes }));
990
- this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
1014
+ if (!agentSession || this.prompt) this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
991
1015
  this.ws.send(JSON.stringify({ type: "rates", playback: 44100, input: 16e3 }));
992
1016
  sdkTrace("ws.send.config", {
993
1017
  promptLen: this.prompt?.length || 0,
@@ -1048,6 +1072,10 @@ var VoiceAgentClient = class {
1048
1072
  sdkTrace("server_rates", { playback: this.serverPlaybackRate, input: this.serverInputRate });
1049
1073
  break;
1050
1074
  case "transcript":
1075
+ if (msg.partial) {
1076
+ this.emit("transcription_partial", msg.data);
1077
+ break;
1078
+ }
1051
1079
  const role = msg.role === "user" ? "user" : "agent";
1052
1080
  this.messages.push({
1053
1081
  role,
@@ -1055,11 +1083,9 @@ var VoiceAgentClient = class {
1055
1083
  timestamp: nowMs()
1056
1084
  });
1057
1085
  if (msg.role === "user") {
1058
- if (this.onTranscription) this.onTranscription(msg.data);
1059
1086
  this.emit("transcription", msg.data);
1060
1087
  console.log(`\u{1F4AC} You: ${msg.data}`);
1061
1088
  } else {
1062
- if (this.onResponse) this.onResponse(msg.data);
1063
1089
  this.emit("response", msg.data);
1064
1090
  console.log(`\u{1F916} Agent: ${msg.data}`);
1065
1091
  }
@@ -1081,7 +1107,6 @@ var VoiceAgentClient = class {
1081
1107
  if (msg.data === "interrupted" && this.audioManager) {
1082
1108
  this.audioManager.stopPlayback();
1083
1109
  }
1084
- if (this.onStatus) this.onStatus(msg.data);
1085
1110
  this.emit("status", msg.data);
1086
1111
  const icons = {
1087
1112
  "interrupted": "\u26A1",
@@ -1131,11 +1156,12 @@ var VoiceAgentClient = class {
1131
1156
  }
1132
1157
  break;
1133
1158
  }
1134
- case "tool_call":
1135
- this.emit("tool_call", { name: msg.name, arguments: msg.arguments });
1159
+ case "tool_call": {
1160
+ const call = { name: msg.name, arguments: msg.arguments, call_id: msg.call_id };
1136
1161
  console.log(`\u{1F6E0}\uFE0F Tool Call: ${msg.name}(${msg.arguments})`);
1137
- this.emit("tool_call", msg);
1162
+ this.emit("tool_call", call);
1138
1163
  break;
1164
+ }
1139
1165
  }
1140
1166
  } catch (e) {
1141
1167
  sdkTrace("ws.recv.parse_error", { preview: text?.slice(0, 120) });
@@ -1249,7 +1275,7 @@ var VoiceAgentClient = class {
1249
1275
  * No authentication required.
1250
1276
  */
1251
1277
  static async fetchVoices(baseUrl) {
1252
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1278
+ const url = apiOrigin(baseUrl);
1253
1279
  const data = await fetchJson(`${url}/voices`);
1254
1280
  return data.voices || [];
1255
1281
  }
@@ -1258,7 +1284,7 @@ var VoiceAgentClient = class {
1258
1284
  * No authentication required.
1259
1285
  */
1260
1286
  static async fetchLanguages(baseUrl) {
1261
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1287
+ const url = apiOrigin(baseUrl);
1262
1288
  const data = await fetchJson(`${url}/languages`);
1263
1289
  return data.languages || [];
1264
1290
  }
@@ -1267,7 +1293,7 @@ var VoiceAgentClient = class {
1267
1293
  * No authentication required.
1268
1294
  */
1269
1295
  static async fetchModels(baseUrl) {
1270
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1296
+ const url = apiOrigin(baseUrl);
1271
1297
  const data = await fetchJson(`${url}/models`);
1272
1298
  return data.models || [];
1273
1299
  }
@@ -1276,7 +1302,7 @@ var VoiceAgentClient = class {
1276
1302
  * No authentication required.
1277
1303
  */
1278
1304
  static async fetchConfig(baseUrl) {
1279
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1305
+ const url = apiOrigin(baseUrl);
1280
1306
  return fetchJson(`${url}/config`);
1281
1307
  }
1282
1308
  /**
@@ -1284,16 +1310,16 @@ var VoiceAgentClient = class {
1284
1310
  * No authentication required.
1285
1311
  */
1286
1312
  static async fetchStatus(baseUrl) {
1287
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1313
+ const url = apiOrigin(baseUrl);
1288
1314
  return fetchJson(`${url}/status`);
1289
1315
  }
1290
1316
  /**
1291
- * Fetch health/liveness status from the server.
1292
- * No authentication required.
1317
+ * Fetch readiness from the server (GET /ready: 200 once it can serve).
1318
+ * No authentication required. (/health is the operator dashboard's and needs its password.)
1293
1319
  */
1294
1320
  static async fetchHealth(baseUrl) {
1295
- const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
1296
- return fetchJson(`${url}/health`);
1321
+ const url = apiOrigin(baseUrl);
1322
+ return fetchJson(`${url}/ready`);
1297
1323
  }
1298
1324
  /**
1299
1325
  * Update the system prompt mid-conversation
@@ -1318,6 +1344,7 @@ var VoiceAgentClient = class {
1318
1344
  */
1319
1345
  updateVoice(voice) {
1320
1346
  this.voice = voice;
1347
+ this.voiceExplicit = true;
1321
1348
  if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
1322
1349
  this.ws.send(JSON.stringify({ type: "voice", data: voice }));
1323
1350
  console.log(`\u2699\uFE0F Updated voice: ${voice}`);
@@ -1328,6 +1355,7 @@ var VoiceAgentClient = class {
1328
1355
  */
1329
1356
  updateLanguage(language) {
1330
1357
  this.language = language;
1358
+ this.languageExplicit = true;
1331
1359
  if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
1332
1360
  this.ws.send(JSON.stringify({ type: "language", data: language }));
1333
1361
  console.log(`\u2699\uFE0F Updated language: ${language}`);
@@ -1387,98 +1415,120 @@ var TTSClient = class {
1387
1415
  */
1388
1416
  synthesize(options) {
1389
1417
  return new Promise((resolve, reject) => {
1390
- let activityTimeout;
1418
+ let timer;
1391
1419
  let ws;
1392
1420
  let startTime;
1393
1421
  let firstByteReceived = false;
1394
- const refreshTimeout = () => {
1395
- if (activityTimeout) clearTimeout(activityTimeout);
1396
- activityTimeout = setTimeout(() => {
1397
- console.log("\u23F1\uFE0F TTS synthesis reached inactivity timeout (2s) - resolving");
1398
- if (ws) ws.close();
1422
+ let finished = false;
1423
+ const finish = (err) => {
1424
+ if (finished) return;
1425
+ finished = true;
1426
+ if (timer) clearTimeout(timer);
1427
+ try {
1428
+ ws?.close();
1429
+ } catch {
1430
+ }
1431
+ if (err) {
1432
+ if (options.onError) options.onError(err);
1433
+ reject(err);
1434
+ } else {
1399
1435
  resolve();
1400
- }, 2e3);
1436
+ }
1437
+ };
1438
+ const arm = () => {
1439
+ if (timer) clearTimeout(timer);
1440
+ timer = setTimeout(
1441
+ () => finish(new LokutorError(
1442
+ "internal.timeout",
1443
+ firstByteReceived ? "TTS stream stalled" : "No audio within 30 s",
1444
+ { retryable: true }
1445
+ )),
1446
+ firstByteReceived ? 15e3 : 3e4
1447
+ );
1401
1448
  };
1402
1449
  try {
1403
1450
  let url = DEFAULT_URLS.TTS;
1404
1451
  if (this.apiKey) {
1405
1452
  const separator = url.includes("?") ? "&" : "?";
1406
- url += `${separator}api_key=${this.apiKey}`;
1453
+ url += `${separator}api_key=${encodeURIComponent(this.apiKey)}`;
1407
1454
  }
1408
1455
  ws = new WebSocket(url);
1409
1456
  ws.binaryType = "arraybuffer";
1410
1457
  ws.onopen = () => {
1411
- refreshTimeout();
1458
+ arm();
1412
1459
  const req = {
1413
1460
  text: options.text,
1414
1461
  voice: options.voice || "F1" /* F1 */,
1415
1462
  language: options.language || "en" /* ENGLISH */,
1416
- speed: options.speed || 1.05,
1417
- steps: options.steps || 24,
1418
1463
  visemes: options.visemes || false
1419
1464
  };
1465
+ if (options.speed !== void 0) req.speed = options.speed;
1466
+ if (options.steps !== void 0) req.steps = options.steps;
1420
1467
  ws.send(JSON.stringify(req));
1421
1468
  startTime = nowMs();
1422
1469
  };
1423
1470
  ws.onmessage = async (event) => {
1424
- refreshTimeout();
1425
1471
  if (event.data instanceof ArrayBuffer) {
1426
1472
  if (!firstByteReceived) {
1427
1473
  const ttfb = nowMs() - startTime;
1428
1474
  if (options.onTTFB) options.onTTFB(ttfb);
1429
1475
  firstByteReceived = true;
1430
1476
  }
1477
+ arm();
1431
1478
  if (options.onAudio) options.onAudio(new Uint8Array(event.data));
1432
1479
  return;
1433
1480
  }
1434
1481
  const text = event.data.toString();
1435
1482
  if (text === "EOS") {
1436
- if (activityTimeout) clearTimeout(activityTimeout);
1437
- ws.close();
1438
- resolve();
1483
+ finish();
1439
1484
  return;
1440
1485
  }
1486
+ let msg;
1441
1487
  try {
1442
- const msg = JSON.parse(text);
1443
- if (msg.type === "audio" && msg.data) {
1444
- const audioBuffer = base64ToUint8Array(msg.data);
1445
- if (!firstByteReceived) {
1446
- const ttfb = nowMs() - startTime;
1447
- if (options.onTTFB) options.onTTFB(ttfb);
1448
- firstByteReceived = true;
1449
- }
1450
- if (options.onAudio) options.onAudio(audioBuffer);
1451
- return;
1452
- }
1453
- if (msg.type === "visemes" && Array.isArray(msg.data) && options.onVisemes) {
1454
- options.onVisemes(normalizeVisemes(msg.data));
1455
- return;
1456
- }
1457
- if (Array.isArray(msg) && options.onVisemes) {
1458
- options.onVisemes(normalizeVisemes(msg));
1459
- return;
1460
- }
1461
- if (msg.type === "eos") {
1462
- if (activityTimeout) clearTimeout(activityTimeout);
1463
- ws.close();
1464
- resolve();
1488
+ msg = JSON.parse(text);
1489
+ } catch {
1490
+ return;
1491
+ }
1492
+ if (msg.type === "error") {
1493
+ const data = typeof msg.data === "object" && msg.data ? msg.data : { message: msg.data };
1494
+ finish(new LokutorError(data.code ?? "internal.error", data.message ?? "TTS failed", {
1495
+ detail: data.detail,
1496
+ retryable: data.retryable ?? false
1497
+ }));
1498
+ return;
1499
+ }
1500
+ if (msg.type === "audio" && msg.data) {
1501
+ const audioBuffer = base64ToUint8Array(msg.data);
1502
+ if (!firstByteReceived) {
1503
+ const ttfb = nowMs() - startTime;
1504
+ if (options.onTTFB) options.onTTFB(ttfb);
1505
+ firstByteReceived = true;
1465
1506
  }
1466
- } catch (e) {
1507
+ arm();
1508
+ if (options.onAudio) options.onAudio(audioBuffer);
1509
+ return;
1510
+ }
1511
+ if (msg.type === "visemes" && Array.isArray(msg.data) && options.onVisemes) {
1512
+ options.onVisemes(normalizeVisemes(msg.data));
1513
+ return;
1514
+ }
1515
+ if (Array.isArray(msg) && options.onVisemes) {
1516
+ options.onVisemes(normalizeVisemes(msg));
1517
+ return;
1518
+ }
1519
+ if (msg.type === "eos") {
1520
+ finish();
1467
1521
  }
1468
1522
  };
1469
- ws.onerror = (err) => {
1470
- if (activityTimeout) clearTimeout(activityTimeout);
1471
- if (options.onError) options.onError(err);
1472
- reject(err);
1473
- };
1474
- ws.onclose = () => {
1475
- if (activityTimeout) clearTimeout(activityTimeout);
1476
- resolve();
1523
+ ws.onerror = (err) => finish(err);
1524
+ ws.onclose = (ev) => {
1525
+ if (!finished) {
1526
+ if (firstByteReceived && ev.code === 1e3) finish();
1527
+ else finish(new LokutorError("internal.error", `TTS socket closed (${ev.code}${ev.reason ? `: ${ev.reason}` : ""})`, { retryable: true }));
1528
+ }
1477
1529
  };
1478
1530
  } catch (err) {
1479
- if (activityTimeout) clearTimeout(activityTimeout);
1480
- if (options.onError) options.onError(err);
1481
- reject(err);
1531
+ finish(err);
1482
1532
  }
1483
1533
  });
1484
1534
  }
@@ -1584,17 +1634,28 @@ var SpeechToTextClient = class {
1584
1634
  try {
1585
1635
  let url = this.serverUrl;
1586
1636
  const separator = url.includes("?") ? "&" : "?";
1587
- url += `${separator}api_key=${this.apiKey}`;
1637
+ url += `${separator}api_key=${encodeURIComponent(this.apiKey)}`;
1588
1638
  this.ws = new WebSocket(url);
1589
1639
  this.ws.onopen = async () => {
1590
1640
  this.isConnected = true;
1591
1641
  this.onStatusChange?.("connected");
1592
1642
  this.ws.send(JSON.stringify({ language: this.language || "en" /* ENGLISH */, vad: this.vad }));
1593
- await this.audioManager.startMicrophone((data) => {
1594
- if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
1595
- this.ws.send(data);
1643
+ try {
1644
+ await this.audioManager.startMicrophone((data) => {
1645
+ if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
1646
+ this.ws.send(data);
1647
+ }
1648
+ });
1649
+ } catch (err) {
1650
+ const error = err instanceof LokutorError ? err : new LokutorError("audio.microphone_unavailable", "Could not start the microphone", { original: err });
1651
+ this.onError?.(error);
1652
+ try {
1653
+ this.ws?.close();
1654
+ } catch {
1596
1655
  }
1597
- });
1656
+ settle(() => reject(error));
1657
+ return;
1658
+ }
1598
1659
  settle(() => resolve(true));
1599
1660
  };
1600
1661
  this.ws.onmessage = (event) => {
@@ -1608,7 +1669,11 @@ var SpeechToTextClient = class {
1608
1669
  this.onPartialTranscript?.(msg.data ?? "");
1609
1670
  }
1610
1671
  } else if (msg.type === "error") {
1611
- this.onError?.(new LokutorError("internal.error", msg.data ?? "STT error"));
1672
+ const data = typeof msg.data === "object" && msg.data ? msg.data : { message: msg.data };
1673
+ this.onError?.(new LokutorError(data.code ?? "internal.error", data.message ?? "STT error", {
1674
+ detail: data.detail,
1675
+ retryable: data.retryable ?? false
1676
+ }));
1612
1677
  }
1613
1678
  } catch {
1614
1679
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokutor/sdk",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "JavaScript/TypeScript SDK for Lokutor Real-time Voice AI",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",