@openclaw/voice-call 2026.7.2-beta.2 → 2026.7.2-beta.4
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 +1 -1
- package/dist/doctor-contract-api.js +1 -0
- package/dist/{guarded-json-api-BKOgUU1q.js → guarded-json-api-Dxhe25Zi.js} +1 -2
- package/dist/index.js +10 -6
- package/dist/{plivo-ByAIoPFp.js → plivo-B-KvPVji.js} +2 -3
- package/dist/{realtime-handler-DJk-eYyf.js → realtime-handler-hIVPEZxN.js} +95 -175
- package/dist/{response-generator-Cdj8EuLg.js → response-generator-z6M9229t.js} +1 -1
- package/dist/{runtime-entry-DSgyqIwE.js → runtime-entry-CQtitvRd.js} +89 -152
- package/dist/runtime-entry.js +1 -1
- package/dist/{telnyx-DMbLYwj4.js → telnyx-BG56lL7z.js} +1 -1
- package/dist/{twilio-BTKKlkLT.js → twilio-B3FQ_P9x.js} +9 -5
- package/node_modules/typebox/build/compile/validator.d.mts +1 -4
- package/node_modules/typebox/build/guard/guard.d.mts +1 -1
- package/node_modules/typebox/build/guard/guard.mjs +5 -2
- package/node_modules/typebox/build/guard/string.mjs +16 -4
- package/node_modules/typebox/build/schema/engine/_functions.mjs +34 -15
- package/node_modules/typebox/build/type/action/readonly_object.d.mts +5 -0
- package/node_modules/typebox/build/type/action/readonly_object.mjs +8 -0
- 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 +29 -32
- package/node_modules/ws/lib/receiver.js +15 -32
- package/node_modules/ws/lib/websocket-server.js +4 -4
- package/node_modules/ws/lib/websocket.js +4 -4
- package/node_modules/ws/package.json +5 -1
- package/npm-shrinkwrap.json +11 -11
- package/openclaw.plugin.json +1 -15
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -115,7 +115,7 @@ See the plugin docs for recommended ranges and production examples:
|
|
|
115
115
|
|
|
116
116
|
## TTS for calls
|
|
117
117
|
|
|
118
|
-
Voice Call uses the core `
|
|
118
|
+
Voice Call uses the core `tts` configuration for
|
|
119
119
|
streaming speech on calls. Override examples and provider caveats live here:
|
|
120
120
|
`https://docs.openclaw.ai/plugins/voice-call#tts-for-calls`
|
|
121
121
|
|
|
@@ -63,6 +63,7 @@ function describeVoiceCallSchemaMigration(migration) {
|
|
|
63
63
|
case "agent-databases-composite-primary-key": return "agent database registry primary key -> agent_id,path";
|
|
64
64
|
case "audit-events-v2": return "audit event ledger -> versioned message lifecycle schema";
|
|
65
65
|
case "operator-approvals-system-agent": return "operator approvals -> OpenClaw system changes";
|
|
66
|
+
case "session-watch-cursor-provenance-v4": return "session watch cursors -> provenance column";
|
|
66
67
|
case "strict-tables-v3": return "tables -> SQLite STRICT typing";
|
|
67
68
|
}
|
|
68
69
|
return migration.kind;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import { n as normalizeProxyIp,
|
|
3
|
+
import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-CQtitvRd.js";
|
|
4
4
|
import { n as markWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-aaP79dI8.js";
|
|
5
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
6
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
@@ -462,7 +462,6 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
462
462
|
if (options?.publicUrl) try {
|
|
463
463
|
const req = new URL(reconstructed);
|
|
464
464
|
const base = new URL(options.publicUrl);
|
|
465
|
-
base.pathname = req.pathname;
|
|
466
465
|
base.search = req.search;
|
|
467
466
|
verificationUrl = base.toString();
|
|
468
467
|
} catch {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { definePluginEntry, sleep } from "./runtime-api.js";
|
|
2
2
|
import "./api.js";
|
|
3
|
-
import {
|
|
3
|
+
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-CQtitvRd.js";
|
|
4
4
|
import { g as setVoiceCallStateRuntime, t as resolveDefaultVoiceCallStoreDir, u as getCallHistoryFromStore } from "./store-path-nYL-yM0S.js";
|
|
5
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
6
|
import { ErrorCodes, callGatewayFromCli, errorShape } from "openclaw/plugin-sdk/gateway-runtime";
|
|
@@ -87,13 +87,17 @@ function readCompletedContinueResult(payload) {
|
|
|
87
87
|
}
|
|
88
88
|
async function pollVoiceCallContinueGateway(params) {
|
|
89
89
|
const deadlineMs = resolveVoiceCallDeadlineMs(params.timeoutMs);
|
|
90
|
-
|
|
91
|
-
const
|
|
90
|
+
for (;;) {
|
|
91
|
+
const remainingMs = deadlineMs - Date.now();
|
|
92
|
+
if (remainingMs <= 0) break;
|
|
93
|
+
const gateway = await callVoiceCallGateway("voicecall.continue.result", { operationId: params.operationId }, { timeoutMs: Math.min(VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS, remainingMs) });
|
|
92
94
|
if (!gateway.ok) throw new Error(`gateway unavailable while waiting for voicecall continue result: ${formatErrorMessage(gateway.error)}`);
|
|
93
95
|
const result = readCompletedContinueResult(gateway.payload);
|
|
94
96
|
if (result.status === "completed") return result.result;
|
|
95
97
|
if (result.status === "failed") throw new Error(result.error);
|
|
96
|
-
|
|
98
|
+
const sleepMs = Math.min(VOICE_CALL_GATEWAY_POLL_INTERVAL_MS, deadlineMs - Date.now());
|
|
99
|
+
if (sleepMs <= 0) break;
|
|
100
|
+
await sleep(sleepMs);
|
|
97
101
|
}
|
|
98
102
|
throw new Error("voicecall continue timed out waiting for gateway operation");
|
|
99
103
|
}
|
|
@@ -533,7 +537,7 @@ const VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS = 300 * 1e3;
|
|
|
533
537
|
function createVoiceCallContinueOperationStore(params) {
|
|
534
538
|
const operations = /* @__PURE__ */ new Map();
|
|
535
539
|
const resolvePollTimeoutMs = (rt) => {
|
|
536
|
-
const ttsTimeoutMs = rt.config.tts?.timeoutMs ?? params.config.tts?.timeoutMs ?? params.coreConfig.
|
|
540
|
+
const ttsTimeoutMs = rt.config.tts?.timeoutMs ?? params.config.tts?.timeoutMs ?? params.coreConfig.tts?.timeoutMs ?? 8e3;
|
|
537
541
|
return resolveTimerTimeoutMs((rt.config.transcriptTimeoutMs ?? params.config.transcriptTimeoutMs) + ttsTimeoutMs + VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS, VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS);
|
|
538
542
|
};
|
|
539
543
|
const scheduleCleanup = (operationId) => {
|
|
@@ -822,7 +826,7 @@ const voiceCallConfigSchema = {
|
|
|
822
826
|
},
|
|
823
827
|
"tts.provider": {
|
|
824
828
|
label: "TTS Provider Override",
|
|
825
|
-
help: "Deep-merges with
|
|
829
|
+
help: "Deep-merges with tts (Microsoft is ignored for calls).",
|
|
826
830
|
advanced: true
|
|
827
831
|
},
|
|
828
832
|
"tts.providers": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-
|
|
1
|
+
import { _ as escapeXml, s as getHeader } from "./runtime-entry-CQtitvRd.js";
|
|
2
|
+
import { a as reconstructWebhookUrl, o as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Dxhe25Zi.js";
|
|
3
3
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
//#region extensions/voice-call/src/providers/plivo.ts
|
|
@@ -371,7 +371,6 @@ var PlivoProvider = class PlivoProvider {
|
|
|
371
371
|
try {
|
|
372
372
|
if (this.options.publicUrl) {
|
|
373
373
|
const base = new URL(this.options.publicUrl);
|
|
374
|
-
base.pathname = new URL(ctx.url).pathname;
|
|
375
374
|
return `${base.origin}${base.pathname}`;
|
|
376
375
|
}
|
|
377
376
|
const u = new URL(reconstructWebhookUrl(ctx, {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { o as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-CQtitvRd.js";
|
|
1
2
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
2
3
|
import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
3
4
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
4
|
-
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse,
|
|
5
|
+
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, calculateMulawRms, createRealtimeVoiceSessionHarness, createSpeechThresholdGate, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult } from "openclaw/plugin-sdk/realtime-voice";
|
|
5
6
|
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
6
7
|
import { randomUUID } from "node:crypto";
|
|
7
8
|
import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
@@ -11,13 +12,7 @@ import WebSocket$1, { WebSocketServer } from "ws";
|
|
|
11
12
|
const TELEPHONY_SAMPLE_RATE = 8e3;
|
|
12
13
|
const TELEPHONY_CHUNK_BYTES = 160;
|
|
13
14
|
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;
|
|
17
15
|
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);
|
|
21
16
|
/** Paces outgoing mulaw audio frames at telephony cadence. */
|
|
22
17
|
var RealtimeAudioPacer = class {
|
|
23
18
|
constructor(params) {
|
|
@@ -112,52 +107,6 @@ var RealtimeAudioPacer = class {
|
|
|
112
107
|
if (this.queue.length > 0) this.timer = setTimeout(() => this.pump(), delayMs);
|
|
113
108
|
}
|
|
114
109
|
};
|
|
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
|
-
}
|
|
161
110
|
//#endregion
|
|
162
111
|
//#region extensions/voice-call/src/webhook/stream-frame-adapter.ts
|
|
163
112
|
/** Parse numeric timestamps sent as numbers or integer strings. */
|
|
@@ -181,22 +130,15 @@ function readRecordField(record, field) {
|
|
|
181
130
|
const value = record[field];
|
|
182
131
|
return typeof value === "object" && value !== null ? value : void 0;
|
|
183
132
|
}
|
|
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
|
-
}
|
|
192
133
|
/** Parse a common provider media frame. */
|
|
193
134
|
function parseMediaFrame(msg) {
|
|
194
135
|
const mediaData = readRecordField(msg, "media");
|
|
195
136
|
const payload = typeof mediaData?.payload === "string" ? mediaData.payload : void 0;
|
|
196
|
-
|
|
137
|
+
const canonicalPayload = payload ? canonicalizeVoiceCallMediaBase64(payload) : void 0;
|
|
138
|
+
if (!canonicalPayload) return { kind: "ignored" };
|
|
197
139
|
return {
|
|
198
140
|
kind: "media",
|
|
199
|
-
payloadBase64:
|
|
141
|
+
payloadBase64: canonicalPayload,
|
|
200
142
|
timestampMs: parseTimestampMs(mediaData?.timestamp),
|
|
201
143
|
track: typeof mediaData?.track === "string" ? mediaData.track : void 0
|
|
202
144
|
};
|
|
@@ -475,7 +417,6 @@ var RealtimeCallHandler = class {
|
|
|
475
417
|
this.partialUserTranscriptUpdatedAtByCallId = /* @__PURE__ */ new Map();
|
|
476
418
|
this.recentFinalUserTranscriptsByCallId = /* @__PURE__ */ new Map();
|
|
477
419
|
this.recentFinalUserTranscriptTimersByCallId = /* @__PURE__ */ new Map();
|
|
478
|
-
this.forcedConsultCoordinatorsByCallId = /* @__PURE__ */ new Map();
|
|
479
420
|
this.forcedConsultsByCallId = /* @__PURE__ */ new Map();
|
|
480
421
|
this.nativeConsultsInFlightByCallId = /* @__PURE__ */ new Map();
|
|
481
422
|
this.publicOrigin = null;
|
|
@@ -661,56 +602,51 @@ var RealtimeCallHandler = class {
|
|
|
661
602
|
}
|
|
662
603
|
const { callId, instructions, initialGreetingInstructions } = registration;
|
|
663
604
|
const callRecord = this.manager.getCallByProviderCallId(callSid);
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
callId,
|
|
697
|
-
providerCallId: callSid,
|
|
698
|
-
reason
|
|
699
|
-
} }));
|
|
700
|
-
};
|
|
605
|
+
const harness = createRealtimeVoiceSessionHarness({
|
|
606
|
+
talk: {
|
|
607
|
+
sessionId: `voice-call:${callId}:realtime`,
|
|
608
|
+
mode: "realtime",
|
|
609
|
+
transport: "gateway-relay",
|
|
610
|
+
brain: "agent-consult",
|
|
611
|
+
provider: this.realtimeProvider.id
|
|
612
|
+
},
|
|
613
|
+
talkPayloads: {
|
|
614
|
+
turnStarted: () => ({
|
|
615
|
+
callId,
|
|
616
|
+
providerCallId: callSid
|
|
617
|
+
}),
|
|
618
|
+
turnEnded: (reason) => ({
|
|
619
|
+
callId,
|
|
620
|
+
providerCallId: callSid,
|
|
621
|
+
reason
|
|
622
|
+
}),
|
|
623
|
+
inputAudioDelta: (audio) => ({ byteLength: audio.byteLength }),
|
|
624
|
+
outputAudioStarted: () => ({
|
|
625
|
+
callId,
|
|
626
|
+
providerCallId: callSid
|
|
627
|
+
}),
|
|
628
|
+
outputAudioDelta: (audio) => ({ byteLength: audio.byteLength }),
|
|
629
|
+
outputAudioDone: (reason) => ({
|
|
630
|
+
callId,
|
|
631
|
+
providerCallId: callSid,
|
|
632
|
+
reason
|
|
633
|
+
})
|
|
634
|
+
},
|
|
635
|
+
onTalkEvent: (event) => appendRecentTalkEventMetadata(callRecord, event)
|
|
636
|
+
});
|
|
701
637
|
const providerHandlesInputAudioBargeIn = this.realtimeProvider.capabilities?.handlesInputAudioBargeIn === true;
|
|
702
638
|
const cancelOutputAudioForBargeIn = (source, interruptProvider, clearedAudioBytes = 0) => {
|
|
703
|
-
const outputAudioActive = talk.outputAudioActive;
|
|
639
|
+
const outputAudioActive = harness.talk.outputAudioActive;
|
|
704
640
|
const pendingTelephonyAudio = audioPacer.hasPendingAudio();
|
|
705
641
|
if (source === "provider" && !outputAudioActive && !pendingTelephonyAudio && clearedAudioBytes === 0) return;
|
|
706
|
-
const interruptedTurnId = talk.activeTurnId;
|
|
642
|
+
const interruptedTurnId = harness.talk.activeTurnId;
|
|
707
643
|
interruptProvider?.(outputAudioActive || pendingTelephonyAudio);
|
|
708
644
|
const clearedBytes = clearedAudioBytes + (source === "local" || pendingTelephonyAudio ? audioPacer.clearAudio() : 0);
|
|
709
645
|
console.log(`[voice-call] realtime outbound audio cleared by ${source} barge-in callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
|
|
710
646
|
if (!outputAudioActive || !interruptedTurnId) return;
|
|
711
647
|
const reason = `${source}-barge-in`;
|
|
712
|
-
finishOutputAudio(reason);
|
|
713
|
-
|
|
648
|
+
harness.finishOutputAudio(reason);
|
|
649
|
+
harness.talk.cancelTurn({
|
|
714
650
|
turnId: interruptedTurnId,
|
|
715
651
|
payload: {
|
|
716
652
|
callId,
|
|
@@ -718,9 +654,8 @@ var RealtimeCallHandler = class {
|
|
|
718
654
|
reason
|
|
719
655
|
}
|
|
720
656
|
});
|
|
721
|
-
if (cancelled.ok) rememberTalkEvent(cancelled.event);
|
|
722
657
|
};
|
|
723
|
-
|
|
658
|
+
harness.emit({
|
|
724
659
|
type: "session.started",
|
|
725
660
|
payload: {
|
|
726
661
|
callId,
|
|
@@ -762,9 +697,13 @@ var RealtimeCallHandler = class {
|
|
|
762
697
|
if (ws.readyState === WebSocket$1.OPEN) ws.close(1013, "Backpressure: paced audio queue exceeded");
|
|
763
698
|
}
|
|
764
699
|
});
|
|
765
|
-
const speechDetector =
|
|
700
|
+
const speechDetector = createSpeechThresholdGate({
|
|
701
|
+
rmsThreshold: .035,
|
|
702
|
+
speechFrames: BARGE_IN_REQUIRED_LOUD_CHUNKS,
|
|
703
|
+
silenceFrames: 12
|
|
704
|
+
});
|
|
766
705
|
const interruptResponseOnInputAudio = typeof this.providerConfig.interruptResponseOnInputAudio === "boolean" ? this.providerConfig.interruptResponseOnInputAudio : void 0;
|
|
767
|
-
const session =
|
|
706
|
+
const session = harness.createBridge({
|
|
768
707
|
provider: this.realtimeProvider,
|
|
769
708
|
cfg: this.coreConfig,
|
|
770
709
|
providerConfig: this.providerConfig,
|
|
@@ -776,46 +715,38 @@ var RealtimeCallHandler = class {
|
|
|
776
715
|
audioSink: {
|
|
777
716
|
isOpen: () => ws.readyState === WebSocket$1.OPEN,
|
|
778
717
|
sendAudio: (muLaw) => {
|
|
779
|
-
|
|
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
|
-
});
|
|
718
|
+
harness.recordOutputAudio(muLaw);
|
|
792
719
|
audioPacer.sendAudio(muLaw);
|
|
793
720
|
},
|
|
794
721
|
clearAudio: (reason) => {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
722
|
+
harness.flushOutput(() => {
|
|
723
|
+
const clearedBytes = audioPacer.clearAudio();
|
|
724
|
+
if (reason === "barge-in") {
|
|
725
|
+
cancelOutputAudioForBargeIn("provider", void 0, clearedBytes);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
console.log(`[voice-call] realtime outbound audio clear requested callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
|
|
729
|
+
harness.finishOutputAudio("clear");
|
|
730
|
+
});
|
|
802
731
|
},
|
|
803
732
|
sendMark: (markName) => {
|
|
804
733
|
audioPacer.sendMark(markName);
|
|
805
734
|
}
|
|
806
735
|
},
|
|
807
736
|
onTranscript: (role, text, isFinal) => {
|
|
808
|
-
const turnId =
|
|
809
|
-
|
|
810
|
-
|
|
737
|
+
const turnId = harness.ensureTurn();
|
|
738
|
+
const eventType = role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta";
|
|
739
|
+
const payload = role === "assistant" ? { text } : {
|
|
740
|
+
role,
|
|
741
|
+
text
|
|
742
|
+
};
|
|
743
|
+
harness.emit({
|
|
744
|
+
type: eventType,
|
|
811
745
|
turnId,
|
|
812
|
-
payload
|
|
813
|
-
role,
|
|
814
|
-
text
|
|
815
|
-
},
|
|
746
|
+
payload,
|
|
816
747
|
final: isFinal
|
|
817
748
|
});
|
|
818
|
-
if (role === "user" && isFinal)
|
|
749
|
+
if (role === "user" && isFinal) harness.emit({
|
|
819
750
|
type: "input.audio.committed",
|
|
820
751
|
turnId,
|
|
821
752
|
payload: {
|
|
@@ -851,6 +782,7 @@ var RealtimeCallHandler = class {
|
|
|
851
782
|
};
|
|
852
783
|
this.manager.processEvent(event);
|
|
853
784
|
this.scheduleForcedAgentConsult({
|
|
785
|
+
harness,
|
|
854
786
|
session,
|
|
855
787
|
callId,
|
|
856
788
|
callSid,
|
|
@@ -872,8 +804,8 @@ var RealtimeCallHandler = class {
|
|
|
872
804
|
});
|
|
873
805
|
},
|
|
874
806
|
onToolCall: (toolEvent, sessionLocal) => {
|
|
875
|
-
const turnId =
|
|
876
|
-
|
|
807
|
+
const turnId = harness.ensureTurn();
|
|
808
|
+
harness.emit({
|
|
877
809
|
type: "tool.call",
|
|
878
810
|
turnId,
|
|
879
811
|
itemId: toolEvent.itemId,
|
|
@@ -884,17 +816,17 @@ var RealtimeCallHandler = class {
|
|
|
884
816
|
}
|
|
885
817
|
});
|
|
886
818
|
console.log(`[voice-call] realtime tool call received callId=${callId} providerCallId=${callSid} tool=${toolEvent.name}`);
|
|
887
|
-
return this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId,
|
|
819
|
+
return this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, harness);
|
|
888
820
|
},
|
|
889
821
|
onEvent: (event) => {
|
|
890
822
|
if (event.type === "input_audio_buffer.speech_started") {
|
|
891
|
-
|
|
823
|
+
harness.ensureTurn();
|
|
892
824
|
return;
|
|
893
825
|
}
|
|
894
826
|
if (event.type === "input_audio_buffer.speech_stopped") {
|
|
895
|
-
const turnId = talk.activeTurnId;
|
|
827
|
+
const turnId = harness.talk.activeTurnId;
|
|
896
828
|
if (!turnId) return;
|
|
897
|
-
|
|
829
|
+
harness.emit({
|
|
898
830
|
type: "input.audio.committed",
|
|
899
831
|
turnId,
|
|
900
832
|
payload: {
|
|
@@ -907,18 +839,18 @@ var RealtimeCallHandler = class {
|
|
|
907
839
|
return;
|
|
908
840
|
}
|
|
909
841
|
if (event.type === "response.done") {
|
|
910
|
-
finishOutputAudio("response.done");
|
|
911
|
-
|
|
842
|
+
harness.finishOutputAudio("response.done");
|
|
843
|
+
harness.endTurn("response.done");
|
|
912
844
|
return;
|
|
913
845
|
}
|
|
914
|
-
if (event.type === "error")
|
|
846
|
+
if (event.type === "error") harness.emit({
|
|
915
847
|
type: "session.error",
|
|
916
848
|
payload: { message: event.detail ?? "Realtime provider error" },
|
|
917
849
|
final: true
|
|
918
850
|
});
|
|
919
851
|
},
|
|
920
852
|
onReady: () => {
|
|
921
|
-
|
|
853
|
+
harness.emit({
|
|
922
854
|
type: "session.ready",
|
|
923
855
|
payload: {
|
|
924
856
|
callId,
|
|
@@ -928,7 +860,7 @@ var RealtimeCallHandler = class {
|
|
|
928
860
|
},
|
|
929
861
|
onError: (error) => {
|
|
930
862
|
console.error("[voice-call] realtime voice error:", error.message);
|
|
931
|
-
|
|
863
|
+
harness.emit({
|
|
932
864
|
type: "session.error",
|
|
933
865
|
payload: { message: error.message },
|
|
934
866
|
final: true
|
|
@@ -940,8 +872,8 @@ var RealtimeCallHandler = class {
|
|
|
940
872
|
this.activeTelephonyClosersByCallId.delete(callId);
|
|
941
873
|
this.activeTelephonyClosersByCallId.delete(callSid);
|
|
942
874
|
this.clearUserTranscriptState(callId);
|
|
943
|
-
finishOutputAudio(reason);
|
|
944
|
-
|
|
875
|
+
harness.finishOutputAudio(reason);
|
|
876
|
+
harness.emit({
|
|
945
877
|
type: "session.closed",
|
|
946
878
|
payload: { reason },
|
|
947
879
|
final: true
|
|
@@ -971,17 +903,16 @@ var RealtimeCallHandler = class {
|
|
|
971
903
|
this.activeTelephonyClosersByCallId.set(callSid, closeTelephony);
|
|
972
904
|
const sendAudioToSession = session.sendAudio.bind(session);
|
|
973
905
|
session.sendAudio = (audio) => {
|
|
974
|
-
if (speechDetector.accept(
|
|
906
|
+
if (speechDetector.accept({
|
|
907
|
+
rms: calculateMulawRms(audio),
|
|
908
|
+
peak: 0
|
|
909
|
+
})) {
|
|
975
910
|
console.log(`[voice-call] realtime local speech detected callId=${callId} providerCallId=${callSid}`);
|
|
976
911
|
if (!providerHandlesInputAudioBargeIn) cancelOutputAudioForBargeIn("local", (audioPlaybackActive) => {
|
|
977
912
|
session.handleBargeIn({ audioPlaybackActive });
|
|
978
913
|
});
|
|
979
914
|
}
|
|
980
|
-
|
|
981
|
-
type: "input.audio.delta",
|
|
982
|
-
turnId: ensureTalkTurn(),
|
|
983
|
-
payload: { byteLength: audio.length }
|
|
984
|
-
});
|
|
915
|
+
harness.recordInputAudio(audio);
|
|
985
916
|
sendAudioToSession(audio);
|
|
986
917
|
};
|
|
987
918
|
const closeSession = session.close.bind(session);
|
|
@@ -997,7 +928,8 @@ var RealtimeCallHandler = class {
|
|
|
997
928
|
this.activeTelephonyClosersByCallId.delete(callId);
|
|
998
929
|
this.activeTelephonyClosersByCallId.delete(callSid);
|
|
999
930
|
this.clearUserTranscriptState(callId);
|
|
1000
|
-
this.
|
|
931
|
+
this.forcedConsultsByCallId.delete(callId);
|
|
932
|
+
harness.close();
|
|
1001
933
|
audioPacer.close();
|
|
1002
934
|
}
|
|
1003
935
|
};
|
|
@@ -1080,18 +1012,6 @@ var RealtimeCallHandler = class {
|
|
|
1080
1012
|
});
|
|
1081
1013
|
}
|
|
1082
1014
|
}
|
|
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
|
-
}
|
|
1095
1015
|
closeTelephonyBridge(callIdOrSid, bridge, reason) {
|
|
1096
1016
|
const closer = this.activeTelephonyClosersByCallId.get(callIdOrSid);
|
|
1097
1017
|
if (closer) {
|
|
@@ -1108,7 +1028,7 @@ var RealtimeCallHandler = class {
|
|
|
1108
1028
|
if (!handler) return;
|
|
1109
1029
|
const existingForcedConsult = this.forcedConsultsByCallId.get(params.callId);
|
|
1110
1030
|
if (existingForcedConsult && !existingForcedConsult.completedAt) return;
|
|
1111
|
-
const coordinator =
|
|
1031
|
+
const coordinator = params.harness.forcedConsults;
|
|
1112
1032
|
if (coordinator.hasRecentNativeConsult(question, { allowUnknownQuestion: true })) return;
|
|
1113
1033
|
coordinator.clearPending();
|
|
1114
1034
|
const pending = coordinator.prepare(question);
|
|
@@ -1124,7 +1044,7 @@ var RealtimeCallHandler = class {
|
|
|
1124
1044
|
});
|
|
1125
1045
|
}
|
|
1126
1046
|
async runForcedAgentConsult(params) {
|
|
1127
|
-
const coordinator =
|
|
1047
|
+
const coordinator = params.harness.forcedConsults;
|
|
1128
1048
|
coordinator.markStarted(params.handle);
|
|
1129
1049
|
const startedAt = Date.now();
|
|
1130
1050
|
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}`);
|
|
@@ -1216,14 +1136,14 @@ var RealtimeCallHandler = class {
|
|
|
1216
1136
|
reason
|
|
1217
1137
|
});
|
|
1218
1138
|
}
|
|
1219
|
-
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId,
|
|
1139
|
+
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, harness) {
|
|
1220
1140
|
const handler = this.toolHandlers.get(name);
|
|
1221
1141
|
const startedAt = Date.now();
|
|
1222
1142
|
const hasResultError = (result) => {
|
|
1223
1143
|
return Boolean(result && typeof result === "object" && !Array.isArray(result) && "error" in result);
|
|
1224
1144
|
};
|
|
1225
1145
|
const emitFinalToolEvent = (result) => {
|
|
1226
|
-
|
|
1146
|
+
harness.emit({
|
|
1227
1147
|
type: hasResultError(result) ? "tool.error" : "tool.result",
|
|
1228
1148
|
turnId,
|
|
1229
1149
|
callId: bridgeCallId,
|
|
@@ -1241,7 +1161,7 @@ var RealtimeCallHandler = class {
|
|
|
1241
1161
|
const submitWorkingResponse = async () => {
|
|
1242
1162
|
if (handler && name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && bridge.bridge.supportsToolResultContinuation && !this.config.fastContext.enabled) {
|
|
1243
1163
|
await bridge.submitToolResult(bridgeCallId, buildRealtimeVoiceAgentConsultWorkingResponse("caller"), { willContinue: true });
|
|
1244
|
-
|
|
1164
|
+
harness.emit({
|
|
1245
1165
|
type: "tool.progress",
|
|
1246
1166
|
turnId,
|
|
1247
1167
|
callId: bridgeCallId,
|
|
@@ -1253,7 +1173,7 @@ var RealtimeCallHandler = class {
|
|
|
1253
1173
|
}
|
|
1254
1174
|
};
|
|
1255
1175
|
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
|
|
1256
|
-
const coordinator =
|
|
1176
|
+
const coordinator = harness.forcedConsults;
|
|
1257
1177
|
const forcedMatch = coordinator.recordNativeConsult(args, bridgeCallId);
|
|
1258
1178
|
if (forcedMatch.kind === "none") {
|
|
1259
1179
|
const pending = coordinator.consumePending();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as resolveCallAgentId, m as resolveVoiceResponseModel, x as resolveVoiceCallSessionKey } from "./runtime-entry-CQtitvRd.js";
|
|
2
2
|
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { ModelSelectionLockedError, applyModelOverrideToSessionEntry, resolvePersistedSessionRuntimeId } from "openclaw/plugin-sdk/model-session-runtime";
|