@openclaw/voice-call 2026.7.2-beta.5 → 2026.8.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/doctor-contract-api.js +10 -8
- package/dist/{guarded-json-api-Dxhe25Zi.js → guarded-json-api-DEDwgf4J.js} +43 -53
- package/dist/index.js +328 -637
- package/dist/{mock-YsvBTX-7.js → mock-BCq0q36l.js} +2 -3
- package/dist/{plivo-B-KvPVji.js → plivo-DbI17uAc.js} +45 -8
- package/dist/{realtime-handler-hIVPEZxN.js → realtime-handler-uXR0QgVa.js} +366 -109
- package/dist/{response-generator-z6M9229t.js → response-generator-D-hkzo5Y.js} +13 -5
- package/dist/{runtime-entry-CQtitvRd.js → runtime-entry-F13eJ4pp.js} +279 -172
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +17 -24
- package/dist/{store-path-nYL-yM0S.js → store-path-51_oC_xn.js} +70 -13
- package/dist/{telnyx-BG56lL7z.js → telnyx-sIS_WgnY.js} +3 -2
- package/dist/{twilio-B3FQ_P9x.js → twilio-CbeVrVAl.js} +20 -26
- package/dist/{webhook-replay-aaP79dI8.js → webhook-replay-FQrEGVSM.js} +20 -6
- package/openclaw.plugin.json +49 -4
- package/package.json +4 -4
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { o as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-
|
|
1
|
+
import { o as canonicalizeVoiceCallMediaBase64 } from "./runtime-entry-F13eJ4pp.js";
|
|
2
2
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
3
|
-
import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
4
3
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
5
4
|
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultWorkingResponse, calculateMulawRms, createRealtimeVoiceSessionHarness, createSpeechThresholdGate, readRealtimeVoiceConsultQuestion, readSpeakableRealtimeVoiceToolResult } from "openclaw/plugin-sdk/realtime-voice";
|
|
6
5
|
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
7
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { isFutureDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
8
8
|
import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
9
9
|
import "node:http";
|
|
10
10
|
import WebSocket$1, { WebSocketServer } from "ws";
|
|
@@ -13,11 +13,13 @@ const TELEPHONY_SAMPLE_RATE = 8e3;
|
|
|
13
13
|
const TELEPHONY_CHUNK_BYTES = 160;
|
|
14
14
|
const TELEPHONY_CHUNK_MS = 20;
|
|
15
15
|
const DEFAULT_MAX_QUEUED_AUDIO_BYTES = TELEPHONY_SAMPLE_RATE * 120;
|
|
16
|
+
const QUEUE_COMPACT_HEAD_THRESHOLD = 256;
|
|
16
17
|
/** Paces outgoing mulaw audio frames at telephony cadence. */
|
|
17
18
|
var RealtimeAudioPacer = class {
|
|
18
19
|
constructor(params) {
|
|
19
20
|
this.params = params;
|
|
20
21
|
this.queue = [];
|
|
22
|
+
this.queueHead = 0;
|
|
21
23
|
this.timer = null;
|
|
22
24
|
this.queuedAudioBytes = 0;
|
|
23
25
|
this.closed = false;
|
|
@@ -55,7 +57,7 @@ var RealtimeAudioPacer = class {
|
|
|
55
57
|
if (this.closed) return 0;
|
|
56
58
|
const clearedAudioBytes = this.queuedAudioBytes;
|
|
57
59
|
this.clearTimer();
|
|
58
|
-
this.
|
|
60
|
+
this.resetQueue();
|
|
59
61
|
this.queuedAudioBytes = 0;
|
|
60
62
|
this.params.send(this.params.serializer.clear());
|
|
61
63
|
return clearedAudioBytes;
|
|
@@ -68,7 +70,7 @@ var RealtimeAudioPacer = class {
|
|
|
68
70
|
close() {
|
|
69
71
|
this.closed = true;
|
|
70
72
|
this.clearTimer();
|
|
71
|
-
this.
|
|
73
|
+
this.resetQueue();
|
|
72
74
|
this.queuedAudioBytes = 0;
|
|
73
75
|
}
|
|
74
76
|
/** Clear the scheduled pump timer. */
|
|
@@ -86,11 +88,33 @@ var RealtimeAudioPacer = class {
|
|
|
86
88
|
this.close();
|
|
87
89
|
this.params.onBackpressure?.();
|
|
88
90
|
}
|
|
91
|
+
get pendingQueueSize() {
|
|
92
|
+
return Math.max(0, this.queue.length - this.queueHead);
|
|
93
|
+
}
|
|
94
|
+
/** Take one queued item without shifting the remaining paced-audio backlog. */
|
|
95
|
+
takeNextItem() {
|
|
96
|
+
if (this.queueHead >= this.queue.length) {
|
|
97
|
+
this.resetQueue();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const item = this.queue[this.queueHead];
|
|
101
|
+
this.queueHead += 1;
|
|
102
|
+
if (this.queueHead >= this.queue.length) this.resetQueue();
|
|
103
|
+
else if (this.queueHead > QUEUE_COMPACT_HEAD_THRESHOLD && this.queueHead * 2 > this.queue.length) {
|
|
104
|
+
this.queue.splice(0, this.queueHead);
|
|
105
|
+
this.queueHead = 0;
|
|
106
|
+
}
|
|
107
|
+
return item;
|
|
108
|
+
}
|
|
109
|
+
resetQueue() {
|
|
110
|
+
this.queue.length = 0;
|
|
111
|
+
this.queueHead = 0;
|
|
112
|
+
}
|
|
89
113
|
/** Send one queued item and schedule the next send based on audio duration. */
|
|
90
114
|
pump() {
|
|
91
115
|
this.timer = null;
|
|
92
116
|
if (this.closed) return;
|
|
93
|
-
const item = this.
|
|
117
|
+
const item = this.takeNextItem();
|
|
94
118
|
if (!item) return;
|
|
95
119
|
let delayMs = 0;
|
|
96
120
|
let sent;
|
|
@@ -100,11 +124,11 @@ var RealtimeAudioPacer = class {
|
|
|
100
124
|
delayMs = item.durationMs || TELEPHONY_CHUNK_MS;
|
|
101
125
|
} else sent = this.params.send(this.params.serializer.mark(item.name));
|
|
102
126
|
if (!sent) {
|
|
103
|
-
this.
|
|
127
|
+
this.resetQueue();
|
|
104
128
|
this.queuedAudioBytes = 0;
|
|
105
129
|
return;
|
|
106
130
|
}
|
|
107
|
-
if (this.
|
|
131
|
+
if (this.pendingQueueSize > 0) this.timer = setTimeout(() => this.pump(), delayMs);
|
|
108
132
|
}
|
|
109
133
|
};
|
|
110
134
|
//#endregion
|
|
@@ -377,6 +401,12 @@ function buildForcedConsultSpeechPrompt(result) {
|
|
|
377
401
|
trimmed.length <= FORCED_CONSULT_RESULT_MAX_CHARS ? trimmed : `${truncateUtf16Safe(trimmed, FORCED_CONSULT_RESULT_MAX_CHARS - 16).trimEnd()} [truncated]`
|
|
378
402
|
].join("\n");
|
|
379
403
|
}
|
|
404
|
+
async function waitForNativeConsult(state) {
|
|
405
|
+
return await Promise.race([state.promise.then((result) => ({
|
|
406
|
+
kind: "completed",
|
|
407
|
+
result
|
|
408
|
+
})), state.cancellation.then(() => ({ kind: "cancelled" }))]);
|
|
409
|
+
}
|
|
380
410
|
function appendRecentTalkEventMetadata(call, event) {
|
|
381
411
|
if (!call) return;
|
|
382
412
|
const metadata = call.metadata ?? {};
|
|
@@ -410,15 +440,16 @@ var RealtimeCallHandler = class {
|
|
|
410
440
|
this.resolveInstructions = resolveInstructions;
|
|
411
441
|
this.toolHandlers = /* @__PURE__ */ new Map();
|
|
412
442
|
this.pendingStreamTokens = /* @__PURE__ */ new Map();
|
|
443
|
+
this.activeSockets = /* @__PURE__ */ new Set();
|
|
444
|
+
this.serverClosingSockets = /* @__PURE__ */ new WeakSet();
|
|
413
445
|
this.activeBridgesByCallId = /* @__PURE__ */ new Map();
|
|
414
446
|
this.activeTelephonyClosersByCallId = /* @__PURE__ */ new Map();
|
|
415
|
-
this.
|
|
416
|
-
this.rawPartialUserTranscriptsByCallId = /* @__PURE__ */ new Map();
|
|
417
|
-
this.partialUserTranscriptUpdatedAtByCallId = /* @__PURE__ */ new Map();
|
|
418
|
-
this.recentFinalUserTranscriptsByCallId = /* @__PURE__ */ new Map();
|
|
419
|
-
this.recentFinalUserTranscriptTimersByCallId = /* @__PURE__ */ new Map();
|
|
447
|
+
this.userTranscriptStatesByCallId = /* @__PURE__ */ new Map();
|
|
420
448
|
this.forcedConsultsByCallId = /* @__PURE__ */ new Map();
|
|
449
|
+
this.consultSessionsByCallId = /* @__PURE__ */ new Map();
|
|
421
450
|
this.nativeConsultsInFlightByCallId = /* @__PURE__ */ new Map();
|
|
451
|
+
this.closePromise = null;
|
|
452
|
+
this.closing = false;
|
|
422
453
|
this.publicOrigin = null;
|
|
423
454
|
this.publicPathPrefix = "";
|
|
424
455
|
}
|
|
@@ -464,6 +495,11 @@ var RealtimeCallHandler = class {
|
|
|
464
495
|
}
|
|
465
496
|
}
|
|
466
497
|
handleWebSocketUpgrade(request, socket, head) {
|
|
498
|
+
if (this.closing) {
|
|
499
|
+
socket.write("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n");
|
|
500
|
+
socket.destroy();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
467
503
|
const token = new URL(request.url ?? "/", "wss://localhost").pathname.split("/").pop() ?? null;
|
|
468
504
|
const callerMeta = token ? this.consumeStreamToken(token) : null;
|
|
469
505
|
if (!callerMeta) {
|
|
@@ -476,6 +512,7 @@ var RealtimeCallHandler = class {
|
|
|
476
512
|
noServer: true,
|
|
477
513
|
maxPayload: MAX_REALTIME_MESSAGE_BYTES
|
|
478
514
|
}).handleUpgrade(request, socket, head, (ws) => {
|
|
515
|
+
this.activeSockets.add(ws);
|
|
479
516
|
let bridge = null;
|
|
480
517
|
let initialized = false;
|
|
481
518
|
let activeCallSid = "unknown";
|
|
@@ -530,13 +567,39 @@ var RealtimeCallHandler = class {
|
|
|
530
567
|
}
|
|
531
568
|
});
|
|
532
569
|
ws.on("close", (code) => {
|
|
533
|
-
|
|
570
|
+
this.activeSockets.delete(ws);
|
|
571
|
+
const reason = this.serverClosingSockets.has(ws) || stopReceived || code === 1e3 || code === 1005 ? "completed" : "error";
|
|
534
572
|
this.closeTelephonyBridge(activeCallSid, bridge, reason);
|
|
535
573
|
});
|
|
536
574
|
ws.on("error", (error) => {
|
|
537
575
|
console.error("[voice-call] realtime WS error:", error);
|
|
538
576
|
});
|
|
577
|
+
if (this.closing) {
|
|
578
|
+
this.serverClosingSockets.add(ws);
|
|
579
|
+
ws.terminate();
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
close(shutdownBarrier = Promise.resolve()) {
|
|
584
|
+
if (this.closePromise) return this.closePromise;
|
|
585
|
+
this.closing = true;
|
|
586
|
+
this.pendingStreamTokens.clear();
|
|
587
|
+
const sockets = [...this.activeSockets];
|
|
588
|
+
this.closePromise = Promise.all([shutdownBarrier, ...sockets.map((ws) => new Promise((resolve) => {
|
|
589
|
+
if (ws.readyState === WebSocket$1.CLOSED) {
|
|
590
|
+
resolve();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
this.serverClosingSockets.add(ws);
|
|
594
|
+
ws.once("close", () => resolve());
|
|
595
|
+
ws.terminate();
|
|
596
|
+
}))]).then(() => {
|
|
597
|
+
this.pendingStreamTokens.clear();
|
|
598
|
+
}).finally(() => {
|
|
599
|
+
this.closing = false;
|
|
600
|
+
this.closePromise = null;
|
|
539
601
|
});
|
|
602
|
+
return this.closePromise;
|
|
540
603
|
}
|
|
541
604
|
registerToolHandler(name, fn) {
|
|
542
605
|
this.toolHandlers.set(name, fn);
|
|
@@ -634,7 +697,7 @@ var RealtimeCallHandler = class {
|
|
|
634
697
|
},
|
|
635
698
|
onTalkEvent: (event) => appendRecentTalkEventMetadata(callRecord, event)
|
|
636
699
|
});
|
|
637
|
-
|
|
700
|
+
let providerHandlesInputAudioBargeIn = this.realtimeProvider.capabilities?.handlesInputAudioBargeIn === true;
|
|
638
701
|
const cancelOutputAudioForBargeIn = (source, interruptProvider, clearedAudioBytes = 0) => {
|
|
639
702
|
const outputAudioActive = harness.talk.outputAudioActive;
|
|
640
703
|
const pendingTelephonyAudio = audioPacer.hasPendingAudio();
|
|
@@ -669,6 +732,14 @@ var RealtimeCallHandler = class {
|
|
|
669
732
|
if (callEndEmitted) return;
|
|
670
733
|
callEndEmitted = true;
|
|
671
734
|
this.endCallInManager(callSid, callId, reason);
|
|
735
|
+
if (reason !== "error") return;
|
|
736
|
+
Promise.resolve(this.provider.hangupCall({
|
|
737
|
+
callId,
|
|
738
|
+
providerCallId: callSid,
|
|
739
|
+
reason
|
|
740
|
+
})).catch((error) => {
|
|
741
|
+
console.warn(`[voice-call] Failed to hang up realtime call ${callSid}: ${formatErrorMessage(error)}`);
|
|
742
|
+
});
|
|
672
743
|
};
|
|
673
744
|
const sendString = (message) => {
|
|
674
745
|
if (ws.readyState !== WebSocket$1.OPEN) return false;
|
|
@@ -703,7 +774,11 @@ var RealtimeCallHandler = class {
|
|
|
703
774
|
silenceFrames: 12
|
|
704
775
|
});
|
|
705
776
|
const interruptResponseOnInputAudio = typeof this.providerConfig.interruptResponseOnInputAudio === "boolean" ? this.providerConfig.interruptResponseOnInputAudio : void 0;
|
|
706
|
-
const
|
|
777
|
+
const hadPredecessorOnAdmission = this.activeBridgesByCallId.has(callId);
|
|
778
|
+
const nativeConsultOwner = {};
|
|
779
|
+
const userTranscriptAdoption = this.beginUserTranscriptOwnerAdoption(callId);
|
|
780
|
+
const userTranscriptOwner = userTranscriptAdoption.owner;
|
|
781
|
+
const bridgeParams = {
|
|
707
782
|
provider: this.realtimeProvider,
|
|
708
783
|
cfg: this.coreConfig,
|
|
709
784
|
providerConfig: this.providerConfig,
|
|
@@ -734,6 +809,8 @@ var RealtimeCallHandler = class {
|
|
|
734
809
|
}
|
|
735
810
|
},
|
|
736
811
|
onTranscript: (role, text, isFinal) => {
|
|
812
|
+
const owner = nativeConsultOwner.current;
|
|
813
|
+
if (!this.getUserTranscriptState(callId, userTranscriptOwner) || owner && !this.isActiveBridgeOwner(callId, owner)) return;
|
|
737
814
|
const turnId = harness.ensureTurn();
|
|
738
815
|
const eventType = role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta";
|
|
739
816
|
const payload = role === "assistant" ? { text } : {
|
|
@@ -757,19 +834,22 @@ var RealtimeCallHandler = class {
|
|
|
757
834
|
});
|
|
758
835
|
if (!isFinal) {
|
|
759
836
|
if (role === "user" && text.trim()) {
|
|
760
|
-
const transcript = this.recordPartialUserTranscript(callId, text);
|
|
837
|
+
const transcript = this.recordPartialUserTranscript(callId, userTranscriptOwner, text);
|
|
838
|
+
if (!transcript) return;
|
|
761
839
|
console.log(`[voice-call] realtime input transcript callId=${callId} providerCallId=${callSid} final=false chars=${text.trim().length} aggregateChars=${transcript.length}`);
|
|
762
840
|
}
|
|
763
841
|
return;
|
|
764
842
|
}
|
|
765
843
|
if (role === "user") {
|
|
844
|
+
const state = this.getUserTranscriptState(callId, userTranscriptOwner);
|
|
845
|
+
if (!state) return;
|
|
766
846
|
const transcript = resolveFinalTranscriptText({
|
|
767
|
-
partial:
|
|
768
|
-
rawPartial:
|
|
847
|
+
partial: state.partial,
|
|
848
|
+
rawPartial: state.rawPartial,
|
|
769
849
|
final: text
|
|
770
850
|
});
|
|
771
|
-
this.clearPartialUserTranscript(callId);
|
|
772
|
-
this.setRecentFinalUserTranscript(callId, transcript);
|
|
851
|
+
this.clearPartialUserTranscript(callId, userTranscriptOwner);
|
|
852
|
+
this.setRecentFinalUserTranscript(callId, userTranscriptOwner, transcript);
|
|
773
853
|
console.log(`[voice-call] realtime input transcript callId=${callId} providerCallId=${callSid} final=true chars=${text.trim().length} aggregateChars=${transcript.length}`);
|
|
774
854
|
const event = {
|
|
775
855
|
id: `realtime-speech-${callSid}-${Date.now()}`,
|
|
@@ -787,6 +867,7 @@ var RealtimeCallHandler = class {
|
|
|
787
867
|
callId,
|
|
788
868
|
callSid,
|
|
789
869
|
transcript,
|
|
870
|
+
userTranscriptOwner,
|
|
790
871
|
clearAudio: () => {
|
|
791
872
|
const clearedBytes = audioPacer.clearAudio();
|
|
792
873
|
console.log(`[voice-call] realtime forced consult cleared outbound audio callId=${callId} providerCallId=${callSid} queuedBytes=${clearedBytes}`);
|
|
@@ -816,9 +897,30 @@ var RealtimeCallHandler = class {
|
|
|
816
897
|
}
|
|
817
898
|
});
|
|
818
899
|
console.log(`[voice-call] realtime tool call received callId=${callId} providerCallId=${callSid} tool=${toolEvent.name}`);
|
|
819
|
-
return this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, harness);
|
|
900
|
+
return this.executeToolCall(sessionLocal, callId, toolEvent.callId || toolEvent.itemId, toolEvent.name, toolEvent.args, turnId, harness, userTranscriptOwner);
|
|
820
901
|
},
|
|
821
902
|
onEvent: (event) => {
|
|
903
|
+
if (event.direction === "client" && event.type === "session.continuity.reset") {
|
|
904
|
+
const turnId = harness.talk.activeTurnId;
|
|
905
|
+
const owner = nativeConsultOwner.current;
|
|
906
|
+
if (owner && this.isActiveBridgeOwner(callId, owner)) {
|
|
907
|
+
this.resetUserTranscriptState(callId, userTranscriptOwner);
|
|
908
|
+
this.resetConsultSessionForContinuity(callId, owner);
|
|
909
|
+
}
|
|
910
|
+
harness.flushOutput(() => {
|
|
911
|
+
audioPacer.clearAudio();
|
|
912
|
+
harness.finishOutputAudio(event.type);
|
|
913
|
+
});
|
|
914
|
+
if (turnId) harness.talk.cancelTurn({
|
|
915
|
+
turnId,
|
|
916
|
+
payload: {
|
|
917
|
+
callId,
|
|
918
|
+
providerCallId: callSid,
|
|
919
|
+
reason: event.type
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
822
924
|
if (event.type === "input_audio_buffer.speech_started") {
|
|
823
925
|
harness.ensureTurn();
|
|
824
926
|
return;
|
|
@@ -838,17 +940,15 @@ var RealtimeCallHandler = class {
|
|
|
838
940
|
});
|
|
839
941
|
return;
|
|
840
942
|
}
|
|
841
|
-
if (event.type === "response.done") {
|
|
842
|
-
harness.finishOutputAudio("response.done");
|
|
843
|
-
harness.endTurn("response.done");
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
943
|
if (event.type === "error") harness.emit({
|
|
847
944
|
type: "session.error",
|
|
848
945
|
payload: { message: event.detail ?? "Realtime provider error" },
|
|
849
946
|
final: true
|
|
850
947
|
});
|
|
851
948
|
},
|
|
949
|
+
onResponseDone: (outcome) => {
|
|
950
|
+
if (outcome.status === "failed" || outcome.status === "incomplete") console.warn(`[voice-call] realtime response ${outcome.status}: ${outcome.message}`);
|
|
951
|
+
},
|
|
852
952
|
onReady: () => {
|
|
853
953
|
harness.emit({
|
|
854
954
|
type: "session.ready",
|
|
@@ -867,11 +967,13 @@ var RealtimeCallHandler = class {
|
|
|
867
967
|
});
|
|
868
968
|
},
|
|
869
969
|
onClose: (reason) => {
|
|
870
|
-
|
|
871
|
-
this.
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
970
|
+
const owner = nativeConsultOwner.current;
|
|
971
|
+
const ownsCallState = owner ? this.isActiveBridgeOwner(callId, owner) : false;
|
|
972
|
+
if (owner) {
|
|
973
|
+
this.clearActiveBridgeMappings(callId, callSid, owner);
|
|
974
|
+
this.cancelConsultSession(callId, owner);
|
|
975
|
+
}
|
|
976
|
+
if (ownsCallState) this.clearUserTranscriptState(callId, userTranscriptOwner);
|
|
875
977
|
harness.finishOutputAudio(reason);
|
|
876
978
|
harness.emit({
|
|
877
979
|
type: "session.closed",
|
|
@@ -879,17 +981,26 @@ var RealtimeCallHandler = class {
|
|
|
879
981
|
final: true
|
|
880
982
|
});
|
|
881
983
|
if (reason !== "error") return;
|
|
882
|
-
emitCallEnd("error");
|
|
883
984
|
if (ws.readyState === WebSocket$1.OPEN) ws.close(1011, "Bridge disconnected");
|
|
884
|
-
this.
|
|
885
|
-
|
|
886
|
-
providerCallId: callSid,
|
|
887
|
-
reason: "error"
|
|
888
|
-
}).catch((error) => {
|
|
889
|
-
console.warn(`[voice-call] Failed to hang up realtime call ${callSid}: ${formatErrorMessage(error)}`);
|
|
890
|
-
});
|
|
985
|
+
if (owner && !ownsCallState || !owner && hadPredecessorOnAdmission && this.activeBridgesByCallId.has(callId)) return;
|
|
986
|
+
emitCallEnd("error");
|
|
891
987
|
}
|
|
892
|
-
}
|
|
988
|
+
};
|
|
989
|
+
let session;
|
|
990
|
+
try {
|
|
991
|
+
session = harness.createBridge(bridgeParams);
|
|
992
|
+
} catch (error) {
|
|
993
|
+
this.rollbackUserTranscriptOwnerAdoption(callId, userTranscriptAdoption);
|
|
994
|
+
harness.close();
|
|
995
|
+
audioPacer.close();
|
|
996
|
+
if (!hadPredecessorOnAdmission || !this.activeBridgesByCallId.has(callId)) emitCallEnd("error");
|
|
997
|
+
if (ws.readyState === WebSocket$1.OPEN) ws.close(1011, "Failed to create realtime bridge");
|
|
998
|
+
console.error("[voice-call] Failed to create realtime bridge:", error);
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
this.commitUserTranscriptOwnerAdoption(callId, userTranscriptAdoption);
|
|
1002
|
+
nativeConsultOwner.current = session;
|
|
1003
|
+
providerHandlesInputAudioBargeIn = session.bridge.handlesInputAudioBargeIn ?? providerHandlesInputAudioBargeIn;
|
|
893
1004
|
const closeTelephony = (reason) => {
|
|
894
1005
|
try {
|
|
895
1006
|
session.close();
|
|
@@ -897,10 +1008,20 @@ var RealtimeCallHandler = class {
|
|
|
897
1008
|
emitCallEnd(reason);
|
|
898
1009
|
}
|
|
899
1010
|
};
|
|
1011
|
+
const previousConsultSession = this.consultSessionsByCallId.get(callId);
|
|
1012
|
+
if (previousConsultSession && previousConsultSession.owner !== session) this.cancelConsultSession(callId, previousConsultSession.owner);
|
|
1013
|
+
this.consultSessionsByCallId.set(callId, {
|
|
1014
|
+
owner: session,
|
|
1015
|
+
coordinator: harness.forcedConsults
|
|
1016
|
+
});
|
|
900
1017
|
this.activeBridgesByCallId.set(callId, session);
|
|
901
1018
|
this.activeBridgesByCallId.set(callSid, session);
|
|
902
|
-
|
|
903
|
-
|
|
1019
|
+
const telephonyCloser = {
|
|
1020
|
+
owner: session,
|
|
1021
|
+
close: closeTelephony
|
|
1022
|
+
};
|
|
1023
|
+
this.activeTelephonyClosersByCallId.set(callId, telephonyCloser);
|
|
1024
|
+
this.activeTelephonyClosersByCallId.set(callSid, telephonyCloser);
|
|
904
1025
|
const sendAudioToSession = session.sendAudio.bind(session);
|
|
905
1026
|
session.sendAudio = (audio) => {
|
|
906
1027
|
if (speechDetector.accept({
|
|
@@ -923,86 +1044,181 @@ var RealtimeCallHandler = class {
|
|
|
923
1044
|
try {
|
|
924
1045
|
closeSession();
|
|
925
1046
|
} finally {
|
|
926
|
-
this.
|
|
927
|
-
this.
|
|
928
|
-
this.
|
|
929
|
-
this.activeTelephonyClosersByCallId.delete(callSid);
|
|
930
|
-
this.clearUserTranscriptState(callId);
|
|
931
|
-
this.forcedConsultsByCallId.delete(callId);
|
|
1047
|
+
this.clearActiveBridgeMappings(callId, callSid, session);
|
|
1048
|
+
this.cancelConsultSession(callId, session);
|
|
1049
|
+
this.clearUserTranscriptState(callId, userTranscriptOwner);
|
|
932
1050
|
harness.close();
|
|
933
1051
|
audioPacer.close();
|
|
934
1052
|
}
|
|
935
1053
|
};
|
|
936
1054
|
session.connect().catch((error) => {
|
|
937
1055
|
console.error("[voice-call] Failed to connect realtime bridge:", error);
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1056
|
+
const ownsCallState = this.isActiveBridgeOwner(callId, session);
|
|
1057
|
+
try {
|
|
1058
|
+
session.close();
|
|
1059
|
+
} catch (closeError) {
|
|
1060
|
+
console.warn(`[voice-call] Failed to close realtime bridge ${callSid}: ${formatErrorMessage(closeError)}`);
|
|
1061
|
+
} finally {
|
|
1062
|
+
if (ownsCallState) emitCallEnd("error");
|
|
1063
|
+
ws.close(1011, "Failed to connect");
|
|
1064
|
+
}
|
|
941
1065
|
});
|
|
942
1066
|
return session;
|
|
943
1067
|
}
|
|
944
|
-
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
this.
|
|
1068
|
+
beginUserTranscriptOwnerAdoption(callId) {
|
|
1069
|
+
const adoption = {
|
|
1070
|
+
owner: {},
|
|
1071
|
+
previous: this.userTranscriptStatesByCallId.get(callId)
|
|
1072
|
+
};
|
|
1073
|
+
this.userTranscriptStatesByCallId.set(callId, adoption.owner);
|
|
1074
|
+
return adoption;
|
|
1075
|
+
}
|
|
1076
|
+
commitUserTranscriptOwnerAdoption(callId, adoption) {
|
|
1077
|
+
if (this.userTranscriptStatesByCallId.get(callId) !== adoption.owner) return;
|
|
1078
|
+
if (adoption.previous?.recentFinalTimer) {
|
|
1079
|
+
clearTimeout(adoption.previous.recentFinalTimer);
|
|
1080
|
+
adoption.previous.recentFinalTimer = void 0;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
rollbackUserTranscriptOwnerAdoption(callId, adoption) {
|
|
1084
|
+
if (this.userTranscriptStatesByCallId.get(callId) !== adoption.owner) return;
|
|
1085
|
+
if (adoption.owner.recentFinalTimer) {
|
|
1086
|
+
clearTimeout(adoption.owner.recentFinalTimer);
|
|
1087
|
+
adoption.owner.recentFinalTimer = void 0;
|
|
1088
|
+
}
|
|
1089
|
+
if (adoption.previous) {
|
|
1090
|
+
this.userTranscriptStatesByCallId.set(callId, adoption.previous);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
this.userTranscriptStatesByCallId.delete(callId);
|
|
1094
|
+
}
|
|
1095
|
+
getUserTranscriptState(callId, owner) {
|
|
1096
|
+
const state = this.userTranscriptStatesByCallId.get(callId);
|
|
1097
|
+
return state === owner ? state : void 0;
|
|
1098
|
+
}
|
|
1099
|
+
recordPartialUserTranscript(callId, owner, text) {
|
|
1100
|
+
const state = this.getUserTranscriptState(callId, owner);
|
|
1101
|
+
if (!state) return;
|
|
1102
|
+
const next = limitPartialUserTranscript(appendTranscriptText(state.partial, text));
|
|
1103
|
+
const raw = limitPartialUserTranscript(`${state.rawPartial ?? ""}${text}`);
|
|
1104
|
+
state.partial = next;
|
|
1105
|
+
state.rawPartial = raw;
|
|
1106
|
+
state.partialUpdatedAt = Date.now();
|
|
950
1107
|
return next;
|
|
951
1108
|
}
|
|
952
|
-
clearPartialUserTranscript(callId) {
|
|
953
|
-
this.
|
|
954
|
-
|
|
955
|
-
|
|
1109
|
+
clearPartialUserTranscript(callId, owner) {
|
|
1110
|
+
const state = this.getUserTranscriptState(callId, owner);
|
|
1111
|
+
if (!state) return;
|
|
1112
|
+
state.partial = void 0;
|
|
1113
|
+
state.rawPartial = void 0;
|
|
1114
|
+
state.partialUpdatedAt = void 0;
|
|
956
1115
|
}
|
|
957
|
-
setRecentFinalUserTranscript(callId, text) {
|
|
958
|
-
this.
|
|
959
|
-
|
|
1116
|
+
setRecentFinalUserTranscript(callId, owner, text) {
|
|
1117
|
+
const state = this.getUserTranscriptState(callId, owner);
|
|
1118
|
+
if (!state) return;
|
|
1119
|
+
this.clearRecentFinalUserTranscript(callId, owner);
|
|
1120
|
+
state.recentFinal = text;
|
|
960
1121
|
const timer = setTimeout(() => {
|
|
961
|
-
if (this.
|
|
962
|
-
|
|
1122
|
+
if (this.userTranscriptStatesByCallId.get(callId) !== state) return;
|
|
1123
|
+
if (state.recentFinal === text) state.recentFinal = void 0;
|
|
1124
|
+
if (state.recentFinalTimer === timer) state.recentFinalTimer = void 0;
|
|
963
1125
|
}, RECENT_FINAL_USER_TRANSCRIPT_TTL_MS);
|
|
964
1126
|
timer.unref?.();
|
|
965
|
-
|
|
1127
|
+
state.recentFinalTimer = timer;
|
|
966
1128
|
}
|
|
967
|
-
clearRecentFinalUserTranscript(callId) {
|
|
968
|
-
const
|
|
969
|
-
if (
|
|
970
|
-
|
|
971
|
-
|
|
1129
|
+
clearRecentFinalUserTranscript(callId, owner) {
|
|
1130
|
+
const state = this.getUserTranscriptState(callId, owner);
|
|
1131
|
+
if (!state) return;
|
|
1132
|
+
if (state.recentFinalTimer) {
|
|
1133
|
+
clearTimeout(state.recentFinalTimer);
|
|
1134
|
+
state.recentFinalTimer = void 0;
|
|
972
1135
|
}
|
|
973
|
-
|
|
1136
|
+
state.recentFinal = void 0;
|
|
1137
|
+
}
|
|
1138
|
+
resetUserTranscriptState(callId, owner) {
|
|
1139
|
+
this.clearPartialUserTranscript(callId, owner);
|
|
1140
|
+
this.clearRecentFinalUserTranscript(callId, owner);
|
|
1141
|
+
}
|
|
1142
|
+
clearUserTranscriptState(callId, owner) {
|
|
1143
|
+
const state = this.getUserTranscriptState(callId, owner);
|
|
1144
|
+
if (!state) return;
|
|
1145
|
+
if (state.recentFinalTimer) clearTimeout(state.recentFinalTimer);
|
|
1146
|
+
if (this.userTranscriptStatesByCallId.get(callId) === state) this.userTranscriptStatesByCallId.delete(callId);
|
|
1147
|
+
}
|
|
1148
|
+
cancelNativeConsult(callId, owner) {
|
|
1149
|
+
const state = this.nativeConsultsInFlightByCallId.get(callId);
|
|
1150
|
+
if (!state || state.owner !== owner) return;
|
|
1151
|
+
state.cancelled = true;
|
|
1152
|
+
this.nativeConsultsInFlightByCallId.delete(callId);
|
|
1153
|
+
state.cancel();
|
|
1154
|
+
}
|
|
1155
|
+
cancelForcedConsult(callId, owner) {
|
|
1156
|
+
const state = this.forcedConsultsByCallId.get(callId);
|
|
1157
|
+
if (!state || state.owner !== owner) return;
|
|
1158
|
+
state.cancelled = true;
|
|
1159
|
+
state.sendSpeechPrompt = false;
|
|
1160
|
+
state.cancel();
|
|
1161
|
+
this.forcedConsultsByCallId.delete(callId);
|
|
1162
|
+
}
|
|
1163
|
+
resetConsultSession(callId, owner) {
|
|
1164
|
+
const session = this.consultSessionsByCallId.get(callId);
|
|
1165
|
+
if (!session || session.owner !== owner) return false;
|
|
1166
|
+
session.coordinator.clearPending();
|
|
1167
|
+
this.cancelForcedConsult(callId, owner);
|
|
1168
|
+
this.cancelNativeConsult(callId, owner);
|
|
1169
|
+
return true;
|
|
1170
|
+
}
|
|
1171
|
+
resetConsultSessionForContinuity(callId, owner) {
|
|
1172
|
+
const session = this.consultSessionsByCallId.get(callId);
|
|
1173
|
+
if (!session || session.owner !== owner) return false;
|
|
1174
|
+
this.cancelForcedConsult(callId, owner);
|
|
1175
|
+
this.cancelNativeConsult(callId, owner);
|
|
1176
|
+
session.coordinator.clear();
|
|
1177
|
+
return true;
|
|
1178
|
+
}
|
|
1179
|
+
cancelConsultSession(callId, owner) {
|
|
1180
|
+
if (!owner || !this.resetConsultSession(callId, owner)) return;
|
|
1181
|
+
this.consultSessionsByCallId.delete(callId);
|
|
1182
|
+
}
|
|
1183
|
+
isActiveBridgeOwner(callId, owner) {
|
|
1184
|
+
return this.activeBridgesByCallId.get(callId) === owner;
|
|
974
1185
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1186
|
+
clearActiveBridgeMappings(callId, callSid, owner) {
|
|
1187
|
+
for (const key of [callId, callSid]) {
|
|
1188
|
+
if (this.activeBridgesByCallId.get(key) !== owner) continue;
|
|
1189
|
+
this.activeBridgesByCallId.delete(key);
|
|
1190
|
+
this.activeTelephonyClosersByCallId.delete(key);
|
|
1191
|
+
}
|
|
978
1192
|
}
|
|
979
|
-
resolveUserTranscriptContext(callId) {
|
|
980
|
-
|
|
1193
|
+
resolveUserTranscriptContext(callId, owner) {
|
|
1194
|
+
const state = this.getUserTranscriptState(callId, owner);
|
|
1195
|
+
return state?.partial ?? state?.recentFinal;
|
|
981
1196
|
}
|
|
982
|
-
consumePartialUserTranscript(callId, consumed) {
|
|
1197
|
+
consumePartialUserTranscript(callId, owner, consumed) {
|
|
983
1198
|
const text = consumed?.trim();
|
|
984
1199
|
if (!text) return;
|
|
985
|
-
const
|
|
1200
|
+
const state = this.getUserTranscriptState(callId, owner);
|
|
1201
|
+
const current = state?.partial;
|
|
986
1202
|
if (!current) return;
|
|
987
1203
|
if (current === text) {
|
|
988
|
-
this.clearPartialUserTranscript(callId);
|
|
1204
|
+
this.clearPartialUserTranscript(callId, owner);
|
|
989
1205
|
return;
|
|
990
1206
|
}
|
|
991
1207
|
if (current.toLowerCase().startsWith(text.toLowerCase())) {
|
|
992
1208
|
const remaining = current.slice(text.length).trimStart();
|
|
993
1209
|
if (remaining) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
} else this.clearPartialUserTranscript(callId);
|
|
1210
|
+
state.partial = remaining;
|
|
1211
|
+
state.rawPartial = remaining;
|
|
1212
|
+
} else this.clearPartialUserTranscript(callId, owner);
|
|
997
1213
|
}
|
|
998
|
-
const recent =
|
|
1214
|
+
const recent = state.recentFinal;
|
|
999
1215
|
if (!recent) return;
|
|
1000
|
-
if (recent === text || recent.toLowerCase().startsWith(text.toLowerCase())) this.clearRecentFinalUserTranscript(callId);
|
|
1216
|
+
if (recent === text || recent.toLowerCase().startsWith(text.toLowerCase())) this.clearRecentFinalUserTranscript(callId, owner);
|
|
1001
1217
|
}
|
|
1002
|
-
async waitForConsultTranscriptSettle(callId, startedAt) {
|
|
1218
|
+
async waitForConsultTranscriptSettle(callId, owner, startedAt) {
|
|
1003
1219
|
const deadline = startedAt + CONSULT_TRANSCRIPT_SETTLE_MAX_MS;
|
|
1004
1220
|
while (true) {
|
|
1005
|
-
const updatedAt = this.
|
|
1221
|
+
const updatedAt = this.getUserTranscriptState(callId, owner)?.partialUpdatedAt;
|
|
1006
1222
|
if (!updatedAt) return;
|
|
1007
1223
|
const now = Date.now();
|
|
1008
1224
|
const quietFor = now - updatedAt;
|
|
@@ -1014,14 +1230,14 @@ var RealtimeCallHandler = class {
|
|
|
1014
1230
|
}
|
|
1015
1231
|
closeTelephonyBridge(callIdOrSid, bridge, reason) {
|
|
1016
1232
|
const closer = this.activeTelephonyClosersByCallId.get(callIdOrSid);
|
|
1017
|
-
if (closer) {
|
|
1018
|
-
closer(reason);
|
|
1233
|
+
if (closer && closer.owner === bridge) {
|
|
1234
|
+
closer.close(reason);
|
|
1019
1235
|
return;
|
|
1020
1236
|
}
|
|
1021
1237
|
bridge?.close();
|
|
1022
1238
|
}
|
|
1023
1239
|
scheduleForcedAgentConsult(params) {
|
|
1024
|
-
if (this.config.consultPolicy !== "always") return;
|
|
1240
|
+
if (this.config.consultPolicy !== "always" || this.activeBridgesByCallId.get(params.callId) !== params.session) return;
|
|
1025
1241
|
const question = params.transcript.trim();
|
|
1026
1242
|
if (!question) return;
|
|
1027
1243
|
const handler = this.toolHandlers.get(REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME);
|
|
@@ -1050,13 +1266,24 @@ var RealtimeCallHandler = class {
|
|
|
1050
1266
|
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}`);
|
|
1051
1267
|
console.log(`[voice-call] realtime forced agent consult starting callId=${params.callId} providerCallId=${params.callSid} chars=${params.handle.question.length}`);
|
|
1052
1268
|
params.clearAudio();
|
|
1269
|
+
const abortController = new AbortController();
|
|
1053
1270
|
const state = {
|
|
1271
|
+
owner: params.session,
|
|
1054
1272
|
sendSpeechPrompt: true,
|
|
1055
|
-
|
|
1273
|
+
cancelled: false,
|
|
1274
|
+
cancel: () => {
|
|
1275
|
+
abortController.abort(/* @__PURE__ */ new Error("Realtime forced consult owner was cancelled."));
|
|
1276
|
+
coordinator.markCancelled(params.handle);
|
|
1277
|
+
},
|
|
1278
|
+
promise: Promise.resolve().then(() => {
|
|
1279
|
+
abortController.signal.throwIfAborted();
|
|
1280
|
+
return params.handler({ question: params.handle.question }, params.callId, { abortSignal: abortController.signal });
|
|
1281
|
+
})
|
|
1056
1282
|
};
|
|
1057
1283
|
this.forcedConsultsByCallId.set(params.callId, state);
|
|
1058
1284
|
try {
|
|
1059
1285
|
const result = await state.promise;
|
|
1286
|
+
if (state.cancelled || this.forcedConsultsByCallId.get(params.callId) !== state) return;
|
|
1060
1287
|
state.completedAt = Date.now();
|
|
1061
1288
|
coordinator.markDelivered(params.handle);
|
|
1062
1289
|
const text = readSpeakableRealtimeVoiceToolResult(result, {
|
|
@@ -1072,11 +1299,12 @@ var RealtimeCallHandler = class {
|
|
|
1072
1299
|
params.session.sendUserMessage(buildForcedConsultSpeechPrompt(text));
|
|
1073
1300
|
}
|
|
1074
1301
|
console.log(`[voice-call] realtime forced agent consult completed callId=${params.callId} providerCallId=${params.callSid} elapsedMs=${Date.now() - startedAt}`);
|
|
1075
|
-
this.consumePartialUserTranscript(params.callId, params.handle.question);
|
|
1302
|
+
this.consumePartialUserTranscript(params.callId, params.userTranscriptOwner, params.handle.question);
|
|
1076
1303
|
} catch (error) {
|
|
1077
|
-
console.warn(`[voice-call] realtime forced agent consult failed callId=${params.callId} providerCallId=${params.callSid} error=${formatErrorMessage(error)}`);
|
|
1304
|
+
if (!state.cancelled) console.warn(`[voice-call] realtime forced agent consult failed callId=${params.callId} providerCallId=${params.callSid} error=${formatErrorMessage(error)}`);
|
|
1078
1305
|
} finally {
|
|
1079
|
-
|
|
1306
|
+
if (!state.cancelled) if (this.forcedConsultsByCallId.get(params.callId) !== state) coordinator.remove(params.handle);
|
|
1307
|
+
else setTimeout(() => {
|
|
1080
1308
|
if (this.forcedConsultsByCallId.get(params.callId) === state) {
|
|
1081
1309
|
this.forcedConsultsByCallId.delete(params.callId);
|
|
1082
1310
|
coordinator.remove(params.handle);
|
|
@@ -1136,7 +1364,7 @@ var RealtimeCallHandler = class {
|
|
|
1136
1364
|
reason
|
|
1137
1365
|
});
|
|
1138
1366
|
}
|
|
1139
|
-
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, harness) {
|
|
1367
|
+
async executeToolCall(bridge, callId, bridgeCallId, name, args, turnId, harness, userTranscriptOwner) {
|
|
1140
1368
|
const handler = this.toolHandlers.get(name);
|
|
1141
1369
|
const startedAt = Date.now();
|
|
1142
1370
|
const hasResultError = (result) => {
|
|
@@ -1173,13 +1401,15 @@ var RealtimeCallHandler = class {
|
|
|
1173
1401
|
}
|
|
1174
1402
|
};
|
|
1175
1403
|
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME) {
|
|
1404
|
+
if (this.activeBridgesByCallId.get(callId) !== bridge) return;
|
|
1176
1405
|
const coordinator = harness.forcedConsults;
|
|
1177
1406
|
const forcedMatch = coordinator.recordNativeConsult(args, bridgeCallId);
|
|
1178
1407
|
if (forcedMatch.kind === "none") {
|
|
1179
1408
|
const pending = coordinator.consumePending();
|
|
1180
1409
|
if (pending) coordinator.remove(pending);
|
|
1181
1410
|
}
|
|
1182
|
-
const
|
|
1411
|
+
const forcedConsultState = this.forcedConsultsByCallId.get(callId);
|
|
1412
|
+
const forcedConsult = forcedConsultState?.owner === bridge && !forcedConsultState.cancelled ? forcedConsultState : void 0;
|
|
1183
1413
|
if (forcedMatch.kind === "already_delivered" && coordinator.isCancelled(forcedMatch.handle)) {
|
|
1184
1414
|
if (forcedConsult) forcedConsult.sendSpeechPrompt = false;
|
|
1185
1415
|
await submitFinalToolResult({
|
|
@@ -1197,24 +1427,50 @@ var RealtimeCallHandler = class {
|
|
|
1197
1427
|
return;
|
|
1198
1428
|
}
|
|
1199
1429
|
forcedConsult.sendSpeechPrompt = false;
|
|
1200
|
-
|
|
1430
|
+
const result = await forcedConsult.promise.catch((error) => ({ error: formatErrorMessage(error) }));
|
|
1431
|
+
if (forcedConsult.cancelled || forcedConsult.owner !== bridge || this.forcedConsultsByCallId.get(callId) !== forcedConsult) return;
|
|
1432
|
+
await submitFinalToolResult(result);
|
|
1201
1433
|
return;
|
|
1202
1434
|
}
|
|
1203
1435
|
const existingNativeConsult = this.nativeConsultsInFlightByCallId.get(callId);
|
|
1204
1436
|
if (existingNativeConsult) {
|
|
1205
1437
|
console.log(`[voice-call] realtime tool call sharing in-flight agent consult callId=${callId} ageMs=${Date.now() - existingNativeConsult.startedAt}`);
|
|
1206
1438
|
await submitWorkingResponse();
|
|
1207
|
-
await
|
|
1439
|
+
const outcome = await waitForNativeConsult(existingNativeConsult);
|
|
1440
|
+
if (outcome.kind === "cancelled") return;
|
|
1441
|
+
await submitFinalToolResult(outcome.result);
|
|
1208
1442
|
return;
|
|
1209
1443
|
}
|
|
1444
|
+
const abortController = new AbortController();
|
|
1445
|
+
let releaseCancellation = () => {};
|
|
1446
|
+
const cancellation = new Promise((resolve) => {
|
|
1447
|
+
releaseCancellation = resolve;
|
|
1448
|
+
});
|
|
1449
|
+
let completeConsult = (_result) => {};
|
|
1210
1450
|
const state = {
|
|
1451
|
+
owner: bridge,
|
|
1211
1452
|
startedAt,
|
|
1212
|
-
promise: Promise
|
|
1453
|
+
promise: new Promise((resolve) => {
|
|
1454
|
+
completeConsult = resolve;
|
|
1455
|
+
}),
|
|
1456
|
+
cancellation,
|
|
1457
|
+
cancelled: false,
|
|
1458
|
+
cancel: () => {
|
|
1459
|
+
abortController.abort(/* @__PURE__ */ new Error("Realtime native consult owner was cancelled."));
|
|
1460
|
+
releaseCancellation();
|
|
1461
|
+
}
|
|
1213
1462
|
};
|
|
1214
|
-
|
|
1463
|
+
this.nativeConsultsInFlightByCallId.set(callId, state);
|
|
1464
|
+
(async () => {
|
|
1215
1465
|
try {
|
|
1216
|
-
await
|
|
1217
|
-
|
|
1466
|
+
await submitWorkingResponse();
|
|
1467
|
+
if (state.cancelled) return;
|
|
1468
|
+
await Promise.race([this.waitForConsultTranscriptSettle(callId, userTranscriptOwner, startedAt), state.cancellation]);
|
|
1469
|
+
if (state.cancelled) return;
|
|
1470
|
+
const context = {
|
|
1471
|
+
partialUserTranscript: this.resolveUserTranscriptContext(callId, userTranscriptOwner),
|
|
1472
|
+
abortSignal: abortController.signal
|
|
1473
|
+
};
|
|
1218
1474
|
state.partialUserTranscript = context.partialUserTranscript;
|
|
1219
1475
|
const handlerArgs = withFallbackConsultQuestion(args, context.partialUserTranscript);
|
|
1220
1476
|
console.log(`[voice-call] realtime tool call executing callId=${callId} tool=${name} hasHandler=${Boolean(handler)}`);
|
|
@@ -1222,29 +1478,30 @@ var RealtimeCallHandler = class {
|
|
|
1222
1478
|
} catch (error) {
|
|
1223
1479
|
return { error: formatErrorMessage(error) };
|
|
1224
1480
|
}
|
|
1225
|
-
});
|
|
1226
|
-
this.nativeConsultsInFlightByCallId.set(callId, state);
|
|
1481
|
+
})().then(completeConsult);
|
|
1227
1482
|
try {
|
|
1228
|
-
const
|
|
1483
|
+
const outcome = await waitForNativeConsult(state);
|
|
1484
|
+
if (outcome.kind === "cancelled") return;
|
|
1485
|
+
const result = outcome.result;
|
|
1229
1486
|
const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
|
|
1230
1487
|
const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
|
|
1231
1488
|
console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
|
|
1232
1489
|
await submitFinalToolResult(result);
|
|
1233
|
-
if (status === "ok") this.consumePartialUserTranscript(callId, state.partialUserTranscript);
|
|
1490
|
+
if (status === "ok") this.consumePartialUserTranscript(callId, userTranscriptOwner, state.partialUserTranscript);
|
|
1234
1491
|
} finally {
|
|
1235
1492
|
if (this.nativeConsultsInFlightByCallId.get(callId) === state) this.nativeConsultsInFlightByCallId.delete(callId);
|
|
1236
1493
|
}
|
|
1237
1494
|
return;
|
|
1238
1495
|
}
|
|
1239
1496
|
console.log(`[voice-call] realtime tool call executing callId=${callId} tool=${name} hasHandler=${Boolean(handler)}`);
|
|
1240
|
-
const context = { partialUserTranscript: this.resolveUserTranscriptContext(callId) };
|
|
1497
|
+
const context = { partialUserTranscript: this.resolveUserTranscriptContext(callId, userTranscriptOwner) };
|
|
1241
1498
|
const handlerArgs = name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME ? withFallbackConsultQuestion(args, context.partialUserTranscript) : args;
|
|
1242
1499
|
const result = !handler ? { error: `Tool "${name}" not available` } : await handler(handlerArgs, callId, context).catch((error) => ({ error: formatErrorMessage(error) }));
|
|
1243
1500
|
const status = result && typeof result === "object" && !Array.isArray(result) && "error" in result ? "error" : "ok";
|
|
1244
1501
|
const error = status === "error" && result && typeof result === "object" && !Array.isArray(result) ? formatErrorMessage(result.error ?? "unknown") : void 0;
|
|
1245
1502
|
console.log(`[voice-call] realtime tool call completed callId=${callId} tool=${name} status=${status} elapsedMs=${Date.now() - startedAt}${error ? ` error=${error}` : ""}`);
|
|
1246
1503
|
await submitFinalToolResult(result);
|
|
1247
|
-
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && status === "ok") this.consumePartialUserTranscript(callId, context.partialUserTranscript);
|
|
1504
|
+
if (name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME && status === "ok") this.consumePartialUserTranscript(callId, userTranscriptOwner, context.partialUserTranscript);
|
|
1248
1505
|
}
|
|
1249
1506
|
};
|
|
1250
1507
|
//#endregion
|