@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,659 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import "
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "../runtime-api.js";
|
|
2
|
+
import "../api.js";
|
|
3
|
+
import { a as resolveVoiceCallEffectiveConfig, c as validateProviderConfig, d as deepMergeDefined, i as resolveVoiceCallConfig, n as normalizeVoiceCallConfig, o as resolveVoiceCallNumberRouteKeyForCall, r as resolveTwilioAuthToken, s as resolveVoiceCallSessionKey } from "./config-BBpso-fA.mjs";
|
|
4
|
+
import { c as findCallMatchesInStore, f as persistCallRecord, g as TerminalStates, h as setVoiceCallStateRuntime, l as getCallHistoryFromStore, u as loadActiveCallsFromStore } from "./store-1UfyY3HS.mjs";
|
|
5
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
6
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
7
7
|
import { MAX_TIMER_TIMEOUT_MS, asDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
8
|
-
import { normalizeAgentId
|
|
8
|
+
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
9
9
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
10
10
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
11
|
-
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
12
11
|
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
13
|
-
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME,
|
|
14
|
-
import { mergeDeep } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
15
|
-
import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
|
16
|
-
import { canonicalizeMainSessionAlias } from "openclaw/plugin-sdk/session-store-runtime";
|
|
17
|
-
import { resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-core";
|
|
12
|
+
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, buildRealtimeVoiceAgentConsultPolicyInstructions, consultRealtimeVoiceAgent, convertPcmToMulaw8k, createTalkSessionController, recordTalkObservabilityEvent, resolveRealtimeVoiceAgentConsultTools, resolveRealtimeVoiceAgentConsultToolsAllow, resolveRealtimeVoiceFastContextConsult } from "openclaw/plugin-sdk/realtime-voice";
|
|
18
13
|
import { WEBHOOK_BODY_READ_DEFAULTS, createWebhookInFlightLimiter, normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
|
|
19
|
-
import { z } from "zod";
|
|
20
14
|
import fs from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
21
17
|
import crypto from "node:crypto";
|
|
22
18
|
import { redactIdentifier } from "openclaw/plugin-sdk/logging-core";
|
|
23
|
-
import path from "node:path";
|
|
24
|
-
import os from "node:os";
|
|
25
19
|
import { root } from "openclaw/plugin-sdk/security-runtime";
|
|
26
20
|
import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
21
|
+
import { parseTtsDirectives } from "openclaw/plugin-sdk/speech";
|
|
27
22
|
import { spawn } from "node:child_process";
|
|
28
|
-
import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime";
|
|
29
23
|
import http from "node:http";
|
|
30
24
|
import { URL as URL$1 } from "node:url";
|
|
31
25
|
import { resolveConfiguredCapabilityProvider } from "openclaw/plugin-sdk/provider-selection-runtime";
|
|
32
26
|
import { WebSocket, WebSocketServer } from "ws";
|
|
33
|
-
import { canonicalizeBase64 } from "openclaw/plugin-sdk/media-runtime";
|
|
34
|
-
//#region extensions/voice-call/src/realtime-defaults.ts
|
|
35
|
-
/** Baseline instructions that keep realtime calls brief and route deep work to agent consult. */
|
|
36
|
-
const DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS = `You are OpenClaw's phone-call realtime voice interface. Keep spoken replies brief and natural. When a question needs deeper reasoning, current information, or tools, call ${REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME} before answering.`;
|
|
37
|
-
//#endregion
|
|
38
|
-
//#region extensions/voice-call/src/config.ts
|
|
39
|
-
/**
|
|
40
|
-
* E.164 phone number format: +[country code][number]
|
|
41
|
-
* Examples use 555 prefix (reserved for fictional numbers)
|
|
42
|
-
*/
|
|
43
|
-
const E164Schema = z.string().regex(/^\+[1-9]\d{1,14}$/, "Expected E.164 format, e.g. +15550001234");
|
|
44
|
-
/**
|
|
45
|
-
* Controls how inbound calls are handled:
|
|
46
|
-
* - "disabled": Block all inbound calls (outbound only)
|
|
47
|
-
* - "allowlist": Only accept calls from numbers in allowFrom
|
|
48
|
-
* - "pairing": Unknown callers can request pairing (future)
|
|
49
|
-
* - "open": Accept all inbound calls (dangerous!)
|
|
50
|
-
*/
|
|
51
|
-
const InboundPolicySchema = z.enum([
|
|
52
|
-
"disabled",
|
|
53
|
-
"allowlist",
|
|
54
|
-
"pairing",
|
|
55
|
-
"open"
|
|
56
|
-
]);
|
|
57
|
-
const SecretInputSchema = buildSecretInputSchema();
|
|
58
|
-
const TelnyxConfigSchema = z.object({
|
|
59
|
-
/** Telnyx API v2 key */
|
|
60
|
-
apiKey: z.string().min(1).optional(),
|
|
61
|
-
/** Telnyx connection ID (from Call Control app) */
|
|
62
|
-
connectionId: z.string().min(1).optional(),
|
|
63
|
-
/** Public key for webhook signature verification */
|
|
64
|
-
publicKey: z.string().min(1).optional()
|
|
65
|
-
}).strict();
|
|
66
|
-
const TwilioConfigSchema = z.object({
|
|
67
|
-
/** Twilio Account SID */
|
|
68
|
-
accountSid: z.string().min(1).optional(),
|
|
69
|
-
/** Twilio Auth Token */
|
|
70
|
-
authToken: SecretInputSchema.optional(),
|
|
71
|
-
/** Twilio processing Region (for example, ie1) */
|
|
72
|
-
region: z.enum(TWILIO_REGIONS).optional()
|
|
73
|
-
}).strict();
|
|
74
|
-
const PlivoConfigSchema = z.object({
|
|
75
|
-
/** Plivo Auth ID (starts with MA/SA) */
|
|
76
|
-
authId: z.string().min(1).optional(),
|
|
77
|
-
/** Plivo Auth Token */
|
|
78
|
-
authToken: z.string().min(1).optional()
|
|
79
|
-
}).strict();
|
|
80
|
-
const VoiceCallNumberRouteConfigSchema = z.object({
|
|
81
|
-
/** Greeting message for inbound calls to this number. */
|
|
82
|
-
inboundGreeting: z.string().optional(),
|
|
83
|
-
/** TTS override for inbound calls to this number. Deep-merges with global voice-call TTS. */
|
|
84
|
-
tts: TtsConfigSchema,
|
|
85
|
-
/** Agent ID to use for voice response generation for this number. */
|
|
86
|
-
agentId: z.string().min(1).optional(),
|
|
87
|
-
/** Optional model override for voice responses for this number. */
|
|
88
|
-
responseModel: z.string().optional(),
|
|
89
|
-
/** System prompt for voice responses for this number. */
|
|
90
|
-
responseSystemPrompt: z.string().optional(),
|
|
91
|
-
/** Timeout for response generation in ms for this number. */
|
|
92
|
-
responseTimeoutMs: z.number().int().positive().optional()
|
|
93
|
-
}).strict();
|
|
94
|
-
const VoiceCallServeConfigSchema = z.object({
|
|
95
|
-
/** Port to listen on */
|
|
96
|
-
port: z.number().int().positive().default(3334),
|
|
97
|
-
/** Bind address */
|
|
98
|
-
bind: z.string().default("127.0.0.1"),
|
|
99
|
-
/** Webhook path */
|
|
100
|
-
path: z.string().min(1).default("/voice/webhook")
|
|
101
|
-
}).strict().default({
|
|
102
|
-
port: 3334,
|
|
103
|
-
bind: "127.0.0.1",
|
|
104
|
-
path: "/voice/webhook"
|
|
105
|
-
});
|
|
106
|
-
const VoiceCallTailscaleConfigSchema = z.object({
|
|
107
|
-
/**
|
|
108
|
-
* Tailscale exposure mode:
|
|
109
|
-
* - "off": No Tailscale exposure
|
|
110
|
-
* - "serve": Tailscale serve (private to tailnet)
|
|
111
|
-
* - "funnel": Tailscale funnel (public HTTPS)
|
|
112
|
-
*/
|
|
113
|
-
mode: z.enum([
|
|
114
|
-
"off",
|
|
115
|
-
"serve",
|
|
116
|
-
"funnel"
|
|
117
|
-
]).default("off"),
|
|
118
|
-
/** Path for Tailscale serve/funnel (should usually match serve.path) */
|
|
119
|
-
path: z.string().min(1).default("/voice/webhook")
|
|
120
|
-
}).strict().default({
|
|
121
|
-
mode: "off",
|
|
122
|
-
path: "/voice/webhook"
|
|
123
|
-
});
|
|
124
|
-
const VoiceCallTunnelConfigSchema = z.object({
|
|
125
|
-
/**
|
|
126
|
-
* Tunnel provider:
|
|
127
|
-
* - "none": No tunnel (use publicUrl if set, or manual setup)
|
|
128
|
-
* - "ngrok": Use ngrok for public HTTPS tunnel
|
|
129
|
-
* - "tailscale-serve": Tailscale serve (private to tailnet)
|
|
130
|
-
* - "tailscale-funnel": Tailscale funnel (public HTTPS)
|
|
131
|
-
*/
|
|
132
|
-
provider: z.enum([
|
|
133
|
-
"none",
|
|
134
|
-
"ngrok",
|
|
135
|
-
"tailscale-serve",
|
|
136
|
-
"tailscale-funnel"
|
|
137
|
-
]).default("none"),
|
|
138
|
-
/** ngrok auth token (optional, enables longer sessions and more features) */
|
|
139
|
-
ngrokAuthToken: z.string().min(1).optional(),
|
|
140
|
-
/** ngrok custom domain (paid feature, e.g., "myapp.ngrok.io") */
|
|
141
|
-
ngrokDomain: z.string().min(1).optional(),
|
|
142
|
-
/**
|
|
143
|
-
* Allow ngrok free tier compatibility mode.
|
|
144
|
-
* When true, forwarded headers may be trusted for loopback requests
|
|
145
|
-
* to reconstruct the public ngrok URL used for signing.
|
|
146
|
-
*
|
|
147
|
-
* IMPORTANT: This does NOT bypass signature verification.
|
|
148
|
-
*/
|
|
149
|
-
allowNgrokFreeTierLoopbackBypass: z.boolean().default(false)
|
|
150
|
-
}).strict().default({
|
|
151
|
-
provider: "none",
|
|
152
|
-
allowNgrokFreeTierLoopbackBypass: false
|
|
153
|
-
});
|
|
154
|
-
const VoiceCallWebhookSecurityConfigSchema = z.object({
|
|
155
|
-
/**
|
|
156
|
-
* Allowed hostnames for webhook URL reconstruction.
|
|
157
|
-
* Only these hosts are accepted from forwarding headers.
|
|
158
|
-
*/
|
|
159
|
-
allowedHosts: z.array(z.string().min(1)).default([]),
|
|
160
|
-
/**
|
|
161
|
-
* Trust X-Forwarded-* headers without a hostname allowlist.
|
|
162
|
-
* WARNING: Only enable if you trust your proxy configuration.
|
|
163
|
-
*/
|
|
164
|
-
trustForwardingHeaders: z.boolean().default(false),
|
|
165
|
-
/**
|
|
166
|
-
* Trusted proxy IP addresses. Forwarded headers are only trusted when
|
|
167
|
-
* the remote IP matches one of these addresses.
|
|
168
|
-
*/
|
|
169
|
-
trustedProxyIPs: z.array(z.string().min(1)).default([])
|
|
170
|
-
}).strict().default({
|
|
171
|
-
allowedHosts: [],
|
|
172
|
-
trustForwardingHeaders: false,
|
|
173
|
-
trustedProxyIPs: []
|
|
174
|
-
});
|
|
175
|
-
/**
|
|
176
|
-
* Call mode determines how outbound calls behave:
|
|
177
|
-
* - "notify": Deliver message and auto-hangup after delay (one-way notification)
|
|
178
|
-
* - "conversation": Stay open for back-and-forth until explicit end or timeout
|
|
179
|
-
*/
|
|
180
|
-
const CallModeSchema = z.enum(["notify", "conversation"]);
|
|
181
|
-
const VoiceCallSessionScopeSchema = z.enum(["per-phone", "per-call"]);
|
|
182
|
-
const OutboundConfigSchema = z.object({
|
|
183
|
-
/** Default call mode for outbound calls */
|
|
184
|
-
defaultMode: CallModeSchema.default("notify"),
|
|
185
|
-
/** Seconds to wait after TTS before auto-hangup in notify mode */
|
|
186
|
-
notifyHangupDelaySec: z.number().int().nonnegative().default(3)
|
|
187
|
-
}).strict().default({
|
|
188
|
-
defaultMode: "notify",
|
|
189
|
-
notifyHangupDelaySec: 3
|
|
190
|
-
});
|
|
191
|
-
const RealtimeToolSchema = z.object({
|
|
192
|
-
type: z.literal("function"),
|
|
193
|
-
name: z.string().min(1),
|
|
194
|
-
description: z.string(),
|
|
195
|
-
parameters: z.object({
|
|
196
|
-
type: z.literal("object"),
|
|
197
|
-
properties: z.record(z.string(), z.unknown()),
|
|
198
|
-
required: z.array(z.string()).optional()
|
|
199
|
-
})
|
|
200
|
-
}).strict();
|
|
201
|
-
const VoiceCallRealtimeProvidersConfigSchema = z.record(z.string(), z.record(z.string(), z.unknown())).default({});
|
|
202
|
-
const VoiceCallRealtimeToolPolicySchema = z.enum(REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES);
|
|
203
|
-
const VoiceCallRealtimeConsultPolicySchema = z.enum([
|
|
204
|
-
"auto",
|
|
205
|
-
"substantive",
|
|
206
|
-
"always"
|
|
207
|
-
]);
|
|
208
|
-
const VoiceCallRealtimeFastContextSourceSchema = z.enum(["memory", "sessions"]);
|
|
209
|
-
const VoiceCallRealtimeFastContextConfigSchema = z.object({
|
|
210
|
-
/** Enable bounded memory/session lookup before the full consult agent. */
|
|
211
|
-
enabled: z.boolean().default(false),
|
|
212
|
-
/** Hard deadline for the fast context lookup. */
|
|
213
|
-
timeoutMs: z.number().int().positive().default(800),
|
|
214
|
-
/** Maximum memory/session hits to inject into the realtime tool result. */
|
|
215
|
-
maxResults: z.number().int().positive().default(3),
|
|
216
|
-
/** Indexed sources used by the fast context lookup. */
|
|
217
|
-
sources: z.array(VoiceCallRealtimeFastContextSourceSchema).min(1).default(["memory", "sessions"]),
|
|
218
|
-
/** Fall back to the full agent consult when fast context has no answer. */
|
|
219
|
-
fallbackToConsult: z.boolean().default(false)
|
|
220
|
-
}).strict().default({
|
|
221
|
-
enabled: false,
|
|
222
|
-
timeoutMs: 800,
|
|
223
|
-
maxResults: 3,
|
|
224
|
-
sources: ["memory", "sessions"],
|
|
225
|
-
fallbackToConsult: false
|
|
226
|
-
});
|
|
227
|
-
const VoiceCallRealtimeAgentContextConfigSchema = z.object({
|
|
228
|
-
/** Inject a compact agent persona/context capsule into realtime voice instructions. */
|
|
229
|
-
enabled: z.boolean().default(false),
|
|
230
|
-
/** Maximum number of characters from the generated capsule to append. */
|
|
231
|
-
maxChars: z.number().int().positive().default(6e3),
|
|
232
|
-
/** Include configured agent identity fields. */
|
|
233
|
-
includeIdentity: z.boolean().default(true),
|
|
234
|
-
/** Include selected workspace files such as SOUL.md and IDENTITY.md. */
|
|
235
|
-
includeWorkspaceFiles: z.boolean().default(true),
|
|
236
|
-
/** Workspace-relative files to include, bounded by maxChars. */
|
|
237
|
-
files: z.array(z.string().min(1)).default([
|
|
238
|
-
"SOUL.md",
|
|
239
|
-
"IDENTITY.md",
|
|
240
|
-
"USER.md"
|
|
241
|
-
])
|
|
242
|
-
}).strict().default({
|
|
243
|
-
enabled: false,
|
|
244
|
-
maxChars: 6e3,
|
|
245
|
-
includeIdentity: true,
|
|
246
|
-
includeWorkspaceFiles: true,
|
|
247
|
-
files: [
|
|
248
|
-
"SOUL.md",
|
|
249
|
-
"IDENTITY.md",
|
|
250
|
-
"USER.md"
|
|
251
|
-
]
|
|
252
|
-
});
|
|
253
|
-
const VoiceCallRealtimeConsultThinkingLevelSchema = z.enum([
|
|
254
|
-
"off",
|
|
255
|
-
"minimal",
|
|
256
|
-
"low",
|
|
257
|
-
"medium",
|
|
258
|
-
"high",
|
|
259
|
-
"xhigh",
|
|
260
|
-
"adaptive",
|
|
261
|
-
"max",
|
|
262
|
-
"ultra"
|
|
263
|
-
]);
|
|
264
|
-
const VoiceCallStreamingProvidersConfigSchema = z.record(z.string(), z.record(z.string(), z.unknown())).default({});
|
|
265
|
-
const VoiceCallRealtimeConfigSchema = z.object({
|
|
266
|
-
/** Enable realtime voice-to-voice mode. */
|
|
267
|
-
enabled: z.boolean().default(false),
|
|
268
|
-
/** Provider id from registered realtime voice providers. */
|
|
269
|
-
provider: z.string().min(1).optional(),
|
|
270
|
-
/** Optional override for the local WebSocket route path. */
|
|
271
|
-
streamPath: z.string().min(1).optional(),
|
|
272
|
-
/** System instructions passed to the realtime provider. */
|
|
273
|
-
instructions: z.string().default(DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS),
|
|
274
|
-
/** Tool policy for the shared OpenClaw agent consult tool. */
|
|
275
|
-
toolPolicy: VoiceCallRealtimeToolPolicySchema.default("safe-read-only"),
|
|
276
|
-
/** Guidance for when the realtime model should call the OpenClaw agent consult tool. */
|
|
277
|
-
consultPolicy: VoiceCallRealtimeConsultPolicySchema.default("auto"),
|
|
278
|
-
/** Optional thinking level override for the regular agent behind realtime consults. */
|
|
279
|
-
consultThinkingLevel: VoiceCallRealtimeConsultThinkingLevelSchema.optional(),
|
|
280
|
-
/** Optional fast mode override for the regular agent behind realtime consults. */
|
|
281
|
-
consultFastMode: z.boolean().optional(),
|
|
282
|
-
/** Tool definitions exposed to the realtime provider. */
|
|
283
|
-
tools: z.array(RealtimeToolSchema).default([]),
|
|
284
|
-
/** Low-latency memory/session context for the consult tool. */
|
|
285
|
-
fastContext: VoiceCallRealtimeFastContextConfigSchema,
|
|
286
|
-
/** Bounded agent persona/context injection for the fast realtime voice path. */
|
|
287
|
-
agentContext: VoiceCallRealtimeAgentContextConfigSchema,
|
|
288
|
-
/** Provider-owned raw config blobs keyed by provider id. */
|
|
289
|
-
providers: VoiceCallRealtimeProvidersConfigSchema
|
|
290
|
-
}).strict().default({
|
|
291
|
-
enabled: false,
|
|
292
|
-
instructions: DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS,
|
|
293
|
-
toolPolicy: "safe-read-only",
|
|
294
|
-
consultPolicy: "auto",
|
|
295
|
-
tools: [],
|
|
296
|
-
fastContext: {
|
|
297
|
-
enabled: false,
|
|
298
|
-
timeoutMs: 800,
|
|
299
|
-
maxResults: 3,
|
|
300
|
-
sources: ["memory", "sessions"],
|
|
301
|
-
fallbackToConsult: false
|
|
302
|
-
},
|
|
303
|
-
agentContext: {
|
|
304
|
-
enabled: false,
|
|
305
|
-
maxChars: 6e3,
|
|
306
|
-
includeIdentity: true,
|
|
307
|
-
includeWorkspaceFiles: true,
|
|
308
|
-
files: [
|
|
309
|
-
"SOUL.md",
|
|
310
|
-
"IDENTITY.md",
|
|
311
|
-
"USER.md"
|
|
312
|
-
]
|
|
313
|
-
},
|
|
314
|
-
providers: {}
|
|
315
|
-
});
|
|
316
|
-
const VoiceCallStreamingConfigSchema = z.object({
|
|
317
|
-
/** Enable Twilio Media Streams for real-time transcription. */
|
|
318
|
-
enabled: z.boolean().default(false),
|
|
319
|
-
/** Provider id from registered realtime transcription providers. */
|
|
320
|
-
provider: z.string().min(1).optional(),
|
|
321
|
-
/** WebSocket path for media stream connections */
|
|
322
|
-
streamPath: z.string().min(1).default("/voice/stream"),
|
|
323
|
-
/** Provider-owned raw config blobs keyed by provider id. */
|
|
324
|
-
providers: VoiceCallStreamingProvidersConfigSchema,
|
|
325
|
-
/**
|
|
326
|
-
* Close unauthenticated media stream sockets if no valid `start` frame arrives in time.
|
|
327
|
-
* Protects against pre-auth idle connection hold attacks.
|
|
328
|
-
*/
|
|
329
|
-
preStartTimeoutMs: z.number().int().positive().default(5e3),
|
|
330
|
-
/** Maximum number of concurrently pending (pre-start) media stream sockets. */
|
|
331
|
-
maxPendingConnections: z.number().int().positive().default(32),
|
|
332
|
-
/** Maximum pending media stream sockets per source IP. */
|
|
333
|
-
maxPendingConnectionsPerIp: z.number().int().positive().default(4),
|
|
334
|
-
/** Hard cap for all open media stream sockets (pending + active). */
|
|
335
|
-
maxConnections: z.number().int().positive().default(128)
|
|
336
|
-
}).strict().default({
|
|
337
|
-
enabled: false,
|
|
338
|
-
streamPath: "/voice/stream",
|
|
339
|
-
providers: {},
|
|
340
|
-
preStartTimeoutMs: 5e3,
|
|
341
|
-
maxPendingConnections: 32,
|
|
342
|
-
maxPendingConnectionsPerIp: 4,
|
|
343
|
-
maxConnections: 128
|
|
344
|
-
});
|
|
345
|
-
const VoiceCallConfigSchema = z.object({
|
|
346
|
-
/** Enable voice call functionality */
|
|
347
|
-
enabled: z.boolean().default(false),
|
|
348
|
-
/** Active provider (telnyx, twilio, plivo, or mock) */
|
|
349
|
-
provider: z.enum([
|
|
350
|
-
"telnyx",
|
|
351
|
-
"twilio",
|
|
352
|
-
"plivo",
|
|
353
|
-
"mock"
|
|
354
|
-
]).optional(),
|
|
355
|
-
/** Telnyx-specific configuration */
|
|
356
|
-
telnyx: TelnyxConfigSchema.optional(),
|
|
357
|
-
/** Twilio-specific configuration */
|
|
358
|
-
twilio: TwilioConfigSchema.optional(),
|
|
359
|
-
/** Plivo-specific configuration */
|
|
360
|
-
plivo: PlivoConfigSchema.optional(),
|
|
361
|
-
/** Phone number to call from (E.164) */
|
|
362
|
-
fromNumber: E164Schema.optional(),
|
|
363
|
-
/** Default phone number to call (E.164) */
|
|
364
|
-
toNumber: E164Schema.optional(),
|
|
365
|
-
/** Inbound call policy */
|
|
366
|
-
inboundPolicy: InboundPolicySchema.default("disabled"),
|
|
367
|
-
/** Allowlist of phone numbers for inbound calls (E.164) */
|
|
368
|
-
allowFrom: z.array(E164Schema).default([]),
|
|
369
|
-
/** Greeting message for inbound calls */
|
|
370
|
-
inboundGreeting: z.string().optional(),
|
|
371
|
-
/** Per-dialed-number overrides for inbound calls. Keys are E.164 numbers. */
|
|
372
|
-
numbers: z.record(E164Schema, VoiceCallNumberRouteConfigSchema).default({}),
|
|
373
|
-
/** Outbound call configuration */
|
|
374
|
-
outbound: OutboundConfigSchema,
|
|
375
|
-
/** Maximum call duration in seconds */
|
|
376
|
-
maxDurationSeconds: z.number().int().positive().default(300),
|
|
377
|
-
/**
|
|
378
|
-
* Maximum age of a call in seconds before it is automatically reaped.
|
|
379
|
-
* Catches calls stuck before answer (for example, local mock calls that
|
|
380
|
-
* never receive provider webhooks). Set to 0 to disable.
|
|
381
|
-
*/
|
|
382
|
-
staleCallReaperSeconds: z.number().int().nonnegative().default(120),
|
|
383
|
-
/** Silence timeout for end-of-speech detection (ms) */
|
|
384
|
-
silenceTimeoutMs: z.number().int().positive().default(800),
|
|
385
|
-
/** Timeout for user transcript (ms) */
|
|
386
|
-
transcriptTimeoutMs: z.number().int().positive().default(18e4),
|
|
387
|
-
/** Ring timeout for outbound calls (ms) */
|
|
388
|
-
ringTimeoutMs: z.number().int().positive().default(3e4),
|
|
389
|
-
/** Maximum concurrent calls */
|
|
390
|
-
maxConcurrentCalls: z.number().int().positive().default(1),
|
|
391
|
-
/** Webhook server configuration */
|
|
392
|
-
serve: VoiceCallServeConfigSchema,
|
|
393
|
-
/** @deprecated Prefer tunnel config. */
|
|
394
|
-
tailscale: VoiceCallTailscaleConfigSchema,
|
|
395
|
-
/** Tunnel configuration (unified ngrok/tailscale) */
|
|
396
|
-
tunnel: VoiceCallTunnelConfigSchema,
|
|
397
|
-
/** Webhook signature reconstruction and proxy trust configuration */
|
|
398
|
-
webhookSecurity: VoiceCallWebhookSecurityConfigSchema,
|
|
399
|
-
/** Real-time audio streaming configuration */
|
|
400
|
-
streaming: VoiceCallStreamingConfigSchema,
|
|
401
|
-
/** Realtime voice-to-voice configuration */
|
|
402
|
-
realtime: VoiceCallRealtimeConfigSchema,
|
|
403
|
-
/** Session memory scope for voice conversations. */
|
|
404
|
-
sessionScope: VoiceCallSessionScopeSchema.default("per-phone"),
|
|
405
|
-
/** Public webhook URL override (if set, bypasses tunnel auto-detection) */
|
|
406
|
-
publicUrl: z.string().url().optional(),
|
|
407
|
-
/** Skip webhook signature verification (development only, NOT for production) */
|
|
408
|
-
skipSignatureVerification: z.boolean().default(false),
|
|
409
|
-
/** TTS override (deep-merges with core tts) */
|
|
410
|
-
tts: TtsConfigSchema,
|
|
411
|
-
/** Store path for call logs */
|
|
412
|
-
store: z.string().optional(),
|
|
413
|
-
/** Agent ID to use for voice response generation. Defaults to "main". */
|
|
414
|
-
agentId: z.string().min(1).optional(),
|
|
415
|
-
/** Optional model override for generating voice responses. */
|
|
416
|
-
responseModel: z.string().optional(),
|
|
417
|
-
/** System prompt for voice responses */
|
|
418
|
-
responseSystemPrompt: z.string().optional(),
|
|
419
|
-
/** Timeout for response generation in ms (default 30s) */
|
|
420
|
-
responseTimeoutMs: z.number().int().positive().default(3e4)
|
|
421
|
-
}).strict();
|
|
422
|
-
const TWILIO_AUTH_TOKEN_PATH = "plugins.entries.voice-call.config.twilio.authToken";
|
|
423
|
-
const DEFAULT_VOICE_CALL_CONFIG = VoiceCallConfigSchema.parse({});
|
|
424
|
-
function cloneDefaultVoiceCallConfig() {
|
|
425
|
-
return structuredClone(DEFAULT_VOICE_CALL_CONFIG);
|
|
426
|
-
}
|
|
427
|
-
function defaultRealtimeStreamPathForServePath(servePath) {
|
|
428
|
-
const normalized = normalizeWebhookPath(servePath);
|
|
429
|
-
if (normalized.endsWith("/webhook")) return `${normalized.slice(0, -8)}/stream/realtime`;
|
|
430
|
-
if (normalized === "/") return "/voice/stream/realtime";
|
|
431
|
-
return `${normalized}/stream/realtime`;
|
|
432
|
-
}
|
|
433
|
-
function normalizeVoiceCallTtsConfig(defaults, overrides) {
|
|
434
|
-
if (!defaults && !overrides) return;
|
|
435
|
-
return TtsConfigSchema.parse(mergeDeep(defaults ?? {}, overrides ?? {}));
|
|
436
|
-
}
|
|
437
|
-
function normalizePhoneRouteKey(phone) {
|
|
438
|
-
return phone?.replace(/\D/g, "") ?? "";
|
|
439
|
-
}
|
|
440
|
-
function resolveVoiceCallNumberRouteKey(config, phone) {
|
|
441
|
-
const routes = config.numbers;
|
|
442
|
-
if (!routes) return;
|
|
443
|
-
if (phone && Object.hasOwn(routes, phone)) return phone;
|
|
444
|
-
const normalizedPhone = normalizePhoneRouteKey(phone);
|
|
445
|
-
if (!normalizedPhone) return;
|
|
446
|
-
return Object.keys(routes).find((routeKey) => normalizePhoneRouteKey(routeKey) === normalizedPhone);
|
|
447
|
-
}
|
|
448
|
-
/** Resolve inbound-only number routing from a persisted call record. */
|
|
449
|
-
function resolveVoiceCallNumberRouteKeyForCall(call) {
|
|
450
|
-
if (call.direction !== "inbound") return;
|
|
451
|
-
const storedRouteKey = call.metadata?.numberRouteKey;
|
|
452
|
-
if (typeof storedRouteKey === "string") return storedRouteKey;
|
|
453
|
-
return call.to;
|
|
454
|
-
}
|
|
455
|
-
function resolveVoiceCallEffectiveConfig(config, phoneOrRouteKey) {
|
|
456
|
-
const numberRouteKey = resolveVoiceCallNumberRouteKey(config, phoneOrRouteKey);
|
|
457
|
-
if (!numberRouteKey) return { config };
|
|
458
|
-
const route = config.numbers[numberRouteKey];
|
|
459
|
-
if (!route) return { config };
|
|
460
|
-
return {
|
|
461
|
-
numberRouteKey,
|
|
462
|
-
config: {
|
|
463
|
-
...config,
|
|
464
|
-
...route,
|
|
465
|
-
tts: normalizeVoiceCallTtsConfig(config.tts, route.tts),
|
|
466
|
-
numbers: config.numbers
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
function sanitizeVoiceCallProviderConfigs(value) {
|
|
471
|
-
if (!value) return {};
|
|
472
|
-
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0));
|
|
473
|
-
}
|
|
474
|
-
function sanitizeVoiceCallNumberRoutes(value) {
|
|
475
|
-
if (!value) return {};
|
|
476
|
-
return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0).map(([key, route]) => [key, VoiceCallNumberRouteConfigSchema.parse(route)]));
|
|
477
|
-
}
|
|
478
|
-
function resolveTwilioAuthToken(config) {
|
|
479
|
-
return normalizeResolvedSecretInputString({
|
|
480
|
-
value: config.twilio?.authToken,
|
|
481
|
-
path: TWILIO_AUTH_TOKEN_PATH
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
function normalizeVoiceCallConfig(config) {
|
|
485
|
-
const defaults = cloneDefaultVoiceCallConfig();
|
|
486
|
-
const serve = {
|
|
487
|
-
...defaults.serve,
|
|
488
|
-
...config.serve
|
|
489
|
-
};
|
|
490
|
-
const streamingProvider = config.streaming?.provider;
|
|
491
|
-
const streamingProviders = sanitizeVoiceCallProviderConfigs(config.streaming?.providers ?? defaults.streaming.providers);
|
|
492
|
-
const realtimeProvider = config.realtime?.provider ?? defaults.realtime.provider;
|
|
493
|
-
const realtimeProviders = sanitizeVoiceCallProviderConfigs(config.realtime?.providers ?? defaults.realtime.providers);
|
|
494
|
-
const realtimeFastContext = {
|
|
495
|
-
...defaults.realtime.fastContext,
|
|
496
|
-
...config.realtime?.fastContext,
|
|
497
|
-
sources: config.realtime?.fastContext?.sources ?? defaults.realtime.fastContext.sources
|
|
498
|
-
};
|
|
499
|
-
const realtimeAgentContext = {
|
|
500
|
-
...defaults.realtime.agentContext,
|
|
501
|
-
...config.realtime?.agentContext,
|
|
502
|
-
files: config.realtime?.agentContext?.files ?? defaults.realtime.agentContext.files
|
|
503
|
-
};
|
|
504
|
-
return {
|
|
505
|
-
...defaults,
|
|
506
|
-
...config,
|
|
507
|
-
allowFrom: config.allowFrom ?? defaults.allowFrom,
|
|
508
|
-
numbers: sanitizeVoiceCallNumberRoutes(config.numbers ?? defaults.numbers),
|
|
509
|
-
outbound: {
|
|
510
|
-
...defaults.outbound,
|
|
511
|
-
...config.outbound
|
|
512
|
-
},
|
|
513
|
-
serve,
|
|
514
|
-
tailscale: {
|
|
515
|
-
...defaults.tailscale,
|
|
516
|
-
...config.tailscale
|
|
517
|
-
},
|
|
518
|
-
tunnel: {
|
|
519
|
-
...defaults.tunnel,
|
|
520
|
-
...config.tunnel
|
|
521
|
-
},
|
|
522
|
-
webhookSecurity: {
|
|
523
|
-
...defaults.webhookSecurity,
|
|
524
|
-
...config.webhookSecurity,
|
|
525
|
-
allowedHosts: config.webhookSecurity?.allowedHosts ?? defaults.webhookSecurity.allowedHosts,
|
|
526
|
-
trustedProxyIPs: config.webhookSecurity?.trustedProxyIPs ?? defaults.webhookSecurity.trustedProxyIPs
|
|
527
|
-
},
|
|
528
|
-
streaming: {
|
|
529
|
-
...defaults.streaming,
|
|
530
|
-
...config.streaming,
|
|
531
|
-
provider: streamingProvider,
|
|
532
|
-
providers: streamingProviders
|
|
533
|
-
},
|
|
534
|
-
realtime: {
|
|
535
|
-
...defaults.realtime,
|
|
536
|
-
...config.realtime,
|
|
537
|
-
provider: realtimeProvider,
|
|
538
|
-
streamPath: config.realtime?.streamPath ?? defaultRealtimeStreamPathForServePath(serve.path ?? defaults.serve.path),
|
|
539
|
-
tools: config.realtime?.tools ?? defaults.realtime.tools,
|
|
540
|
-
consultThinkingLevel: VoiceCallRealtimeConsultThinkingLevelSchema.optional().parse(config.realtime?.consultThinkingLevel ?? defaults.realtime.consultThinkingLevel),
|
|
541
|
-
consultFastMode: config.realtime?.consultFastMode ?? defaults.realtime.consultFastMode,
|
|
542
|
-
fastContext: realtimeFastContext,
|
|
543
|
-
agentContext: realtimeAgentContext,
|
|
544
|
-
providers: realtimeProviders
|
|
545
|
-
},
|
|
546
|
-
tts: normalizeVoiceCallTtsConfig(defaults.tts, config.tts)
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
function resolveVoiceCallSessionKey(params) {
|
|
550
|
-
const explicit = params.explicitSessionKey?.trim();
|
|
551
|
-
if (explicit) return resolveVoiceCallAgentSessionKey({
|
|
552
|
-
config: params.config,
|
|
553
|
-
sessionKey: explicit,
|
|
554
|
-
coreSession: params.coreSession
|
|
555
|
-
});
|
|
556
|
-
const prefix = `agent:${normalizeAgentId(params.config.agentId)}:voice`;
|
|
557
|
-
if (params.config.sessionScope === "per-call") return `${prefix}:call:${params.callId}`.toLowerCase();
|
|
558
|
-
const normalizedPhone = params.phone?.replace(/\D/g, "");
|
|
559
|
-
return (normalizedPhone ? `${prefix}:${normalizedPhone}` : `${prefix}:${params.callId}`).toLowerCase();
|
|
560
|
-
}
|
|
561
|
-
/** Resolve persisted or integration-provided keys into the configured agent namespace. */
|
|
562
|
-
function resolveVoiceCallAgentSessionKey(params) {
|
|
563
|
-
const sessionKey = params.sessionKey.trim();
|
|
564
|
-
if (!sessionKey) throw new Error("Voice Call session key cannot be empty");
|
|
565
|
-
const lower = sessionKey.toLowerCase();
|
|
566
|
-
const agentId = normalizeAgentId(params.config.agentId);
|
|
567
|
-
if (lower === "global" || lower === "unknown") return lower;
|
|
568
|
-
const parsedInput = parseAgentSessionKey(sessionKey);
|
|
569
|
-
let normalizedScopedKey;
|
|
570
|
-
if (parsedInput && normalizeAgentId(parsedInput.agentId) === parsedInput.agentId && parsedInput.agentId === agentId) normalizedScopedKey = `agent:${parsedInput.agentId}:${parsedInput.rest}`;
|
|
571
|
-
else {
|
|
572
|
-
const wrappedInput = parseAgentSessionKey(`agent:${agentId}:${sessionKey}`);
|
|
573
|
-
if (!wrappedInput) throw new Error("Voice Call session key could not be normalized");
|
|
574
|
-
normalizedScopedKey = `agent:${agentId}:${wrappedInput.rest}`;
|
|
575
|
-
}
|
|
576
|
-
const canonicalMain = canonicalizeMainSessionAlias({
|
|
577
|
-
cfg: { session: params.coreSession },
|
|
578
|
-
agentId,
|
|
579
|
-
sessionKey: normalizedScopedKey
|
|
580
|
-
});
|
|
581
|
-
return canonicalMain === normalizedScopedKey ? normalizedScopedKey : canonicalMain;
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Resolves the configuration by merging environment variables into missing fields.
|
|
585
|
-
* Returns a new configuration object with environment variables applied.
|
|
586
|
-
*/
|
|
587
|
-
function resolveVoiceCallConfig(config) {
|
|
588
|
-
const resolved = normalizeVoiceCallConfig(config);
|
|
589
|
-
if (resolved.provider === "telnyx") {
|
|
590
|
-
resolved.telnyx = resolved.telnyx ?? {};
|
|
591
|
-
resolved.telnyx.apiKey = resolved.telnyx.apiKey ?? resolveSpeechProviderApiKey(process.env.TELNYX_API_KEY);
|
|
592
|
-
resolved.telnyx.connectionId = resolved.telnyx.connectionId ?? resolveSpeechProviderApiKey(process.env.TELNYX_CONNECTION_ID);
|
|
593
|
-
resolved.telnyx.publicKey = resolved.telnyx.publicKey ?? resolveSpeechProviderApiKey(process.env.TELNYX_PUBLIC_KEY);
|
|
594
|
-
}
|
|
595
|
-
if (resolved.provider === "twilio") {
|
|
596
|
-
resolved.fromNumber = resolved.fromNumber ?? resolveSpeechProviderApiKey(process.env.TWILIO_FROM_NUMBER);
|
|
597
|
-
resolved.twilio = resolved.twilio ?? {};
|
|
598
|
-
resolved.twilio.accountSid = resolved.twilio.accountSid ?? resolveSpeechProviderApiKey(process.env.TWILIO_ACCOUNT_SID);
|
|
599
|
-
resolved.twilio.authToken = resolved.twilio.authToken ?? resolveSpeechProviderApiKey(process.env.TWILIO_AUTH_TOKEN);
|
|
600
|
-
}
|
|
601
|
-
if (resolved.provider === "plivo") {
|
|
602
|
-
resolved.plivo = resolved.plivo ?? {};
|
|
603
|
-
resolved.plivo.authId = resolved.plivo.authId ?? resolveSpeechProviderApiKey(process.env.PLIVO_AUTH_ID);
|
|
604
|
-
resolved.plivo.authToken = resolved.plivo.authToken ?? resolveSpeechProviderApiKey(process.env.PLIVO_AUTH_TOKEN);
|
|
605
|
-
}
|
|
606
|
-
resolved.tunnel = resolved.tunnel ?? {
|
|
607
|
-
provider: "none",
|
|
608
|
-
allowNgrokFreeTierLoopbackBypass: false
|
|
609
|
-
};
|
|
610
|
-
resolved.tunnel.allowNgrokFreeTierLoopbackBypass = resolved.tunnel.allowNgrokFreeTierLoopbackBypass ?? false;
|
|
611
|
-
resolved.tunnel.ngrokAuthToken = resolved.tunnel.ngrokAuthToken ?? resolveSpeechProviderApiKey(process.env.NGROK_AUTHTOKEN);
|
|
612
|
-
resolved.tunnel.ngrokDomain = resolved.tunnel.ngrokDomain ?? resolveSpeechProviderApiKey(process.env.NGROK_DOMAIN);
|
|
613
|
-
resolved.webhookSecurity = resolved.webhookSecurity ?? {
|
|
614
|
-
allowedHosts: [],
|
|
615
|
-
trustForwardingHeaders: false,
|
|
616
|
-
trustedProxyIPs: []
|
|
617
|
-
};
|
|
618
|
-
resolved.webhookSecurity.allowedHosts = resolved.webhookSecurity.allowedHosts ?? [];
|
|
619
|
-
resolved.webhookSecurity.trustForwardingHeaders = resolved.webhookSecurity.trustForwardingHeaders ?? false;
|
|
620
|
-
resolved.webhookSecurity.trustedProxyIPs = resolved.webhookSecurity.trustedProxyIPs ?? [];
|
|
621
|
-
return normalizeVoiceCallConfig(resolved);
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Validate that the configuration has all required fields for the selected provider.
|
|
625
|
-
*/
|
|
626
|
-
function validateProviderConfig(config) {
|
|
627
|
-
const errors = [];
|
|
628
|
-
if (!config.enabled) return {
|
|
629
|
-
valid: true,
|
|
630
|
-
errors: []
|
|
631
|
-
};
|
|
632
|
-
if (!config.provider) errors.push("plugins.entries.voice-call.config.provider is required");
|
|
633
|
-
if (!config.fromNumber && config.provider !== "mock") errors.push(config.provider === "twilio" ? "plugins.entries.voice-call.config.fromNumber is required (or set TWILIO_FROM_NUMBER env)" : "plugins.entries.voice-call.config.fromNumber is required");
|
|
634
|
-
if (config.provider === "telnyx") {
|
|
635
|
-
if (!config.telnyx?.apiKey) errors.push("plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)");
|
|
636
|
-
if (!config.telnyx?.connectionId) errors.push("plugins.entries.voice-call.config.telnyx.connectionId is required (or set TELNYX_CONNECTION_ID env)");
|
|
637
|
-
if (!config.skipSignatureVerification && !config.telnyx?.publicKey) errors.push("plugins.entries.voice-call.config.telnyx.publicKey is required (or set TELNYX_PUBLIC_KEY env)");
|
|
638
|
-
}
|
|
639
|
-
if (config.provider === "twilio") {
|
|
640
|
-
if (!config.twilio?.accountSid) errors.push("plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)");
|
|
641
|
-
if (!hasConfiguredSecretInput(config.twilio?.authToken)) errors.push("plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)");
|
|
642
|
-
}
|
|
643
|
-
if (config.provider === "plivo") {
|
|
644
|
-
if (!config.plivo?.authId) errors.push("plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)");
|
|
645
|
-
if (!config.plivo?.authToken) errors.push("plugins.entries.voice-call.config.plivo.authToken is required (or set PLIVO_AUTH_TOKEN env)");
|
|
646
|
-
}
|
|
647
|
-
if (config.realtime.enabled && config.inboundPolicy === "disabled") errors.push("plugins.entries.voice-call.config.inboundPolicy must not be \"disabled\" when realtime.enabled is true");
|
|
648
|
-
if (config.realtime.enabled && config.streaming.enabled) errors.push("plugins.entries.voice-call.config.realtime.enabled and plugins.entries.voice-call.config.streaming.enabled cannot both be true");
|
|
649
|
-
if (config.streaming.enabled && config.provider && config.provider !== "twilio") errors.push("plugins.entries.voice-call.config.provider must be \"twilio\" when streaming.enabled is true");
|
|
650
|
-
if (config.realtime.enabled && config.provider && config.provider !== "twilio" && config.provider !== "telnyx" && config.provider !== "mock") errors.push("plugins.entries.voice-call.config.provider must be \"twilio\", \"telnyx\", or \"mock\" when realtime.enabled is true");
|
|
651
|
-
return {
|
|
652
|
-
valid: errors.length === 0,
|
|
653
|
-
errors
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
//#endregion
|
|
657
27
|
//#region extensions/voice-call/src/allowlist.ts
|
|
658
28
|
/** Normalize a phone number to digits only. */
|
|
659
29
|
function normalizePhoneNumber(input) {
|
|
@@ -876,7 +246,7 @@ const DEFAULT_POLLY_VOICE = "Polly.Joanna";
|
|
|
876
246
|
function mapVoiceToPolly(voice) {
|
|
877
247
|
if (!voice) return DEFAULT_POLLY_VOICE;
|
|
878
248
|
if (voice.startsWith("Polly.") || voice.startsWith("Google.")) return voice;
|
|
879
|
-
return OPENAI_TO_POLLY_MAP[normalizeLowercaseStringOrEmpty(voice)] ||
|
|
249
|
+
return OPENAI_TO_POLLY_MAP[normalizeLowercaseStringOrEmpty(voice)] || "Polly.Joanna";
|
|
880
250
|
}
|
|
881
251
|
//#endregion
|
|
882
252
|
//#region extensions/voice-call/src/manager/twiml.ts
|
|
@@ -1509,18 +879,16 @@ function processEvent(ctx, event) {
|
|
|
1509
879
|
endedAt: event.timestamp
|
|
1510
880
|
});
|
|
1511
881
|
return { kind: "processed" };
|
|
1512
|
-
case "call.error":
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
}
|
|
1523
|
-
break;
|
|
882
|
+
case "call.error": if (!event.retryable) {
|
|
883
|
+
finalizeCall({
|
|
884
|
+
ctx,
|
|
885
|
+
call,
|
|
886
|
+
endReason: "error",
|
|
887
|
+
endedAt: event.timestamp,
|
|
888
|
+
transcriptRejectReason: `Call error: ${event.error}`
|
|
889
|
+
});
|
|
890
|
+
return { kind: "processed" };
|
|
891
|
+
}
|
|
1524
892
|
}
|
|
1525
893
|
persistCallRecord(ctx.storePath, call);
|
|
1526
894
|
return result;
|
|
@@ -1554,7 +922,14 @@ function resolveRestoredMaxDurationAnchor(call) {
|
|
|
1554
922
|
function resolveDefaultStoreBase(config, storePath) {
|
|
1555
923
|
const rawOverride = storePath?.trim() || config.store?.trim();
|
|
1556
924
|
if (rawOverride) return resolveUserPath(rawOverride);
|
|
1557
|
-
|
|
925
|
+
const preferred = path.join(os.homedir(), ".openclaw", "voice-calls");
|
|
926
|
+
return [preferred].map((dir) => resolveUserPath(dir)).find((dir) => {
|
|
927
|
+
try {
|
|
928
|
+
return fs.existsSync(path.join(dir, "calls.jsonl")) || fs.existsSync(dir);
|
|
929
|
+
} catch {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
}) ?? resolveUserPath(preferred);
|
|
1558
933
|
}
|
|
1559
934
|
/**
|
|
1560
935
|
* Manages voice calls: state ownership and delegation to manager helper modules.
|
|
@@ -1840,7 +1215,7 @@ async function buildRealtimeVoiceInstructions(params) {
|
|
|
1840
1215
|
if (consultGuidance) sections.push(consultGuidance);
|
|
1841
1216
|
const contextConfig = config.realtime.agentContext;
|
|
1842
1217
|
if (!contextConfig.enabled) return sections.filter(Boolean).join("\n\n");
|
|
1843
|
-
const
|
|
1218
|
+
const agentId = config.agentId ?? "main";
|
|
1844
1219
|
const capsule = [
|
|
1845
1220
|
"OpenClaw agent voice context:",
|
|
1846
1221
|
`- Agent id: ${agentId}`,
|
|
@@ -1914,26 +1289,26 @@ function chunkAudio(audio, chunkSize = 160) {
|
|
|
1914
1289
|
/** Default timeout for one telephony synthesis request. */
|
|
1915
1290
|
const TELEPHONY_DEFAULT_TTS_TIMEOUT_MS = 8e3;
|
|
1916
1291
|
/** Create a TTS provider that honors voice-call overrides and converts PCM to mulaw. */
|
|
1917
|
-
|
|
1292
|
+
function createTelephonyTtsProvider(params) {
|
|
1918
1293
|
const { coreConfig, ttsOverride, runtime, logger } = params;
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1294
|
+
const mergedConfig = applyTtsOverride(coreConfig, ttsOverride);
|
|
1295
|
+
const ttsConfig = mergedConfig.messages?.tts;
|
|
1296
|
+
const modelOverrides = resolveTelephonyModelOverridePolicy(readTelephonyModelOverrides(ttsConfig));
|
|
1297
|
+
const providerConfigs = collectTelephonyProviderConfigs(ttsConfig);
|
|
1298
|
+
const activeProvider = normalizeProviderId(ttsConfig?.provider);
|
|
1924
1299
|
return {
|
|
1925
|
-
synthesisTimeoutMs: resolveTimerTimeoutMs(
|
|
1300
|
+
synthesisTimeoutMs: resolveTimerTimeoutMs(mergedConfig.messages?.tts?.timeoutMs, TELEPHONY_DEFAULT_TTS_TIMEOUT_MS),
|
|
1926
1301
|
synthesizeForTelephony: async (text) => {
|
|
1927
|
-
const
|
|
1928
|
-
cfg:
|
|
1929
|
-
|
|
1302
|
+
const directives = parseTtsDirectives(text, modelOverrides, {
|
|
1303
|
+
cfg: mergedConfig,
|
|
1304
|
+
providerConfigs,
|
|
1305
|
+
preferredProviderId: activeProvider
|
|
1930
1306
|
});
|
|
1931
|
-
const directives = prepared.directives;
|
|
1932
1307
|
if (directives.warnings.length > 0) logger?.warn?.(`[voice-call] Ignored telephony TTS directive overrides (${directives.warnings.join("; ")})`);
|
|
1933
1308
|
const cleanText = directives.hasDirective ? directives.ttsText?.trim() || directives.cleanedText.trim() : text;
|
|
1934
1309
|
const result = await runtime.textToSpeechTelephony({
|
|
1935
1310
|
text: cleanText,
|
|
1936
|
-
cfg:
|
|
1311
|
+
cfg: mergedConfig,
|
|
1937
1312
|
overrides: directives.overrides
|
|
1938
1313
|
});
|
|
1939
1314
|
if (!result.success || !result.audioBuffer || !result.sampleRate) throw new Error(result.error ?? "TTS conversion failed");
|
|
@@ -1945,6 +1320,94 @@ async function createTelephonyTtsProvider(params) {
|
|
|
1945
1320
|
}
|
|
1946
1321
|
};
|
|
1947
1322
|
}
|
|
1323
|
+
/** Apply voice-call TTS overrides to core config without mutating the original object. */
|
|
1324
|
+
function applyTtsOverride(coreConfig, override) {
|
|
1325
|
+
if (!override) return coreConfig;
|
|
1326
|
+
const base = coreConfig.messages?.tts;
|
|
1327
|
+
const merged = mergeTtsConfig(base, override);
|
|
1328
|
+
if (!merged) return coreConfig;
|
|
1329
|
+
return {
|
|
1330
|
+
...coreConfig,
|
|
1331
|
+
messages: {
|
|
1332
|
+
...coreConfig.messages,
|
|
1333
|
+
tts: merged
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
/** Merge core and voice-call TTS config, keeping undefined override fields out. */
|
|
1338
|
+
function mergeTtsConfig(base, override) {
|
|
1339
|
+
if (!base && !override) return;
|
|
1340
|
+
if (!override) return base;
|
|
1341
|
+
if (!base) return override;
|
|
1342
|
+
return deepMergeDefined(base, override);
|
|
1343
|
+
}
|
|
1344
|
+
/** Resolve directive override policy for telephony synthesis. */
|
|
1345
|
+
function resolveTelephonyModelOverridePolicy(overrides) {
|
|
1346
|
+
if (!(overrides?.enabled ?? true)) return {
|
|
1347
|
+
enabled: false,
|
|
1348
|
+
allowText: false,
|
|
1349
|
+
allowProvider: false,
|
|
1350
|
+
allowVoice: false,
|
|
1351
|
+
allowModelId: false,
|
|
1352
|
+
allowVoiceSettings: false,
|
|
1353
|
+
allowNormalization: false,
|
|
1354
|
+
allowSeed: false
|
|
1355
|
+
};
|
|
1356
|
+
const allow = (value, defaultValue = true) => value ?? defaultValue;
|
|
1357
|
+
return {
|
|
1358
|
+
enabled: true,
|
|
1359
|
+
allowText: allow(overrides?.allowText),
|
|
1360
|
+
allowProvider: allow(overrides?.allowProvider, false),
|
|
1361
|
+
allowVoice: allow(overrides?.allowVoice),
|
|
1362
|
+
allowModelId: allow(overrides?.allowModelId),
|
|
1363
|
+
allowVoiceSettings: allow(overrides?.allowVoiceSettings),
|
|
1364
|
+
allowNormalization: allow(overrides?.allowNormalization),
|
|
1365
|
+
allowSeed: allow(overrides?.allowSeed)
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
/** Read model override policy from TTS config when present. */
|
|
1369
|
+
function readTelephonyModelOverrides(ttsConfig) {
|
|
1370
|
+
const value = ttsConfig?.modelOverrides;
|
|
1371
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1372
|
+
}
|
|
1373
|
+
/** Normalize provider ids for config lookup. */
|
|
1374
|
+
function normalizeProviderId(value) {
|
|
1375
|
+
return typeof value === "string" ? value.trim().toLowerCase() || void 0 : void 0;
|
|
1376
|
+
}
|
|
1377
|
+
/** Coerce provider config objects while rejecting arrays and primitives. */
|
|
1378
|
+
function asProviderConfig(value) {
|
|
1379
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1380
|
+
}
|
|
1381
|
+
/** Collect named provider configs from canonical and legacy TTS config shapes. */
|
|
1382
|
+
function collectTelephonyProviderConfigs(ttsConfig) {
|
|
1383
|
+
if (!ttsConfig) return {};
|
|
1384
|
+
const entries = {};
|
|
1385
|
+
const rawProviders = ttsConfig.providers && typeof ttsConfig.providers === "object" && !Array.isArray(ttsConfig.providers) ? ttsConfig.providers : {};
|
|
1386
|
+
for (const [providerId, value] of Object.entries(rawProviders)) {
|
|
1387
|
+
const normalized = normalizeProviderId(providerId) ?? providerId;
|
|
1388
|
+
entries[normalized] = asProviderConfig(value);
|
|
1389
|
+
}
|
|
1390
|
+
const reservedKeys = /* @__PURE__ */ new Set([
|
|
1391
|
+
"auto",
|
|
1392
|
+
"enabled",
|
|
1393
|
+
"maxTextLength",
|
|
1394
|
+
"mode",
|
|
1395
|
+
"modelOverrides",
|
|
1396
|
+
"persona",
|
|
1397
|
+
"personas",
|
|
1398
|
+
"prefsPath",
|
|
1399
|
+
"provider",
|
|
1400
|
+
"providers",
|
|
1401
|
+
"summaryModel",
|
|
1402
|
+
"timeoutMs"
|
|
1403
|
+
]);
|
|
1404
|
+
for (const [key, value] of Object.entries(ttsConfig)) {
|
|
1405
|
+
if (reservedKeys.has(key) || typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
1406
|
+
const normalized = normalizeProviderId(key) ?? key;
|
|
1407
|
+
entries[normalized] ??= asProviderConfig(value);
|
|
1408
|
+
}
|
|
1409
|
+
return entries;
|
|
1410
|
+
}
|
|
1948
1411
|
//#endregion
|
|
1949
1412
|
//#region extensions/voice-call/src/bounded-child-output.ts
|
|
1950
1413
|
const DEFAULT_MAX_OUTPUT_CHARS = 16384;
|
|
@@ -1973,33 +1436,78 @@ function formatBoundedChildOutput(output) {
|
|
|
1973
1436
|
}
|
|
1974
1437
|
//#endregion
|
|
1975
1438
|
//#region extensions/voice-call/src/webhook/tailscale.ts
|
|
1976
|
-
const TAILSCALE_COMMAND_STDOUT_MAX_BYTES =
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1439
|
+
const TAILSCALE_COMMAND_STDOUT_MAX_BYTES = 4194304;
|
|
1440
|
+
function appendTailscaleCommandStdout(current, data, maxBytes = TAILSCALE_COMMAND_STDOUT_MAX_BYTES) {
|
|
1441
|
+
if (current.exceeded) return current;
|
|
1442
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
1443
|
+
const bytes = current.bytes + buffer.byteLength;
|
|
1444
|
+
if (bytes > maxBytes) return {
|
|
1445
|
+
bytes,
|
|
1446
|
+
exceeded: true,
|
|
1447
|
+
text: ""
|
|
1448
|
+
};
|
|
1449
|
+
return {
|
|
1450
|
+
bytes,
|
|
1451
|
+
exceeded: false,
|
|
1452
|
+
text: `${current.text}${buffer.toString("utf8")}`
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function runTailscaleCommand(args, timeoutMs = 2500) {
|
|
1456
|
+
return new Promise((resolve) => {
|
|
1457
|
+
const proc = spawn("tailscale", args, { stdio: [
|
|
1458
|
+
"ignore",
|
|
1459
|
+
"pipe",
|
|
1460
|
+
"ignore"
|
|
1461
|
+
] });
|
|
1462
|
+
let stdout = {
|
|
1463
|
+
bytes: 0,
|
|
1464
|
+
exceeded: false,
|
|
1465
|
+
text: ""
|
|
1996
1466
|
};
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
1467
|
+
let settled = false;
|
|
1468
|
+
const finish = (result) => {
|
|
1469
|
+
if (settled) return;
|
|
1470
|
+
settled = true;
|
|
1471
|
+
clearTimeout(timer);
|
|
1472
|
+
resolve(result);
|
|
2001
1473
|
};
|
|
2002
|
-
|
|
1474
|
+
const timer = setTimeout(() => {
|
|
1475
|
+
proc.kill("SIGKILL");
|
|
1476
|
+
finish({
|
|
1477
|
+
code: -1,
|
|
1478
|
+
stdout: ""
|
|
1479
|
+
});
|
|
1480
|
+
}, timeoutMs);
|
|
1481
|
+
proc.stdout.on("data", (data) => {
|
|
1482
|
+
stdout = appendTailscaleCommandStdout(stdout, data);
|
|
1483
|
+
if (stdout.exceeded) {
|
|
1484
|
+
proc.kill("SIGKILL");
|
|
1485
|
+
finish({
|
|
1486
|
+
code: -1,
|
|
1487
|
+
stdout: ""
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
proc.stdout.on("error", () => {
|
|
1492
|
+
proc.kill("SIGKILL");
|
|
1493
|
+
finish({
|
|
1494
|
+
code: -1,
|
|
1495
|
+
stdout: ""
|
|
1496
|
+
});
|
|
1497
|
+
});
|
|
1498
|
+
proc.on("error", () => {
|
|
1499
|
+
finish({
|
|
1500
|
+
code: -1,
|
|
1501
|
+
stdout: ""
|
|
1502
|
+
});
|
|
1503
|
+
});
|
|
1504
|
+
proc.on("close", (code) => {
|
|
1505
|
+
finish({
|
|
1506
|
+
code: code ?? -1,
|
|
1507
|
+
stdout: stdout.text
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
});
|
|
2003
1511
|
}
|
|
2004
1512
|
async function getTailscaleSelfInfo() {
|
|
2005
1513
|
const { code, stdout } = await runTailscaleCommand([
|
|
@@ -2070,32 +1578,6 @@ async function cleanupTailscaleExposure(config) {
|
|
|
2070
1578
|
//#endregion
|
|
2071
1579
|
//#region extensions/voice-call/src/tunnel.ts
|
|
2072
1580
|
const NGROK_LOG_BUFFER_MAX_CHARS = 16384;
|
|
2073
|
-
const NGROK_ERROR_MARKER = "ERR_NGROK";
|
|
2074
|
-
const TUNNEL_COMMAND_OUTPUT_MAX_BYTES = 16384;
|
|
2075
|
-
const NGROK_STOP_GRACE_MS = 2e3;
|
|
2076
|
-
const NGROK_FORCE_KILL_WAIT_MS = 1e3;
|
|
2077
|
-
async function terminateNgrokProcess(proc, isClosed) {
|
|
2078
|
-
if (isClosed()) return;
|
|
2079
|
-
await new Promise((resolve) => {
|
|
2080
|
-
let finished = false;
|
|
2081
|
-
let forceKillWaitTimer;
|
|
2082
|
-
const finish = () => {
|
|
2083
|
-
if (finished) return;
|
|
2084
|
-
finished = true;
|
|
2085
|
-
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
2086
|
-
if (forceKillWaitTimer) clearTimeout(forceKillWaitTimer);
|
|
2087
|
-
proc.off("close", finish);
|
|
2088
|
-
resolve();
|
|
2089
|
-
};
|
|
2090
|
-
proc.once("close", finish);
|
|
2091
|
-
const forceKillTimer = setTimeout(() => {
|
|
2092
|
-
forceKillWaitTimer = setTimeout(finish, NGROK_FORCE_KILL_WAIT_MS);
|
|
2093
|
-
if (!isClosed()) proc.kill("SIGKILL");
|
|
2094
|
-
}, NGROK_STOP_GRACE_MS);
|
|
2095
|
-
proc.kill("SIGTERM");
|
|
2096
|
-
if (isClosed()) finish();
|
|
2097
|
-
});
|
|
2098
|
-
}
|
|
2099
1581
|
function listenForChildStreamErrors(proc, onError) {
|
|
2100
1582
|
proc.stdout.on("error", (error) => onError("stdout", error));
|
|
2101
1583
|
proc.stderr.on("error", (error) => onError("stderr", error));
|
|
@@ -2131,25 +1613,22 @@ async function startNgrokTunnel(config) {
|
|
|
2131
1613
|
"pipe",
|
|
2132
1614
|
"pipe"
|
|
2133
1615
|
] });
|
|
2134
|
-
let
|
|
2135
|
-
let
|
|
1616
|
+
let resolved = false;
|
|
1617
|
+
let closed = false;
|
|
2136
1618
|
let publicUrl = null;
|
|
2137
1619
|
let outputBuffer = "";
|
|
2138
|
-
let stderrTail = "";
|
|
2139
1620
|
const timeout = setTimeout(() => {
|
|
2140
|
-
if (!
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
});
|
|
1621
|
+
if (!resolved) {
|
|
1622
|
+
resolved = true;
|
|
1623
|
+
proc.kill("SIGTERM");
|
|
1624
|
+
reject(/* @__PURE__ */ new Error("ngrok startup timed out (30s)"));
|
|
2145
1625
|
}
|
|
2146
1626
|
}, 3e4);
|
|
2147
|
-
timeout.unref();
|
|
2148
1627
|
const rejectIfPending = (message, kill = false) => {
|
|
2149
|
-
if (!
|
|
2150
|
-
|
|
1628
|
+
if (!resolved) {
|
|
1629
|
+
resolved = true;
|
|
2151
1630
|
clearTimeout(timeout);
|
|
2152
|
-
if (kill && !
|
|
1631
|
+
if (kill && !closed) proc.kill("SIGKILL");
|
|
2153
1632
|
reject(new Error(message));
|
|
2154
1633
|
}
|
|
2155
1634
|
};
|
|
@@ -2158,8 +1637,8 @@ async function startNgrokTunnel(config) {
|
|
|
2158
1637
|
const log = JSON.parse(line);
|
|
2159
1638
|
if (log.msg === "started tunnel" && log.url) publicUrl = log.url;
|
|
2160
1639
|
if (log.addr && log.url && !publicUrl) publicUrl = log.url;
|
|
2161
|
-
if (publicUrl && !
|
|
2162
|
-
|
|
1640
|
+
if (publicUrl && !resolved) {
|
|
1641
|
+
resolved = true;
|
|
2163
1642
|
clearTimeout(timeout);
|
|
2164
1643
|
const fullUrl = publicUrl + config.path;
|
|
2165
1644
|
console.log(`[voice-call] ngrok tunnel active: ${fullUrl}`);
|
|
@@ -2167,24 +1646,39 @@ async function startNgrokTunnel(config) {
|
|
|
2167
1646
|
publicUrl: fullUrl,
|
|
2168
1647
|
provider: "ngrok",
|
|
2169
1648
|
stop: async () => {
|
|
2170
|
-
|
|
1649
|
+
if (closed) return;
|
|
1650
|
+
await new Promise((res) => {
|
|
1651
|
+
let finished = false;
|
|
1652
|
+
const finish = () => {
|
|
1653
|
+
if (finished) return;
|
|
1654
|
+
finished = true;
|
|
1655
|
+
clearTimeout(fallback);
|
|
1656
|
+
proc.off("close", finish);
|
|
1657
|
+
res();
|
|
1658
|
+
};
|
|
1659
|
+
if (closed) {
|
|
1660
|
+
res();
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
proc.once("close", finish);
|
|
1664
|
+
const fallback = setTimeout(finish, 2e3);
|
|
1665
|
+
proc.kill("SIGTERM");
|
|
1666
|
+
if (closed) finish();
|
|
1667
|
+
});
|
|
2171
1668
|
}
|
|
2172
1669
|
});
|
|
2173
1670
|
}
|
|
2174
1671
|
} catch {}
|
|
2175
1672
|
};
|
|
2176
|
-
proc.stdout.
|
|
2177
|
-
|
|
2178
|
-
proc.stdout.on("data", (chunk) => {
|
|
2179
|
-
const lines = (outputBuffer + chunk).split("\n");
|
|
1673
|
+
proc.stdout.on("data", (data) => {
|
|
1674
|
+
const lines = (outputBuffer + data.toString()).split("\n");
|
|
2180
1675
|
outputBuffer = lines.pop() || "";
|
|
2181
1676
|
if (outputBuffer.length > NGROK_LOG_BUFFER_MAX_CHARS) outputBuffer = sliceUtf16Safe(outputBuffer, -16384);
|
|
2182
1677
|
for (const line of lines) if (line.trim()) processLine(line);
|
|
2183
1678
|
});
|
|
2184
|
-
proc.stderr.on("data", (
|
|
2185
|
-
const
|
|
2186
|
-
if (
|
|
2187
|
-
stderrTail = sliceUtf16Safe(combined, -8);
|
|
1679
|
+
proc.stderr.on("data", (data) => {
|
|
1680
|
+
const msg = data.toString();
|
|
1681
|
+
if (msg.includes("ERR_NGROK")) rejectIfPending(`ngrok error: ${formatBoundedChildOutput(appendBoundedChildOutput(emptyBoundedChildOutput(), msg))}`, true);
|
|
2188
1682
|
});
|
|
2189
1683
|
listenForChildStreamErrors(proc, (stream, error) => {
|
|
2190
1684
|
rejectIfPending(`ngrok ${stream} error: ${error.message}`, true);
|
|
@@ -2193,9 +1687,9 @@ async function startNgrokTunnel(config) {
|
|
|
2193
1687
|
rejectIfPending(`Failed to start ngrok: ${err.message}`);
|
|
2194
1688
|
});
|
|
2195
1689
|
proc.on("close", (code) => {
|
|
2196
|
-
|
|
2197
|
-
if (!
|
|
2198
|
-
|
|
1690
|
+
closed = true;
|
|
1691
|
+
if (!resolved) {
|
|
1692
|
+
resolved = true;
|
|
2199
1693
|
clearTimeout(timeout);
|
|
2200
1694
|
reject(/* @__PURE__ */ new Error(`ngrok exited unexpectedly with code ${code}`));
|
|
2201
1695
|
}
|
|
@@ -2206,22 +1700,41 @@ async function startNgrokTunnel(config) {
|
|
|
2206
1700
|
* Run an ngrok command and wait for completion.
|
|
2207
1701
|
*/
|
|
2208
1702
|
async function runNgrokCommand(args) {
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
1703
|
+
return new Promise((resolve, reject) => {
|
|
1704
|
+
const proc = spawn("ngrok", args, { stdio: [
|
|
1705
|
+
"ignore",
|
|
1706
|
+
"pipe",
|
|
1707
|
+
"pipe"
|
|
1708
|
+
] });
|
|
1709
|
+
let stdout = emptyBoundedChildOutput();
|
|
1710
|
+
let stderr = emptyBoundedChildOutput();
|
|
1711
|
+
let settled = false;
|
|
1712
|
+
const rejectIfPending = (error, kill = false) => {
|
|
1713
|
+
if (settled) return;
|
|
1714
|
+
settled = true;
|
|
1715
|
+
if (kill) proc.kill("SIGKILL");
|
|
1716
|
+
reject(error);
|
|
1717
|
+
};
|
|
1718
|
+
proc.stdout.on("data", (data) => {
|
|
1719
|
+
stdout = appendBoundedChildOutput(stdout, data.toString());
|
|
1720
|
+
});
|
|
1721
|
+
proc.stderr.on("data", (data) => {
|
|
1722
|
+
stderr = appendBoundedChildOutput(stderr, data.toString());
|
|
1723
|
+
});
|
|
1724
|
+
listenForChildStreamErrors(proc, (stream, error) => {
|
|
1725
|
+
rejectIfPending(/* @__PURE__ */ new Error(`ngrok command ${stream} error: ${error.message}`), true);
|
|
1726
|
+
});
|
|
1727
|
+
proc.on("close", (code) => {
|
|
1728
|
+
if (settled) return;
|
|
1729
|
+
settled = true;
|
|
1730
|
+
if (code === 0) resolve(stdout.text);
|
|
1731
|
+
else {
|
|
1732
|
+
const output = stderr.text ? stderr : stdout;
|
|
1733
|
+
reject(/* @__PURE__ */ new Error(`ngrok command failed: ${formatBoundedChildOutput(output)}`));
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
proc.on("error", (error) => rejectIfPending(error));
|
|
2214
1737
|
});
|
|
2215
|
-
if (result.termination === "timeout") throw new Error("ngrok command timed out");
|
|
2216
|
-
if (result.code === 0) return result.stdout;
|
|
2217
|
-
const output = result.stderr ? {
|
|
2218
|
-
text: result.stderr,
|
|
2219
|
-
truncated: Boolean(result.stderrTruncatedBytes)
|
|
2220
|
-
} : {
|
|
2221
|
-
text: result.stdout,
|
|
2222
|
-
truncated: Boolean(result.stdoutTruncatedBytes)
|
|
2223
|
-
};
|
|
2224
|
-
throw new Error(`ngrok command failed: ${formatBoundedChildOutput(output)}`);
|
|
2225
1738
|
}
|
|
2226
1739
|
/**
|
|
2227
1740
|
* Start a Tailscale serve/funnel tunnel.
|
|
@@ -2231,56 +1744,89 @@ async function startTailscaleTunnel(config) {
|
|
|
2231
1744
|
if (!dnsName) throw new Error("Could not get Tailscale DNS name. Is Tailscale running?");
|
|
2232
1745
|
const path = config.path.startsWith("/") ? config.path : `/${config.path}`;
|
|
2233
1746
|
const localUrl = `http://127.0.0.1:${config.port}${path}`;
|
|
2234
|
-
|
|
2235
|
-
"tailscale",
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
const
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
1747
|
+
return new Promise((resolve, reject) => {
|
|
1748
|
+
const proc = spawn("tailscale", [
|
|
1749
|
+
config.mode,
|
|
1750
|
+
"--bg",
|
|
1751
|
+
"--yes",
|
|
1752
|
+
"--set-path",
|
|
1753
|
+
path,
|
|
1754
|
+
localUrl
|
|
1755
|
+
], { stdio: [
|
|
1756
|
+
"ignore",
|
|
1757
|
+
"pipe",
|
|
1758
|
+
"pipe"
|
|
1759
|
+
] });
|
|
1760
|
+
let resolved = false;
|
|
1761
|
+
let stdout = emptyBoundedChildOutput();
|
|
1762
|
+
let stderr = emptyBoundedChildOutput();
|
|
1763
|
+
const rejectIfPending = (error, kill = false) => {
|
|
1764
|
+
if (resolved) return;
|
|
1765
|
+
resolved = true;
|
|
1766
|
+
clearTimeout(timeout);
|
|
1767
|
+
if (kill) proc.kill("SIGKILL");
|
|
1768
|
+
reject(error);
|
|
2256
1769
|
};
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
1770
|
+
const timeout = setTimeout(() => {
|
|
1771
|
+
rejectIfPending(/* @__PURE__ */ new Error(`Tailscale ${config.mode} timed out`), true);
|
|
1772
|
+
}, 1e4);
|
|
1773
|
+
proc.stdout.on("data", (data) => {
|
|
1774
|
+
stdout = appendBoundedChildOutput(stdout, data.toString());
|
|
1775
|
+
});
|
|
1776
|
+
proc.stderr.on("data", (data) => {
|
|
1777
|
+
stderr = appendBoundedChildOutput(stderr, data.toString());
|
|
1778
|
+
});
|
|
1779
|
+
listenForChildStreamErrors(proc, (stream, error) => {
|
|
1780
|
+
rejectIfPending(/* @__PURE__ */ new Error(`Tailscale ${config.mode} ${stream} error: ${error.message}`), true);
|
|
1781
|
+
});
|
|
1782
|
+
proc.on("close", (code) => {
|
|
1783
|
+
clearTimeout(timeout);
|
|
1784
|
+
if (resolved) return;
|
|
1785
|
+
resolved = true;
|
|
1786
|
+
if (code === 0) {
|
|
1787
|
+
const publicUrl = `https://${dnsName}${path}`;
|
|
1788
|
+
console.log(`[voice-call] Tailscale ${config.mode} active: ${publicUrl}`);
|
|
1789
|
+
resolve({
|
|
1790
|
+
publicUrl,
|
|
1791
|
+
provider: `tailscale-${config.mode}`,
|
|
1792
|
+
stop: async () => {
|
|
1793
|
+
await stopTailscaleTunnel(config.mode, path);
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
} else {
|
|
1797
|
+
const output = stderr.text ? stderr : stdout;
|
|
1798
|
+
const detail = output.text ? `: ${formatBoundedChildOutput(output)}` : "";
|
|
1799
|
+
reject(/* @__PURE__ */ new Error(`Tailscale ${config.mode} failed with code ${code}${detail}`));
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
proc.on("error", (err) => {
|
|
1803
|
+
rejectIfPending(err);
|
|
1804
|
+
});
|
|
1805
|
+
});
|
|
2269
1806
|
}
|
|
2270
1807
|
/**
|
|
2271
1808
|
* Stop a Tailscale serve/funnel tunnel.
|
|
2272
1809
|
*/
|
|
2273
1810
|
async function stopTailscaleTunnel(mode, path) {
|
|
2274
|
-
|
|
2275
|
-
"tailscale",
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
1811
|
+
return new Promise((resolve) => {
|
|
1812
|
+
const proc = spawn("tailscale", [
|
|
1813
|
+
mode,
|
|
1814
|
+
"off",
|
|
1815
|
+
path
|
|
1816
|
+
], { stdio: "ignore" });
|
|
1817
|
+
const timeout = setTimeout(() => {
|
|
1818
|
+
proc.kill("SIGKILL");
|
|
1819
|
+
resolve();
|
|
1820
|
+
}, 5e3);
|
|
1821
|
+
proc.on("close", () => {
|
|
1822
|
+
clearTimeout(timeout);
|
|
1823
|
+
resolve();
|
|
1824
|
+
});
|
|
1825
|
+
proc.on("error", () => {
|
|
1826
|
+
clearTimeout(timeout);
|
|
1827
|
+
resolve();
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
2284
1830
|
}
|
|
2285
1831
|
/**
|
|
2286
1832
|
* Start a tunnel based on configuration.
|
|
@@ -2369,18 +1915,13 @@ function getHeader(headers, name) {
|
|
|
2369
1915
|
return value;
|
|
2370
1916
|
}
|
|
2371
1917
|
//#endregion
|
|
2372
|
-
//#region extensions/voice-call/src/media-base64.ts
|
|
2373
|
-
function canonicalizeVoiceCallMediaBase64(payloadBase64) {
|
|
2374
|
-
return canonicalizeBase64(payloadBase64.replaceAll("-", "+").replaceAll("_", "/"));
|
|
2375
|
-
}
|
|
2376
|
-
//#endregion
|
|
2377
1918
|
//#region extensions/voice-call/src/media-stream.ts
|
|
2378
1919
|
const DEFAULT_PRE_START_TIMEOUT_MS = 5e3;
|
|
2379
1920
|
const DEFAULT_MAX_PENDING_CONNECTIONS = 32;
|
|
2380
1921
|
const DEFAULT_MAX_PENDING_CONNECTIONS_PER_IP = 4;
|
|
2381
1922
|
const DEFAULT_MAX_CONNECTIONS = 128;
|
|
2382
|
-
const MAX_INBOUND_MESSAGE_BYTES =
|
|
2383
|
-
const MAX_WS_BUFFERED_BYTES =
|
|
1923
|
+
const MAX_INBOUND_MESSAGE_BYTES = 65536;
|
|
1924
|
+
const MAX_WS_BUFFERED_BYTES = 1048576;
|
|
2384
1925
|
const CLOSE_REASON_LOG_MAX_CHARS = 120;
|
|
2385
1926
|
function sanitizeLogText(value, maxChars) {
|
|
2386
1927
|
const sanitized = value.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").trim();
|
|
@@ -2488,9 +2029,7 @@ var MediaStreamHandler = class {
|
|
|
2488
2029
|
break;
|
|
2489
2030
|
case "media":
|
|
2490
2031
|
if (session && message.media?.payload) {
|
|
2491
|
-
const
|
|
2492
|
-
if (!canonicalPayload) break;
|
|
2493
|
-
const audioBuffer = Buffer.from(canonicalPayload, "base64");
|
|
2032
|
+
const audioBuffer = Buffer.from(message.media.payload, "base64");
|
|
2494
2033
|
const turnId = this.ensureActiveTurn(session);
|
|
2495
2034
|
this.emitTalkEvent(session, {
|
|
2496
2035
|
type: "input.audio.delta",
|
|
@@ -2504,14 +2043,10 @@ var MediaStreamHandler = class {
|
|
|
2504
2043
|
session.sttSession.sendAudio(audioBuffer);
|
|
2505
2044
|
}
|
|
2506
2045
|
break;
|
|
2507
|
-
case "stop":
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
}
|
|
2512
|
-
break;
|
|
2513
|
-
case "clear":
|
|
2514
|
-
case "mark": break;
|
|
2046
|
+
case "stop": if (session) {
|
|
2047
|
+
this.handleStop(session);
|
|
2048
|
+
session = null;
|
|
2049
|
+
}
|
|
2515
2050
|
}
|
|
2516
2051
|
} catch (error) {
|
|
2517
2052
|
console.error("[MediaStream] Error processing message:", error);
|
|
@@ -2771,7 +2306,9 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
|
|
|
2771
2306
|
bufferedAfterBytes: session.ws.bufferedAmount
|
|
2772
2307
|
};
|
|
2773
2308
|
if (bufferedBeforeBytes > MAX_WS_BUFFERED_BYTES) {
|
|
2774
|
-
|
|
2309
|
+
try {
|
|
2310
|
+
session.ws.close(1013, "Backpressure: send buffer exceeded");
|
|
2311
|
+
} catch {}
|
|
2775
2312
|
return {
|
|
2776
2313
|
sent: false,
|
|
2777
2314
|
readyState,
|
|
@@ -2783,7 +2320,9 @@ Content-Length: ${Buffer.byteLength(body)}\r\n\r
|
|
|
2783
2320
|
session.ws.send(JSON.stringify(message));
|
|
2784
2321
|
const bufferedAfterBytes = session.ws.bufferedAmount;
|
|
2785
2322
|
if (bufferedAfterBytes > MAX_WS_BUFFERED_BYTES) {
|
|
2786
|
-
|
|
2323
|
+
try {
|
|
2324
|
+
session.ws.close(1013, "Backpressure: send buffer exceeded");
|
|
2325
|
+
} catch {}
|
|
2787
2326
|
return {
|
|
2788
2327
|
sent: false,
|
|
2789
2328
|
readyState,
|
|
@@ -3043,19 +2582,15 @@ function startStaleCallReaper(params) {
|
|
|
3043
2582
|
const maxAgeSeconds = params.staleCallReaperSeconds;
|
|
3044
2583
|
if (!maxAgeSeconds || maxAgeSeconds <= 0) return null;
|
|
3045
2584
|
const maxAgeMs = maxAgeSeconds * 1e3;
|
|
3046
|
-
const callsBeingReaped = /* @__PURE__ */ new Set();
|
|
3047
2585
|
const interval = setInterval(() => {
|
|
3048
2586
|
const now = Date.now();
|
|
3049
2587
|
for (const call of params.manager.getActiveCalls()) {
|
|
3050
2588
|
if (call.answeredAt || TerminalStates.has(call.state) || LiveConversationStates.has(call.state)) continue;
|
|
3051
2589
|
const age = now - call.startedAt;
|
|
3052
|
-
if (age > maxAgeMs
|
|
3053
|
-
callsBeingReaped.add(call.callId);
|
|
2590
|
+
if (age > maxAgeMs) {
|
|
3054
2591
|
console.log(`[voice-call] Reaping stale call ${call.callId} (age: ${Math.round(age / 1e3)}s, state: ${call.state})`);
|
|
3055
2592
|
params.manager.endCall(call.callId).catch((err) => {
|
|
3056
2593
|
console.warn(`[voice-call] Reaper failed to end call ${call.callId}:`, err);
|
|
3057
|
-
}).finally(() => {
|
|
3058
|
-
callsBeingReaped.delete(call.callId);
|
|
3059
2594
|
});
|
|
3060
2595
|
}
|
|
3061
2596
|
}
|
|
@@ -3070,8 +2605,8 @@ const MAX_WEBHOOK_BODY_BYTES = WEBHOOK_BODY_READ_DEFAULTS.preAuth.maxBytes;
|
|
|
3070
2605
|
const WEBHOOK_BODY_TIMEOUT_MS = WEBHOOK_BODY_READ_DEFAULTS.preAuth.timeoutMs;
|
|
3071
2606
|
const MISSING_REMOTE_ADDRESS_IN_FLIGHT_KEY = "__voice_call_no_remote__";
|
|
3072
2607
|
const STREAM_DISCONNECT_HANGUP_GRACE_MS = 2e3;
|
|
3073
|
-
const loadRealtimeTranscriptionRuntime = createLazyRuntimeModule(() => import("./realtime-transcription.runtime-CbJAs5t_.
|
|
3074
|
-
const loadResponseGeneratorModule = createLazyRuntimeModule(() => import("./response-generator-
|
|
2608
|
+
const loadRealtimeTranscriptionRuntime = createLazyRuntimeModule(() => import("./realtime-transcription.runtime-CbJAs5t_.mjs"));
|
|
2609
|
+
const loadResponseGeneratorModule = createLazyRuntimeModule(() => import("./response-generator-DoC6lfcm.mjs"));
|
|
3075
2610
|
function appendRecentTalkEventMetadata(call, event) {
|
|
3076
2611
|
const metadata = call.metadata ?? {};
|
|
3077
2612
|
const recent = Array.isArray(metadata.recentTalkEvents) ? metadata.recentTalkEvents.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)) : [];
|
|
@@ -3128,7 +2663,7 @@ function buildTwilioReplayTwiML() {
|
|
|
3128
2663
|
body: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response></Response>"
|
|
3129
2664
|
};
|
|
3130
2665
|
}
|
|
3131
|
-
const WEBHOOK_REPLAY_RESPONSE_TTL_MS =
|
|
2666
|
+
const WEBHOOK_REPLAY_RESPONSE_TTL_MS = 6e5;
|
|
3132
2667
|
const WEBHOOK_REPLAY_RESPONSE_MAX_ENTRIES = 1e4;
|
|
3133
2668
|
const WEBHOOK_REPLAY_RESPONSE_PRUNE_INTERVAL = 64;
|
|
3134
2669
|
function cloneWebhookResponsePayload(payload) {
|
|
@@ -3263,14 +2798,12 @@ var VoiceCallWebhookServer = class {
|
|
|
3263
2798
|
maxConnections: streaming.maxConnections,
|
|
3264
2799
|
resolveClientIp: (request) => this.resolveMediaStreamClientIp(request),
|
|
3265
2800
|
shouldAcceptStream: ({ callId, token }) => {
|
|
3266
|
-
if (this.provider.name !== "twilio") {
|
|
3267
|
-
this.logger.warn(`Rejecting media stream: provider ${this.provider.name} does not support authenticated classic streaming`);
|
|
3268
|
-
return false;
|
|
3269
|
-
}
|
|
3270
2801
|
if (!this.manager.getCallByProviderCallId(callId)) return false;
|
|
3271
|
-
if (
|
|
3272
|
-
this.
|
|
3273
|
-
|
|
2802
|
+
if (this.provider.name === "twilio") {
|
|
2803
|
+
if (!this.provider.isValidStreamToken(callId, token)) {
|
|
2804
|
+
this.logger.warn(`Rejecting media stream: invalid token for ${callId}`);
|
|
2805
|
+
return false;
|
|
2806
|
+
}
|
|
3274
2807
|
}
|
|
3275
2808
|
return true;
|
|
3276
2809
|
},
|
|
@@ -3715,6 +3248,7 @@ var VoiceCallWebhookServer = class {
|
|
|
3715
3248
|
callId,
|
|
3716
3249
|
sessionKey: call.sessionKey,
|
|
3717
3250
|
from: call.from,
|
|
3251
|
+
senderIsOwner: call.direction === "inbound" ? false : void 0,
|
|
3718
3252
|
agentId: resolveCallAgentId(call, effectiveConfig),
|
|
3719
3253
|
transcript: call.transcript,
|
|
3720
3254
|
userMessage,
|
|
@@ -3746,12 +3280,12 @@ const REALTIME_VOICE_CONSULT_SYSTEM_PROMPT = [
|
|
|
3746
3280
|
"Do not print secret values or dump environment variables; only check whether required configuration is present.",
|
|
3747
3281
|
"Be accurate, brief, and speakable."
|
|
3748
3282
|
].join(" ");
|
|
3749
|
-
const loadTelnyxProvider = createLazyRuntimeModule(() => import("./telnyx-
|
|
3750
|
-
const loadTwilioProvider = createLazyRuntimeModule(() => import("./twilio-
|
|
3751
|
-
const loadPlivoProvider = createLazyRuntimeModule(() => import("./plivo-
|
|
3752
|
-
const loadMockProvider = createLazyRuntimeModule(() => import("./mock-
|
|
3753
|
-
const loadRealtimeVoiceRuntime = createLazyRuntimeModule(() => import("./realtime-voice.runtime-vtdCOWg-.
|
|
3754
|
-
const loadRealtimeHandler = createLazyRuntimeModule(() => import("./realtime-handler
|
|
3283
|
+
const loadTelnyxProvider = createLazyRuntimeModule(() => import("./telnyx-Bb8X-nin.mjs"));
|
|
3284
|
+
const loadTwilioProvider = createLazyRuntimeModule(() => import("./twilio-Bsbf3O9A.mjs"));
|
|
3285
|
+
const loadPlivoProvider = createLazyRuntimeModule(() => import("./plivo-DFHh72MH.mjs"));
|
|
3286
|
+
const loadMockProvider = createLazyRuntimeModule(() => import("./mock-DY4yTqR2.mjs"));
|
|
3287
|
+
const loadRealtimeVoiceRuntime = createLazyRuntimeModule(() => import("./realtime-voice.runtime-vtdCOWg-.mjs"));
|
|
3288
|
+
const loadRealtimeHandler = createLazyRuntimeModule(() => import("./realtime-handler-DWLxvZuO.mjs"));
|
|
3755
3289
|
function resolveVoiceCallConsultSessionKey(call) {
|
|
3756
3290
|
return resolveVoiceCallSessionKey({
|
|
3757
3291
|
config: call.config,
|
|
@@ -3876,8 +3410,7 @@ async function createRealtimeInstructionsResolver(params) {
|
|
|
3876
3410
|
baseInstructions: params.config.realtime.instructions,
|
|
3877
3411
|
config: genericConfig,
|
|
3878
3412
|
coreConfig: params.coreConfig,
|
|
3879
|
-
agentRuntime: params.agentRuntime
|
|
3880
|
-
agentId: params.config.agentId
|
|
3413
|
+
agentRuntime: params.agentRuntime
|
|
3881
3414
|
});
|
|
3882
3415
|
const entries = await Promise.all(listRealtimeAgentIds(params.config, params.coreConfig).map(async (agentId) => {
|
|
3883
3416
|
return [agentId, await buildRealtimeVoiceInstructions({
|
|
@@ -3887,8 +3420,7 @@ async function createRealtimeInstructionsResolver(params) {
|
|
|
3887
3420
|
agentId
|
|
3888
3421
|
},
|
|
3889
3422
|
coreConfig: params.coreConfig,
|
|
3890
|
-
agentRuntime: params.agentRuntime
|
|
3891
|
-
agentId
|
|
3423
|
+
agentRuntime: params.agentRuntime
|
|
3892
3424
|
})];
|
|
3893
3425
|
}));
|
|
3894
3426
|
const instructionsByAgentId = new Map(entries);
|
|
@@ -3906,13 +3438,8 @@ async function createVoiceCallRuntime(params) {
|
|
|
3906
3438
|
error: console.error,
|
|
3907
3439
|
debug: console.debug
|
|
3908
3440
|
};
|
|
3441
|
+
const config = resolveVoiceCallConfig(rawConfig);
|
|
3909
3442
|
const cfg = fullConfig ?? coreConfig;
|
|
3910
|
-
const unresolvedConfig = resolveVoiceCallConfig(rawConfig);
|
|
3911
|
-
const configuredAgentId = unresolvedConfig.agentId ? normalizeAgentId(unresolvedConfig.agentId) : resolveDefaultAgentId(cfg);
|
|
3912
|
-
const config = {
|
|
3913
|
-
...unresolvedConfig,
|
|
3914
|
-
agentId: configuredAgentId
|
|
3915
|
-
};
|
|
3916
3443
|
if (!config.enabled) throw new Error("Voice call disabled. Enable the plugin entry in config.");
|
|
3917
3444
|
if (config.skipSignatureVerification) log.warn("[voice-call] SECURITY WARNING: skipSignatureVerification=true disables webhook signature verification (development only). Do not use in production.");
|
|
3918
3445
|
const validation = validateProviderConfig(config);
|
|
@@ -3951,14 +3478,6 @@ async function createVoiceCallRuntime(params) {
|
|
|
3951
3478
|
coreSession: cfg.session
|
|
3952
3479
|
});
|
|
3953
3480
|
const requesterSessionKey = typeof call.metadata?.requesterSessionKey === "string" ? call.metadata.requesterSessionKey : void 0;
|
|
3954
|
-
const modelLockParams = {
|
|
3955
|
-
cfg,
|
|
3956
|
-
agentRuntime,
|
|
3957
|
-
agentId,
|
|
3958
|
-
sessionKey,
|
|
3959
|
-
spawnedBy: requesterSessionKey
|
|
3960
|
-
};
|
|
3961
|
-
assertRealtimeVoiceAgentConsultModelSelectionUnlocked(modelLockParams);
|
|
3962
3481
|
const fastContext = await resolveRealtimeFastContextConsult({
|
|
3963
3482
|
cfg,
|
|
3964
3483
|
agentId,
|
|
@@ -3967,10 +3486,7 @@ async function createVoiceCallRuntime(params) {
|
|
|
3967
3486
|
args,
|
|
3968
3487
|
logger: log
|
|
3969
3488
|
});
|
|
3970
|
-
if (fastContext.handled)
|
|
3971
|
-
assertRealtimeVoiceAgentConsultModelSelectionUnlocked(modelLockParams);
|
|
3972
|
-
return fastContext.result;
|
|
3973
|
-
}
|
|
3489
|
+
if (fastContext.handled) return fastContext.result;
|
|
3974
3490
|
const { provider: agentProvider, model } = resolveVoiceResponseModel({
|
|
3975
3491
|
voiceConfig: effectiveConfig,
|
|
3976
3492
|
agentRuntime
|
|
@@ -4038,8 +3554,8 @@ async function createVoiceCallRuntime(params) {
|
|
|
4038
3554
|
if (provider.name === "twilio" && config.streaming?.enabled) {
|
|
4039
3555
|
const twilioProvider = provider;
|
|
4040
3556
|
if (ttsRuntime?.textToSpeechTelephony) try {
|
|
4041
|
-
const ttsProvider =
|
|
4042
|
-
coreConfig
|
|
3557
|
+
const ttsProvider = createTelephonyTtsProvider({
|
|
3558
|
+
coreConfig,
|
|
4043
3559
|
ttsOverride: config.tts,
|
|
4044
3560
|
runtime: ttsRuntime,
|
|
4045
3561
|
logger: log
|
|
@@ -4077,4 +3593,4 @@ async function createVoiceCallRuntime(params) {
|
|
|
4077
3593
|
}
|
|
4078
3594
|
}
|
|
4079
3595
|
//#endregion
|
|
4080
|
-
export {
|
|
3596
|
+
export { mapVoiceToPolly as _, normalizeProviderStatus as a, cleanupTailscaleExposureRoute as c, TELEPHONY_DEFAULT_TTS_TIMEOUT_MS as d, chunkAudio as f, escapeXml as g, resolveUserPath as h, mapProviderStatusToEndReason as i, getTailscaleSelfInfo as l, resolveCallAgentId as m, normalizeProxyIp as n, getHeader as o, resolveVoiceResponseModel as p, isProviderStatusTerminal as r, resolveWebhookExposureStatus as s, createVoiceCallRuntime as t, setupTailscaleExposureRoute as u };
|