@lokutor/sdk 1.1.35 → 1.1.37
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.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +71 -9
- package/dist/index.mjs +71 -9
- package/package.json +1 -1
- package/src/browser-audio.ts +4 -0
- package/src/client.ts +74 -12
package/dist/index.d.mts
CHANGED
|
@@ -236,10 +236,14 @@ declare class VoiceAgentClient {
|
|
|
236
236
|
private enableAudio;
|
|
237
237
|
private currentGeneration;
|
|
238
238
|
private listeners;
|
|
239
|
+
private configConfirmed;
|
|
239
240
|
private isUserDisconnect;
|
|
240
241
|
private reconnecting;
|
|
241
242
|
private reconnectAttempts;
|
|
242
243
|
private maxReconnectAttempts;
|
|
244
|
+
private reconnectTimer;
|
|
245
|
+
private serverPlaybackRate;
|
|
246
|
+
private serverInputRate;
|
|
243
247
|
private serverUrl;
|
|
244
248
|
constructor(config: LokutorConfig & {
|
|
245
249
|
prompt: string;
|
|
@@ -346,6 +350,18 @@ declare class VoiceAgentClient {
|
|
|
346
350
|
* Update the system prompt mid-conversation
|
|
347
351
|
*/
|
|
348
352
|
updatePrompt(newPrompt: string): void;
|
|
353
|
+
/**
|
|
354
|
+
* Change the voice style mid-conversation
|
|
355
|
+
*/
|
|
356
|
+
updateVoice(voice: VoiceStyle): void;
|
|
357
|
+
/**
|
|
358
|
+
* Change the language mid-conversation
|
|
359
|
+
*/
|
|
360
|
+
updateLanguage(language: Language): void;
|
|
361
|
+
/**
|
|
362
|
+
* Update tool definitions mid-conversation
|
|
363
|
+
*/
|
|
364
|
+
updateTools(tools: ToolDefinition[]): void;
|
|
349
365
|
/**
|
|
350
366
|
* Get full conversation transcript
|
|
351
367
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -236,10 +236,14 @@ declare class VoiceAgentClient {
|
|
|
236
236
|
private enableAudio;
|
|
237
237
|
private currentGeneration;
|
|
238
238
|
private listeners;
|
|
239
|
+
private configConfirmed;
|
|
239
240
|
private isUserDisconnect;
|
|
240
241
|
private reconnecting;
|
|
241
242
|
private reconnectAttempts;
|
|
242
243
|
private maxReconnectAttempts;
|
|
244
|
+
private reconnectTimer;
|
|
245
|
+
private serverPlaybackRate;
|
|
246
|
+
private serverInputRate;
|
|
243
247
|
private serverUrl;
|
|
244
248
|
constructor(config: LokutorConfig & {
|
|
245
249
|
prompt: string;
|
|
@@ -346,6 +350,18 @@ declare class VoiceAgentClient {
|
|
|
346
350
|
* Update the system prompt mid-conversation
|
|
347
351
|
*/
|
|
348
352
|
updatePrompt(newPrompt: string): void;
|
|
353
|
+
/**
|
|
354
|
+
* Change the voice style mid-conversation
|
|
355
|
+
*/
|
|
356
|
+
updateVoice(voice: VoiceStyle): void;
|
|
357
|
+
/**
|
|
358
|
+
* Change the language mid-conversation
|
|
359
|
+
*/
|
|
360
|
+
updateLanguage(language: Language): void;
|
|
361
|
+
/**
|
|
362
|
+
* Update tool definitions mid-conversation
|
|
363
|
+
*/
|
|
364
|
+
updateTools(tools: ToolDefinition[]): void;
|
|
349
365
|
/**
|
|
350
366
|
* Get full conversation transcript
|
|
351
367
|
*/
|
package/dist/index.js
CHANGED
|
@@ -408,6 +408,9 @@ var BrowserAudioManager = class {
|
|
|
408
408
|
console.warn("AudioContext not initialized");
|
|
409
409
|
return;
|
|
410
410
|
}
|
|
411
|
+
if (this.audioContext.state === "suspended") {
|
|
412
|
+
this.audioContext.resume();
|
|
413
|
+
}
|
|
411
414
|
if (pcm16Data.length % 2 !== 0) {
|
|
412
415
|
console.warn(`Discarding odd-length PCM buffer (${pcm16Data.length} bytes)`);
|
|
413
416
|
return;
|
|
@@ -656,11 +659,16 @@ var VoiceAgentClient = class {
|
|
|
656
659
|
enableAudio = false;
|
|
657
660
|
currentGeneration = 0;
|
|
658
661
|
listeners = {};
|
|
662
|
+
configConfirmed = false;
|
|
659
663
|
// Connection resilience
|
|
660
664
|
isUserDisconnect = false;
|
|
661
665
|
reconnecting = false;
|
|
662
666
|
reconnectAttempts = 0;
|
|
663
667
|
maxReconnectAttempts = 5;
|
|
668
|
+
reconnectTimer = null;
|
|
669
|
+
// Runtime state
|
|
670
|
+
serverPlaybackRate = 44100;
|
|
671
|
+
serverInputRate = 16e3;
|
|
664
672
|
serverUrl;
|
|
665
673
|
constructor(config) {
|
|
666
674
|
this.apiKey = config.apiKey;
|
|
@@ -759,6 +767,7 @@ var VoiceAgentClient = class {
|
|
|
759
767
|
};
|
|
760
768
|
this.ws.onclose = (event) => {
|
|
761
769
|
this.isConnected = false;
|
|
770
|
+
this.configConfirmed = false;
|
|
762
771
|
const diagnostic = {
|
|
763
772
|
code: event.code,
|
|
764
773
|
reason: event.reason,
|
|
@@ -787,12 +796,19 @@ var VoiceAgentClient = class {
|
|
|
787
796
|
const backoffDelay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 1e4);
|
|
788
797
|
console.warn(`Connection lost. Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
789
798
|
if (this.onStatus) this.onStatus("reconnecting");
|
|
790
|
-
|
|
791
|
-
|
|
799
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
800
|
+
this.reconnectTimer = setTimeout(() => {
|
|
801
|
+
this.reconnectTimer = null;
|
|
802
|
+
this.connect().then(() => {
|
|
803
|
+
this.emit("reconnected");
|
|
804
|
+
}).catch((e) => console.error("Reconnect failed", e));
|
|
792
805
|
}, backoffDelay);
|
|
793
|
-
} else {
|
|
806
|
+
} else if (this.isUserDisconnect) {
|
|
794
807
|
console.log("Disconnected");
|
|
795
808
|
if (this.onStatus) this.onStatus("disconnected");
|
|
809
|
+
} else {
|
|
810
|
+
console.log("Max reconnection attempts reached");
|
|
811
|
+
if (this.onStatus) this.onStatus("disconnected");
|
|
796
812
|
}
|
|
797
813
|
};
|
|
798
814
|
} catch (err) {
|
|
@@ -824,9 +840,13 @@ var VoiceAgentClient = class {
|
|
|
824
840
|
*/
|
|
825
841
|
sendConfig() {
|
|
826
842
|
if (!this.ws || !this.isConnected) return;
|
|
827
|
-
this.
|
|
843
|
+
this.configConfirmed = false;
|
|
828
844
|
this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
|
|
829
845
|
this.ws.send(JSON.stringify({ type: "language", data: this.language }));
|
|
846
|
+
if (this.tools && this.tools.length > 0) {
|
|
847
|
+
this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
|
|
848
|
+
}
|
|
849
|
+
this.ws.send(JSON.stringify({ type: "visemes", data: this.wantVisemes }));
|
|
830
850
|
this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
|
|
831
851
|
this.ws.send(JSON.stringify({ type: "rates", playback: 44100, input: 16e3 }));
|
|
832
852
|
sdkTrace("ws.send.config", {
|
|
@@ -836,9 +856,6 @@ var VoiceAgentClient = class {
|
|
|
836
856
|
visemes: this.wantVisemes,
|
|
837
857
|
tools: this.tools?.length || 0
|
|
838
858
|
});
|
|
839
|
-
if (this.tools && this.tools.length > 0) {
|
|
840
|
-
this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
|
|
841
|
-
}
|
|
842
859
|
console.log(`\u2699\uFE0F Configured: voice=${this.voice}, language=${this.language}, visemes=${this.wantVisemes}, tools=${this.tools.length}`);
|
|
843
860
|
}
|
|
844
861
|
/**
|
|
@@ -885,6 +902,11 @@ var VoiceAgentClient = class {
|
|
|
885
902
|
this.handleBinaryMessage(buffer, msg.generation);
|
|
886
903
|
}
|
|
887
904
|
break;
|
|
905
|
+
case "server_rates":
|
|
906
|
+
if (msg.playback) this.serverPlaybackRate = msg.playback;
|
|
907
|
+
if (msg.input) this.serverInputRate = msg.input;
|
|
908
|
+
sdkTrace("server_rates", { playback: this.serverPlaybackRate, input: this.serverInputRate });
|
|
909
|
+
break;
|
|
888
910
|
case "transcript":
|
|
889
911
|
const role = msg.role === "user" ? "user" : "agent";
|
|
890
912
|
this.messages.push({
|
|
@@ -903,12 +925,17 @@ var VoiceAgentClient = class {
|
|
|
903
925
|
}
|
|
904
926
|
break;
|
|
905
927
|
case "status":
|
|
928
|
+
if (msg.data === "prompt_set") {
|
|
929
|
+
this.configConfirmed = true;
|
|
930
|
+
}
|
|
906
931
|
if (msg.data === "thinking") {
|
|
907
932
|
const newGen = msg.generation || 0;
|
|
908
933
|
if (newGen > this.currentGeneration) {
|
|
909
|
-
console.log(`\u{1F9E0} New thought (Gen ${newGen})
|
|
934
|
+
console.log(`\u{1F9E0} New thought (Gen ${newGen})`);
|
|
910
935
|
this.currentGeneration = newGen;
|
|
911
|
-
if (this.audioManager)
|
|
936
|
+
if (this.audioManager) {
|
|
937
|
+
this.audioManager.stopPlayback();
|
|
938
|
+
}
|
|
912
939
|
}
|
|
913
940
|
}
|
|
914
941
|
if (msg.data === "interrupted" && this.audioManager) {
|
|
@@ -1019,6 +1046,10 @@ var VoiceAgentClient = class {
|
|
|
1019
1046
|
*/
|
|
1020
1047
|
disconnect() {
|
|
1021
1048
|
this.isUserDisconnect = true;
|
|
1049
|
+
if (this.reconnectTimer) {
|
|
1050
|
+
clearTimeout(this.reconnectTimer);
|
|
1051
|
+
this.reconnectTimer = null;
|
|
1052
|
+
}
|
|
1022
1053
|
if (this.ws) {
|
|
1023
1054
|
this.ws.close();
|
|
1024
1055
|
this.ws = null;
|
|
@@ -1027,6 +1058,7 @@ var VoiceAgentClient = class {
|
|
|
1027
1058
|
this.audioManager.cleanup();
|
|
1028
1059
|
}
|
|
1029
1060
|
this.isConnected = false;
|
|
1061
|
+
this.configConfirmed = false;
|
|
1030
1062
|
this.reconnecting = false;
|
|
1031
1063
|
this.reconnectAttempts = 0;
|
|
1032
1064
|
}
|
|
@@ -1133,6 +1165,36 @@ var VoiceAgentClient = class {
|
|
|
1133
1165
|
console.warn("Not connected - prompt will be updated on next connection");
|
|
1134
1166
|
}
|
|
1135
1167
|
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Change the voice style mid-conversation
|
|
1170
|
+
*/
|
|
1171
|
+
updateVoice(voice) {
|
|
1172
|
+
this.voice = voice;
|
|
1173
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
1174
|
+
this.ws.send(JSON.stringify({ type: "voice", data: voice }));
|
|
1175
|
+
console.log(`\u2699\uFE0F Updated voice: ${voice}`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Change the language mid-conversation
|
|
1180
|
+
*/
|
|
1181
|
+
updateLanguage(language) {
|
|
1182
|
+
this.language = language;
|
|
1183
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
1184
|
+
this.ws.send(JSON.stringify({ type: "language", data: language }));
|
|
1185
|
+
console.log(`\u2699\uFE0F Updated language: ${language}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Update tool definitions mid-conversation
|
|
1190
|
+
*/
|
|
1191
|
+
updateTools(tools) {
|
|
1192
|
+
this.tools = tools;
|
|
1193
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
1194
|
+
this.ws.send(JSON.stringify({ type: "tools", data: tools }));
|
|
1195
|
+
console.log(`\u2699\uFE0F Updated ${tools.length} tool(s)`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1136
1198
|
/**
|
|
1137
1199
|
* Get full conversation transcript
|
|
1138
1200
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -361,6 +361,9 @@ var BrowserAudioManager = class {
|
|
|
361
361
|
console.warn("AudioContext not initialized");
|
|
362
362
|
return;
|
|
363
363
|
}
|
|
364
|
+
if (this.audioContext.state === "suspended") {
|
|
365
|
+
this.audioContext.resume();
|
|
366
|
+
}
|
|
364
367
|
if (pcm16Data.length % 2 !== 0) {
|
|
365
368
|
console.warn(`Discarding odd-length PCM buffer (${pcm16Data.length} bytes)`);
|
|
366
369
|
return;
|
|
@@ -609,11 +612,16 @@ var VoiceAgentClient = class {
|
|
|
609
612
|
enableAudio = false;
|
|
610
613
|
currentGeneration = 0;
|
|
611
614
|
listeners = {};
|
|
615
|
+
configConfirmed = false;
|
|
612
616
|
// Connection resilience
|
|
613
617
|
isUserDisconnect = false;
|
|
614
618
|
reconnecting = false;
|
|
615
619
|
reconnectAttempts = 0;
|
|
616
620
|
maxReconnectAttempts = 5;
|
|
621
|
+
reconnectTimer = null;
|
|
622
|
+
// Runtime state
|
|
623
|
+
serverPlaybackRate = 44100;
|
|
624
|
+
serverInputRate = 16e3;
|
|
617
625
|
serverUrl;
|
|
618
626
|
constructor(config) {
|
|
619
627
|
this.apiKey = config.apiKey;
|
|
@@ -712,6 +720,7 @@ var VoiceAgentClient = class {
|
|
|
712
720
|
};
|
|
713
721
|
this.ws.onclose = (event) => {
|
|
714
722
|
this.isConnected = false;
|
|
723
|
+
this.configConfirmed = false;
|
|
715
724
|
const diagnostic = {
|
|
716
725
|
code: event.code,
|
|
717
726
|
reason: event.reason,
|
|
@@ -740,12 +749,19 @@ var VoiceAgentClient = class {
|
|
|
740
749
|
const backoffDelay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 1e4);
|
|
741
750
|
console.warn(`Connection lost. Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
742
751
|
if (this.onStatus) this.onStatus("reconnecting");
|
|
743
|
-
|
|
744
|
-
|
|
752
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
753
|
+
this.reconnectTimer = setTimeout(() => {
|
|
754
|
+
this.reconnectTimer = null;
|
|
755
|
+
this.connect().then(() => {
|
|
756
|
+
this.emit("reconnected");
|
|
757
|
+
}).catch((e) => console.error("Reconnect failed", e));
|
|
745
758
|
}, backoffDelay);
|
|
746
|
-
} else {
|
|
759
|
+
} else if (this.isUserDisconnect) {
|
|
747
760
|
console.log("Disconnected");
|
|
748
761
|
if (this.onStatus) this.onStatus("disconnected");
|
|
762
|
+
} else {
|
|
763
|
+
console.log("Max reconnection attempts reached");
|
|
764
|
+
if (this.onStatus) this.onStatus("disconnected");
|
|
749
765
|
}
|
|
750
766
|
};
|
|
751
767
|
} catch (err) {
|
|
@@ -777,9 +793,13 @@ var VoiceAgentClient = class {
|
|
|
777
793
|
*/
|
|
778
794
|
sendConfig() {
|
|
779
795
|
if (!this.ws || !this.isConnected) return;
|
|
780
|
-
this.
|
|
796
|
+
this.configConfirmed = false;
|
|
781
797
|
this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
|
|
782
798
|
this.ws.send(JSON.stringify({ type: "language", data: this.language }));
|
|
799
|
+
if (this.tools && this.tools.length > 0) {
|
|
800
|
+
this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
|
|
801
|
+
}
|
|
802
|
+
this.ws.send(JSON.stringify({ type: "visemes", data: this.wantVisemes }));
|
|
783
803
|
this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
|
|
784
804
|
this.ws.send(JSON.stringify({ type: "rates", playback: 44100, input: 16e3 }));
|
|
785
805
|
sdkTrace("ws.send.config", {
|
|
@@ -789,9 +809,6 @@ var VoiceAgentClient = class {
|
|
|
789
809
|
visemes: this.wantVisemes,
|
|
790
810
|
tools: this.tools?.length || 0
|
|
791
811
|
});
|
|
792
|
-
if (this.tools && this.tools.length > 0) {
|
|
793
|
-
this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
|
|
794
|
-
}
|
|
795
812
|
console.log(`\u2699\uFE0F Configured: voice=${this.voice}, language=${this.language}, visemes=${this.wantVisemes}, tools=${this.tools.length}`);
|
|
796
813
|
}
|
|
797
814
|
/**
|
|
@@ -838,6 +855,11 @@ var VoiceAgentClient = class {
|
|
|
838
855
|
this.handleBinaryMessage(buffer, msg.generation);
|
|
839
856
|
}
|
|
840
857
|
break;
|
|
858
|
+
case "server_rates":
|
|
859
|
+
if (msg.playback) this.serverPlaybackRate = msg.playback;
|
|
860
|
+
if (msg.input) this.serverInputRate = msg.input;
|
|
861
|
+
sdkTrace("server_rates", { playback: this.serverPlaybackRate, input: this.serverInputRate });
|
|
862
|
+
break;
|
|
841
863
|
case "transcript":
|
|
842
864
|
const role = msg.role === "user" ? "user" : "agent";
|
|
843
865
|
this.messages.push({
|
|
@@ -856,12 +878,17 @@ var VoiceAgentClient = class {
|
|
|
856
878
|
}
|
|
857
879
|
break;
|
|
858
880
|
case "status":
|
|
881
|
+
if (msg.data === "prompt_set") {
|
|
882
|
+
this.configConfirmed = true;
|
|
883
|
+
}
|
|
859
884
|
if (msg.data === "thinking") {
|
|
860
885
|
const newGen = msg.generation || 0;
|
|
861
886
|
if (newGen > this.currentGeneration) {
|
|
862
|
-
console.log(`\u{1F9E0} New thought (Gen ${newGen})
|
|
887
|
+
console.log(`\u{1F9E0} New thought (Gen ${newGen})`);
|
|
863
888
|
this.currentGeneration = newGen;
|
|
864
|
-
if (this.audioManager)
|
|
889
|
+
if (this.audioManager) {
|
|
890
|
+
this.audioManager.stopPlayback();
|
|
891
|
+
}
|
|
865
892
|
}
|
|
866
893
|
}
|
|
867
894
|
if (msg.data === "interrupted" && this.audioManager) {
|
|
@@ -972,6 +999,10 @@ var VoiceAgentClient = class {
|
|
|
972
999
|
*/
|
|
973
1000
|
disconnect() {
|
|
974
1001
|
this.isUserDisconnect = true;
|
|
1002
|
+
if (this.reconnectTimer) {
|
|
1003
|
+
clearTimeout(this.reconnectTimer);
|
|
1004
|
+
this.reconnectTimer = null;
|
|
1005
|
+
}
|
|
975
1006
|
if (this.ws) {
|
|
976
1007
|
this.ws.close();
|
|
977
1008
|
this.ws = null;
|
|
@@ -980,6 +1011,7 @@ var VoiceAgentClient = class {
|
|
|
980
1011
|
this.audioManager.cleanup();
|
|
981
1012
|
}
|
|
982
1013
|
this.isConnected = false;
|
|
1014
|
+
this.configConfirmed = false;
|
|
983
1015
|
this.reconnecting = false;
|
|
984
1016
|
this.reconnectAttempts = 0;
|
|
985
1017
|
}
|
|
@@ -1086,6 +1118,36 @@ var VoiceAgentClient = class {
|
|
|
1086
1118
|
console.warn("Not connected - prompt will be updated on next connection");
|
|
1087
1119
|
}
|
|
1088
1120
|
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Change the voice style mid-conversation
|
|
1123
|
+
*/
|
|
1124
|
+
updateVoice(voice) {
|
|
1125
|
+
this.voice = voice;
|
|
1126
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
1127
|
+
this.ws.send(JSON.stringify({ type: "voice", data: voice }));
|
|
1128
|
+
console.log(`\u2699\uFE0F Updated voice: ${voice}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Change the language mid-conversation
|
|
1133
|
+
*/
|
|
1134
|
+
updateLanguage(language) {
|
|
1135
|
+
this.language = language;
|
|
1136
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
1137
|
+
this.ws.send(JSON.stringify({ type: "language", data: language }));
|
|
1138
|
+
console.log(`\u2699\uFE0F Updated language: ${language}`);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Update tool definitions mid-conversation
|
|
1143
|
+
*/
|
|
1144
|
+
updateTools(tools) {
|
|
1145
|
+
this.tools = tools;
|
|
1146
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
1147
|
+
this.ws.send(JSON.stringify({ type: "tools", data: tools }));
|
|
1148
|
+
console.log(`\u2699\uFE0F Updated ${tools.length} tool(s)`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1089
1151
|
/**
|
|
1090
1152
|
* Get full conversation transcript
|
|
1091
1153
|
*/
|
package/package.json
CHANGED
package/src/browser-audio.ts
CHANGED
|
@@ -230,6 +230,10 @@ export class BrowserAudioManager {
|
|
|
230
230
|
return;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
if (this.audioContext.state === 'suspended') {
|
|
234
|
+
this.audioContext.resume();
|
|
235
|
+
}
|
|
236
|
+
|
|
233
237
|
if (pcm16Data.length % 2 !== 0) {
|
|
234
238
|
console.warn(`Discarding odd-length PCM buffer (${pcm16Data.length} bytes)`);
|
|
235
239
|
return;
|
package/src/client.ts
CHANGED
|
@@ -166,12 +166,18 @@ export class VoiceAgentClient {
|
|
|
166
166
|
private enableAudio: boolean = false;
|
|
167
167
|
private currentGeneration: number = 0;
|
|
168
168
|
private listeners: Record<string, Function[]> = {};
|
|
169
|
+
private configConfirmed: boolean = false;
|
|
169
170
|
|
|
170
171
|
// Connection resilience
|
|
171
172
|
private isUserDisconnect: boolean = false;
|
|
172
173
|
private reconnecting: boolean = false;
|
|
173
174
|
private reconnectAttempts: number = 0;
|
|
174
175
|
private maxReconnectAttempts: number = 5;
|
|
176
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
177
|
+
|
|
178
|
+
// Runtime state
|
|
179
|
+
private serverPlaybackRate: number = 44100;
|
|
180
|
+
private serverInputRate: number = 16000;
|
|
175
181
|
|
|
176
182
|
private serverUrl: string;
|
|
177
183
|
|
|
@@ -296,6 +302,7 @@ export class VoiceAgentClient {
|
|
|
296
302
|
|
|
297
303
|
this.ws.onclose = (event) => {
|
|
298
304
|
this.isConnected = false;
|
|
305
|
+
this.configConfirmed = false;
|
|
299
306
|
const diagnostic = {
|
|
300
307
|
code: event.code,
|
|
301
308
|
reason: event.reason,
|
|
@@ -331,12 +338,19 @@ export class VoiceAgentClient {
|
|
|
331
338
|
|
|
332
339
|
if (this.onStatus) this.onStatus('reconnecting');
|
|
333
340
|
|
|
334
|
-
|
|
335
|
-
|
|
341
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
342
|
+
this.reconnectTimer = setTimeout(() => {
|
|
343
|
+
this.reconnectTimer = null;
|
|
344
|
+
this.connect().then(() => {
|
|
345
|
+
this.emit('reconnected');
|
|
346
|
+
}).catch(e => console.error("Reconnect failed", e));
|
|
336
347
|
}, backoffDelay);
|
|
337
|
-
} else {
|
|
348
|
+
} else if (this.isUserDisconnect) {
|
|
338
349
|
console.log('Disconnected');
|
|
339
350
|
if (this.onStatus) this.onStatus('disconnected');
|
|
351
|
+
} else {
|
|
352
|
+
console.log('Max reconnection attempts reached');
|
|
353
|
+
if (this.onStatus) this.onStatus('disconnected');
|
|
340
354
|
}
|
|
341
355
|
};
|
|
342
356
|
|
|
@@ -374,14 +388,18 @@ export class VoiceAgentClient {
|
|
|
374
388
|
private sendConfig() {
|
|
375
389
|
if (!this.ws || !this.isConnected) return;
|
|
376
390
|
|
|
377
|
-
|
|
378
|
-
|
|
391
|
+
this.configConfirmed = false;
|
|
392
|
+
|
|
393
|
+
// Send feature/config flags — order matches the test agent for consistency.
|
|
379
394
|
this.ws.send(JSON.stringify({ type: 'voice', data: this.voice }));
|
|
380
395
|
this.ws.send(JSON.stringify({ type: 'language', data: this.language }));
|
|
396
|
+
if (this.tools && this.tools.length > 0) {
|
|
397
|
+
this.ws.send(JSON.stringify({ type: 'tools', data: this.tools }));
|
|
398
|
+
}
|
|
399
|
+
this.ws.send(JSON.stringify({ type: 'visemes', data: this.wantVisemes }));
|
|
381
400
|
this.ws.send(JSON.stringify({ type: 'prompt', data: this.prompt }));
|
|
382
401
|
|
|
383
402
|
// Inform the server of our sample rates for echo cancellation.
|
|
384
|
-
// These match the SDK defaults: 16 kHz mic input, 44.1 kHz speaker output.
|
|
385
403
|
this.ws.send(JSON.stringify({ type: 'rates', playback: 44100, input: 16000 }));
|
|
386
404
|
|
|
387
405
|
sdkTrace('ws.send.config', {
|
|
@@ -392,10 +410,6 @@ export class VoiceAgentClient {
|
|
|
392
410
|
tools: this.tools?.length || 0
|
|
393
411
|
});
|
|
394
412
|
|
|
395
|
-
if (this.tools && this.tools.length > 0) {
|
|
396
|
-
this.ws.send(JSON.stringify({ type: 'tools', data: this.tools }));
|
|
397
|
-
}
|
|
398
|
-
|
|
399
413
|
console.log(`⚙️ Configured: voice=${this.voice}, language=${this.language}, visemes=${this.wantVisemes}, tools=${this.tools.length}`);
|
|
400
414
|
}
|
|
401
415
|
|
|
@@ -445,6 +459,11 @@ export class VoiceAgentClient {
|
|
|
445
459
|
this.handleBinaryMessage(buffer, msg.generation);
|
|
446
460
|
}
|
|
447
461
|
break;
|
|
462
|
+
case 'server_rates':
|
|
463
|
+
if (msg.playback) this.serverPlaybackRate = msg.playback;
|
|
464
|
+
if (msg.input) this.serverInputRate = msg.input;
|
|
465
|
+
sdkTrace('server_rates', { playback: this.serverPlaybackRate, input: this.serverInputRate });
|
|
466
|
+
break;
|
|
448
467
|
case 'transcript':
|
|
449
468
|
const role = msg.role === 'user' ? 'user' : 'agent';
|
|
450
469
|
// Store in history
|
|
@@ -465,12 +484,17 @@ export class VoiceAgentClient {
|
|
|
465
484
|
}
|
|
466
485
|
break;
|
|
467
486
|
case 'status':
|
|
487
|
+
if (msg.data === 'prompt_set') {
|
|
488
|
+
this.configConfirmed = true;
|
|
489
|
+
}
|
|
468
490
|
if (msg.data === 'thinking') {
|
|
469
491
|
const newGen = msg.generation || 0;
|
|
470
492
|
if (newGen > this.currentGeneration) {
|
|
471
|
-
console.log(`🧠 New thought (Gen ${newGen})
|
|
493
|
+
console.log(`🧠 New thought (Gen ${newGen})`);
|
|
472
494
|
this.currentGeneration = newGen;
|
|
473
|
-
if (this.audioManager)
|
|
495
|
+
if (this.audioManager) {
|
|
496
|
+
this.audioManager.stopPlayback();
|
|
497
|
+
}
|
|
474
498
|
}
|
|
475
499
|
}
|
|
476
500
|
if (msg.data === 'interrupted' && this.audioManager) {
|
|
@@ -590,6 +614,10 @@ export class VoiceAgentClient {
|
|
|
590
614
|
*/
|
|
591
615
|
public disconnect() {
|
|
592
616
|
this.isUserDisconnect = true;
|
|
617
|
+
if (this.reconnectTimer) {
|
|
618
|
+
clearTimeout(this.reconnectTimer);
|
|
619
|
+
this.reconnectTimer = null;
|
|
620
|
+
}
|
|
593
621
|
if (this.ws) {
|
|
594
622
|
this.ws.close();
|
|
595
623
|
this.ws = null;
|
|
@@ -598,6 +626,7 @@ export class VoiceAgentClient {
|
|
|
598
626
|
this.audioManager.cleanup();
|
|
599
627
|
}
|
|
600
628
|
this.isConnected = false;
|
|
629
|
+
this.configConfirmed = false;
|
|
601
630
|
this.reconnecting = false;
|
|
602
631
|
this.reconnectAttempts = 0;
|
|
603
632
|
}
|
|
@@ -716,6 +745,39 @@ export class VoiceAgentClient {
|
|
|
716
745
|
}
|
|
717
746
|
}
|
|
718
747
|
|
|
748
|
+
/**
|
|
749
|
+
* Change the voice style mid-conversation
|
|
750
|
+
*/
|
|
751
|
+
public updateVoice(voice: VoiceStyle) {
|
|
752
|
+
this.voice = voice;
|
|
753
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
754
|
+
this.ws.send(JSON.stringify({ type: 'voice', data: voice }));
|
|
755
|
+
console.log(`⚙️ Updated voice: ${voice}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Change the language mid-conversation
|
|
761
|
+
*/
|
|
762
|
+
public updateLanguage(language: Language) {
|
|
763
|
+
this.language = language;
|
|
764
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
765
|
+
this.ws.send(JSON.stringify({ type: 'language', data: language }));
|
|
766
|
+
console.log(`⚙️ Updated language: ${language}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Update tool definitions mid-conversation
|
|
772
|
+
*/
|
|
773
|
+
public updateTools(tools: ToolDefinition[]) {
|
|
774
|
+
this.tools = tools;
|
|
775
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
776
|
+
this.ws.send(JSON.stringify({ type: 'tools', data: tools }));
|
|
777
|
+
console.log(`⚙️ Updated ${tools.length} tool(s)`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
719
781
|
/**
|
|
720
782
|
* Get full conversation transcript
|
|
721
783
|
*/
|