@openclaw/voice-call 2026.7.2-beta.5 → 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-Dxhe25Zi.js → guarded-json-api-DEDwgf4J.js} +43 -53
- package/dist/index.js +328 -637
- package/dist/{mock-YsvBTX-7.js → mock-BCq0q36l.js} +2 -3
- package/dist/{plivo-B-KvPVji.js → plivo-DbI17uAc.js} +45 -8
- package/dist/{realtime-handler-hIVPEZxN.js → realtime-handler-uXR0QgVa.js} +366 -109
- package/dist/{response-generator-z6M9229t.js → response-generator-D-hkzo5Y.js} +13 -5
- package/dist/{runtime-entry-CQtitvRd.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-BG56lL7z.js → telnyx-sIS_WgnY.js} +3 -2
- package/dist/{twilio-B3FQ_P9x.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,20 @@
|
|
|
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";
|
|
17
|
+
import { StringDecoder } from "node:string_decoder";
|
|
16
18
|
import { format } from "node:util";
|
|
17
19
|
//#region extensions/voice-call/src/cli.ts
|
|
18
20
|
const VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS = 5e3;
|
|
@@ -429,22 +431,33 @@ function registerVoiceCallCli(params) {
|
|
|
429
431
|
}
|
|
430
432
|
};
|
|
431
433
|
if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") {
|
|
432
|
-
const initial = fs.readFileSync(file
|
|
433
|
-
|
|
434
|
+
const initial = fs.readFileSync(file);
|
|
435
|
+
let decoder = new StringDecoder("utf8");
|
|
436
|
+
const initialLines = decoder.write(initial).split("\n");
|
|
437
|
+
let pendingLine = initialLines.pop() ?? "";
|
|
438
|
+
const lines = initialLines.filter(Boolean);
|
|
434
439
|
for (const line of lines.slice(Math.max(0, lines.length - since))) writeStdoutLine(line);
|
|
435
|
-
let offset =
|
|
440
|
+
let offset = initial.length;
|
|
441
|
+
let lastObservedSize = initial.length;
|
|
436
442
|
for (;;) {
|
|
437
443
|
try {
|
|
438
444
|
const stat = fs.statSync(file);
|
|
439
|
-
if (stat.size <
|
|
445
|
+
if (stat.size < lastObservedSize) {
|
|
446
|
+
offset = 0;
|
|
447
|
+
decoder = new StringDecoder("utf8");
|
|
448
|
+
pendingLine = "";
|
|
449
|
+
}
|
|
450
|
+
lastObservedSize = stat.size;
|
|
440
451
|
if (stat.size > offset) {
|
|
441
452
|
const fd = fs.openSync(file, "r");
|
|
442
453
|
try {
|
|
443
454
|
const buf = Buffer.alloc(stat.size - offset);
|
|
444
|
-
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
445
|
-
offset
|
|
446
|
-
const text = buf.
|
|
447
|
-
|
|
455
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, offset);
|
|
456
|
+
offset += bytesRead;
|
|
457
|
+
const text = decoder.write(buf.subarray(0, bytesRead));
|
|
458
|
+
const completeLines = `${pendingLine}${text}`.split("\n");
|
|
459
|
+
pendingLine = completeLines.pop() ?? "";
|
|
460
|
+
for (const line of completeLines.filter(Boolean)) writeStdoutLine(line);
|
|
448
461
|
} finally {
|
|
449
462
|
fs.closeSync(fd);
|
|
450
463
|
}
|
|
@@ -531,6 +544,126 @@ function registerVoiceCallCli(params) {
|
|
|
531
544
|
});
|
|
532
545
|
}
|
|
533
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
|
|
534
667
|
//#region extensions/voice-call/src/gateway-continue-operation.ts
|
|
535
668
|
const VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS = 3e4;
|
|
536
669
|
const VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS = 300 * 1e3;
|
|
@@ -557,21 +690,9 @@ function createVoiceCallContinueOperationStore(params) {
|
|
|
557
690
|
startedAtMs,
|
|
558
691
|
pollTimeoutMs
|
|
559
692
|
});
|
|
560
|
-
request.
|
|
693
|
+
request.run().then((result) => {
|
|
561
694
|
const current = operations.get(operationId);
|
|
562
695
|
if (!current || current.status !== "pending") return;
|
|
563
|
-
if (!result.success) {
|
|
564
|
-
operations.set(operationId, {
|
|
565
|
-
operationId,
|
|
566
|
-
status: "failed",
|
|
567
|
-
callId: request.callId,
|
|
568
|
-
startedAtMs,
|
|
569
|
-
completedAtMs: Date.now(),
|
|
570
|
-
pollTimeoutMs,
|
|
571
|
-
error: result.error || "continue failed"
|
|
572
|
-
});
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
696
|
operations.set(operationId, {
|
|
576
697
|
operationId,
|
|
577
698
|
status: "completed",
|
|
@@ -649,223 +770,15 @@ function createVoiceCallContinueOperationStore(params) {
|
|
|
649
770
|
//#region extensions/voice-call/index.ts
|
|
650
771
|
const VOICE_CALL_WRITE_METHOD_SCOPE = { scope: "operator.write" };
|
|
651
772
|
const VOICE_CALL_READ_METHOD_SCOPE = { scope: "operator.read" };
|
|
652
|
-
const voiceCallConfigSchema = {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
},
|
|
662
|
-
uiHints: {
|
|
663
|
-
provider: {
|
|
664
|
-
label: "Provider",
|
|
665
|
-
help: "Use twilio, telnyx, or mock for dev/no-network."
|
|
666
|
-
},
|
|
667
|
-
fromNumber: {
|
|
668
|
-
label: "From Number",
|
|
669
|
-
placeholder: "+15550001234"
|
|
670
|
-
},
|
|
671
|
-
toNumber: {
|
|
672
|
-
label: "Default To Number",
|
|
673
|
-
placeholder: "+15550001234"
|
|
674
|
-
},
|
|
675
|
-
inboundPolicy: { label: "Inbound Policy" },
|
|
676
|
-
allowFrom: { label: "Inbound Allowlist" },
|
|
677
|
-
inboundGreeting: {
|
|
678
|
-
label: "Inbound Greeting",
|
|
679
|
-
advanced: true
|
|
680
|
-
},
|
|
681
|
-
numbers: {
|
|
682
|
-
label: "Per-number Routing",
|
|
683
|
-
help: "Inbound overrides keyed by dialed E.164 number.",
|
|
684
|
-
advanced: true
|
|
685
|
-
},
|
|
686
|
-
"telnyx.apiKey": {
|
|
687
|
-
label: "Telnyx API Key",
|
|
688
|
-
sensitive: true
|
|
689
|
-
},
|
|
690
|
-
"telnyx.connectionId": { label: "Telnyx Connection ID" },
|
|
691
|
-
"telnyx.publicKey": {
|
|
692
|
-
label: "Telnyx Public Key",
|
|
693
|
-
sensitive: true
|
|
694
|
-
},
|
|
695
|
-
"twilio.accountSid": { label: "Twilio Account SID" },
|
|
696
|
-
"twilio.authToken": {
|
|
697
|
-
label: "Twilio Auth Token",
|
|
698
|
-
sensitive: true
|
|
699
|
-
},
|
|
700
|
-
"twilio.region": {
|
|
701
|
-
label: "Twilio Region",
|
|
702
|
-
advanced: true
|
|
703
|
-
},
|
|
704
|
-
"outbound.defaultMode": { label: "Default Call Mode" },
|
|
705
|
-
"outbound.notifyHangupDelaySec": {
|
|
706
|
-
label: "Notify Hangup Delay (sec)",
|
|
707
|
-
advanced: true
|
|
708
|
-
},
|
|
709
|
-
"serve.port": { label: "Webhook Port" },
|
|
710
|
-
"serve.bind": { label: "Webhook Bind" },
|
|
711
|
-
"serve.path": { label: "Webhook Path" },
|
|
712
|
-
"tailscale.mode": {
|
|
713
|
-
label: "Tailscale Mode",
|
|
714
|
-
advanced: true
|
|
715
|
-
},
|
|
716
|
-
"tailscale.path": {
|
|
717
|
-
label: "Tailscale Path",
|
|
718
|
-
advanced: true
|
|
719
|
-
},
|
|
720
|
-
"tunnel.provider": {
|
|
721
|
-
label: "Tunnel Provider",
|
|
722
|
-
advanced: true
|
|
723
|
-
},
|
|
724
|
-
"tunnel.ngrokAuthToken": {
|
|
725
|
-
label: "ngrok Auth Token",
|
|
726
|
-
sensitive: true,
|
|
727
|
-
advanced: true
|
|
728
|
-
},
|
|
729
|
-
"tunnel.ngrokDomain": {
|
|
730
|
-
label: "ngrok Domain",
|
|
731
|
-
advanced: true
|
|
732
|
-
},
|
|
733
|
-
"tunnel.allowNgrokFreeTierLoopbackBypass": {
|
|
734
|
-
label: "Allow ngrok Free Tier (Loopback Bypass)",
|
|
735
|
-
advanced: true
|
|
736
|
-
},
|
|
737
|
-
"streaming.enabled": {
|
|
738
|
-
label: "Enable Streaming",
|
|
739
|
-
help: "Classic streaming transcription currently requires the Twilio call provider.",
|
|
740
|
-
advanced: true
|
|
741
|
-
},
|
|
742
|
-
"streaming.provider": {
|
|
743
|
-
label: "Streaming Provider",
|
|
744
|
-
help: "Uses the first registered realtime transcription provider when unset.",
|
|
745
|
-
advanced: true
|
|
746
|
-
},
|
|
747
|
-
"streaming.providers": {
|
|
748
|
-
label: "Streaming Provider Config",
|
|
749
|
-
advanced: true
|
|
750
|
-
},
|
|
751
|
-
"streaming.streamPath": {
|
|
752
|
-
label: "Media Stream Path",
|
|
753
|
-
advanced: true
|
|
754
|
-
},
|
|
755
|
-
"realtime.enabled": {
|
|
756
|
-
label: "Enable Realtime Voice",
|
|
757
|
-
advanced: true
|
|
758
|
-
},
|
|
759
|
-
"realtime.provider": {
|
|
760
|
-
label: "Realtime Voice Provider",
|
|
761
|
-
help: "Uses the first registered realtime voice provider when unset.",
|
|
762
|
-
advanced: true
|
|
763
|
-
},
|
|
764
|
-
"realtime.streamPath": {
|
|
765
|
-
label: "Realtime Stream Path",
|
|
766
|
-
advanced: true
|
|
767
|
-
},
|
|
768
|
-
"realtime.instructions": {
|
|
769
|
-
label: "Realtime Instructions",
|
|
770
|
-
advanced: true
|
|
771
|
-
},
|
|
772
|
-
"realtime.toolPolicy": {
|
|
773
|
-
label: "Realtime Tool Policy",
|
|
774
|
-
help: "Controls the shared openclaw_agent_consult tool.",
|
|
775
|
-
advanced: true
|
|
776
|
-
},
|
|
777
|
-
"realtime.consultPolicy": {
|
|
778
|
-
label: "Realtime Consult Policy",
|
|
779
|
-
help: "Guides when the realtime voice model should call openclaw_agent_consult.",
|
|
780
|
-
advanced: true
|
|
781
|
-
},
|
|
782
|
-
"realtime.fastContext.enabled": {
|
|
783
|
-
label: "Enable Fast Realtime Context",
|
|
784
|
-
help: "Searches memory/session context before the full consult agent.",
|
|
785
|
-
advanced: true
|
|
786
|
-
},
|
|
787
|
-
"realtime.fastContext.timeoutMs": {
|
|
788
|
-
label: "Fast Context Timeout",
|
|
789
|
-
advanced: true
|
|
790
|
-
},
|
|
791
|
-
"realtime.fastContext.maxResults": {
|
|
792
|
-
label: "Fast Context Result Limit",
|
|
793
|
-
advanced: true
|
|
794
|
-
},
|
|
795
|
-
"realtime.fastContext.sources": {
|
|
796
|
-
label: "Fast Context Sources",
|
|
797
|
-
advanced: true
|
|
798
|
-
},
|
|
799
|
-
"realtime.fastContext.fallbackToConsult": {
|
|
800
|
-
label: "Fallback To Full Consult",
|
|
801
|
-
advanced: true
|
|
802
|
-
},
|
|
803
|
-
"realtime.agentContext.enabled": {
|
|
804
|
-
label: "Enable Agent Voice Context",
|
|
805
|
-
help: "Injects a compact agent identity and workspace context capsule into realtime voice instructions.",
|
|
806
|
-
advanced: true
|
|
807
|
-
},
|
|
808
|
-
"realtime.agentContext.maxChars": {
|
|
809
|
-
label: "Agent Voice Context Limit",
|
|
810
|
-
advanced: true
|
|
811
|
-
},
|
|
812
|
-
"realtime.agentContext.includeIdentity": {
|
|
813
|
-
label: "Include Agent Identity",
|
|
814
|
-
advanced: true
|
|
815
|
-
},
|
|
816
|
-
"realtime.agentContext.includeWorkspaceFiles": {
|
|
817
|
-
label: "Include Agent Workspace Files",
|
|
818
|
-
advanced: true
|
|
819
|
-
},
|
|
820
|
-
"realtime.agentContext.files": {
|
|
821
|
-
label: "Agent Voice Context Files",
|
|
822
|
-
advanced: true
|
|
823
|
-
},
|
|
824
|
-
"realtime.providers": {
|
|
825
|
-
label: "Realtime Provider Config",
|
|
826
|
-
advanced: true
|
|
827
|
-
},
|
|
828
|
-
"tts.provider": {
|
|
829
|
-
label: "TTS Provider Override",
|
|
830
|
-
help: "Deep-merges with tts (Microsoft is ignored for calls).",
|
|
831
|
-
advanced: true
|
|
832
|
-
},
|
|
833
|
-
"tts.providers": {
|
|
834
|
-
label: "TTS Provider Config",
|
|
835
|
-
advanced: true
|
|
836
|
-
},
|
|
837
|
-
publicUrl: {
|
|
838
|
-
label: "Public Webhook URL",
|
|
839
|
-
advanced: true
|
|
840
|
-
},
|
|
841
|
-
skipSignatureVerification: {
|
|
842
|
-
label: "Skip Signature Verification",
|
|
843
|
-
advanced: true
|
|
844
|
-
},
|
|
845
|
-
store: {
|
|
846
|
-
label: "Call Log Store Path",
|
|
847
|
-
advanced: true
|
|
848
|
-
},
|
|
849
|
-
agentId: {
|
|
850
|
-
label: "Response Agent ID",
|
|
851
|
-
help: "Agent workspace used for voice response generation. Defaults to \"main\".",
|
|
852
|
-
advanced: true
|
|
853
|
-
},
|
|
854
|
-
responseModel: {
|
|
855
|
-
label: "Response Model",
|
|
856
|
-
help: "Optional override. Falls back to the runtime default model when unset.",
|
|
857
|
-
advanced: true
|
|
858
|
-
},
|
|
859
|
-
responseSystemPrompt: {
|
|
860
|
-
label: "Response System Prompt",
|
|
861
|
-
advanced: true
|
|
862
|
-
},
|
|
863
|
-
responseTimeoutMs: {
|
|
864
|
-
label: "Response Timeout (ms)",
|
|
865
|
-
advanced: true
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
};
|
|
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
|
+
} };
|
|
869
782
|
const VoiceCallToolSchema = Type.Union([
|
|
870
783
|
Type.Object({
|
|
871
784
|
action: Type.Literal("initiate_call"),
|
|
@@ -873,7 +786,6 @@ const VoiceCallToolSchema = Type.Union([
|
|
|
873
786
|
message: Type.String({ description: "Intro message" }),
|
|
874
787
|
mode: Type.Optional(Type.Union([Type.Literal("notify"), Type.Literal("conversation")])),
|
|
875
788
|
sessionKey: Type.Optional(Type.String({ description: "OpenClaw session key for the call" })),
|
|
876
|
-
requesterSessionKey: Type.Optional(Type.String({ description: "OpenClaw session key that initiated the call" })),
|
|
877
789
|
dtmfSequence: Type.Optional(Type.String({ description: "DTMF digits to play before connect" }))
|
|
878
790
|
}),
|
|
879
791
|
Type.Object({
|
|
@@ -905,38 +817,28 @@ const VoiceCallToolSchema = Type.Union([
|
|
|
905
817
|
sid: Type.Optional(Type.String({ description: "Call SID" })),
|
|
906
818
|
message: Type.Optional(Type.String({ description: "Optional intro message" })),
|
|
907
819
|
sessionKey: Type.Optional(Type.String({ description: "OpenClaw session key for the call" })),
|
|
908
|
-
requesterSessionKey: Type.Optional(Type.String({ description: "OpenClaw session key that initiated the call" })),
|
|
909
820
|
dtmfSequence: Type.Optional(Type.String({ description: "DTMF digits to play before connect" }))
|
|
910
821
|
})
|
|
911
822
|
]);
|
|
912
|
-
function asParamRecord(params) {
|
|
913
|
-
return params && typeof params === "object" && !Array.isArray(params) ? params : {};
|
|
914
|
-
}
|
|
915
823
|
function isCliOnlyProcess() {
|
|
916
824
|
return process.env.OPENCLAW_CLI === "1" && !process.argv.slice(2).includes("gateway");
|
|
917
825
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
startedAt: call.startedAt,
|
|
926
|
-
...call.answeredAt !== void 0 ? { answeredAt: call.answeredAt } : {},
|
|
927
|
-
...call.endedAt !== void 0 ? { endedAt: call.endedAt } : {},
|
|
928
|
-
...call.endReason !== void 0 ? { endReason: call.endReason } : {}
|
|
929
|
-
};
|
|
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
|
+
}));
|
|
930
833
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
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
|
+
}
|
|
940
842
|
}
|
|
941
843
|
var voice_call_default = definePluginEntry({
|
|
942
844
|
id: "voice-call",
|
|
@@ -946,304 +848,122 @@ var voice_call_default = definePluginEntry({
|
|
|
946
848
|
register(api) {
|
|
947
849
|
const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
|
|
948
850
|
const validation = validateProviderConfig(config);
|
|
949
|
-
const
|
|
851
|
+
const runtimeCoordinator = getVoiceCallRuntimeCoordinator();
|
|
852
|
+
const runtimeGeneration = api.registrationMode !== "full" && runtimeCoordinator.current ? runtimeCoordinator.current : {
|
|
853
|
+
epoch: ++runtimeCoordinator.epochCounter,
|
|
854
|
+
retired: false
|
|
855
|
+
};
|
|
950
856
|
const continueOperationStore = createVoiceCallContinueOperationStore({
|
|
951
857
|
config,
|
|
952
858
|
coreConfig: api.config
|
|
953
859
|
});
|
|
954
860
|
const ensureRuntime = async () => {
|
|
861
|
+
activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
|
|
955
862
|
if (!config.enabled) throw new Error("Voice call disabled in plugin config");
|
|
956
863
|
if (!validation.valid) throw new Error(validation.errors.join("; "));
|
|
957
864
|
while (true) {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
config,
|
|
968
|
-
coreConfig: api.config,
|
|
969
|
-
fullConfig: api.config,
|
|
970
|
-
agentRuntime: api.runtime.agent,
|
|
971
|
-
stateRuntime: api.runtime.state,
|
|
972
|
-
ttsRuntime: api.runtime.tts,
|
|
973
|
-
logger: api.logger
|
|
974
|
-
});
|
|
975
|
-
runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = runtimePromise;
|
|
976
|
-
}
|
|
977
|
-
try {
|
|
978
|
-
const createdRuntime = await runtimePromise;
|
|
979
|
-
if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) continue;
|
|
980
|
-
if (runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] !== runtimePromise) continue;
|
|
981
|
-
runtimeState[VOICE_CALL_RUNTIME_KEY] = createdRuntime;
|
|
982
|
-
return createdRuntime;
|
|
983
|
-
} catch (err) {
|
|
984
|
-
if (runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] === runtimePromise) {
|
|
985
|
-
runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = null;
|
|
986
|
-
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");
|
|
987
874
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
const respondError = (respond, message, code = ErrorCodes.UNAVAILABLE) => {
|
|
993
|
-
respond(false, void 0, errorShape(code, message));
|
|
994
|
-
};
|
|
995
|
-
const sendError = (respond, err) => {
|
|
996
|
-
respondError(respond, formatErrorMessage(err));
|
|
997
|
-
};
|
|
998
|
-
const describeHistoricalCall = async (rt, callId) => {
|
|
999
|
-
const call = await rt.manager.getCallFromMemoryOrStore(callId);
|
|
1000
|
-
if (!call) return;
|
|
1001
|
-
const endedAt = timestampMsToIsoString(call.endedAt);
|
|
1002
|
-
return `call is not active (${[
|
|
1003
|
-
`last state=${call.state}`,
|
|
1004
|
-
call.endReason ? `endReason=${call.endReason}` : void 0,
|
|
1005
|
-
endedAt ? `endedAt=${endedAt}` : void 0
|
|
1006
|
-
].filter(Boolean).join(", ")})`;
|
|
1007
|
-
};
|
|
1008
|
-
const resolveCallMessageRequest = async (params) => {
|
|
1009
|
-
const callId = normalizeOptionalString(params?.callId) ?? "";
|
|
1010
|
-
const message = normalizeOptionalString(params?.message) ?? "";
|
|
1011
|
-
if (!callId || !message) return { error: "callId and message required" };
|
|
1012
|
-
const rt = await ensureRuntime();
|
|
1013
|
-
const activeCall = rt.manager.getCall(callId) ?? rt.manager.getCallByProviderCallId(callId);
|
|
1014
|
-
if (activeCall) return {
|
|
1015
|
-
rt,
|
|
1016
|
-
callId: activeCall.callId,
|
|
1017
|
-
message
|
|
1018
|
-
};
|
|
1019
|
-
return { error: await describeHistoricalCall(rt, callId) ?? "Call not found" };
|
|
1020
|
-
};
|
|
1021
|
-
const initiateCallAndRespond = async (params) => {
|
|
1022
|
-
const result = await params.rt.manager.initiateCall(params.to, params.sessionKey, {
|
|
1023
|
-
message: params.message,
|
|
1024
|
-
mode: params.mode,
|
|
1025
|
-
dtmfSequence: params.dtmfSequence,
|
|
1026
|
-
...params.requesterSessionKey ? { requesterSessionKey: params.requesterSessionKey } : {},
|
|
1027
|
-
...params.agentId ? { agentId: params.agentId } : {}
|
|
1028
|
-
});
|
|
1029
|
-
if (!result.success) {
|
|
1030
|
-
respondError(params.respond, result.error || "initiate failed");
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
params.respond(true, {
|
|
1034
|
-
callId: result.callId,
|
|
1035
|
-
initiated: true
|
|
1036
|
-
});
|
|
1037
|
-
};
|
|
1038
|
-
const respondToCallMessageAction = async (params) => {
|
|
1039
|
-
const request = await resolveCallMessageRequest(params.requestParams);
|
|
1040
|
-
if ("error" in request) {
|
|
1041
|
-
respondError(params.respond, request.error ?? "callId and message required", ErrorCodes.INVALID_REQUEST);
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
const result = await params.action(request);
|
|
1045
|
-
if (!result.success) {
|
|
1046
|
-
respondError(params.respond, result.error || params.failure);
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
params.respond(true, params.includeTranscript ? {
|
|
1050
|
-
success: true,
|
|
1051
|
-
transcript: result.transcript
|
|
1052
|
-
} : { success: true });
|
|
1053
|
-
};
|
|
1054
|
-
api.registerGatewayMethod("voicecall.initiate", async ({ params, respond }) => {
|
|
1055
|
-
try {
|
|
1056
|
-
const message = normalizeOptionalString(params?.message) ?? "";
|
|
1057
|
-
if (!message) {
|
|
1058
|
-
respondError(respond, "message required", ErrorCodes.INVALID_REQUEST);
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
const rt = await ensureRuntime();
|
|
1062
|
-
const to = normalizeOptionalString(params?.to) ?? rt.config.toNumber;
|
|
1063
|
-
if (!to) {
|
|
1064
|
-
respondError(respond, "to required", ErrorCodes.INVALID_REQUEST);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
const mode = params?.mode === "notify" || params?.mode === "conversation" ? params.mode : void 0;
|
|
1068
|
-
await initiateCallAndRespond({
|
|
1069
|
-
rt,
|
|
1070
|
-
respond,
|
|
1071
|
-
to,
|
|
1072
|
-
message,
|
|
1073
|
-
mode,
|
|
1074
|
-
sessionKey: normalizeOptionalString(params?.sessionKey),
|
|
1075
|
-
requesterSessionKey: normalizeOptionalString(params?.requesterSessionKey)
|
|
1076
|
-
});
|
|
1077
|
-
} catch (err) {
|
|
1078
|
-
sendError(respond, err);
|
|
1079
|
-
}
|
|
1080
|
-
}, VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
1081
|
-
api.registerGatewayMethod("voicecall.continue", async ({ params, respond }) => {
|
|
1082
|
-
try {
|
|
1083
|
-
await respondToCallMessageAction({
|
|
1084
|
-
requestParams: params,
|
|
1085
|
-
respond,
|
|
1086
|
-
action: (request) => request.rt.manager.continueCall(request.callId, request.message),
|
|
1087
|
-
failure: "continue failed",
|
|
1088
|
-
includeTranscript: true
|
|
1089
|
-
});
|
|
1090
|
-
} catch (err) {
|
|
1091
|
-
sendError(respond, err);
|
|
1092
|
-
}
|
|
1093
|
-
}, VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
1094
|
-
api.registerGatewayMethod("voicecall.continue.start", async ({ params, respond }) => {
|
|
1095
|
-
try {
|
|
1096
|
-
const request = await resolveCallMessageRequest(params);
|
|
1097
|
-
if ("error" in request) {
|
|
1098
|
-
respondError(respond, request.error ?? "callId and message required", ErrorCodes.INVALID_REQUEST);
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
respond(true, continueOperationStore.start(request));
|
|
1102
|
-
} catch (err) {
|
|
1103
|
-
sendError(respond, err);
|
|
1104
|
-
}
|
|
1105
|
-
}, VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
1106
|
-
api.registerGatewayMethod("voicecall.continue.result", async ({ params, respond }) => {
|
|
1107
|
-
try {
|
|
1108
|
-
const operationId = normalizeOptionalString(params?.operationId) ?? "";
|
|
1109
|
-
if (!operationId) {
|
|
1110
|
-
respondError(respond, "operationId required", ErrorCodes.INVALID_REQUEST);
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
const operation = continueOperationStore.read(operationId);
|
|
1114
|
-
if (!operation.ok) {
|
|
1115
|
-
respondError(respond, operation.error, ErrorCodes.INVALID_REQUEST);
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
respond(true, operation.payload);
|
|
1119
|
-
} catch (err) {
|
|
1120
|
-
sendError(respond, err);
|
|
1121
|
-
}
|
|
1122
|
-
}, VOICE_CALL_READ_METHOD_SCOPE);
|
|
1123
|
-
api.registerGatewayMethod("voicecall.speak", async ({ params, respond }) => {
|
|
1124
|
-
try {
|
|
1125
|
-
const request = await resolveCallMessageRequest(params);
|
|
1126
|
-
if ("error" in request) {
|
|
1127
|
-
respondError(respond, request.error ?? "callId and message required", ErrorCodes.INVALID_REQUEST);
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
if (request.rt.config.realtime.enabled) {
|
|
1131
|
-
const realtimeResult = request.rt.webhookServer.speakRealtime(request.callId, request.message);
|
|
1132
|
-
if (realtimeResult.success) {
|
|
1133
|
-
respond(true, { success: true });
|
|
1134
|
-
return;
|
|
875
|
+
if (slot.state === "running") return slot.runtime;
|
|
876
|
+
if (slot.state === "stopping") {
|
|
877
|
+
await slot.promise;
|
|
878
|
+
continue;
|
|
1135
879
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
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;
|
|
1142
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;
|
|
1143
895
|
}
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (!callId || !digits) {
|
|
1159
|
-
respondError(respond, "callId and digits required", ErrorCodes.INVALID_REQUEST);
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
const result = await (await ensureRuntime()).manager.sendDtmf(callId, digits);
|
|
1163
|
-
if (!result.success) {
|
|
1164
|
-
respondError(respond, result.error || "dtmf failed");
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
respond(true, { success: true });
|
|
1168
|
-
} catch (err) {
|
|
1169
|
-
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
|
+
};
|
|
1170
910
|
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
if (!result.success) {
|
|
1181
|
-
respondError(respond, result.error || "end failed");
|
|
1182
|
-
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)));
|
|
1183
920
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
+
});
|
|
1188
933
|
}, VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
});
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
const call = await rt.manager.getCallFromMemoryOrStore(raw);
|
|
1201
|
-
if (!call) {
|
|
1202
|
-
respond(true, { found: false });
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
respond(true, {
|
|
1206
|
-
found: true,
|
|
1207
|
-
call: toVoiceCallStatus(call)
|
|
1208
|
-
});
|
|
1209
|
-
} catch (err) {
|
|
1210
|
-
sendError(respond, err);
|
|
1211
|
-
}
|
|
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;
|
|
1212
942
|
}, VOICE_CALL_READ_METHOD_SCOPE);
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
message: message || void 0,
|
|
1238
|
-
mode,
|
|
1239
|
-
dtmfSequence,
|
|
1240
|
-
sessionKey,
|
|
1241
|
-
...requesterSessionKey ? { requesterSessionKey } : {},
|
|
1242
|
-
...normalizedAgentId ? { agentId: normalizedAgentId } : {}
|
|
1243
|
-
});
|
|
1244
|
-
} catch (err) {
|
|
1245
|
-
sendError(respond, err);
|
|
1246
|
-
}
|
|
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
|
+
});
|
|
1247
967
|
}, VOICE_CALL_WRITE_METHOD_SCOPE);
|
|
1248
968
|
api.registerTool((toolContext) => ({
|
|
1249
969
|
name: "voice_call",
|
|
@@ -1251,94 +971,52 @@ var voice_call_default = definePluginEntry({
|
|
|
1251
971
|
description: "Make phone calls and have voice conversations via the voice-call plugin.",
|
|
1252
972
|
parameters: VoiceCallToolSchema,
|
|
1253
973
|
async execute(_toolCallId, params) {
|
|
1254
|
-
const rawParams =
|
|
1255
|
-
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;
|
|
1256
978
|
try {
|
|
1257
|
-
|
|
979
|
+
await ensureRuntime();
|
|
1258
980
|
if (typeof rawParams.action === "string") switch (rawParams.action) {
|
|
1259
981
|
case "initiate_call": {
|
|
1260
|
-
const message = normalizeOptionalString(rawParams.message)
|
|
1261
|
-
if (!message) throw new
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
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),
|
|
1265
986
|
message,
|
|
1266
987
|
dtmfSequence: normalizeOptionalString(rawParams.dtmfSequence),
|
|
1267
988
|
mode: rawParams.mode === "notify" || rawParams.mode === "conversation" ? rawParams.mode : void 0,
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
callId: result.callId,
|
|
1273
|
-
initiated: true
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
case "continue_call": {
|
|
1277
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
1278
|
-
const message = normalizeOptionalString(rawParams.message) ?? "";
|
|
1279
|
-
if (!callId || !message) throw new Error("callId and message required");
|
|
1280
|
-
const result = await rt.manager.continueCall(callId, message);
|
|
1281
|
-
if (!result.success) throw new Error(result.error || "continue failed");
|
|
1282
|
-
return jsonResult({
|
|
1283
|
-
success: true,
|
|
1284
|
-
transcript: result.transcript
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
case "speak_to_user": {
|
|
1288
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
1289
|
-
const message = normalizeOptionalString(rawParams.message) ?? "";
|
|
1290
|
-
if (!callId || !message) throw new Error("callId and message required");
|
|
1291
|
-
const result = await rt.manager.speak(callId, message);
|
|
1292
|
-
if (!result.success) throw new Error(result.error || "speak failed");
|
|
1293
|
-
return jsonResult({ success: true });
|
|
1294
|
-
}
|
|
1295
|
-
case "send_dtmf": {
|
|
1296
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
1297
|
-
const digits = normalizeOptionalString(rawParams.digits) ?? "";
|
|
1298
|
-
if (!callId || !digits) throw new Error("callId and digits required");
|
|
1299
|
-
const result = await rt.manager.sendDtmf(callId, digits);
|
|
1300
|
-
if (!result.success) throw new Error(result.error || "dtmf failed");
|
|
1301
|
-
return jsonResult({ success: true });
|
|
1302
|
-
}
|
|
1303
|
-
case "end_call": {
|
|
1304
|
-
const callId = normalizeOptionalString(rawParams.callId) ?? "";
|
|
1305
|
-
if (!callId) throw new Error("callId required");
|
|
1306
|
-
const result = await rt.manager.endCall(callId);
|
|
1307
|
-
if (!result.success) throw new Error(result.error || "end failed");
|
|
1308
|
-
return jsonResult({ success: true });
|
|
989
|
+
sessionKey: normalizeOptionalString(rawParams.sessionKey),
|
|
990
|
+
agentId,
|
|
991
|
+
requesterSessionKey
|
|
992
|
+
}));
|
|
1309
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)));
|
|
1310
1001
|
case "get_status": {
|
|
1311
|
-
const callId = normalizeOptionalString(rawParams.callId)
|
|
1312
|
-
if (!callId) throw new
|
|
1313
|
-
|
|
1314
|
-
return jsonResult(call ? {
|
|
1315
|
-
found: true,
|
|
1316
|
-
call: toVoiceCallStatus(call)
|
|
1317
|
-
} : { found: false });
|
|
1002
|
+
const callId = normalizeOptionalString(rawParams.callId);
|
|
1003
|
+
if (!callId) throw new VoiceCallCommandInputError("callId required");
|
|
1004
|
+
return jsonResult(await commands.status(callId));
|
|
1318
1005
|
}
|
|
1319
1006
|
}
|
|
1320
1007
|
if ((rawParams.mode ?? "call") === "status") {
|
|
1321
1008
|
const sid = normalizeOptionalString(rawParams.sid) ?? "";
|
|
1322
1009
|
if (!sid) throw new Error("sid required for status");
|
|
1323
|
-
|
|
1324
|
-
return jsonResult(call ? {
|
|
1325
|
-
found: true,
|
|
1326
|
-
call: toVoiceCallStatus(call)
|
|
1327
|
-
} : { found: false });
|
|
1010
|
+
return jsonResult(await commands.status(sid));
|
|
1328
1011
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
const result = await rt.manager.initiateCall(to, normalizeOptionalString(rawParams.sessionKey), {
|
|
1012
|
+
return jsonResult(await commands.initiate({
|
|
1013
|
+
to: normalizeOptionalString(rawParams.to),
|
|
1332
1014
|
dtmfSequence: normalizeOptionalString(rawParams.dtmfSequence),
|
|
1333
1015
|
message: normalizeOptionalString(rawParams.message),
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
return jsonResult({
|
|
1339
|
-
callId: result.callId,
|
|
1340
|
-
initiated: true
|
|
1341
|
-
});
|
|
1016
|
+
sessionKey: normalizeOptionalString(rawParams.sessionKey),
|
|
1017
|
+
agentId,
|
|
1018
|
+
requesterSessionKey
|
|
1019
|
+
}, "to required for call"));
|
|
1342
1020
|
} catch (err) {
|
|
1343
1021
|
return jsonResult({ error: formatErrorMessage(err) });
|
|
1344
1022
|
}
|
|
@@ -1358,33 +1036,46 @@ var voice_call_default = definePluginEntry({
|
|
|
1358
1036
|
id: "voicecall",
|
|
1359
1037
|
start: () => {
|
|
1360
1038
|
if (isCliOnlyProcess()) return;
|
|
1039
|
+
try {
|
|
1040
|
+
activateVoiceCallRuntimeGeneration(runtimeCoordinator, runtimeGeneration);
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
if (err instanceof VoiceCallRuntimeLifecycleError) return;
|
|
1043
|
+
throw err;
|
|
1044
|
+
}
|
|
1361
1045
|
if (!config.enabled) return;
|
|
1362
1046
|
if (!validation.valid) {
|
|
1363
1047
|
api.logger.warn(`[voice-call] Runtime not started; setup incomplete: ${validation.errors.join("; ")}`);
|
|
1364
1048
|
return;
|
|
1365
1049
|
}
|
|
1366
1050
|
ensureRuntime().catch((err) => {
|
|
1051
|
+
const staleGeneration = runtimeGeneration.retired || runtimeGeneration.epoch < runtimeCoordinator.registeredEpoch;
|
|
1052
|
+
if (err instanceof VoiceCallRuntimeLifecycleError && staleGeneration) return;
|
|
1367
1053
|
api.logger.error(`[voice-call] Failed to start runtime: ${formatErrorMessage(err)}`);
|
|
1368
1054
|
});
|
|
1369
1055
|
},
|
|
1370
1056
|
stop: async () => {
|
|
1371
|
-
if (
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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;
|
|
1384
1074
|
try {
|
|
1385
1075
|
await stopPromise;
|
|
1386
1076
|
} finally {
|
|
1387
|
-
if (
|
|
1077
|
+
if (stoppingSlot && runtimeCoordinator.slot === stoppingSlot) runtimeCoordinator.slot = void 0;
|
|
1078
|
+
if (runtimeCoordinator.current === runtimeGeneration) runtimeCoordinator.current = void 0;
|
|
1388
1079
|
}
|
|
1389
1080
|
}
|
|
1390
1081
|
});
|