@openclaw/voice-call 2026.7.2-beta.5 → 2026.7.33
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/README.md +3 -2
- package/dist/.setup/config-BBpso-fA.mjs +674 -0
- package/dist/.setup/config-compat-CYRE-SSo.mjs +156 -0
- package/dist/{guarded-json-api-Dxhe25Zi.js → .setup/guarded-json-api-BukKSVNW.mjs} +50 -24
- package/dist/{mock-YsvBTX-7.js → .setup/mock-DY4yTqR2.mjs} +1 -1
- package/dist/{plivo-B-KvPVji.js → .setup/plivo-DFHh72MH.mjs} +3 -2
- package/dist/{realtime-handler-hIVPEZxN.js → .setup/realtime-handler-DWLxvZuO.mjs} +197 -128
- package/dist/{response-generator-z6M9229t.js → .setup/response-generator-DoC6lfcm.mjs} +6 -21
- package/dist/{runtime-entry-CQtitvRd.js → .setup/runtime-entry-CH-CV0Sf.mjs} +384 -868
- package/dist/{store-path-nYL-yM0S.js → .setup/store-1UfyY3HS.mjs} +6 -13
- package/dist/{telnyx-BG56lL7z.js → .setup/telnyx-Bb8X-nin.mjs} +1 -1
- package/dist/{twilio-B3FQ_P9x.js → .setup/twilio-Bsbf3O9A.mjs} +11 -15
- package/dist/{webhook-replay-aaP79dI8.js → .setup/webhook-replay-g3cDA5wr.mjs} +1 -1
- package/dist/cli-metadata.js +1 -5
- package/dist/doctor-contract-api.js +14 -36
- package/dist/index.js +38 -28
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +2 -86
- package/node_modules/commander/LICENSE +22 -0
- package/node_modules/commander/Readme.md +1172 -0
- package/node_modules/commander/index.js +21 -0
- package/node_modules/commander/lib/argument.js +147 -0
- package/node_modules/commander/lib/command.js +2790 -0
- package/node_modules/commander/lib/error.js +36 -0
- package/node_modules/commander/lib/help.js +731 -0
- package/node_modules/commander/lib/option.js +377 -0
- package/node_modules/commander/lib/suggestSimilar.js +99 -0
- package/node_modules/commander/package-support.json +19 -0
- package/node_modules/commander/package.json +64 -0
- package/node_modules/commander/typings/index.d.ts +1113 -0
- package/node_modules/typebox/build/compile/validator.d.mts +4 -1
- package/node_modules/typebox/build/guard/guard.d.mts +1 -1
- package/node_modules/typebox/build/guard/guard.mjs +2 -5
- package/node_modules/typebox/build/guard/string.mjs +4 -16
- package/node_modules/typebox/build/schema/engine/_functions.mjs +15 -34
- package/node_modules/typebox/build/type/action/readonly_object.d.mts +0 -5
- package/node_modules/typebox/build/type/action/readonly_object.mjs +0 -8
- package/node_modules/typebox/build/typebox.d.mts +1 -1
- package/node_modules/typebox/build/typebox.mjs +1 -1
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +32 -29
- package/node_modules/ws/lib/permessage-deflate.js +3 -1
- package/node_modules/ws/package.json +1 -1
- package/openclaw.plugin.json +15 -2
- package/package.json +8 -6
- package/dist/cli-output-mode.js +0 -27
- package/dist/twilio-region-4fkgz3UG.js +0 -25
- /package/dist/{realtime-transcription.runtime-CbJAs5t_.js → .setup/realtime-transcription.runtime-CbJAs5t_.mjs} +0 -0
- /package/dist/{realtime-voice.runtime-vtdCOWg-.js → .setup/realtime-voice.runtime-vtdCOWg-.mjs} +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { o as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-CQtitvRd.js";
|
|
2
1
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
3
2
|
import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
4
3
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
5
|
-
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse,
|
|
4
|
+
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, createRealtimeVoiceBridgeSession, createRealtimeVoiceForcedConsultCoordinator, createTalkSessionController, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult, recordTalkObservabilityEvent } from "openclaw/plugin-sdk/realtime-voice";
|
|
6
5
|
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
7
6
|
import { randomUUID } from "node:crypto";
|
|
8
7
|
import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
@@ -12,7 +11,13 @@ import WebSocket$1, { WebSocketServer } from "ws";
|
|
|
12
11
|
const TELEPHONY_SAMPLE_RATE = 8e3;
|
|
13
12
|
const TELEPHONY_CHUNK_BYTES = 160;
|
|
14
13
|
const TELEPHONY_CHUNK_MS = 20;
|
|
14
|
+
const DEFAULT_SPEECH_RMS_THRESHOLD = .035;
|
|
15
|
+
const DEFAULT_REQUIRED_LOUD_CHUNKS = 4;
|
|
16
|
+
const DEFAULT_REQUIRED_QUIET_CHUNKS = 12;
|
|
15
17
|
const DEFAULT_MAX_QUEUED_AUDIO_BYTES = TELEPHONY_SAMPLE_RATE * 120;
|
|
18
|
+
const PCM16_MAX_AMPLITUDE = 32768;
|
|
19
|
+
const MULAW_LINEAR_SAMPLES = /* @__PURE__ */ new Int16Array(256);
|
|
20
|
+
for (let i = 0; i < MULAW_LINEAR_SAMPLES.length; i += 1) MULAW_LINEAR_SAMPLES[i] = decodeMulawSample(i);
|
|
16
21
|
/** Paces outgoing mulaw audio frames at telephony cadence. */
|
|
17
22
|
var RealtimeAudioPacer = class {
|
|
18
23
|
constructor(params) {
|
|
@@ -107,6 +112,52 @@ var RealtimeAudioPacer = class {
|
|
|
107
112
|
if (this.queue.length > 0) this.timer = setTimeout(() => this.pump(), delayMs);
|
|
108
113
|
}
|
|
109
114
|
};
|
|
115
|
+
/** Calculate normalized RMS from mulaw bytes. */
|
|
116
|
+
function calculateMulawRms(muLaw) {
|
|
117
|
+
if (muLaw.length === 0) return 0;
|
|
118
|
+
let sum = 0;
|
|
119
|
+
for (const sample of muLaw) {
|
|
120
|
+
const normalized = (MULAW_LINEAR_SAMPLES[sample] ?? 0) / PCM16_MAX_AMPLITUDE;
|
|
121
|
+
sum += normalized * normalized;
|
|
122
|
+
}
|
|
123
|
+
return Math.sqrt(sum / muLaw.length);
|
|
124
|
+
}
|
|
125
|
+
/** Detect likely speech start from consecutive loud mulaw chunks. */
|
|
126
|
+
var RealtimeMulawSpeechStartDetector = class {
|
|
127
|
+
constructor(params = {}) {
|
|
128
|
+
this.params = params;
|
|
129
|
+
this.loudChunks = 0;
|
|
130
|
+
this.quietChunks = DEFAULT_REQUIRED_QUIET_CHUNKS;
|
|
131
|
+
this.speaking = false;
|
|
132
|
+
}
|
|
133
|
+
/** Accept one mulaw chunk and return true only on transition into speaking. */
|
|
134
|
+
accept(muLaw) {
|
|
135
|
+
if (calculateMulawRms(muLaw) >= (this.params.rmsThreshold ?? DEFAULT_SPEECH_RMS_THRESHOLD)) {
|
|
136
|
+
this.quietChunks = 0;
|
|
137
|
+
this.loudChunks += 1;
|
|
138
|
+
const requiredLoudChunks = this.params.requiredLoudChunks ?? DEFAULT_REQUIRED_LOUD_CHUNKS;
|
|
139
|
+
if (!this.speaking && this.loudChunks >= requiredLoudChunks) {
|
|
140
|
+
this.speaking = true;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
this.loudChunks = 0;
|
|
146
|
+
this.quietChunks += 1;
|
|
147
|
+
const requiredQuietChunks = this.params.requiredQuietChunks ?? DEFAULT_REQUIRED_QUIET_CHUNKS;
|
|
148
|
+
if (this.quietChunks >= requiredQuietChunks) this.speaking = false;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
/** Decode one G.711 mulaw byte to a linear PCM sample. */
|
|
153
|
+
function decodeMulawSample(value) {
|
|
154
|
+
const muLaw = ~value & 255;
|
|
155
|
+
const sign = muLaw & 128;
|
|
156
|
+
const exponent = muLaw >> 4 & 7;
|
|
157
|
+
let sample = ((muLaw & 15) << 3) + 132 << exponent;
|
|
158
|
+
sample -= 132;
|
|
159
|
+
return sign ? -sample : sample;
|
|
160
|
+
}
|
|
110
161
|
//#endregion
|
|
111
162
|
//#region extensions/voice-call/src/webhook/stream-frame-adapter.ts
|
|
112
163
|
/** Parse numeric timestamps sent as numbers or integer strings. */
|
|
@@ -130,15 +181,22 @@ function readRecordField(record, field) {
|
|
|
130
181
|
const value = record[field];
|
|
131
182
|
return typeof value === "object" && value !== null ? value : void 0;
|
|
132
183
|
}
|
|
184
|
+
/** Normalize base64/base64url padding differences for validation. */
|
|
185
|
+
function normalizeBase64ForCompare(value) {
|
|
186
|
+
return value.replace(/=+$/u, "").replace(/-/gu, "+").replace(/_/gu, "/");
|
|
187
|
+
}
|
|
188
|
+
/** Return true when a payload round-trips as base64. */
|
|
189
|
+
function isValidBase64Payload(value) {
|
|
190
|
+
return normalizeBase64ForCompare(Buffer.from(value, "base64").toString("base64")) === normalizeBase64ForCompare(value);
|
|
191
|
+
}
|
|
133
192
|
/** Parse a common provider media frame. */
|
|
134
193
|
function parseMediaFrame(msg) {
|
|
135
194
|
const mediaData = readRecordField(msg, "media");
|
|
136
195
|
const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
|
|
137
|
-
|
|
138
|
-
if (!canonicalPayload) return { kind: "ignored" };
|
|
196
|
+
if (!payload || !isValidBase64Payload(payload)) return { kind: "ignored" };
|
|
139
197
|
return {
|
|
140
198
|
kind: "media",
|
|
141
|
-
payloadBase64:
|
|
199
|
+
payloadBase64: payload,
|
|
142
200
|
timestampMs: parseTimestampMs(mediaData?.timestamp),
|
|
143
201
|
track: typeof mediaData?.track === "string" ? mediaData.track : void 0
|
|
144
202
|
};
|
|
@@ -271,8 +329,8 @@ var TelnyxStreamFrameAdapter = class {
|
|
|
271
329
|
//#region extensions/voice-call/src/webhook/realtime-handler.ts
|
|
272
330
|
const STREAM_TOKEN_TTL_MS = 3e4;
|
|
273
331
|
const DEFAULT_HOST = "localhost:8443";
|
|
274
|
-
const MAX_REALTIME_MESSAGE_BYTES =
|
|
275
|
-
const MAX_REALTIME_WS_BUFFERED_BYTES =
|
|
332
|
+
const MAX_REALTIME_MESSAGE_BYTES = 262144;
|
|
333
|
+
const MAX_REALTIME_WS_BUFFERED_BYTES = 1048576;
|
|
276
334
|
const FORCED_CONSULT_FALLBACK_DELAY_MS = 200;
|
|
277
335
|
const FORCED_CONSULT_NATIVE_DEDUPE_MS = 2e3;
|
|
278
336
|
const FORCED_CONSULT_RESULT_MAX_CHARS = 1800;
|
|
@@ -374,7 +432,7 @@ function buildForcedConsultSpeechPrompt(result) {
|
|
|
374
432
|
"Internal OpenClaw consult result is ready.",
|
|
375
433
|
"Do not call tools for this internal result.",
|
|
376
434
|
"Speak the following answer to the caller now, briefly and naturally:",
|
|
377
|
-
trimmed.length <= FORCED_CONSULT_RESULT_MAX_CHARS ? trimmed : `${truncateUtf16Safe(trimmed,
|
|
435
|
+
trimmed.length <= FORCED_CONSULT_RESULT_MAX_CHARS ? trimmed : `${truncateUtf16Safe(trimmed, 1784).trimEnd()} [truncated]`
|
|
378
436
|
].join("\n");
|
|
379
437
|
}
|
|
380
438
|
function appendRecentTalkEventMetadata(call, event) {
|
|
@@ -417,6 +475,7 @@ var RealtimeCallHandler = class {
|
|
|
417
475
|
this.partialUserTranscriptUpdatedAtByCallId = /* @__PURE__ */ new Map();
|
|
418
476
|
this.recentFinalUserTranscriptsByCallId = /* @__PURE__ */ new Map();
|
|
419
477
|
this.recentFinalUserTranscriptTimersByCallId = /* @__PURE__ */ new Map();
|
|
478
|
+
this.forcedConsultCoordinatorsByCallId = /* @__PURE__ */ new Map();
|
|
420
479
|
this.forcedConsultsByCallId = /* @__PURE__ */ new Map();
|
|
421
480
|
this.nativeConsultsInFlightByCallId = /* @__PURE__ */ new Map();
|
|
422
481
|
this.publicOrigin = null;
|
|
@@ -602,51 +661,56 @@ var RealtimeCallHandler = class {
|
|
|
602
661
|
}
|
|
603
662
|
const { callId, instructions, initialGreetingInstructions } = registration;
|
|
604
663
|
const callRecord = this.manager.getCallByProviderCallId(callSid);
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
664
|
+
const talk = createTalkSessionController({
|
|
665
|
+
sessionId: `voice-call:${callId}:realtime`,
|
|
666
|
+
mode: "realtime",
|
|
667
|
+
transport: "gateway-relay",
|
|
668
|
+
brain: "agent-consult",
|
|
669
|
+
provider: this.realtimeProvider.id
|
|
670
|
+
}, { onEvent: recordTalkObservabilityEvent });
|
|
671
|
+
const rememberTalkEvent = (event) => {
|
|
672
|
+
if (event) appendRecentTalkEventMetadata(callRecord, event);
|
|
673
|
+
return event;
|
|
674
|
+
};
|
|
675
|
+
const emitTalkEvent = (input) => {
|
|
676
|
+
return rememberTalkEvent(talk.emit(input));
|
|
677
|
+
};
|
|
678
|
+
const ensureTalkTurn = () => {
|
|
679
|
+
const turn = talk.ensureTurn({ payload: {
|
|
680
|
+
callId,
|
|
681
|
+
providerCallId: callSid
|
|
682
|
+
} });
|
|
683
|
+
rememberTalkEvent(turn.event);
|
|
684
|
+
return turn.turnId;
|
|
685
|
+
};
|
|
686
|
+
const endTalkTurn = (reason = "completed") => {
|
|
687
|
+
const ended = talk.endTurn({ payload: {
|
|
688
|
+
callId,
|
|
689
|
+
providerCallId: callSid,
|
|
690
|
+
reason
|
|
691
|
+
} });
|
|
692
|
+
if (ended.ok) rememberTalkEvent(ended.event);
|
|
693
|
+
};
|
|
694
|
+
const finishOutputAudio = (reason) => {
|
|
695
|
+
rememberTalkEvent(talk.finishOutputAudio({ payload: {
|
|
696
|
+
callId,
|
|
697
|
+
providerCallId: callSid,
|
|
698
|
+
reason
|
|
699
|
+
} }));
|
|
700
|
+
};
|
|
637
701
|
const providerHandlesInputAudioBargeIn = this.realtimeProvider.capabilities?.handlesInputAudioBargeIn === true;
|
|
638
702
|
const cancelOutputAudioForBargeIn = (source, interruptProvider, clearedAudioBytes = 0) => {
|
|
639
|
-
const outputAudioActive =
|
|
703
|
+
const outputAudioActive = talk.outputAudioActive;
|
|
640
704
|
const pendingTelephonyAudio = audioPacer.hasPendingAudio();
|
|
641
705
|
if (source === "provider" && !outputAudioActive && !pendingTelephonyAudio && clearedAudioBytes === 0) return;
|
|
642
|
-
const interruptedTurnId =
|
|
706
|
+
const interruptedTurnId = talk.activeTurnId;
|
|
643
707
|
interruptProvider?.(outputAudioActive || pendingTelephonyAudio);
|
|
644
708
|
const clearedBytes = clearedAudioBytes + (source === "local" || pendingTelephonyAudio ? audioPacer.clearAudio() : 0);
|
|
645
709
|
console.log(`[voice-call] realtime outbound audio cleared by ${source} barge-in callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
|
|
646
710
|
if (!outputAudioActive || !interruptedTurnId) return;
|
|
647
711
|
const reason = `${source}-barge-in`;
|
|
648
|
-
|
|
649
|
-
|
|
712
|
+
finishOutputAudio(reason);
|
|
713
|
+
const cancelled = talk.cancelTurn({
|
|
650
714
|
turnId: interruptedTurnId,
|
|
651
715
|
payload: {
|
|
652
716
|
callId,
|
|
@@ -654,8 +718,9 @@ var RealtimeCallHandler = class {
|
|
|
654
718
|
reason
|
|
655
719
|
}
|
|
656
720
|
});
|
|
721
|
+
if (cancelled.ok) rememberTalkEvent(cancelled.event);
|
|
657
722
|
};
|
|
658
|
-
|
|
723
|
+
emitTalkEvent({
|
|
659
724
|
type: "session.started",
|
|
660
725
|
payload: {
|
|
661
726
|
callId,
|
|
@@ -697,13 +762,9 @@ var RealtimeCallHandler = class {
|
|
|
697
762
|
if (ws.readyState === WebSocket$1.OPEN) ws.close(1013, "Backpressure: paced audio queue exceeded");
|
|
698
763
|
}
|
|
699
764
|
});
|
|
700
|
-
const speechDetector =
|
|
701
|
-
rmsThreshold: .035,
|
|
702
|
-
speechFrames: BARGE_IN_REQUIRED_LOUD_CHUNKS,
|
|
703
|
-
silenceFrames: 12
|
|
704
|
-
});
|
|
765
|
+
const speechDetector = new RealtimeMulawSpeechStartDetector({ requiredLoudChunks: BARGE_IN_REQUIRED_LOUD_CHUNKS });
|
|
705
766
|
const interruptResponseOnInputAudio = typeof this.providerConfig.interruptResponseOnInputAudio === "boolean" ? this.providerConfig.interruptResponseOnInputAudio : void 0;
|
|
706
|
-
const session =
|
|
767
|
+
const session = createRealtimeVoiceBridgeSession({
|
|
707
768
|
provider: this.realtimeProvider,
|
|
708
769
|
cfg: this.coreConfig,
|
|
709
770
|
providerConfig: this.providerConfig,
|
|
@@ -715,38 +776,46 @@ var RealtimeCallHandler = class {
|
|
|
715
776
|
audioSink: {
|
|
716
777
|
isOpen: () => ws.readyState === WebSocket$1.OPEN,
|
|
717
778
|
sendAudio: (muLaw) => {
|
|
718
|
-
|
|
779
|
+
const turnId = ensureTalkTurn();
|
|
780
|
+
rememberTalkEvent(talk.startOutputAudio({
|
|
781
|
+
turnId,
|
|
782
|
+
payload: {
|
|
783
|
+
callId,
|
|
784
|
+
providerCallId: callSid
|
|
785
|
+
}
|
|
786
|
+
}).event);
|
|
787
|
+
emitTalkEvent({
|
|
788
|
+
type: "output.audio.delta",
|
|
789
|
+
turnId,
|
|
790
|
+
payload: { byteLength: muLaw.length }
|
|
791
|
+
});
|
|
719
792
|
audioPacer.sendAudio(muLaw);
|
|
720
793
|
},
|
|
721
794
|
clearAudio: (reason) => {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
harness.finishOutputAudio("clear");
|
|
730
|
-
});
|
|
795
|
+
const clearedBytes = audioPacer.clearAudio();
|
|
796
|
+
if (reason === "barge-in") {
|
|
797
|
+
cancelOutputAudioForBargeIn("provider", void 0, clearedBytes);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
console.log(`[voice-call] realtime outbound audio clear requested callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
|
|
801
|
+
finishOutputAudio("clear");
|
|
731
802
|
},
|
|
732
803
|
sendMark: (markName) => {
|
|
733
804
|
audioPacer.sendMark(markName);
|
|
734
805
|
}
|
|
735
806
|
},
|
|
736
807
|
onTranscript: (role, text, isFinal) => {
|
|
737
|
-
const turnId =
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
role,
|
|
741
|
-
text
|
|
742
|
-
};
|
|
743
|
-
harness.emit({
|
|
744
|
-
type: eventType,
|
|
808
|
+
const turnId = ensureTalkTurn();
|
|
809
|
+
emitTalkEvent({
|
|
810
|
+
type: role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta",
|
|
745
811
|
turnId,
|
|
746
|
-
payload
|
|
812
|
+
payload: role === "assistant" ? { text } : {
|
|
813
|
+
role,
|
|
814
|
+
text
|
|
815
|
+
},
|
|
747
816
|
final: isFinal
|
|
748
817
|
});
|
|
749
|
-
if (role === "user" && isFinal)
|
|
818
|
+
if (role === "user" && isFinal) emitTalkEvent({
|
|
750
819
|
type: "input.audio.committed",
|
|
751
820
|
turnId,
|
|
752
821
|
payload: {
|
|
@@ -782,7 +851,6 @@ var RealtimeCallHandler = class {
|
|
|
782
851
|
};
|
|
783
852
|
this.manager.processEvent(event);
|
|
784
853
|
this.scheduleForcedAgentConsult({
|
|
785
|
-
harness,
|
|
786
854
|
session,
|
|
787
855
|
callId,
|
|
788
856
|
callSid,
|
|
@@ -804,8 +872,8 @@ var RealtimeCallHandler = class {
|
|
|
804
872
|
});
|
|
805
873
|
},
|
|
806
874
|
onToolCall: (toolEvent, sessionLocal) => {
|
|
807
|
-
const turnId =
|
|
808
|
-
|
|
875
|
+
const turnId = ensureTalkTurn();
|
|
876
|
+
emitTalkEvent({
|
|
809
877
|
type: "tool.call",
|
|
810
878
|
turnId,
|
|
811
879
|
itemId: toolEvent.itemId,
|
|
@@ -816,17 +884,17 @@ var RealtimeCallHandler = class {
|
|
|
816
884
|
}
|
|
817
885
|
});
|
|
818
886
|
console.log(`[voice-call] realtime tool call received callId=${callId} providerCallId=${callSid} tool=${toolEvent.name}`);
|
|
819
|
-
|
|
887
|
+
this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, emitTalkEvent);
|
|
820
888
|
},
|
|
821
889
|
onEvent: (event) => {
|
|
822
890
|
if (event.type === "input_audio_buffer.speech_started") {
|
|
823
|
-
|
|
891
|
+
ensureTalkTurn();
|
|
824
892
|
return;
|
|
825
893
|
}
|
|
826
894
|
if (event.type === "input_audio_buffer.speech_stopped") {
|
|
827
|
-
const turnId =
|
|
895
|
+
const turnId = talk.activeTurnId;
|
|
828
896
|
if (!turnId) return;
|
|
829
|
-
|
|
897
|
+
emitTalkEvent({
|
|
830
898
|
type: "input.audio.committed",
|
|
831
899
|
turnId,
|
|
832
900
|
payload: {
|
|
@@ -839,18 +907,18 @@ var RealtimeCallHandler = class {
|
|
|
839
907
|
return;
|
|
840
908
|
}
|
|
841
909
|
if (event.type === "response.done") {
|
|
842
|
-
|
|
843
|
-
|
|
910
|
+
finishOutputAudio("response.done");
|
|
911
|
+
endTalkTurn("response.done");
|
|
844
912
|
return;
|
|
845
913
|
}
|
|
846
|
-
if (event.type === "error")
|
|
914
|
+
if (event.type === "error") emitTalkEvent({
|
|
847
915
|
type: "session.error",
|
|
848
916
|
payload: { message: event.detail ?? "Realtime provider error" },
|
|
849
917
|
final: true
|
|
850
918
|
});
|
|
851
919
|
},
|
|
852
920
|
onReady: () => {
|
|
853
|
-
|
|
921
|
+
emitTalkEvent({
|
|
854
922
|
type: "session.ready",
|
|
855
923
|
payload: {
|
|
856
924
|
callId,
|
|
@@ -860,7 +928,7 @@ var RealtimeCallHandler = class {
|
|
|
860
928
|
},
|
|
861
929
|
onError: (error) => {
|
|
862
930
|
console.error("[voice-call] realtime voice error:", error.message);
|
|
863
|
-
|
|
931
|
+
emitTalkEvent({
|
|
864
932
|
type: "session.error",
|
|
865
933
|
payload: { message: error.message },
|
|
866
934
|
final: true
|
|
@@ -872,8 +940,8 @@ var RealtimeCallHandler = class {
|
|
|
872
940
|
this.activeTelephonyClosersByCallId.delete(callId);
|
|
873
941
|
this.activeTelephonyClosersByCallId.delete(callSid);
|
|
874
942
|
this.clearUserTranscriptState(callId);
|
|
875
|
-
|
|
876
|
-
|
|
943
|
+
finishOutputAudio(reason);
|
|
944
|
+
emitTalkEvent({
|
|
877
945
|
type: "session.closed",
|
|
878
946
|
payload: { reason },
|
|
879
947
|
final: true
|
|
@@ -903,16 +971,17 @@ var RealtimeCallHandler = class {
|
|
|
903
971
|
this.activeTelephonyClosersByCallId.set(callSid, closeTelephony);
|
|
904
972
|
const sendAudioToSession = session.sendAudio.bind(session);
|
|
905
973
|
session.sendAudio = (audio) => {
|
|
906
|
-
if (speechDetector.accept({
|
|
907
|
-
rms: calculateMulawRms(audio),
|
|
908
|
-
peak: 0
|
|
909
|
-
})) {
|
|
974
|
+
if (speechDetector.accept(audio)) {
|
|
910
975
|
console.log(`[voice-call] realtime local speech detected callId=${callId} providerCallId=${callSid}`);
|
|
911
976
|
if (!providerHandlesInputAudioBargeIn) cancelOutputAudioForBargeIn("local", (audioPlaybackActive) => {
|
|
912
977
|
session.handleBargeIn({ audioPlaybackActive });
|
|
913
978
|
});
|
|
914
979
|
}
|
|
915
|
-
|
|
980
|
+
emitTalkEvent({
|
|
981
|
+
type: "input.audio.delta",
|
|
982
|
+
turnId: ensureTalkTurn(),
|
|
983
|
+
payload: { byteLength: audio.length }
|
|
984
|
+
});
|
|
916
985
|
sendAudioToSession(audio);
|
|
917
986
|
};
|
|
918
987
|
const closeSession = session.close.bind(session);
|
|
@@ -928,8 +997,7 @@ var RealtimeCallHandler = class {
|
|
|
928
997
|
this.activeTelephonyClosersByCallId.delete(callId);
|
|
929
998
|
this.activeTelephonyClosersByCallId.delete(callSid);
|
|
930
999
|
this.clearUserTranscriptState(callId);
|
|
931
|
-
this.
|
|
932
|
-
harness.close();
|
|
1000
|
+
this.clearForcedConsultState(callId);
|
|
933
1001
|
audioPacer.close();
|
|
934
1002
|
}
|
|
935
1003
|
};
|
|
@@ -1012,6 +1080,18 @@ var RealtimeCallHandler = class {
|
|
|
1012
1080
|
});
|
|
1013
1081
|
}
|
|
1014
1082
|
}
|
|
1083
|
+
clearForcedConsultState(callId) {
|
|
1084
|
+
this.forcedConsultCoordinatorsByCallId.get(callId)?.clear();
|
|
1085
|
+
this.forcedConsultCoordinatorsByCallId.delete(callId);
|
|
1086
|
+
this.forcedConsultsByCallId.delete(callId);
|
|
1087
|
+
}
|
|
1088
|
+
forcedConsultCoordinator(callId) {
|
|
1089
|
+
const existing = this.forcedConsultCoordinatorsByCallId.get(callId);
|
|
1090
|
+
if (existing) return existing;
|
|
1091
|
+
const created = createRealtimeVoiceForcedConsultCoordinator();
|
|
1092
|
+
this.forcedConsultCoordinatorsByCallId.set(callId, created);
|
|
1093
|
+
return created;
|
|
1094
|
+
}
|
|
1015
1095
|
closeTelephonyBridge(callIdOrSid, bridge, reason) {
|
|
1016
1096
|
const closer = this.activeTelephonyClosersByCallId.get(callIdOrSid);
|
|
1017
1097
|
if (closer) {
|
|
@@ -1028,7 +1108,7 @@ var RealtimeCallHandler = class {
|
|
|
1028
1108
|
if (!handler) return;
|
|
1029
1109
|
const existingForcedConsult = this.forcedConsultsByCallId.get(params.callId);
|
|
1030
1110
|
if (existingForcedConsult && !existingForcedConsult.completedAt) return;
|
|
1031
|
-
const coordinator = params.
|
|
1111
|
+
const coordinator = this.forcedConsultCoordinator(params.callId);
|
|
1032
1112
|
if (coordinator.hasRecentNativeConsult(question, { allowUnknownQuestion: true })) return;
|
|
1033
1113
|
coordinator.clearPending();
|
|
1034
1114
|
const pending = coordinator.prepare(question);
|
|
@@ -1044,7 +1124,7 @@ var RealtimeCallHandler = class {
|
|
|
1044
1124
|
});
|
|
1045
1125
|
}
|
|
1046
1126
|
async runForcedAgentConsult(params) {
|
|
1047
|
-
const coordinator = params.
|
|
1127
|
+
const coordinator = this.forcedConsultCoordinator(params.callId);
|
|
1048
1128
|
coordinator.markStarted(params.handle);
|
|
1049
1129
|
const startedAt = Date.now();
|
|
1050
1130
|
logger.debug(`[voice-call] realtime forced agent consult reason=${FORCED_CONSULT_REASON} consultPolicy=always callId=${params.callId} providerCallId=${params.callSid} chars=${params.handle.question.length}`);
|
|
@@ -1136,14 +1216,14 @@ var RealtimeCallHandler = class {
|
|
|
1136
1216
|
reason
|
|
1137
1217
|
});
|
|
1138
1218
|
}
|
|
1139
|
-
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId,
|
|
1219
|
+
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, emitTalkEvent) {
|
|
1140
1220
|
const handler = this.toolHandlers.get(name);
|
|
1141
1221
|
const startedAt = Date.now();
|
|
1142
1222
|
const hasResultError = (result) => {
|
|
1143
1223
|
return Boolean(result && typeof result === "object" && !Array.isArray(result) && "error" in result);
|
|
1144
1224
|
};
|
|
1145
1225
|
const emitFinalToolEvent = (result) => {
|
|
1146
|
-
|
|
1226
|
+
emitTalkEvent?.({
|
|
1147
1227
|
type: hasResultError(result) ? "tool.error" : "tool.result",
|
|
1148
1228
|
turnId,
|
|
1149
1229
|
callId: bridgeCallId,
|
|
@@ -1154,14 +1234,13 @@ var RealtimeCallHandler = class {
|
|
|
1154
1234
|
final: true
|
|
1155
1235
|
});
|
|
1156
1236
|
};
|
|
1157
|
-
const submitFinalToolResult =
|
|
1158
|
-
|
|
1237
|
+
const submitFinalToolResult = (result) => {
|
|
1238
|
+
bridge.submitToolResult(bridgeCallId, result);
|
|
1159
1239
|
emitFinalToolEvent(result);
|
|
1160
1240
|
};
|
|
1161
|
-
const submitWorkingResponse =
|
|
1241
|
+
const submitWorkingResponse = () => {
|
|
1162
1242
|
if (handler && name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && bridge.bridge.supportsToolResultContinuation && !this.config.fastContext.enabled) {
|
|
1163
|
-
|
|
1164
|
-
harness.emit({
|
|
1243
|
+
emitTalkEvent?.({
|
|
1165
1244
|
type: "tool.progress",
|
|
1166
1245
|
turnId,
|
|
1167
1246
|
callId: bridgeCallId,
|
|
@@ -1170,66 +1249,56 @@ var RealtimeCallHandler = class {
|
|
|
1170
1249
|
status: "working"
|
|
1171
1250
|
}
|
|
1172
1251
|
});
|
|
1252
|
+
bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
|
|
1173
1253
|
}
|
|
1174
1254
|
};
|
|
1175
1255
|
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
|
|
1176
|
-
const coordinator =
|
|
1256
|
+
const coordinator = this.forcedConsultCoordinator(callId);
|
|
1177
1257
|
const forcedMatch = coordinator.recordNativeConsult(args, bridgeCallId);
|
|
1178
1258
|
if (forcedMatch.kind === "none") {
|
|
1179
1259
|
const pending = coordinator.consumePending();
|
|
1180
1260
|
if (pending) coordinator.remove(pending);
|
|
1181
1261
|
}
|
|
1182
1262
|
const forcedConsult = this.forcedConsultsByCallId.get(callId);
|
|
1183
|
-
if (forcedMatch.kind === "already_delivered" && coordinator.isCancelled(forcedMatch.handle)) {
|
|
1184
|
-
if (forcedConsult) forcedConsult.sendSpeechPrompt = false;
|
|
1185
|
-
await submitFinalToolResult({
|
|
1186
|
-
status: "cancelled",
|
|
1187
|
-
message: "OpenClaw cancelled this consult before completion. Do not restart it."
|
|
1188
|
-
});
|
|
1189
|
-
return;
|
|
1190
|
-
}
|
|
1191
1263
|
if (forcedConsult) {
|
|
1192
1264
|
if (forcedConsult.completedAt || forcedMatch.kind === "already_delivered") {
|
|
1193
|
-
|
|
1265
|
+
submitFinalToolResult({
|
|
1194
1266
|
status: "already_delivered",
|
|
1195
1267
|
message: "OpenClaw already delivered this consult result internally. Do not repeat it."
|
|
1196
1268
|
});
|
|
1197
1269
|
return;
|
|
1198
1270
|
}
|
|
1199
1271
|
forcedConsult.sendSpeechPrompt = false;
|
|
1200
|
-
|
|
1272
|
+
submitFinalToolResult(await forcedConsult.promise.catch((error) => ({ error: formatErrorMessage(error) })));
|
|
1201
1273
|
return;
|
|
1202
1274
|
}
|
|
1203
1275
|
const existingNativeConsult = this.nativeConsultsInFlightByCallId.get(callId);
|
|
1204
1276
|
if (existingNativeConsult) {
|
|
1205
1277
|
console.log(`[voice-call] realtime tool call sharing in-flight agent consult callId=${callId} ageMs=${Date.now() - existingNativeConsult.startedAt}`);
|
|
1206
|
-
|
|
1207
|
-
|
|
1278
|
+
submitWorkingResponse();
|
|
1279
|
+
submitFinalToolResult(await existingNativeConsult.promise);
|
|
1208
1280
|
return;
|
|
1209
1281
|
}
|
|
1282
|
+
submitWorkingResponse();
|
|
1210
1283
|
const state = {
|
|
1211
1284
|
startedAt,
|
|
1212
1285
|
promise: Promise.resolve()
|
|
1213
1286
|
};
|
|
1214
|
-
state.promise =
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
} catch (error) {
|
|
1223
|
-
return { error: formatErrorMessage(error) };
|
|
1224
|
-
}
|
|
1225
|
-
});
|
|
1287
|
+
state.promise = (async () => {
|
|
1288
|
+
await this.waitForConsultTranscriptSettle(callId, startedAt);
|
|
1289
|
+
const context = { partialUserTranscript: this.resolveUserTranscriptContext(callId) };
|
|
1290
|
+
state.partialUserTranscript = context.partialUserTranscript;
|
|
1291
|
+
const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
|
|
1292
|
+
console.log(`[voice-call] realtime tool call executing callId=${callId} tool=${name} hasHandler=${Boolean(handler)}`);
|
|
1293
|
+
return !handler ? { error: `Tool "${name}" not available` } : await handler(handlerArgs, callId, context);
|
|
1294
|
+
})().catch((error) => ({ error: formatErrorMessage(error) }));
|
|
1226
1295
|
this.nativeConsultsInFlightByCallId.set(callId, state);
|
|
1227
1296
|
try {
|
|
1228
1297
|
const result = await state.promise;
|
|
1229
1298
|
const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
|
|
1230
1299
|
const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
|
|
1231
1300
|
console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
|
|
1232
|
-
|
|
1301
|
+
submitFinalToolResult(result);
|
|
1233
1302
|
if (status === "ok") this.consumePartialUserTranscript(callId, state.partialUserTranscript);
|
|
1234
1303
|
} finally {
|
|
1235
1304
|
if (this.nativeConsultsInFlightByCallId.get(callId) === state) this.nativeConsultsInFlightByCallId.delete(callId);
|
|
@@ -1243,7 +1312,7 @@ var RealtimeCallHandler = class {
|
|
|
1243
1312
|
const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
|
|
1244
1313
|
const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
|
|
1245
1314
|
console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
|
|
1246
|
-
|
|
1315
|
+
submitFinalToolResult(result);
|
|
1247
1316
|
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && status === "ok") this.consumePartialUserTranscript(callId, context.partialUserTranscript);
|
|
1248
1317
|
}
|
|
1249
1318
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { s as resolveVoiceCallSessionKey } from "./config-BBpso-fA.mjs";
|
|
2
|
+
import { m as resolveCallAgentId, p as resolveVoiceResponseModel } from "./runtime-entry-CH-CV0Sf.mjs";
|
|
2
3
|
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
4
|
import crypto from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
+
import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
|
|
5
6
|
//#region extensions/voice-call/src/response-generator.ts
|
|
6
7
|
/**
|
|
7
8
|
* Voice call response generator - uses the embedded OpenClaw agent for tool support.
|
|
@@ -66,11 +67,7 @@ function sanitizePlainSpokenText(text) {
|
|
|
66
67
|
const withoutCodeFences = text.replace(/```[\s\S]*?```/g, " ").trim();
|
|
67
68
|
if (!withoutCodeFences) return null;
|
|
68
69
|
const paragraphs = normalizeStringEntries(withoutCodeFences.split(/\n\s*\n+/));
|
|
69
|
-
while (paragraphs.length > 1)
|
|
70
|
-
const firstParagraph = paragraphs.at(0);
|
|
71
|
-
if (!firstParagraph || !isLikelyMetaReasoningParagraph(firstParagraph)) break;
|
|
72
|
-
paragraphs.shift();
|
|
73
|
-
}
|
|
70
|
+
while (paragraphs.length > 1 && isLikelyMetaReasoningParagraph(paragraphs[0])) paragraphs.shift();
|
|
74
71
|
return normalizeSpokenText(paragraphs.join(" "));
|
|
75
72
|
}
|
|
76
73
|
function extractSpokenTextFromPayloads(payloads) {
|
|
@@ -107,7 +104,7 @@ function resolveVoiceSandboxSessionKey(agentId, sessionKey) {
|
|
|
107
104
|
* Uses the same agent infrastructure as messaging for consistent behavior.
|
|
108
105
|
*/
|
|
109
106
|
async function generateVoiceResponse(params) {
|
|
110
|
-
const { voiceConfig, callId, sessionKey, from, transcript, userMessage, coreConfig, agentRuntime, onEarlyText } = params;
|
|
107
|
+
const { voiceConfig, callId, sessionKey, from, senderIsOwner, transcript, userMessage, coreConfig, agentRuntime, onEarlyText } = params;
|
|
111
108
|
if (!coreConfig) return {
|
|
112
109
|
text: null,
|
|
113
110
|
deliveredEarly: false,
|
|
@@ -145,7 +142,6 @@ async function generateVoiceResponse(params) {
|
|
|
145
142
|
agentRuntime
|
|
146
143
|
});
|
|
147
144
|
let sessionEntry = existingSessionEntry;
|
|
148
|
-
if (sessionEntry?.modelSelectionLocked === true && voiceConfig.responseModel) throw new ModelSelectionLockedError();
|
|
149
145
|
if (!sessionEntry?.sessionId || voiceConfig.responseModel) sessionEntry = await agentRuntime.session.patchSessionEntry({
|
|
150
146
|
storePath,
|
|
151
147
|
sessionKey: resolvedSessionKey,
|
|
@@ -177,8 +173,6 @@ async function generateVoiceResponse(params) {
|
|
|
177
173
|
error: "Voice response session could not be initialized"
|
|
178
174
|
};
|
|
179
175
|
const sessionId = sessionEntry.sessionId;
|
|
180
|
-
const modelSelectionLocked = sessionEntry.modelSelectionLocked === true;
|
|
181
|
-
const persistedRuntimeId = resolvePersistedSessionRuntimeId(sessionEntry);
|
|
182
176
|
const thinkLevel = agentRuntime.resolveThinkingDefault({
|
|
183
177
|
cfg,
|
|
184
178
|
provider,
|
|
@@ -213,11 +207,6 @@ async function generateVoiceResponse(params) {
|
|
|
213
207
|
prompt: userMessage,
|
|
214
208
|
provider,
|
|
215
209
|
model,
|
|
216
|
-
modelSelectionLocked,
|
|
217
|
-
...persistedRuntimeId ? {
|
|
218
|
-
agentHarnessId: persistedRuntimeId,
|
|
219
|
-
agentHarnessRuntimeOverride: persistedRuntimeId
|
|
220
|
-
} : {},
|
|
221
210
|
thinkLevel,
|
|
222
211
|
verboseLevel: "off",
|
|
223
212
|
timeoutMs,
|
|
@@ -225,6 +214,7 @@ async function generateVoiceResponse(params) {
|
|
|
225
214
|
lane: "voice",
|
|
226
215
|
extraSystemPrompt,
|
|
227
216
|
agentDir,
|
|
217
|
+
senderIsOwner,
|
|
228
218
|
toolsAllow,
|
|
229
219
|
abortSignal,
|
|
230
220
|
blockReplyBreak: "text_end",
|
|
@@ -271,11 +261,6 @@ async function generateVoiceResponse(params) {
|
|
|
271
261
|
};
|
|
272
262
|
});
|
|
273
263
|
} catch (err) {
|
|
274
|
-
if (err instanceof ModelSelectionLockedError) return {
|
|
275
|
-
text: null,
|
|
276
|
-
deliveredEarly: false,
|
|
277
|
-
error: err.message
|
|
278
|
-
};
|
|
279
264
|
console.error(`[voice-call] Response generation failed:`, err);
|
|
280
265
|
return {
|
|
281
266
|
text: null,
|