@openclaw/voice-call 2026.7.2-beta.7 → 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-DS9sZa3C.js → .setup/guarded-json-api-BukKSVNW.mjs} +56 -42
- package/dist/{mock-YsvBTX-7.js → .setup/mock-DY4yTqR2.mjs} +1 -1
- package/dist/{plivo-vZ--Wqxv.js → .setup/plivo-DFHh72MH.mjs} +3 -2
- package/dist/{realtime-handler--i_OERv6.js → .setup/realtime-handler-DWLxvZuO.mjs} +198 -130
- package/dist/{response-generator-D-7ApVfT.js → .setup/response-generator-DoC6lfcm.mjs} +6 -21
- package/dist/{runtime-entry-D0UJiLfi.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-Dx6cfLvV.js → .setup/telnyx-Bb8X-nin.mjs} +1 -1
- package/dist/{twilio-D_D8KysU.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 +46 -48
- 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-D0UJiLfi.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
|
-
|
|
637
|
-
|
|
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
|
+
};
|
|
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
|
|
@@ -890,7 +958,6 @@ var RealtimeCallHandler = class {
|
|
|
890
958
|
});
|
|
891
959
|
}
|
|
892
960
|
});
|
|
893
|
-
providerHandlesInputAudioBargeIn = session.bridge.handlesInputAudioBargeIn ?? providerHandlesInputAudioBargeIn;
|
|
894
961
|
const closeTelephony = (reason) => {
|
|
895
962
|
try {
|
|
896
963
|
session.close();
|
|
@@ -904,16 +971,17 @@ var RealtimeCallHandler = class {
|
|
|
904
971
|
this.activeTelephonyClosersByCallId.set(callSid, closeTelephony);
|
|
905
972
|
const sendAudioToSession = session.sendAudio.bind(session);
|
|
906
973
|
session.sendAudio = (audio) => {
|
|
907
|
-
if (speechDetector.accept({
|
|
908
|
-
rms: calculateMulawRms(audio),
|
|
909
|
-
peak: 0
|
|
910
|
-
})) {
|
|
974
|
+
if (speechDetector.accept(audio)) {
|
|
911
975
|
console.log(`[voice-call] realtime local speech detected callId=${callId} providerCallId=${callSid}`);
|
|
912
976
|
if (!providerHandlesInputAudioBargeIn) cancelOutputAudioForBargeIn("local", (audioPlaybackActive) => {
|
|
913
977
|
session.handleBargeIn({ audioPlaybackActive });
|
|
914
978
|
});
|
|
915
979
|
}
|
|
916
|
-
|
|
980
|
+
emitTalkEvent({
|
|
981
|
+
type: "input.audio.delta",
|
|
982
|
+
turnId: ensureTalkTurn(),
|
|
983
|
+
payload: { byteLength: audio.length }
|
|
984
|
+
});
|
|
917
985
|
sendAudioToSession(audio);
|
|
918
986
|
};
|
|
919
987
|
const closeSession = session.close.bind(session);
|
|
@@ -929,8 +997,7 @@ var RealtimeCallHandler = class {
|
|
|
929
997
|
this.activeTelephonyClosersByCallId.delete(callId);
|
|
930
998
|
this.activeTelephonyClosersByCallId.delete(callSid);
|
|
931
999
|
this.clearUserTranscriptState(callId);
|
|
932
|
-
this.
|
|
933
|
-
harness.close();
|
|
1000
|
+
this.clearForcedConsultState(callId);
|
|
934
1001
|
audioPacer.close();
|
|
935
1002
|
}
|
|
936
1003
|
};
|
|
@@ -1013,6 +1080,18 @@ var RealtimeCallHandler = class {
|
|
|
1013
1080
|
});
|
|
1014
1081
|
}
|
|
1015
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
|
+
}
|
|
1016
1095
|
closeTelephonyBridge(callIdOrSid, bridge, reason) {
|
|
1017
1096
|
const closer = this.activeTelephonyClosersByCallId.get(callIdOrSid);
|
|
1018
1097
|
if (closer) {
|
|
@@ -1029,7 +1108,7 @@ var RealtimeCallHandler = class {
|
|
|
1029
1108
|
if (!handler) return;
|
|
1030
1109
|
const existingForcedConsult = this.forcedConsultsByCallId.get(params.callId);
|
|
1031
1110
|
if (existingForcedConsult && !existingForcedConsult.completedAt) return;
|
|
1032
|
-
const coordinator = params.
|
|
1111
|
+
const coordinator = this.forcedConsultCoordinator(params.callId);
|
|
1033
1112
|
if (coordinator.hasRecentNativeConsult(question, { allowUnknownQuestion: true })) return;
|
|
1034
1113
|
coordinator.clearPending();
|
|
1035
1114
|
const pending = coordinator.prepare(question);
|
|
@@ -1045,7 +1124,7 @@ var RealtimeCallHandler = class {
|
|
|
1045
1124
|
});
|
|
1046
1125
|
}
|
|
1047
1126
|
async runForcedAgentConsult(params) {
|
|
1048
|
-
const coordinator = params.
|
|
1127
|
+
const coordinator = this.forcedConsultCoordinator(params.callId);
|
|
1049
1128
|
coordinator.markStarted(params.handle);
|
|
1050
1129
|
const startedAt = Date.now();
|
|
1051
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}`);
|
|
@@ -1137,14 +1216,14 @@ var RealtimeCallHandler = class {
|
|
|
1137
1216
|
reason
|
|
1138
1217
|
});
|
|
1139
1218
|
}
|
|
1140
|
-
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId,
|
|
1219
|
+
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, emitTalkEvent) {
|
|
1141
1220
|
const handler = this.toolHandlers.get(name);
|
|
1142
1221
|
const startedAt = Date.now();
|
|
1143
1222
|
const hasResultError = (result) => {
|
|
1144
1223
|
return Boolean(result && typeof result === "object" && !Array.isArray(result) && "error" in result);
|
|
1145
1224
|
};
|
|
1146
1225
|
const emitFinalToolEvent = (result) => {
|
|
1147
|
-
|
|
1226
|
+
emitTalkEvent?.({
|
|
1148
1227
|
type: hasResultError(result) ? "tool.error" : "tool.result",
|
|
1149
1228
|
turnId,
|
|
1150
1229
|
callId: bridgeCallId,
|
|
@@ -1155,14 +1234,13 @@ var RealtimeCallHandler = class {
|
|
|
1155
1234
|
final: true
|
|
1156
1235
|
});
|
|
1157
1236
|
};
|
|
1158
|
-
const submitFinalToolResult =
|
|
1159
|
-
|
|
1237
|
+
const submitFinalToolResult = (result) => {
|
|
1238
|
+
bridge.submitToolResult(bridgeCallId, result);
|
|
1160
1239
|
emitFinalToolEvent(result);
|
|
1161
1240
|
};
|
|
1162
|
-
const submitWorkingResponse =
|
|
1241
|
+
const submitWorkingResponse = () => {
|
|
1163
1242
|
if (handler && name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && bridge.bridge.supportsToolResultContinuation && !this.config.fastContext.enabled) {
|
|
1164
|
-
|
|
1165
|
-
harness.emit({
|
|
1243
|
+
emitTalkEvent?.({
|
|
1166
1244
|
type: "tool.progress",
|
|
1167
1245
|
turnId,
|
|
1168
1246
|
callId: bridgeCallId,
|
|
@@ -1171,66 +1249,56 @@ var RealtimeCallHandler = class {
|
|
|
1171
1249
|
status: "working"
|
|
1172
1250
|
}
|
|
1173
1251
|
});
|
|
1252
|
+
bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
|
|
1174
1253
|
}
|
|
1175
1254
|
};
|
|
1176
1255
|
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
|
|
1177
|
-
const coordinator =
|
|
1256
|
+
const coordinator = this.forcedConsultCoordinator(callId);
|
|
1178
1257
|
const forcedMatch = coordinator.recordNativeConsult(args, bridgeCallId);
|
|
1179
1258
|
if (forcedMatch.kind === "none") {
|
|
1180
1259
|
const pending = coordinator.consumePending();
|
|
1181
1260
|
if (pending) coordinator.remove(pending);
|
|
1182
1261
|
}
|
|
1183
1262
|
const forcedConsult = this.forcedConsultsByCallId.get(callId);
|
|
1184
|
-
if (forcedMatch.kind === "already_delivered" && coordinator.isCancelled(forcedMatch.handle)) {
|
|
1185
|
-
if (forcedConsult) forcedConsult.sendSpeechPrompt = false;
|
|
1186
|
-
await submitFinalToolResult({
|
|
1187
|
-
status: "cancelled",
|
|
1188
|
-
message: "OpenClaw cancelled this consult before completion. Do not restart it."
|
|
1189
|
-
});
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
1263
|
if (forcedConsult) {
|
|
1193
1264
|
if (forcedConsult.completedAt || forcedMatch.kind === "already_delivered") {
|
|
1194
|
-
|
|
1265
|
+
submitFinalToolResult({
|
|
1195
1266
|
status: "already_delivered",
|
|
1196
1267
|
message: "OpenClaw already delivered this consult result internally. Do not repeat it."
|
|
1197
1268
|
});
|
|
1198
1269
|
return;
|
|
1199
1270
|
}
|
|
1200
1271
|
forcedConsult.sendSpeechPrompt = false;
|
|
1201
|
-
|
|
1272
|
+
submitFinalToolResult(await forcedConsult.promise.catch((error) => ({ error: formatErrorMessage(error) })));
|
|
1202
1273
|
return;
|
|
1203
1274
|
}
|
|
1204
1275
|
const existingNativeConsult = this.nativeConsultsInFlightByCallId.get(callId);
|
|
1205
1276
|
if (existingNativeConsult) {
|
|
1206
1277
|
console.log(`[voice-call] realtime tool call sharing in-flight agent consult callId=${callId} ageMs=${Date.now() - existingNativeConsult.startedAt}`);
|
|
1207
|
-
|
|
1208
|
-
|
|
1278
|
+
submitWorkingResponse();
|
|
1279
|
+
submitFinalToolResult(await existingNativeConsult.promise);
|
|
1209
1280
|
return;
|
|
1210
1281
|
}
|
|
1282
|
+
submitWorkingResponse();
|
|
1211
1283
|
const state = {
|
|
1212
1284
|
startedAt,
|
|
1213
1285
|
promise: Promise.resolve()
|
|
1214
1286
|
};
|
|
1215
|
-
state.promise =
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
} catch (error) {
|
|
1224
|
-
return { error: formatErrorMessage(error) };
|
|
1225
|
-
}
|
|
1226
|
-
});
|
|
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) }));
|
|
1227
1295
|
this.nativeConsultsInFlightByCallId.set(callId, state);
|
|
1228
1296
|
try {
|
|
1229
1297
|
const result = await state.promise;
|
|
1230
1298
|
const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
|
|
1231
1299
|
const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
|
|
1232
1300
|
console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
|
|
1233
|
-
|
|
1301
|
+
submitFinalToolResult(result);
|
|
1234
1302
|
if (status === "ok") this.consumePartialUserTranscript(callId, state.partialUserTranscript);
|
|
1235
1303
|
} finally {
|
|
1236
1304
|
if (this.nativeConsultsInFlightByCallId.get(callId) === state) this.nativeConsultsInFlightByCallId.delete(callId);
|
|
@@ -1244,7 +1312,7 @@ var RealtimeCallHandler = class {
|
|
|
1244
1312
|
const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
|
|
1245
1313
|
const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
|
|
1246
1314
|
console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
|
|
1247
|
-
|
|
1315
|
+
submitFinalToolResult(result);
|
|
1248
1316
|
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && status === "ok") this.consumePartialUserTranscript(callId, context.partialUserTranscript);
|
|
1249
1317
|
}
|
|
1250
1318
|
};
|