@openclaw/voice-call 2026.7.2-beta.7 → 2026.8.1-beta.2
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/doctor-contract-api.js +10 -8
- package/dist/{guarded-json-api-DS9sZa3C.js → guarded-json-api-DEDwgf4J.js} +24 -46
- package/dist/index.js +308 -629
- package/dist/{mock-YsvBTX-7.js → mock-BCq0q36l.js} +2 -3
- package/dist/{plivo-vZ--Wqxv.js → plivo-DbI17uAc.js} +45 -8
- package/dist/{realtime-handler--i_OERv6.js → realtime-handler-uXR0QgVa.js} +364 -108
- package/dist/{response-generator-D-7ApVfT.js → response-generator-D-hkzo5Y.js} +13 -5
- package/dist/{runtime-entry-D0UJiLfi.js → runtime-entry-F13eJ4pp.js} +279 -172
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +17 -24
- package/dist/{store-path-nYL-yM0S.js → store-path-51_oC_xn.js} +70 -13
- package/dist/{telnyx-Dx6cfLvV.js → telnyx-sIS_WgnY.js} +3 -2
- package/dist/{twilio-D_D8KysU.js → twilio-CbeVrVAl.js} +20 -26
- package/dist/{webhook-replay-aaP79dI8.js → webhook-replay-FQrEGVSM.js} +20 -6
- package/openclaw.plugin.json +49 -4
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { definePluginEntry, sleep } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
3
|
import { VOICE_CALL_CLI_DESCRIPTOR } from "./cli-output-mode.js";
|
|
4
|
-
import { S as validateProviderConfig, b as resolveVoiceCallConfig, c as resolveWebhookExposureStatus, d as setupTailscaleExposureRoute, g as resolveUserPath, l as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime, u as getTailscaleSelfInfo, y as VoiceCallConfigSchema } from "./runtime-entry-
|
|
5
|
-
import {
|
|
4
|
+
import { S as validateProviderConfig, b as resolveVoiceCallConfig, c as resolveWebhookExposureStatus, d as setupTailscaleExposureRoute, g as resolveUserPath, l as cleanupTailscaleExposureRoute, t as createVoiceCallRuntime, u as getTailscaleSelfInfo, y as VoiceCallConfigSchema } from "./runtime-entry-F13eJ4pp.js";
|
|
5
|
+
import { b as setVoiceCallStateRuntime, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore } from "./store-path-51_oC_xn.js";
|
|
6
6
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
7
|
import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
8
|
-
import {
|
|
9
|
-
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
10
|
-
import { asOptionalRecord, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
|
+
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/global-singleton";
|
|
9
|
+
import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
|
10
|
+
import { asNonArrayRecord, asOptionalRecord, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
11
11
|
import { jsonResult } from "openclaw/plugin-sdk/tool-results";
|
|
12
12
|
import { Type } from "typebox";
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
15
|
import path from "node:path";
|
|
16
|
+
import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger, resolveTimerTimeoutMs, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
|
16
17
|
import { StringDecoder } from "node:string_decoder";
|
|
17
18
|
import { format } from "node:util";
|
|
18
19
|
//#region extensions/voice-call/src/cli.ts
|
|
@@ -543,6 +544,126 @@ function registerVoiceCallCli(params) {
|
|
|
543
544
|
});
|
|
544
545
|
}
|
|
545
546
|
//#endregion
|
|
547
|
+
//#region extensions/voice-call/src/command-service.ts
|
|
548
|
+
var VoiceCallCommandInputError = class extends Error {};
|
|
549
|
+
function toVoiceCallStatus(call) {
|
|
550
|
+
return {
|
|
551
|
+
callId: call.callId,
|
|
552
|
+
...call.providerCallId !== void 0 ? { providerCallId: call.providerCallId } : {},
|
|
553
|
+
provider: call.provider,
|
|
554
|
+
direction: call.direction,
|
|
555
|
+
state: call.state,
|
|
556
|
+
startedAt: call.startedAt,
|
|
557
|
+
...call.answeredAt !== void 0 ? { answeredAt: call.answeredAt } : {},
|
|
558
|
+
...call.endedAt !== void 0 ? { endedAt: call.endedAt } : {},
|
|
559
|
+
...call.endReason !== void 0 ? { endReason: call.endReason } : {}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function requireInput(value, message) {
|
|
563
|
+
if (!value) throw new VoiceCallCommandInputError(message);
|
|
564
|
+
return value;
|
|
565
|
+
}
|
|
566
|
+
function requireSuccess(result, fallback) {
|
|
567
|
+
if (!result.success) throw new Error(result.error || fallback);
|
|
568
|
+
}
|
|
569
|
+
function createVoiceCallCommandService(ensureRuntime) {
|
|
570
|
+
const describeHistoricalCall = async (rt, callId) => {
|
|
571
|
+
const call = await rt.manager.getCallFromMemoryOrStore(callId);
|
|
572
|
+
if (!call) return;
|
|
573
|
+
const endedAt = timestampMsToIsoString(call.endedAt);
|
|
574
|
+
return `call is not active (${[
|
|
575
|
+
`last state=${call.state}`,
|
|
576
|
+
call.endReason ? `endReason=${call.endReason}` : void 0,
|
|
577
|
+
endedAt ? `endedAt=${endedAt}` : void 0
|
|
578
|
+
].filter(Boolean).join(", ")})`;
|
|
579
|
+
};
|
|
580
|
+
const resolveCallMessage = async (callId, message) => {
|
|
581
|
+
const resolvedCallId = requireInput(callId, "callId and message required");
|
|
582
|
+
const resolvedMessage = requireInput(message, "callId and message required");
|
|
583
|
+
const rt = await ensureRuntime();
|
|
584
|
+
const activeCall = rt.manager.getCall(resolvedCallId) ?? rt.manager.getCallByProviderCallId(resolvedCallId);
|
|
585
|
+
if (!activeCall) throw new VoiceCallCommandInputError(await describeHistoricalCall(rt, resolvedCallId) ?? "Call not found");
|
|
586
|
+
return {
|
|
587
|
+
rt,
|
|
588
|
+
callId: activeCall.callId,
|
|
589
|
+
message: resolvedMessage
|
|
590
|
+
};
|
|
591
|
+
};
|
|
592
|
+
const prepareContinue = async (callId, message) => {
|
|
593
|
+
const request = await resolveCallMessage(callId, message);
|
|
594
|
+
return {
|
|
595
|
+
rt: request.rt,
|
|
596
|
+
callId: request.callId,
|
|
597
|
+
run: async () => {
|
|
598
|
+
const result = await request.rt.manager.continueCall(request.callId, request.message);
|
|
599
|
+
requireSuccess(result, "continue failed");
|
|
600
|
+
return {
|
|
601
|
+
success: true,
|
|
602
|
+
transcript: result.transcript
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
};
|
|
607
|
+
return {
|
|
608
|
+
prepareContinue,
|
|
609
|
+
async initiate(params, missingToMessage = "to required") {
|
|
610
|
+
const rt = await ensureRuntime();
|
|
611
|
+
const to = requireInput(params.to ?? rt.config.toNumber, missingToMessage);
|
|
612
|
+
const result = await rt.manager.initiateCall(to, params.sessionKey, {
|
|
613
|
+
message: params.message,
|
|
614
|
+
mode: params.mode,
|
|
615
|
+
dtmfSequence: params.dtmfSequence,
|
|
616
|
+
...params.requesterSessionKey ? { requesterSessionKey: params.requesterSessionKey } : {},
|
|
617
|
+
...params.agentId ? { agentId: params.agentId } : {}
|
|
618
|
+
});
|
|
619
|
+
requireSuccess(result, "initiate failed");
|
|
620
|
+
return {
|
|
621
|
+
callId: result.callId,
|
|
622
|
+
initiated: true
|
|
623
|
+
};
|
|
624
|
+
},
|
|
625
|
+
async continueCall(callId, message) {
|
|
626
|
+
return await (await prepareContinue(callId, message)).run();
|
|
627
|
+
},
|
|
628
|
+
async speak(params) {
|
|
629
|
+
const request = await resolveCallMessage(params.callId, params.message);
|
|
630
|
+
if (request.rt.config.realtime.enabled) {
|
|
631
|
+
const realtimeResult = request.rt.webhookServer.speakRealtime(request.callId, request.message);
|
|
632
|
+
if (realtimeResult.success) return { success: true };
|
|
633
|
+
if (params.allowTwimlFallback === false) return {
|
|
634
|
+
success: false,
|
|
635
|
+
error: realtimeResult.error ?? "Realtime bridge is not active"
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
requireSuccess(await request.rt.manager.speak(request.callId, request.message), "speak failed");
|
|
639
|
+
return { success: true };
|
|
640
|
+
},
|
|
641
|
+
async sendDtmf(callId, digits) {
|
|
642
|
+
const resolvedCallId = requireInput(callId, "callId and digits required");
|
|
643
|
+
const resolvedDigits = requireInput(digits, "callId and digits required");
|
|
644
|
+
requireSuccess(await (await ensureRuntime()).manager.sendDtmf(resolvedCallId, resolvedDigits), "dtmf failed");
|
|
645
|
+
return { success: true };
|
|
646
|
+
},
|
|
647
|
+
async endCall(callId) {
|
|
648
|
+
const resolvedCallId = requireInput(callId, "callId required");
|
|
649
|
+
requireSuccess(await (await ensureRuntime()).manager.endCall(resolvedCallId), "end failed");
|
|
650
|
+
return { success: true };
|
|
651
|
+
},
|
|
652
|
+
async status(callId) {
|
|
653
|
+
const rt = await ensureRuntime();
|
|
654
|
+
if (!callId) return {
|
|
655
|
+
found: true,
|
|
656
|
+
calls: rt.manager.getActiveCalls().map(toVoiceCallStatus)
|
|
657
|
+
};
|
|
658
|
+
const call = await rt.manager.getCallFromMemoryOrStore(callId);
|
|
659
|
+
return call ? {
|
|
660
|
+
found: true,
|
|
661
|
+
call: toVoiceCallStatus(call)
|
|
662
|
+
} : { found: false };
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
546
667
|
//#region extensions/voice-call/src/gateway-continue-operation.ts
|
|
547
668
|
const VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS = 3e4;
|
|
548
669
|
const VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS = 300 * 1e3;
|
|
@@ -569,21 +690,9 @@ function createVoiceCallContinueOperationStore(params) {
|
|
|
569
690
|
startedAtMs,
|
|
570
691
|
pollTimeoutMs
|
|
571
692
|
});
|
|
572
|
-
request.
|
|
693
|
+
request.run().then((result) => {
|
|
573
694
|
const current = operations.get(operationId);
|
|
574
695
|
if (!current || current.status !== "pending") return;
|
|
575
|
-
if (!result.success) {
|
|
576
|
-
operations.set(operationId, {
|
|
577
|
-
operationId,
|
|
578
|
-
status: "failed",
|
|
579
|
-
callId: request.callId,
|
|
580
|
-
startedAtMs,
|
|
581
|
-
completedAtMs: Date.now(),
|
|
582
|
-
pollTimeoutMs,
|
|
583
|
-
error: result.error || "continue failed"
|
|
584
|
-
});
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
696
|
operations.set(operationId, {
|
|
588
697
|
operationId,
|
|
589
698
|
status: "completed",
|
|
@@ -661,223 +770,15 @@ function createVoiceCallContinueOperationStore(params) {
|
|
|
661
770
|
//#region extensions/voice-call/index.ts
|
|
662
771
|
const VOICE_CALL_WRITE_METHOD_SCOPE = { scope: "operator.write" };
|
|
663
772
|
const VOICE_CALL_READ_METHOD_SCOPE = { scope: "operator.read" };
|
|
664
|
-
const voiceCallConfigSchema = {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
},
|
|
674
|
-
uiHints: {
|
|
675
|
-
provider: {
|
|
676
|
-
label: "Provider",
|
|
677
|
-
help: "Use twilio, telnyx, or mock for dev/no-network."
|
|
678
|
-
},
|
|
679
|
-
fromNumber: {
|
|
680
|
-
label: "From Number",
|
|
681
|
-
placeholder: "+15550001234"
|
|
682
|
-
},
|
|
683
|
-
toNumber: {
|
|
684
|
-
label: "Default To Number",
|
|
685
|
-
placeholder: "+15550001234"
|
|
686
|
-
},
|
|
687
|
-
inboundPolicy: { label: "Inbound Policy" },
|
|
688
|
-
allowFrom: { label: "Inbound Allowlist" },
|
|
689
|
-
inboundGreeting: {
|
|
690
|
-
label: "Inbound Greeting",
|
|
691
|
-
advanced: true
|
|
692
|
-
},
|
|
693
|
-
numbers: {
|
|
694
|
-
label: "Per-number Routing",
|
|
695
|
-
help: "Inbound overrides keyed by dialed E.164 number.",
|
|
696
|
-
advanced: true
|
|
697
|
-
},
|
|
698
|
-
"telnyx.apiKey": {
|
|
699
|
-
label: "Telnyx API Key",
|
|
700
|
-
sensitive: true
|
|
701
|
-
},
|
|
702
|
-
"telnyx.connectionId": { label: "Telnyx Connection ID" },
|
|
703
|
-
"telnyx.publicKey": {
|
|
704
|
-
label: "Telnyx Public Key",
|
|
705
|
-
sensitive: true
|
|
706
|
-
},
|
|
707
|
-
"twilio.accountSid": { label: "Twilio Account SID" },
|
|
708
|
-
"twilio.authToken": {
|
|
709
|
-
label: "Twilio Auth Token",
|
|
710
|
-
sensitive: true
|
|
711
|
-
},
|
|
712
|
-
"twilio.region": {
|
|
713
|
-
label: "Twilio Region",
|
|
714
|
-
advanced: true
|
|
715
|
-
},
|
|
716
|
-
"outbound.defaultMode": { label: "Default Call Mode" },
|
|
717
|
-
"outbound.notifyHangupDelaySec": {
|
|
718
|
-
label: "Notify Hangup Delay (sec)",
|
|
719
|
-
advanced: true
|
|
720
|
-
},
|
|
721
|
-
"serve.port": { label: "Webhook Port" },
|
|
722
|
-
"serve.bind": { label: "Webhook Bind" },
|
|
723
|
-
"serve.path": { label: "Webhook Path" },
|
|
724
|
-
"tailscale.mode": {
|
|
725
|
-
label: "Tailscale Mode",
|
|
726
|
-
advanced: true
|
|
727
|
-
},
|
|
728
|
-
"tailscale.path": {
|
|
729
|
-
label: "Tailscale Path",
|
|
730
|
-
advanced: true
|
|
731
|
-
},
|
|
732
|
-
"tunnel.provider": {
|
|
733
|
-
label: "Tunnel Provider",
|
|
734
|
-
advanced: true
|
|
735
|
-
},
|
|
736
|
-
"tunnel.ngrokAuthToken": {
|
|
737
|
-
label: "ngrok Auth Token",
|
|
738
|
-
sensitive: true,
|
|
739
|
-
advanced: true
|
|
740
|
-
},
|
|
741
|
-
"tunnel.ngrokDomain": {
|
|
742
|
-
label: "ngrok Domain",
|
|
743
|
-
advanced: true
|
|
744
|
-
},
|
|
745
|
-
"tunnel.allowNgrokFreeTierLoopbackBypass": {
|
|
746
|
-
label: "Allow ngrok Free Tier (Loopback Bypass)",
|
|
747
|
-
advanced: true
|
|
748
|
-
},
|
|
749
|
-
"streaming.enabled": {
|
|
750
|
-
label: "Enable Streaming",
|
|
751
|
-
help: "Classic streaming transcription currently requires the Twilio call provider.",
|
|
752
|
-
advanced: true
|
|
753
|
-
},
|
|
754
|
-
"streaming.provider": {
|
|
755
|
-
label: "Streaming Provider",
|
|
756
|
-
help: "Uses the first registered realtime transcription provider when unset.",
|
|
757
|
-
advanced: true
|
|
758
|
-
},
|
|
759
|
-
"streaming.providers": {
|
|
760
|
-
label: "Streaming Provider Config",
|
|
761
|
-
advanced: true
|
|
762
|
-
},
|
|
763
|
-
"streaming.streamPath": {
|
|
764
|
-
label: "Media Stream Path",
|
|
765
|
-
advanced: true
|
|
766
|
-
},
|
|
767
|
-
"realtime.enabled": {
|
|
768
|
-
label: "Enable Realtime Voice",
|
|
769
|
-
advanced: true
|
|
770
|
-
},
|
|
771
|
-
"realtime.provider": {
|
|
772
|
-
label: "Realtime Voice Provider",
|
|
773
|
-
help: "Uses the first registered realtime voice provider when unset.",
|
|
774
|
-
advanced: true
|
|
775
|
-
},
|
|
776
|
-
"realtime.streamPath": {
|
|
777
|
-
label: "Realtime Stream Path",
|
|
778
|
-
advanced: true
|
|
779
|
-
},
|
|
780
|
-
"realtime.instructions": {
|
|
781
|
-
label: "Realtime Instructions",
|
|
782
|
-
advanced: true
|
|
783
|
-
},
|
|
784
|
-
"realtime.toolPolicy": {
|
|
785
|
-
label: "Realtime Tool Policy",
|
|
786
|
-
help: "Controls the shared openclaw_agent_consult tool.",
|
|
787
|
-
advanced: true
|
|
788
|
-
},
|
|
789
|
-
"realtime.consultPolicy": {
|
|
790
|
-
label: "Realtime Consult Policy",
|
|
791
|
-
help: "Guides when the realtime voice model should call openclaw_agent_consult.",
|
|
792
|
-
advanced: true
|
|
793
|
-
},
|
|
794
|
-
"realtime.fastContext.enabled": {
|
|
795
|
-
label: "Enable Fast Realtime Context",
|
|
796
|
-
help: "Searches memory/session context before the full consult agent.",
|
|
797
|
-
advanced: true
|
|
798
|
-
},
|
|
799
|
-
"realtime.fastContext.timeoutMs": {
|
|
800
|
-
label: "Fast Context Timeout",
|
|
801
|
-
advanced: true
|
|
802
|
-
},
|
|
803
|
-
"realtime.fastContext.maxResults": {
|
|
804
|
-
label: "Fast Context Result Limit",
|
|
805
|
-
advanced: true
|
|
806
|
-
},
|
|
807
|
-
"realtime.fastContext.sources": {
|
|
808
|
-
label: "Fast Context Sources",
|
|
809
|
-
advanced: true
|
|
810
|
-
},
|
|
811
|
-
"realtime.fastContext.fallbackToConsult": {
|
|
812
|
-
label: "Fallback To Full Consult",
|
|
813
|
-
advanced: true
|
|
814
|
-
},
|
|
815
|
-
"realtime.agentContext.enabled": {
|
|
816
|
-
label: "Enable Agent Voice Context",
|
|
817
|
-
help: "Injects a compact agent identity and workspace context capsule into realtime voice instructions.",
|
|
818
|
-
advanced: true
|
|
819
|
-
},
|
|
820
|
-
"realtime.agentContext.maxChars": {
|
|
821
|
-
label: "Agent Voice Context Limit",
|
|
822
|
-
advanced: true
|
|
823
|
-
},
|
|
824
|
-
"realtime.agentContext.includeIdentity": {
|
|
825
|
-
label: "Include Agent Identity",
|
|
826
|
-
advanced: true
|
|
827
|
-
},
|
|
828
|
-
"realtime.agentContext.includeWorkspaceFiles": {
|
|
829
|
-
label: "Include Agent Workspace Files",
|
|
830
|
-
advanced: true
|
|
831
|
-
},
|
|
832
|
-
"realtime.agentContext.files": {
|
|
833
|
-
label: "Agent Voice Context Files",
|
|
834
|
-
advanced: true
|
|
835
|
-
},
|
|
836
|
-
"realtime.providers": {
|
|
837
|
-
label: "Realtime Provider Config",
|
|
838
|
-
advanced: true
|
|
839
|
-
},
|
|
840
|
-
"tts.provider": {
|
|
841
|
-
label: "TTS Provider Override",
|
|
842
|
-
help: "Deep-merges with tts (Microsoft is ignored for calls).",
|
|
843
|
-
advanced: true
|
|
844
|
-
},
|
|
845
|
-
"tts.providers": {
|
|
846
|
-
label: "TTS Provider Config",
|
|
847
|
-
advanced: true
|
|
848
|
-
},
|
|
849
|
-
publicUrl: {
|
|
850
|
-
label: "Public Webhook URL",
|
|
851
|
-
advanced: true
|
|
852
|
-
},
|
|
853
|
-
skipSignatureVerification: {
|
|
854
|
-
label: "Skip Signature Verification",
|
|
855
|
-
advanced: true
|
|
856
|
-
},
|
|
857
|
-
store: {
|
|
858
|
-
label: "Call Log Store Path",
|
|
859
|
-
advanced: true
|
|
860
|
-
},
|
|
861
|
-
agentId: {
|
|
862
|
-
label: "Response Agent ID",
|
|
863
|
-
help: "Agent workspace used for voice response generation. Defaults to \"main\".",
|
|
864
|
-
advanced: true
|
|
865
|
-
},
|
|
866
|
-
responseModel: {
|
|
867
|
-
label: "Response Model",
|
|
868
|
-
help: "Optional override. Falls back to the runtime default model when unset.",
|
|
869
|
-
advanced: true
|
|
870
|
-
},
|
|
871
|
-
responseSystemPrompt: {
|
|
872
|
-
label: "Response System Prompt",
|
|
873
|
-
advanced: true
|
|
874
|
-
},
|
|
875
|
-
responseTimeoutMs: {
|
|
876
|
-
label: "Response Timeout (ms)",
|
|
877
|
-
advanced: true
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
};
|
|
773
|
+
const voiceCallConfigSchema = { parse(value) {
|
|
774
|
+
const config = asOptionalRecord(value) ?? {};
|
|
775
|
+
const enabled = typeof config.enabled === "boolean" ? config.enabled : true;
|
|
776
|
+
return VoiceCallConfigSchema.parse({
|
|
777
|
+
...config,
|
|
778
|
+
enabled,
|
|
779
|
+
provider: config.provider ?? (enabled ? "mock" : void 0)
|
|
780
|
+
});
|
|
781
|
+
} };
|
|
881
782
|
const VoiceCallToolSchema = Type.Union([
|
|
882
783
|
Type.Object({
|
|
883
784
|
action: Type.Literal("initiate_call"),
|
|
@@ -885,7 +786,6 @@ const VoiceCallToolSchema = Type.Union([
|
|
|
885
786
|
message: Type.String({ description: "Intro message" }),
|
|
886
787
|
mode: Type.Optional(Type.Union([Type.Literal("notify"), Type.Literal("conversation")])),
|
|
887
788
|
sessionKey: Type.Optional(Type.String({ description: "OpenClaw session key for the call" })),
|
|
888
|
-
requesterSessionKey: Type.Optional(Type.String({ description: "OpenClaw session key that initiated the call" })),
|
|
889
789
|
dtmfSequence: Type.Optional(Type.String({ description: "DTMF digits to play before connect" }))
|
|
890
790
|
}),
|
|
891
791
|
Type.Object({
|
|
@@ -917,38 +817,28 @@ const VoiceCallToolSchema = Type.Union([
|
|
|
917
817
|
sid: Type.Optional(Type.String({ description: "Call SID" })),
|
|
918
818
|
message: Type.Optional(Type.String({ description: "Optional intro message" })),
|
|
919
819
|
sessionKey: Type.Optional(Type.String({ description: "OpenClaw session key for the call" })),
|
|
920
|
-
requesterSessionKey: Type.Optional(Type.String({ description: "OpenClaw session key that initiated the call" })),
|
|
921
820
|
dtmfSequence: Type.Optional(Type.String({ description: "DTMF digits to play before connect" }))
|
|
922
821
|
})
|
|
923
822
|
]);
|
|
924
|
-
function asParamRecord(params) {
|
|
925
|
-
return params && typeof params === "object" && !Array.isArray(params) ? params : {};
|
|
926
|
-
}
|
|
927
823
|
function isCliOnlyProcess() {
|
|
928
824
|
return process.env.OPENCLAW_CLI === "1" && !process.argv.slice(2).includes("gateway");
|
|
929
825
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
startedAt: call.startedAt,
|
|
938
|
-
...call.answeredAt !== void 0 ? { answeredAt: call.answeredAt } : {},
|
|
939
|
-
...call.endedAt !== void 0 ? { endedAt: call.endedAt } : {},
|
|
940
|
-
...call.endReason !== void 0 ? { endReason: call.endReason } : {}
|
|
941
|
-
};
|
|
826
|
+
const VOICE_CALL_RUNTIME_COORDINATOR_KEY = Symbol.for("openclaw.voice-call.runtimeCoordinator");
|
|
827
|
+
var VoiceCallRuntimeLifecycleError = class extends Error {};
|
|
828
|
+
function getVoiceCallRuntimeCoordinator() {
|
|
829
|
+
return resolveGlobalSingleton(VOICE_CALL_RUNTIME_COORDINATOR_KEY, () => ({
|
|
830
|
+
epochCounter: 0,
|
|
831
|
+
registeredEpoch: 0
|
|
832
|
+
}));
|
|
942
833
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
return state;
|
|
834
|
+
function activateVoiceCallRuntimeGeneration(coordinator, generation) {
|
|
835
|
+
if (generation.epoch < coordinator.registeredEpoch) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation was superseded; use the current plugin registration");
|
|
836
|
+
if (generation.retired) throw new VoiceCallRuntimeLifecycleError("Voice call runtime generation is retired; use the current plugin registration");
|
|
837
|
+
if (coordinator.current !== generation) {
|
|
838
|
+
if (coordinator.current) coordinator.current.retired = true;
|
|
839
|
+
coordinator.current = generation;
|
|
840
|
+
coordinator.registeredEpoch = generation.epoch;
|
|
841
|
+
}
|
|
952
842
|
}
|
|
953
843
|
var voice_call_default = definePluginEntry({
|
|
954
844
|
id: "voice-call",
|
|
@@ -958,304 +848,122 @@ var voice_call_default = definePluginEntry({
|
|
|
958
848
|
register(api) {
|
|
959
849
|
const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
|
|
960
850
|
const validation = validateProviderConfig(config);
|
|
961
|
-
const
|
|
851
|
+
const runtimeCoordinator = getVoiceCallRuntimeCoordinator();
|
|
852
|
+
const runtimeGeneration = api.registrationMode !== "full" && runtimeCoordinator.current ? runtimeCoordinator.current : {
|
|
853
|
+
epoch: ++runtimeCoordinator.epochCounter,
|
|
854
|
+
retired: false
|
|
855
|
+
};
|
|
962
856
|
const continueOperationStore = createVoiceCallContinueOperationStore({
|
|
963
857
|
config,
|
|
964
858
|
coreConfig: api.config
|
|
965
859
|
});
|
|
966
860
|
const ensureRuntime = async () => {
|
|
861
|
+
activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
|
|
967
862
|
if (!config.enabled) throw new Error("Voice call disabled in plugin config");
|
|
968
863
|
if (!validation.valid) throw new Error(validation.errors.join("; "));
|
|
969
864
|
while (true) {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
config,
|
|
980
|
-
coreConfig: api.config,
|
|
981
|
-
fullConfig: api.config,
|
|
982
|
-
agentRuntime: api.runtime.agent,
|
|
983
|
-
stateRuntime: api.runtime.state,
|
|
984
|
-
ttsRuntime: api.runtime.tts,
|
|
985
|
-
logger: api.logger
|
|
986
|
-
});
|
|
987
|
-
runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = runtimePromise;
|
|
988
|
-
}
|
|
989
|
-
try {
|
|
990
|
-
const createdRuntime = await runtimePromise;
|
|
991
|
-
if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) continue;
|
|
992
|
-
if (runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] !== runtimePromise) continue;
|
|
993
|
-
runtimeState[VOICE_CALL_RUNTIME_KEY] = createdRuntime;
|
|
994
|
-
return createdRuntime;
|
|
995
|
-
} catch (err) {
|
|
996
|
-
if (runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] === runtimePromise) {
|
|
997
|
-
runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = null;
|
|
998
|
-
runtimeState[VOICE_CALL_RUNTIME_KEY] = null;
|
|
865
|
+
activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
|
|
866
|
+
const slot = runtimeCoordinator.slot;
|
|
867
|
+
if (slot) {
|
|
868
|
+
if (slot.owner !== runtimeGeneration) {
|
|
869
|
+
if (slot.state === "stopping") {
|
|
870
|
+
await slot.promise;
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
throw new VoiceCallRuntimeLifecycleError("A previous voice call runtime generation is still active; retry after it stops");
|
|
999
874
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
const respondError = (respond, message, code = ErrorCodes.UNAVAILABLE) => {
|
|
1005
|
-
respond(false, void 0, errorShape(code, message));
|
|
1006
|
-
};
|
|
1007
|
-
const sendError = (respond, err) => {
|
|
1008
|
-
respondError(respond, formatErrorMessage(err));
|
|
1009
|
-
};
|
|
1010
|
-
const describeHistoricalCall = async (rt, callId) => {
|
|
1011
|
-
const call = await rt.manager.getCallFromMemoryOrStore(callId);
|
|
1012
|
-
if (!call) return;
|
|
1013
|
-
const endedAt = timestampMsToIsoString(call.endedAt);
|
|
1014
|
-
return `call is not active (${[
|
|
1015
|
-
`last state=${call.state}`,
|
|
1016
|
-
call.endReason ? `endReason=${call.endReason}` : void 0,
|
|
1017
|
-
endedAt ? `endedAt=${endedAt}` : void 0
|
|
1018
|
-
].filter(Boolean).join(", ")})`;
|
|
1019
|
-
};
|
|
1020
|
-
const resolveCallMessageRequest = async (params) => {
|
|
1021
|
-
const callId = normalizeOptionalString(params?.callId) ?? "";
|
|
1022
|
-
const message = normalizeOptionalString(params?.message) ?? "";
|
|
1023
|
-
if (!callId || !message) return { error: "callId and message required" };
|
|
1024
|
-
const rt = await ensureRuntime();
|
|
1025
|
-
const activeCall = rt.manager.getCall(callId) ?? rt.manager.getCallByProviderCallId(callId);
|
|
1026
|
-
if (activeCall) return {
|
|
1027
|
-
rt,
|
|
1028
|
-
callId: activeCall.callId,
|
|
1029
|
-
message
|
|
1030
|
-
};
|
|
1031
|
-
return { error: await describeHistoricalCall(rt, callId) ?? "Call not found" };
|
|
1032
|
-
};
|
|
1033
|
-
const initiateCallAndRespond = async (params) => {
|
|
1034
|
-
const result = await params.rt.manager.initiateCall(params.to, params.sessionKey, {
|
|
1035
|
-
message: params.message,
|
|
1036
|
-
mode: params.mode,
|
|
1037
|
-
dtmfSequence: params.dtmfSequence,
|
|
1038
|
-
...params.requesterSessionKey ? { requesterSessionKey: params.requesterSessionKey } : {},
|
|
1039
|
-
...params.agentId ? { agentId: params.agentId } : {}
|
|
1040
|
-
});
|
|
1041
|
-
if (!result.success) {
|
|
1042
|
-
respondError(params.respond, result.error || "initiate failed");
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
params.respond(true, {
|
|
1046
|
-
callId: result.callId,
|
|
1047
|
-
initiated: true
|
|
1048
|
-
});
|
|
1049
|
-
};
|
|
1050
|
-
const respondToCallMessageAction = async (params) => {
|
|
1051
|
-
const request = await resolveCallMessageRequest(params.requestParams);
|
|
1052
|
-
if ("error" in request) {
|
|
1053
|
-
respondError(params.respond, request.error ?? "callId and message required", ErrorCodes.INVALID_REQUEST);
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
const result = await params.action(request);
|
|
1057
|
-
if (!result.success) {
|
|
1058
|
-
respondError(params.respond, result.error || params.failure);
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
params.respond(true, params.includeTranscript ? {
|
|
1062
|
-
success: true,
|
|
1063
|
-
transcript: result.transcript
|
|
1064
|
-
} : { success: true });
|
|
1065
|
-
};
|
|
1066
|
-
api.registerGatewayMethod("voicecall.initiate", async ({ params, respond }) => {
|
|
1067
|
-
try {
|
|
1068
|
-
const message = normalizeOptionalString(params?.message) ?? "";
|
|
1069
|
-
if (!message) {
|
|
1070
|
-
respondError(respond, "message required", ErrorCodes.INVALID_REQUEST);
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
const rt = await ensureRuntime();
|
|
1074
|
-
const to = normalizeOptionalString(params?.to) ?? rt.config.toNumber;
|
|
1075
|
-
if (!to) {
|
|
1076
|
-
respondError(respond, "to required", ErrorCodes.INVALID_REQUEST);
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
const mode = params?.mode === "notify" || params?.mode === "conversation" ? params.mode : void 0;
|
|
1080
|
-
await initiateCallAndRespond({
|
|
1081
|
-
rt,
|
|
1082
|
-
respond,
|
|
1083
|
-
to,
|
|
1084
|
-
message,
|
|
1085
|
-
mode,
|
|
1086
|
-
sessionKey: normalizeOptionalString(params?.sessionKey),
|
|
1087
|
-
requesterSessionKey: normalizeOptionalString(params?.requesterSessionKey)
|
|
1088
|
-
});
|
|
1089
|
-
} catch (err) {
|
|
1090
|
-
sendError(respond, err);
|
|
1091
|
-
}
|
|
1092
|
-
}, VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
1093
|
-
api.registerGatewayMethod("voicecall.continue", async ({ params, respond }) => {
|
|
1094
|
-
try {
|
|
1095
|
-
await respondToCallMessageAction({
|
|
1096
|
-
requestParams: params,
|
|
1097
|
-
respond,
|
|
1098
|
-
action: (request) => request.rt.manager.continueCall(request.callId, request.message),
|
|
1099
|
-
failure: "continue failed",
|
|
1100
|
-
includeTranscript: true
|
|
1101
|
-
});
|
|
1102
|
-
} catch (err) {
|
|
1103
|
-
sendError(respond, err);
|
|
1104
|
-
}
|
|
1105
|
-
}, VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
1106
|
-
api.registerGatewayMethod("voicecall.continue.start", async ({ params, respond }) => {
|
|
1107
|
-
try {
|
|
1108
|
-
const request = await resolveCallMessageRequest(params);
|
|
1109
|
-
if ("error" in request) {
|
|
1110
|
-
respondError(respond, request.error ?? "callId and message required", ErrorCodes.INVALID_REQUEST);
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
respond(true, continueOperationStore.start(request));
|
|
1114
|
-
} catch (err) {
|
|
1115
|
-
sendError(respond, err);
|
|
1116
|
-
}
|
|
1117
|
-
}, VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
1118
|
-
api.registerGatewayMethod("voicecall.continue.result", async ({ params, respond }) => {
|
|
1119
|
-
try {
|
|
1120
|
-
const operationId = normalizeOptionalString(params?.operationId) ?? "";
|
|
1121
|
-
if (!operationId) {
|
|
1122
|
-
respondError(respond, "operationId required", ErrorCodes.INVALID_REQUEST);
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
const operation = continueOperationStore.read(operationId);
|
|
1126
|
-
if (!operation.ok) {
|
|
1127
|
-
respondError(respond, operation.error, ErrorCodes.INVALID_REQUEST);
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
respond(true, operation.payload);
|
|
1131
|
-
} catch (err) {
|
|
1132
|
-
sendError(respond, err);
|
|
1133
|
-
}
|
|
1134
|
-
}, VOICE_CALL_READ_METHOD_SCOPE);
|
|
1135
|
-
api.registerGatewayMethod("voicecall.speak", async ({ params, respond }) => {
|
|
1136
|
-
try {
|
|
1137
|
-
const request = await resolveCallMessageRequest(params);
|
|
1138
|
-
if ("error" in request) {
|
|
1139
|
-
respondError(respond, request.error ?? "callId and message required", ErrorCodes.INVALID_REQUEST);
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
if (request.rt.config.realtime.enabled) {
|
|
1143
|
-
const realtimeResult = request.rt.webhookServer.speakRealtime(request.callId, request.message);
|
|
1144
|
-
if (realtimeResult.success) {
|
|
1145
|
-
respond(true, { success: true });
|
|
1146
|
-
return;
|
|
875
|
+
if (slot.state === "running") return slot.runtime;
|
|
876
|
+
if (slot.state === "stopping") {
|
|
877
|
+
await slot.promise;
|
|
878
|
+
continue;
|
|
1147
879
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
880
|
+
let createdRuntime;
|
|
881
|
+
try {
|
|
882
|
+
createdRuntime = await slot.promise;
|
|
883
|
+
} catch (err) {
|
|
884
|
+
if (runtimeCoordinator.slot === slot) runtimeCoordinator.slot = void 0;
|
|
885
|
+
throw err;
|
|
1154
886
|
}
|
|
887
|
+
activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
|
|
888
|
+
if (runtimeCoordinator.slot !== slot) continue;
|
|
889
|
+
runtimeCoordinator.slot = {
|
|
890
|
+
state: "running",
|
|
891
|
+
owner: runtimeGeneration,
|
|
892
|
+
runtime: createdRuntime
|
|
893
|
+
};
|
|
894
|
+
return createdRuntime;
|
|
1155
895
|
}
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
if (!callId || !digits) {
|
|
1171
|
-
respondError(respond, "callId and digits required", ErrorCodes.INVALID_REQUEST);
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
const result = await (await ensureRuntime()).manager.sendDtmf(callId, digits);
|
|
1175
|
-
if (!result.success) {
|
|
1176
|
-
respondError(respond, result.error || "dtmf failed");
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
respond(true, { success: true });
|
|
1180
|
-
} catch (err) {
|
|
1181
|
-
sendError(respond, err);
|
|
896
|
+
const runtimePromise = createVoiceCallRuntime({
|
|
897
|
+
config,
|
|
898
|
+
coreConfig: api.config,
|
|
899
|
+
fullConfig: api.config,
|
|
900
|
+
agentRuntime: api.runtime.agent,
|
|
901
|
+
stateRuntime: api.runtime.state,
|
|
902
|
+
ttsRuntime: api.runtime.tts,
|
|
903
|
+
logger: api.logger
|
|
904
|
+
});
|
|
905
|
+
runtimeCoordinator.slot = {
|
|
906
|
+
state: "starting",
|
|
907
|
+
owner: runtimeGeneration,
|
|
908
|
+
promise: runtimePromise
|
|
909
|
+
};
|
|
1182
910
|
}
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
if (!result.success) {
|
|
1193
|
-
respondError(respond, result.error || "end failed");
|
|
1194
|
-
return;
|
|
911
|
+
};
|
|
912
|
+
const commands = createVoiceCallCommandService(ensureRuntime);
|
|
913
|
+
const registerGatewayCommand = (method, handler, scope) => {
|
|
914
|
+
api.registerGatewayMethod(method, async (options) => {
|
|
915
|
+
try {
|
|
916
|
+
options.respond(true, await handler(options));
|
|
917
|
+
} catch (err) {
|
|
918
|
+
const code = err instanceof VoiceCallCommandInputError ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE;
|
|
919
|
+
options.respond(false, void 0, errorShape(code, formatErrorMessage(err)));
|
|
1195
920
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
921
|
+
}, scope);
|
|
922
|
+
};
|
|
923
|
+
registerGatewayCommand("voicecall.initiate", async ({ params }) => {
|
|
924
|
+
const message = normalizeOptionalString(params?.message);
|
|
925
|
+
if (!message) throw new VoiceCallCommandInputError("message required");
|
|
926
|
+
return await commands.initiate({
|
|
927
|
+
to: normalizeOptionalString(params?.to),
|
|
928
|
+
message,
|
|
929
|
+
mode: params?.mode === "notify" || params?.mode === "conversation" ? params.mode : void 0,
|
|
930
|
+
sessionKey: normalizeOptionalString(params?.sessionKey),
|
|
931
|
+
requesterSessionKey: normalizeOptionalString(params?.requesterSessionKey)
|
|
932
|
+
});
|
|
1200
933
|
}, VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
});
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
const call = await rt.manager.getCallFromMemoryOrStore(raw);
|
|
1213
|
-
if (!call) {
|
|
1214
|
-
respond(true, { found: false });
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
respond(true, {
|
|
1218
|
-
found: true,
|
|
1219
|
-
call: toVoiceCallStatus(call)
|
|
1220
|
-
});
|
|
1221
|
-
} catch (err) {
|
|
1222
|
-
sendError(respond, err);
|
|
1223
|
-
}
|
|
934
|
+
registerGatewayCommand("voicecall.continue", ({ params }) => commands.continueCall(normalizeOptionalString(params?.callId), normalizeOptionalString(params?.message)), VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
935
|
+
registerGatewayCommand("voicecall.continue.start", async ({ params }) => continueOperationStore.start(await commands.prepareContinue(normalizeOptionalString(params?.callId), normalizeOptionalString(params?.message))), VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
936
|
+
registerGatewayCommand("voicecall.continue.result", ({ params }) => {
|
|
937
|
+
const operationId = normalizeOptionalString(params?.operationId);
|
|
938
|
+
if (!operationId) throw new VoiceCallCommandInputError("operationId required");
|
|
939
|
+
const operation = continueOperationStore.read(operationId);
|
|
940
|
+
if (!operation.ok) throw new VoiceCallCommandInputError(operation.error);
|
|
941
|
+
return operation.payload;
|
|
1224
942
|
}, VOICE_CALL_READ_METHOD_SCOPE);
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
message: message || void 0,
|
|
1250
|
-
mode,
|
|
1251
|
-
dtmfSequence,
|
|
1252
|
-
sessionKey,
|
|
1253
|
-
...requesterSessionKey ? { requesterSessionKey } : {},
|
|
1254
|
-
...normalizedAgentId ? { agentId: normalizedAgentId } : {}
|
|
1255
|
-
});
|
|
1256
|
-
} catch (err) {
|
|
1257
|
-
sendError(respond, err);
|
|
1258
|
-
}
|
|
943
|
+
registerGatewayCommand("voicecall.speak", ({ params }) => commands.speak({
|
|
944
|
+
callId: normalizeOptionalString(params?.callId),
|
|
945
|
+
message: normalizeOptionalString(params?.message),
|
|
946
|
+
allowTwimlFallback: params?.allowTwimlFallback !== false
|
|
947
|
+
}), VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
948
|
+
registerGatewayCommand("voicecall.dtmf", ({ params }) => commands.sendDtmf(normalizeOptionalString(params?.callId), normalizeOptionalString(params?.digits)), VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
949
|
+
registerGatewayCommand("voicecall.end", ({ params }) => commands.endCall(normalizeOptionalString(params?.callId)), VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
950
|
+
registerGatewayCommand("voicecall.status", ({ params }) => commands.status(normalizeOptionalString(params?.callId) ?? normalizeOptionalString(params?.sid)), VOICE_CALL_READ_METHOD_SCOPE);
|
|
951
|
+
registerGatewayCommand("voicecall.start", async ({ params, client }) => {
|
|
952
|
+
const to = normalizeOptionalString(params?.to);
|
|
953
|
+
const requestedAgentId = normalizeOptionalString(params?.agentId);
|
|
954
|
+
const normalizedAgentId = requestedAgentId ? normalizeAgentId(requestedAgentId) : void 0;
|
|
955
|
+
const pluginOwnerId = normalizeOptionalString(client?.internal?.pluginRuntimeOwnerId);
|
|
956
|
+
if (requestedAgentId && (!pluginOwnerId || normalizedAgentId !== requestedAgentId.toLowerCase())) throw new VoiceCallCommandInputError("agentId requires a trusted plugin caller and a valid agent id");
|
|
957
|
+
if (!to) throw new VoiceCallCommandInputError("to required");
|
|
958
|
+
return await commands.initiate({
|
|
959
|
+
to,
|
|
960
|
+
message: normalizeOptionalString(params?.message),
|
|
961
|
+
mode: params?.mode === "notify" || params?.mode === "conversation" ? params.mode : void 0,
|
|
962
|
+
dtmfSequence: normalizeOptionalString(params?.dtmfSequence),
|
|
963
|
+
sessionKey: normalizeOptionalString(params?.sessionKey),
|
|
964
|
+
requesterSessionKey: normalizeOptionalString(params?.requesterSessionKey),
|
|
965
|
+
agentId: normalizedAgentId
|
|
966
|
+
});
|
|
1259
967
|
}, VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
1260
968
|
api.registerTool((toolContext) => ({
|
|
1261
969
|
name: "voice_call",
|
|
@@ -1263,94 +971,52 @@ var voice_call_default = definePluginEntry({
|
|
|
1263
971
|
description: "Make phone calls and have voice conversations via the voice-call plugin.",
|
|
1264
972
|
parameters: VoiceCallToolSchema,
|
|
1265
973
|
async execute(_toolCallId, params) {
|
|
1266
|
-
const rawParams =
|
|
1267
|
-
const
|
|
974
|
+
const rawParams = asNonArrayRecord(params);
|
|
975
|
+
const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
|
|
976
|
+
const contextAgentId = normalizeOptionalString(toolContext.agentId) ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
|
|
977
|
+
const agentId = contextAgentId ? normalizeAgentId(contextAgentId) : void 0;
|
|
1268
978
|
try {
|
|
1269
|
-
|
|
979
|
+
await ensureRuntime();
|
|
1270
980
|
if (typeof rawParams.action === "string") switch (rawParams.action) {
|
|
1271
981
|
case "initiate_call": {
|
|
1272
|
-
const message = normalizeOptionalString(rawParams.message)
|
|
1273
|
-
if (!message) throw new
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
const result = await rt.manager.initiateCall(to, void 0, {
|
|
982
|
+
const message = normalizeOptionalString(rawParams.message);
|
|
983
|
+
if (!message) throw new VoiceCallCommandInputError("message required");
|
|
984
|
+
return jsonResult(await commands.initiate({
|
|
985
|
+
to: normalizeOptionalString(rawParams.to),
|
|
1277
986
|
message,
|
|
1278
987
|
dtmfSequence: normalizeOptionalString(rawParams.dtmfSequence),
|
|
1279
988
|
mode: rawParams.mode === "notify" || rawParams.mode === "conversation" ? rawParams.mode : void 0,
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
callId: result.callId,
|
|
1285
|
-
initiated: true
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
case "continue_call": {
|
|
1289
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
1290
|
-
const message = normalizeOptionalString(rawParams.message) ?? "";
|
|
1291
|
-
if (!callId || !message) throw new Error("callId and message required");
|
|
1292
|
-
const result = await rt.manager.continueCall(callId, message);
|
|
1293
|
-
if (!result.success) throw new Error(result.error || "continue failed");
|
|
1294
|
-
return jsonResult({
|
|
1295
|
-
success: true,
|
|
1296
|
-
transcript: result.transcript
|
|
1297
|
-
});
|
|
1298
|
-
}
|
|
1299
|
-
case "speak_to_user": {
|
|
1300
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
1301
|
-
const message = normalizeOptionalString(rawParams.message) ?? "";
|
|
1302
|
-
if (!callId || !message) throw new Error("callId and message required");
|
|
1303
|
-
const result = await rt.manager.speak(callId, message);
|
|
1304
|
-
if (!result.success) throw new Error(result.error || "speak failed");
|
|
1305
|
-
return jsonResult({ success: true });
|
|
1306
|
-
}
|
|
1307
|
-
case "send_dtmf": {
|
|
1308
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
1309
|
-
const digits = normalizeOptionalString(rawParams.digits) ?? "";
|
|
1310
|
-
if (!callId || !digits) throw new Error("callId and digits required");
|
|
1311
|
-
const result = await rt.manager.sendDtmf(callId, digits);
|
|
1312
|
-
if (!result.success) throw new Error(result.error || "dtmf failed");
|
|
1313
|
-
return jsonResult({ success: true });
|
|
1314
|
-
}
|
|
1315
|
-
case "end_call": {
|
|
1316
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
1317
|
-
if (!callId) throw new Error("callId required");
|
|
1318
|
-
const result = await rt.manager.endCall(callId);
|
|
1319
|
-
if (!result.success) throw new Error(result.error || "end failed");
|
|
1320
|
-
return jsonResult({ success: true });
|
|
989
|
+
sessionKey: normalizeOptionalString(rawParams.sessionKey),
|
|
990
|
+
agentId,
|
|
991
|
+
requesterSessionKey
|
|
992
|
+
}));
|
|
1321
993
|
}
|
|
994
|
+
case "continue_call": return jsonResult(await commands.continueCall(normalizeOptionalString(rawParams.callId), normalizeOptionalString(rawParams.message)));
|
|
995
|
+
case "speak_to_user": return jsonResult(await commands.speak({
|
|
996
|
+
callId: normalizeOptionalString(rawParams.callId),
|
|
997
|
+
message: normalizeOptionalString(rawParams.message)
|
|
998
|
+
}));
|
|
999
|
+
case "send_dtmf": return jsonResult(await commands.sendDtmf(normalizeOptionalString(rawParams.callId), normalizeOptionalString(rawParams.digits)));
|
|
1000
|
+
case "end_call": return jsonResult(await commands.endCall(normalizeOptionalString(rawParams.callId)));
|
|
1322
1001
|
case "get_status": {
|
|
1323
|
-
const callId = normalizeOptionalString(rawParams.callId)
|
|
1324
|
-
if (!callId) throw new
|
|
1325
|
-
|
|
1326
|
-
return jsonResult(call ? {
|
|
1327
|
-
found: true,
|
|
1328
|
-
call: toVoiceCallStatus(call)
|
|
1329
|
-
} : { found: false });
|
|
1002
|
+
const callId = normalizeOptionalString(rawParams.callId);
|
|
1003
|
+
if (!callId) throw new VoiceCallCommandInputError("callId required");
|
|
1004
|
+
return jsonResult(await commands.status(callId));
|
|
1330
1005
|
}
|
|
1331
1006
|
}
|
|
1332
1007
|
if ((rawParams.mode ?? "call") === "status") {
|
|
1333
1008
|
const sid = normalizeOptionalString(rawParams.sid) ?? "";
|
|
1334
1009
|
if (!sid) throw new Error("sid required for status");
|
|
1335
|
-
|
|
1336
|
-
return jsonResult(call ? {
|
|
1337
|
-
found: true,
|
|
1338
|
-
call: toVoiceCallStatus(call)
|
|
1339
|
-
} : { found: false });
|
|
1010
|
+
return jsonResult(await commands.status(sid));
|
|
1340
1011
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
const result = await rt.manager.initiateCall(to, normalizeOptionalString(rawParams.sessionKey), {
|
|
1012
|
+
return jsonResult(await commands.initiate({
|
|
1013
|
+
to: normalizeOptionalString(rawParams.to),
|
|
1344
1014
|
dtmfSequence: normalizeOptionalString(rawParams.dtmfSequence),
|
|
1345
1015
|
message: normalizeOptionalString(rawParams.message),
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
return jsonResult({
|
|
1351
|
-
callId: result.callId,
|
|
1352
|
-
initiated: true
|
|
1353
|
-
});
|
|
1016
|
+
sessionKey: normalizeOptionalString(rawParams.sessionKey),
|
|
1017
|
+
agentId,
|
|
1018
|
+
requesterSessionKey
|
|
1019
|
+
}, "to required for call"));
|
|
1354
1020
|
} catch (err) {
|
|
1355
1021
|
return jsonResult({ error: formatErrorMessage(err) });
|
|
1356
1022
|
}
|
|
@@ -1370,33 +1036,46 @@ var voice_call_default = definePluginEntry({
|
|
|
1370
1036
|
id: "voicecall",
|
|
1371
1037
|
start: () => {
|
|
1372
1038
|
if (isCliOnlyProcess()) return;
|
|
1039
|
+
try {
|
|
1040
|
+
activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
if (err instanceof VoiceCallRuntimeLifecycleError) return;
|
|
1043
|
+
throw err;
|
|
1044
|
+
}
|
|
1373
1045
|
if (!config.enabled) return;
|
|
1374
1046
|
if (!validation.valid) {
|
|
1375
1047
|
api.logger.warn(`[voice-call] Runtime not started; setup incomplete: ${validation.errors.join("; ")}`);
|
|
1376
1048
|
return;
|
|
1377
1049
|
}
|
|
1378
1050
|
ensureRuntime().catch((err) => {
|
|
1051
|
+
const staleGeneration = runtimeGeneration.retired || runtimeGeneration.epoch < runtimeCoordinator.registeredEpoch;
|
|
1052
|
+
if (err instanceof VoiceCallRuntimeLifecycleError && staleGeneration) return;
|
|
1379
1053
|
api.logger.error(`[voice-call] Failed to start runtime: ${formatErrorMessage(err)}`);
|
|
1380
1054
|
});
|
|
1381
1055
|
},
|
|
1382
1056
|
stop: async () => {
|
|
1383
|
-
if (
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1057
|
+
if (runtimeGeneration.stopPromise) return await runtimeGeneration.stopPromise;
|
|
1058
|
+
runtimeGeneration.retired = true;
|
|
1059
|
+
const ownedSlot = runtimeCoordinator.slot?.owner === runtimeGeneration ? runtimeCoordinator.slot : void 0;
|
|
1060
|
+
let stoppingSlot;
|
|
1061
|
+
const stopPromise = ownedSlot?.state === "stopping" ? ownedSlot.promise : Promise.resolve().then(async () => {
|
|
1062
|
+
if (!ownedSlot) return;
|
|
1063
|
+
await (ownedSlot.state === "running" ? ownedSlot.runtime : await ownedSlot.promise).stop();
|
|
1064
|
+
});
|
|
1065
|
+
runtimeGeneration.stopPromise = stopPromise;
|
|
1066
|
+
if (ownedSlot && ownedSlot.state !== "stopping") {
|
|
1067
|
+
stoppingSlot = {
|
|
1068
|
+
state: "stopping",
|
|
1069
|
+
owner: runtimeGeneration,
|
|
1070
|
+
promise: stopPromise
|
|
1071
|
+
};
|
|
1072
|
+
if (runtimeCoordinator.slot === ownedSlot) runtimeCoordinator.slot = stoppingSlot;
|
|
1073
|
+
} else if (ownedSlot) stoppingSlot = ownedSlot;
|
|
1396
1074
|
try {
|
|
1397
1075
|
await stopPromise;
|
|
1398
1076
|
} finally {
|
|
1399
|
-
if (
|
|
1077
|
+
if (stoppingSlot && runtimeCoordinator.slot === stoppingSlot) runtimeCoordinator.slot = void 0;
|
|
1078
|
+
if (runtimeCoordinator.current === runtimeGeneration) runtimeCoordinator.current = void 0;
|
|
1400
1079
|
}
|
|
1401
1080
|
}
|
|
1402
1081
|
});
|