@lokutor/sdk 1.1.36 → 1.1.39
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 +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +77 -13
- package/dist/index.mjs +77 -13
- package/package.json +1 -1
- package/src/client.ts +77 -14
- package/src/conversational-panel.ts +11 -8
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
|
*/
|
|
@@ -671,6 +687,7 @@ declare class ConversationalPanel {
|
|
|
671
687
|
showError(text: string): void;
|
|
672
688
|
/** Destroy the component, removing all DOM and stopping any active session */
|
|
673
689
|
destroy(): void;
|
|
690
|
+
private fmt;
|
|
674
691
|
private startTimer;
|
|
675
692
|
private toggleMute;
|
|
676
693
|
private playConnectingSound;
|
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
|
*/
|
|
@@ -671,6 +687,7 @@ declare class ConversationalPanel {
|
|
|
671
687
|
showError(text: string): void;
|
|
672
688
|
/** Destroy the component, removing all DOM and stopping any active session */
|
|
673
689
|
destroy(): void;
|
|
690
|
+
private fmt;
|
|
674
691
|
private startTimer;
|
|
675
692
|
private toggleMute;
|
|
676
693
|
private playConnectingSound;
|
package/dist/index.js
CHANGED
|
@@ -659,11 +659,16 @@ var VoiceAgentClient = class {
|
|
|
659
659
|
enableAudio = false;
|
|
660
660
|
currentGeneration = 0;
|
|
661
661
|
listeners = {};
|
|
662
|
+
configConfirmed = false;
|
|
662
663
|
// Connection resilience
|
|
663
664
|
isUserDisconnect = false;
|
|
664
665
|
reconnecting = false;
|
|
665
666
|
reconnectAttempts = 0;
|
|
666
667
|
maxReconnectAttempts = 5;
|
|
668
|
+
reconnectTimer = null;
|
|
669
|
+
// Runtime state
|
|
670
|
+
serverPlaybackRate = 44100;
|
|
671
|
+
serverInputRate = 16e3;
|
|
667
672
|
serverUrl;
|
|
668
673
|
constructor(config) {
|
|
669
674
|
this.apiKey = config.apiKey;
|
|
@@ -762,6 +767,7 @@ var VoiceAgentClient = class {
|
|
|
762
767
|
};
|
|
763
768
|
this.ws.onclose = (event) => {
|
|
764
769
|
this.isConnected = false;
|
|
770
|
+
this.configConfirmed = false;
|
|
765
771
|
const diagnostic = {
|
|
766
772
|
code: event.code,
|
|
767
773
|
reason: event.reason,
|
|
@@ -790,12 +796,19 @@ var VoiceAgentClient = class {
|
|
|
790
796
|
const backoffDelay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 1e4);
|
|
791
797
|
console.warn(`Connection lost. Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
792
798
|
if (this.onStatus) this.onStatus("reconnecting");
|
|
793
|
-
|
|
794
|
-
|
|
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));
|
|
795
805
|
}, backoffDelay);
|
|
796
|
-
} else {
|
|
806
|
+
} else if (this.isUserDisconnect) {
|
|
797
807
|
console.log("Disconnected");
|
|
798
808
|
if (this.onStatus) this.onStatus("disconnected");
|
|
809
|
+
} else {
|
|
810
|
+
console.log("Max reconnection attempts reached");
|
|
811
|
+
if (this.onStatus) this.onStatus("disconnected");
|
|
799
812
|
}
|
|
800
813
|
};
|
|
801
814
|
} catch (err) {
|
|
@@ -827,9 +840,13 @@ var VoiceAgentClient = class {
|
|
|
827
840
|
*/
|
|
828
841
|
sendConfig() {
|
|
829
842
|
if (!this.ws || !this.isConnected) return;
|
|
830
|
-
this.
|
|
843
|
+
this.configConfirmed = false;
|
|
831
844
|
this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
|
|
832
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 }));
|
|
833
850
|
this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
|
|
834
851
|
this.ws.send(JSON.stringify({ type: "rates", playback: 44100, input: 16e3 }));
|
|
835
852
|
sdkTrace("ws.send.config", {
|
|
@@ -839,9 +856,6 @@ var VoiceAgentClient = class {
|
|
|
839
856
|
visemes: this.wantVisemes,
|
|
840
857
|
tools: this.tools?.length || 0
|
|
841
858
|
});
|
|
842
|
-
if (this.tools && this.tools.length > 0) {
|
|
843
|
-
this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
|
|
844
|
-
}
|
|
845
859
|
console.log(`\u2699\uFE0F Configured: voice=${this.voice}, language=${this.language}, visemes=${this.wantVisemes}, tools=${this.tools.length}`);
|
|
846
860
|
}
|
|
847
861
|
/**
|
|
@@ -888,6 +902,11 @@ var VoiceAgentClient = class {
|
|
|
888
902
|
this.handleBinaryMessage(buffer, msg.generation);
|
|
889
903
|
}
|
|
890
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;
|
|
891
910
|
case "transcript":
|
|
892
911
|
const role = msg.role === "user" ? "user" : "agent";
|
|
893
912
|
this.messages.push({
|
|
@@ -906,11 +925,17 @@ var VoiceAgentClient = class {
|
|
|
906
925
|
}
|
|
907
926
|
break;
|
|
908
927
|
case "status":
|
|
928
|
+
if (msg.data === "prompt_set") {
|
|
929
|
+
this.configConfirmed = true;
|
|
930
|
+
}
|
|
909
931
|
if (msg.data === "thinking") {
|
|
910
932
|
const newGen = msg.generation || 0;
|
|
911
933
|
if (newGen > this.currentGeneration) {
|
|
912
934
|
console.log(`\u{1F9E0} New thought (Gen ${newGen})`);
|
|
913
935
|
this.currentGeneration = newGen;
|
|
936
|
+
if (this.audioManager) {
|
|
937
|
+
this.audioManager.stopPlayback();
|
|
938
|
+
}
|
|
914
939
|
}
|
|
915
940
|
}
|
|
916
941
|
if (msg.data === "interrupted" && this.audioManager) {
|
|
@@ -1021,6 +1046,10 @@ var VoiceAgentClient = class {
|
|
|
1021
1046
|
*/
|
|
1022
1047
|
disconnect() {
|
|
1023
1048
|
this.isUserDisconnect = true;
|
|
1049
|
+
if (this.reconnectTimer) {
|
|
1050
|
+
clearTimeout(this.reconnectTimer);
|
|
1051
|
+
this.reconnectTimer = null;
|
|
1052
|
+
}
|
|
1024
1053
|
if (this.ws) {
|
|
1025
1054
|
this.ws.close();
|
|
1026
1055
|
this.ws = null;
|
|
@@ -1029,6 +1058,7 @@ var VoiceAgentClient = class {
|
|
|
1029
1058
|
this.audioManager.cleanup();
|
|
1030
1059
|
}
|
|
1031
1060
|
this.isConnected = false;
|
|
1061
|
+
this.configConfirmed = false;
|
|
1032
1062
|
this.reconnecting = false;
|
|
1033
1063
|
this.reconnectAttempts = 0;
|
|
1034
1064
|
}
|
|
@@ -1135,6 +1165,36 @@ var VoiceAgentClient = class {
|
|
|
1135
1165
|
console.warn("Not connected - prompt will be updated on next connection");
|
|
1136
1166
|
}
|
|
1137
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
|
+
}
|
|
1138
1198
|
/**
|
|
1139
1199
|
* Get full conversation transcript
|
|
1140
1200
|
*/
|
|
@@ -1809,27 +1869,31 @@ var ConversationalPanel = class {
|
|
|
1809
1869
|
this.el.remove();
|
|
1810
1870
|
}
|
|
1811
1871
|
// ─── internal helpers ────────────────────────────────────
|
|
1872
|
+
fmt(s) {
|
|
1873
|
+
const m = Math.floor(s / 60).toString().padStart(2, "0");
|
|
1874
|
+
const sec = (s % 60).toString().padStart(2, "0");
|
|
1875
|
+
return `${m}:${sec}`;
|
|
1876
|
+
}
|
|
1812
1877
|
startTimer() {
|
|
1813
1878
|
let elapsed = 0;
|
|
1814
1879
|
const maxDur = this.cfg.maxDuration || 300;
|
|
1815
1880
|
const silentMax = this.cfg.silenceTimeout || 30;
|
|
1816
1881
|
this._lastSpeechTime = Date.now();
|
|
1817
|
-
this.timerEl.textContent =
|
|
1882
|
+
this.timerEl.textContent = this.fmt(maxDur);
|
|
1818
1883
|
this.timerTicker = window.setInterval(() => {
|
|
1819
1884
|
elapsed++;
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
this.timerEl.textContent = `${m}:${s}`;
|
|
1885
|
+
const remaining = Math.max(0, maxDur - elapsed);
|
|
1886
|
+
this.timerEl.textContent = this.fmt(remaining);
|
|
1823
1887
|
if (elapsed >= maxDur) {
|
|
1824
1888
|
this._locked = true;
|
|
1825
1889
|
this.stop();
|
|
1826
|
-
this.showError("
|
|
1890
|
+
this.showError("Tiempo l\xEDmite alcanzado.");
|
|
1827
1891
|
return;
|
|
1828
1892
|
}
|
|
1829
1893
|
if (Date.now() - this._lastSpeechTime > silentMax * 1e3) {
|
|
1830
1894
|
this._locked = true;
|
|
1831
1895
|
this.stop();
|
|
1832
|
-
this.showError("
|
|
1896
|
+
this.showError("Sesi\xF3n finalizada por inactividad.");
|
|
1833
1897
|
}
|
|
1834
1898
|
}, 1e3);
|
|
1835
1899
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -612,11 +612,16 @@ var VoiceAgentClient = class {
|
|
|
612
612
|
enableAudio = false;
|
|
613
613
|
currentGeneration = 0;
|
|
614
614
|
listeners = {};
|
|
615
|
+
configConfirmed = false;
|
|
615
616
|
// Connection resilience
|
|
616
617
|
isUserDisconnect = false;
|
|
617
618
|
reconnecting = false;
|
|
618
619
|
reconnectAttempts = 0;
|
|
619
620
|
maxReconnectAttempts = 5;
|
|
621
|
+
reconnectTimer = null;
|
|
622
|
+
// Runtime state
|
|
623
|
+
serverPlaybackRate = 44100;
|
|
624
|
+
serverInputRate = 16e3;
|
|
620
625
|
serverUrl;
|
|
621
626
|
constructor(config) {
|
|
622
627
|
this.apiKey = config.apiKey;
|
|
@@ -715,6 +720,7 @@ var VoiceAgentClient = class {
|
|
|
715
720
|
};
|
|
716
721
|
this.ws.onclose = (event) => {
|
|
717
722
|
this.isConnected = false;
|
|
723
|
+
this.configConfirmed = false;
|
|
718
724
|
const diagnostic = {
|
|
719
725
|
code: event.code,
|
|
720
726
|
reason: event.reason,
|
|
@@ -743,12 +749,19 @@ var VoiceAgentClient = class {
|
|
|
743
749
|
const backoffDelay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 1e4);
|
|
744
750
|
console.warn(`Connection lost. Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
745
751
|
if (this.onStatus) this.onStatus("reconnecting");
|
|
746
|
-
|
|
747
|
-
|
|
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));
|
|
748
758
|
}, backoffDelay);
|
|
749
|
-
} else {
|
|
759
|
+
} else if (this.isUserDisconnect) {
|
|
750
760
|
console.log("Disconnected");
|
|
751
761
|
if (this.onStatus) this.onStatus("disconnected");
|
|
762
|
+
} else {
|
|
763
|
+
console.log("Max reconnection attempts reached");
|
|
764
|
+
if (this.onStatus) this.onStatus("disconnected");
|
|
752
765
|
}
|
|
753
766
|
};
|
|
754
767
|
} catch (err) {
|
|
@@ -780,9 +793,13 @@ var VoiceAgentClient = class {
|
|
|
780
793
|
*/
|
|
781
794
|
sendConfig() {
|
|
782
795
|
if (!this.ws || !this.isConnected) return;
|
|
783
|
-
this.
|
|
796
|
+
this.configConfirmed = false;
|
|
784
797
|
this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
|
|
785
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 }));
|
|
786
803
|
this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
|
|
787
804
|
this.ws.send(JSON.stringify({ type: "rates", playback: 44100, input: 16e3 }));
|
|
788
805
|
sdkTrace("ws.send.config", {
|
|
@@ -792,9 +809,6 @@ var VoiceAgentClient = class {
|
|
|
792
809
|
visemes: this.wantVisemes,
|
|
793
810
|
tools: this.tools?.length || 0
|
|
794
811
|
});
|
|
795
|
-
if (this.tools && this.tools.length > 0) {
|
|
796
|
-
this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
|
|
797
|
-
}
|
|
798
812
|
console.log(`\u2699\uFE0F Configured: voice=${this.voice}, language=${this.language}, visemes=${this.wantVisemes}, tools=${this.tools.length}`);
|
|
799
813
|
}
|
|
800
814
|
/**
|
|
@@ -841,6 +855,11 @@ var VoiceAgentClient = class {
|
|
|
841
855
|
this.handleBinaryMessage(buffer, msg.generation);
|
|
842
856
|
}
|
|
843
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;
|
|
844
863
|
case "transcript":
|
|
845
864
|
const role = msg.role === "user" ? "user" : "agent";
|
|
846
865
|
this.messages.push({
|
|
@@ -859,11 +878,17 @@ var VoiceAgentClient = class {
|
|
|
859
878
|
}
|
|
860
879
|
break;
|
|
861
880
|
case "status":
|
|
881
|
+
if (msg.data === "prompt_set") {
|
|
882
|
+
this.configConfirmed = true;
|
|
883
|
+
}
|
|
862
884
|
if (msg.data === "thinking") {
|
|
863
885
|
const newGen = msg.generation || 0;
|
|
864
886
|
if (newGen > this.currentGeneration) {
|
|
865
887
|
console.log(`\u{1F9E0} New thought (Gen ${newGen})`);
|
|
866
888
|
this.currentGeneration = newGen;
|
|
889
|
+
if (this.audioManager) {
|
|
890
|
+
this.audioManager.stopPlayback();
|
|
891
|
+
}
|
|
867
892
|
}
|
|
868
893
|
}
|
|
869
894
|
if (msg.data === "interrupted" && this.audioManager) {
|
|
@@ -974,6 +999,10 @@ var VoiceAgentClient = class {
|
|
|
974
999
|
*/
|
|
975
1000
|
disconnect() {
|
|
976
1001
|
this.isUserDisconnect = true;
|
|
1002
|
+
if (this.reconnectTimer) {
|
|
1003
|
+
clearTimeout(this.reconnectTimer);
|
|
1004
|
+
this.reconnectTimer = null;
|
|
1005
|
+
}
|
|
977
1006
|
if (this.ws) {
|
|
978
1007
|
this.ws.close();
|
|
979
1008
|
this.ws = null;
|
|
@@ -982,6 +1011,7 @@ var VoiceAgentClient = class {
|
|
|
982
1011
|
this.audioManager.cleanup();
|
|
983
1012
|
}
|
|
984
1013
|
this.isConnected = false;
|
|
1014
|
+
this.configConfirmed = false;
|
|
985
1015
|
this.reconnecting = false;
|
|
986
1016
|
this.reconnectAttempts = 0;
|
|
987
1017
|
}
|
|
@@ -1088,6 +1118,36 @@ var VoiceAgentClient = class {
|
|
|
1088
1118
|
console.warn("Not connected - prompt will be updated on next connection");
|
|
1089
1119
|
}
|
|
1090
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
|
+
}
|
|
1091
1151
|
/**
|
|
1092
1152
|
* Get full conversation transcript
|
|
1093
1153
|
*/
|
|
@@ -1762,27 +1822,31 @@ var ConversationalPanel = class {
|
|
|
1762
1822
|
this.el.remove();
|
|
1763
1823
|
}
|
|
1764
1824
|
// ─── internal helpers ────────────────────────────────────
|
|
1825
|
+
fmt(s) {
|
|
1826
|
+
const m = Math.floor(s / 60).toString().padStart(2, "0");
|
|
1827
|
+
const sec = (s % 60).toString().padStart(2, "0");
|
|
1828
|
+
return `${m}:${sec}`;
|
|
1829
|
+
}
|
|
1765
1830
|
startTimer() {
|
|
1766
1831
|
let elapsed = 0;
|
|
1767
1832
|
const maxDur = this.cfg.maxDuration || 300;
|
|
1768
1833
|
const silentMax = this.cfg.silenceTimeout || 30;
|
|
1769
1834
|
this._lastSpeechTime = Date.now();
|
|
1770
|
-
this.timerEl.textContent =
|
|
1835
|
+
this.timerEl.textContent = this.fmt(maxDur);
|
|
1771
1836
|
this.timerTicker = window.setInterval(() => {
|
|
1772
1837
|
elapsed++;
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
this.timerEl.textContent = `${m}:${s}`;
|
|
1838
|
+
const remaining = Math.max(0, maxDur - elapsed);
|
|
1839
|
+
this.timerEl.textContent = this.fmt(remaining);
|
|
1776
1840
|
if (elapsed >= maxDur) {
|
|
1777
1841
|
this._locked = true;
|
|
1778
1842
|
this.stop();
|
|
1779
|
-
this.showError("
|
|
1843
|
+
this.showError("Tiempo l\xEDmite alcanzado.");
|
|
1780
1844
|
return;
|
|
1781
1845
|
}
|
|
1782
1846
|
if (Date.now() - this._lastSpeechTime > silentMax * 1e3) {
|
|
1783
1847
|
this._locked = true;
|
|
1784
1848
|
this.stop();
|
|
1785
|
-
this.showError("
|
|
1849
|
+
this.showError("Sesi\xF3n finalizada por inactividad.");
|
|
1786
1850
|
}
|
|
1787
1851
|
}, 1e3);
|
|
1788
1852
|
}
|
package/package.json
CHANGED
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,18 @@ 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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
492
|
+
if (newGen > this.currentGeneration) {
|
|
493
|
+
console.log(`🧠 New thought (Gen ${newGen})`);
|
|
494
|
+
this.currentGeneration = newGen;
|
|
495
|
+
if (this.audioManager) {
|
|
496
|
+
this.audioManager.stopPlayback();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
474
499
|
}
|
|
475
500
|
if (msg.data === 'interrupted' && this.audioManager) {
|
|
476
501
|
this.audioManager.stopPlayback();
|
|
@@ -589,6 +614,10 @@ export class VoiceAgentClient {
|
|
|
589
614
|
*/
|
|
590
615
|
public disconnect() {
|
|
591
616
|
this.isUserDisconnect = true;
|
|
617
|
+
if (this.reconnectTimer) {
|
|
618
|
+
clearTimeout(this.reconnectTimer);
|
|
619
|
+
this.reconnectTimer = null;
|
|
620
|
+
}
|
|
592
621
|
if (this.ws) {
|
|
593
622
|
this.ws.close();
|
|
594
623
|
this.ws = null;
|
|
@@ -597,6 +626,7 @@ export class VoiceAgentClient {
|
|
|
597
626
|
this.audioManager.cleanup();
|
|
598
627
|
}
|
|
599
628
|
this.isConnected = false;
|
|
629
|
+
this.configConfirmed = false;
|
|
600
630
|
this.reconnecting = false;
|
|
601
631
|
this.reconnectAttempts = 0;
|
|
602
632
|
}
|
|
@@ -715,6 +745,39 @@ export class VoiceAgentClient {
|
|
|
715
745
|
}
|
|
716
746
|
}
|
|
717
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
|
+
|
|
718
781
|
/**
|
|
719
782
|
* Get full conversation transcript
|
|
720
783
|
*/
|
|
@@ -601,31 +601,34 @@ export class ConversationalPanel {
|
|
|
601
601
|
|
|
602
602
|
// ─── internal helpers ────────────────────────────────────
|
|
603
603
|
|
|
604
|
+
private fmt(s: number): string {
|
|
605
|
+
const m = Math.floor(s / 60).toString().padStart(2, '0');
|
|
606
|
+
const sec = (s % 60).toString().padStart(2, '0');
|
|
607
|
+
return `${m}:${sec}`;
|
|
608
|
+
}
|
|
609
|
+
|
|
604
610
|
private startTimer() {
|
|
605
611
|
let elapsed = 0;
|
|
606
612
|
const maxDur = this.cfg.maxDuration || 300;
|
|
607
613
|
const silentMax = this.cfg.silenceTimeout || 30;
|
|
608
614
|
this._lastSpeechTime = Date.now();
|
|
609
|
-
this.timerEl.textContent =
|
|
615
|
+
this.timerEl.textContent = this.fmt(maxDur);
|
|
610
616
|
this.timerTicker = window.setInterval(() => {
|
|
611
617
|
elapsed++;
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
this.timerEl.textContent = `${m}:${s}`;
|
|
618
|
+
const remaining = Math.max(0, maxDur - elapsed);
|
|
619
|
+
this.timerEl.textContent = this.fmt(remaining);
|
|
615
620
|
|
|
616
|
-
// Check max duration
|
|
617
621
|
if (elapsed >= maxDur) {
|
|
618
622
|
this._locked = true;
|
|
619
623
|
this.stop();
|
|
620
|
-
this.showError('
|
|
624
|
+
this.showError('Tiempo límite alcanzado.');
|
|
621
625
|
return;
|
|
622
626
|
}
|
|
623
627
|
|
|
624
|
-
// Check silence timeout
|
|
625
628
|
if (Date.now() - this._lastSpeechTime > silentMax * 1000) {
|
|
626
629
|
this._locked = true;
|
|
627
630
|
this.stop();
|
|
628
|
-
this.showError('
|
|
631
|
+
this.showError('Sesión finalizada por inactividad.');
|
|
629
632
|
}
|
|
630
633
|
}, 1000);
|
|
631
634
|
}
|