realtime-voice-agents 2.0.0

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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +298 -0
  3. package/assets/elevator-jazz.ulaw +1 -0
  4. package/assets/keyboard-typing.ulaw +1 -0
  5. package/assets/lofi.ulaw +1 -0
  6. package/assets/ringing.ulaw +1 -0
  7. package/assets/thinking-hum.ulaw +1 -0
  8. package/dist/BackgroundAudioPlayer-iMcivjis.mjs +282 -0
  9. package/dist/BackgroundAudioPlayer-jfRULWKC.cjs +313 -0
  10. package/dist/BaseRealtimeProvider-BQigr5mB.mjs +62 -0
  11. package/dist/BaseRealtimeProvider-BehPNT1r.d.cts +239 -0
  12. package/dist/BaseRealtimeProvider-ClP8Wx1X.d.mts +239 -0
  13. package/dist/BaseRealtimeProvider-DI4pKtOb.cjs +91 -0
  14. package/dist/GeminiLiveProvider-DvkTgzjG.d.cts +101 -0
  15. package/dist/GeminiLiveProvider-x2nyx5aO.d.mts +101 -0
  16. package/dist/InMemorySessionStore-B0_i-DOU.cjs +34 -0
  17. package/dist/InMemorySessionStore-B5_rq61L.d.cts +83 -0
  18. package/dist/InMemorySessionStore-B5_rq61L.d.mts +83 -0
  19. package/dist/InMemorySessionStore-DYParOJO.mjs +29 -0
  20. package/dist/OpenAICompatibleProvider-Bdtl-UXH.mjs +402 -0
  21. package/dist/OpenAICompatibleProvider-NS4cKVQj.cjs +409 -0
  22. package/dist/audio.cjs +21 -0
  23. package/dist/audio.d.cts +165 -0
  24. package/dist/audio.d.mts +165 -0
  25. package/dist/audio.mjs +4 -0
  26. package/dist/env-DSnGaERV.cjs +19 -0
  27. package/dist/env-DUwUWTsg.mjs +14 -0
  28. package/dist/events-BUMYdETO.d.cts +24 -0
  29. package/dist/events-BUMYdETO.d.mts +24 -0
  30. package/dist/events-BxDTIKKq.cjs +48 -0
  31. package/dist/events-BylBSBW-.mjs +43 -0
  32. package/dist/gemini.cjs +401 -0
  33. package/dist/gemini.d.cts +41 -0
  34. package/dist/gemini.d.mts +41 -0
  35. package/dist/gemini.mjs +395 -0
  36. package/dist/index.cjs +2142 -0
  37. package/dist/index.d.cts +920 -0
  38. package/dist/index.d.mts +920 -0
  39. package/dist/index.mjs +2113 -0
  40. package/dist/mulaw--cwU2c9L.mjs +64 -0
  41. package/dist/mulaw-DLUObjdP.cjs +117 -0
  42. package/dist/openai.cjs +61 -0
  43. package/dist/openai.d.cts +121 -0
  44. package/dist/openai.d.mts +121 -0
  45. package/dist/openai.mjs +53 -0
  46. package/dist/presets-Bf75YXs5.d.cts +24 -0
  47. package/dist/presets-Bf75YXs5.d.mts +24 -0
  48. package/dist/rest-BYqiVOhe.mjs +265 -0
  49. package/dist/rest-BvUKut_k.cjs +300 -0
  50. package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
  51. package/dist/session-config-BVLl7-ha.mjs +74 -0
  52. package/dist/session-config-c8sOw1XL.cjs +85 -0
  53. package/dist/store.cjs +3 -0
  54. package/dist/store.d.cts +2 -0
  55. package/dist/store.d.mts +2 -0
  56. package/dist/store.mjs +2 -0
  57. package/dist/testing.cjs +544 -0
  58. package/dist/testing.d.cts +191 -0
  59. package/dist/testing.d.mts +191 -0
  60. package/dist/testing.mjs +538 -0
  61. package/dist/transcode-C9aJG7_W.cjs +215 -0
  62. package/dist/transcode-CsAp97G9.mjs +198 -0
  63. package/dist/transport-B_PJFIVd.d.cts +155 -0
  64. package/dist/transport-CEaLFV4E.d.mts +155 -0
  65. package/dist/twilio.cjs +8 -0
  66. package/dist/twilio.d.cts +3 -0
  67. package/dist/twilio.d.mts +3 -0
  68. package/dist/twilio.mjs +2 -0
  69. package/dist/twiml-z9LjoF4_.d.cts +67 -0
  70. package/dist/twiml-z9LjoF4_.d.mts +67 -0
  71. package/dist/xai.cjs +92 -0
  72. package/dist/xai.d.cts +35 -0
  73. package/dist/xai.d.mts +35 -0
  74. package/dist/xai.mjs +86 -0
  75. package/package.json +183 -0
@@ -0,0 +1,920 @@
1
+ import { n as BackgroundAudioPreset, r as BackgroundAudioSpec, t as BackgroundAudioOptions } from "./presets-Bf75YXs5.mjs";
2
+ import { t as TypedEmitter } from "./events-BUMYdETO.mjs";
3
+ import { _ as noopLogger, a as ProviderToolSchema, c as VadConfig, d as ProviderEvents, f as ProviderToolCall, g as consoleLogger, h as Logger, i as ProviderSessionInit, l as ProviderAudioDelta, m as ProviderCapabilities, n as ProviderFactory, o as SendTextOptions, p as ProviderUsage, r as ProviderFactoryContext, s as SendToolResultOptions, t as BaseRealtimeProvider, u as ProviderCloseInfo } from "./BaseRealtimeProvider-ClP8Wx1X.mjs";
4
+ import { i as WebSocketLike, m as TwilioStartEvent, r as TwilioMediaTransport } from "./transport-CEaLFV4E.mjs";
5
+ import { a as emptyUsage, i as UsageInfo, n as SessionStore, o as TranscriptEntry, r as CallSnapshot, t as InMemorySessionStore } from "./InMemorySessionStore-B5_rq61L.mjs";
6
+ import { i as TwilioRestClient, n as connectStreamTwiml } from "./twiml-z9LjoF4_.mjs";
7
+ import { IncomingMessage } from "node:http";
8
+ //#region src/tools/tool.d.ts
9
+ /** Structural Zod schema shape — works with both Zod 3 and Zod 4. */
10
+ interface ZodSchemaLike {
11
+ safeParse(data: unknown): {
12
+ success: true;
13
+ data: any;
14
+ } | {
15
+ success: false;
16
+ error: {
17
+ message?: string;
18
+ issues?: unknown[];
19
+ };
20
+ };
21
+ }
22
+ type IsAny<T> = 0 extends 1 & T ? true : false;
23
+ /** Output type of a Zod schema, for either Zod major. */
24
+ type InferSchema<S> = IsAny<S> extends true ? any : S extends {
25
+ _zod: {
26
+ output: infer O;
27
+ };
28
+ } ? O : S extends {
29
+ _output: infer O;
30
+ } ? O : unknown;
31
+ /**
32
+ * How a tool call is executed relative to the conversation:
33
+ * - `sync` — the model waits for the result before speaking (default)
34
+ * - `dispatch` — fire-and-forget: the model immediately gets `{status:'queued'}`
35
+ * and keeps talking; the work runs detached
36
+ * - `deferred` — the model gets `{status:'pending'}` and keeps talking; the
37
+ * real result is injected as a new turn when it arrives
38
+ * - `humanInTheLoop` — execution pauses for `session.approveTool()` /
39
+ * `rejectTool()`; an approval event fires for your backend
40
+ */
41
+ type ToolStrategy = 'sync' | 'dispatch' | 'deferred' | 'humanInTheLoop';
42
+ interface ToolDefinition<S extends ZodSchemaLike = ZodSchemaLike, Out = unknown> {
43
+ /** Function name shown to the model: [a-zA-Z0-9_-], ≤64 chars. */
44
+ name: string;
45
+ description: string;
46
+ parameters: S;
47
+ execute: (input: InferSchema<S>, ctx: ToolContext) => Out | Promise<Out>;
48
+ strategy?: ToolStrategy;
49
+ /** Abort execute() and return a timeout error to the model. Default 15000. */
50
+ timeoutMs?: number;
51
+ /** Hold audio while the tool runs. `false` disables even bridge defaults. */
52
+ backgroundAudio?: BackgroundAudioSpec | false;
53
+ /** For `humanInTheLoop`: auto-reject if not approved in time. Default 30000. */
54
+ approvalTimeoutMs?: number;
55
+ /** May veto (throw) or replace the input. */
56
+ onBeforeExecute?: (input: InferSchema<S>, ctx: ToolContext) => void | InferSchema<S> | Promise<void | InferSchema<S>>;
57
+ /** May observe or replace the result sent to the model. */
58
+ onAfterExecute?: (result: Out, ctx: ToolContext) => void | unknown | Promise<void | unknown>;
59
+ /** May map an error to a model-visible payload (instead of a generic one). */
60
+ onError?: (error: unknown, ctx: ToolContext) => void | unknown | Promise<void | unknown>;
61
+ }
62
+ interface Tool<S extends ZodSchemaLike = any, Out = any> extends ToolDefinition<S, Out> {
63
+ strategy: ToolStrategy;
64
+ /** JSON Schema for the provider tool declaration (computed once). */
65
+ readonly parametersJsonSchema: Record<string, unknown>;
66
+ }
67
+ /** Define a provider-agnostic tool with a Zod parameters schema. */
68
+ declare function tool<S extends ZodSchemaLike, Out>(definition: ToolDefinition<S, Out>): Tool<S, Out>;
69
+ //#endregion
70
+ //#region src/agents/Agent.d.ts
71
+ interface AgentDefinition {
72
+ /** Human-readable name ("Billing Department"). */
73
+ name: string;
74
+ /** Stable id used in handoff tool names; derived from name if omitted. */
75
+ id?: string;
76
+ /** System instructions — static or computed from session context. */
77
+ instructions: string | ((context: SessionContext) => string);
78
+ voice?: string;
79
+ /** Provider model override for this agent (used where supported). */
80
+ model?: string;
81
+ tools?: Tool[];
82
+ /** Agents this one can hand the call to (transfer tools are generated). */
83
+ handoffs?: Agent[];
84
+ /** Shown to the transferring model as the transfer tool's description. */
85
+ handoffDescription?: string;
86
+ /** Provider-native session options for this agent (escape hatch). */
87
+ providerOptions?: Record<string, unknown>;
88
+ }
89
+ declare class Agent {
90
+ readonly id: string;
91
+ readonly name: string;
92
+ readonly voice?: string;
93
+ readonly model?: string;
94
+ readonly tools: Tool[];
95
+ readonly handoffs: Agent[];
96
+ readonly handoffDescription?: string;
97
+ readonly providerOptions?: Record<string, unknown>;
98
+ private readonly instructionsSource;
99
+ constructor(definition: AgentDefinition);
100
+ resolveInstructions(context: SessionContext): string;
101
+ }
102
+ /**
103
+ * Walk the handoff graph (BFS) from a root agent. Cycles are fine — swarms
104
+ * commonly hand back and forth. Two different Agent instances claiming the
105
+ * same id is a configuration error caught here, at session build.
106
+ */
107
+ declare function collectAgentGraph(root: Agent): Map<string, Agent>;
108
+ //#endregion
109
+ //#region src/tools/context.d.ts
110
+ /**
111
+ * Session-scoped key/value context: seeded at call start (tenant id, caller
112
+ * profile, …), readable and writable from any tool during the call, carried
113
+ * across agent handoffs, and included in store snapshots.
114
+ */
115
+ declare class SessionContext {
116
+ private readonly data;
117
+ constructor(initial?: Record<string, unknown>);
118
+ get<T = unknown>(key: string): T | undefined;
119
+ set(key: string, value: unknown): void;
120
+ has(key: string): boolean;
121
+ delete(key: string): boolean;
122
+ toJSON(): Record<string, unknown>;
123
+ }
124
+ interface ToolCallInfo {
125
+ from?: string;
126
+ to?: string;
127
+ direction: 'inbound' | 'outbound';
128
+ customParameters: Record<string, string>;
129
+ }
130
+ /**
131
+ * Capability closures handed to tools — everything a tool may do to the live
132
+ * call without ever touching the engine, the provider socket, or the Twilio
133
+ * SDK (the sinwan ToolControl pattern).
134
+ */
135
+ interface CallSessionFacade {
136
+ readonly callSid: string;
137
+ /** Inject a text turn (e.g. steer the model or log a system note). */
138
+ sendText(text: string, options?: {
139
+ role?: 'user' | 'system';
140
+ triggerResponse?: boolean;
141
+ }): void;
142
+ /** Gracefully end the call (goodbye-aware two-phase hangup). */
143
+ finishCall(options?: {
144
+ finalMessage?: string;
145
+ }): Promise<void>;
146
+ /** Transfer the PSTN leg to another number (announced, playout-aware). */
147
+ transferTo(phoneNumber: string, options?: {
148
+ callerId?: string;
149
+ announcement?: string;
150
+ }): Promise<void>;
151
+ /** Swap the active agent (multi-agent handoff). */
152
+ handoffTo(agent: Agent | string): Promise<void>;
153
+ /** Start/stop background audio manually. */
154
+ playBackgroundAudio(spec: BackgroundAudioSpec, options?: {
155
+ volume?: number;
156
+ }): Promise<void>;
157
+ stopBackgroundAudio(options?: {
158
+ fadeOutMs?: number;
159
+ }): Promise<void>;
160
+ /** Complete a `deferred` tool from outside its execute() promise. */
161
+ submitToolResult(toolCallId: string, result: unknown): void;
162
+ }
163
+ interface ToolContext {
164
+ callSid: string;
165
+ /** The agent this tool call belongs to. */
166
+ agent: Agent;
167
+ session: CallSessionFacade;
168
+ context: SessionContext;
169
+ callInfo: ToolCallInfo;
170
+ logger: Logger;
171
+ /** Aborted on tool timeout or call teardown — pass to fetch etc. */
172
+ signal: AbortSignal;
173
+ /** Provider tool-call id (correlates deferred results). */
174
+ toolCallId: string;
175
+ }
176
+ //#endregion
177
+ //#region src/tools/middleware.d.ts
178
+ interface ToolDecoration {
179
+ description?: string;
180
+ parameters?: Record<string, unknown>;
181
+ }
182
+ interface ToolMiddleware {
183
+ /** Rewrite the schema/description the model is shown for this tool. */
184
+ decorate?(tool: Tool): ToolDecoration | void;
185
+ /** Wrap execution; call `next()` to proceed inward, or short-circuit. */
186
+ wrapExecute?(tool: Tool, input: unknown, ctx: ToolContext, next: () => Promise<unknown>): Promise<unknown>;
187
+ }
188
+ /** Apply every middleware's decoration, first-registered first. */
189
+ declare function decorateTool(tool: Tool, middlewares: readonly ToolMiddleware[]): {
190
+ description: string;
191
+ parameters: Record<string, unknown>;
192
+ };
193
+ /** Compose wrapExecute chains around an innermost executor. */
194
+ declare function composeExecution(tool: Tool, input: unknown, ctx: ToolContext, middlewares: readonly ToolMiddleware[], innermost: () => Promise<unknown>): Promise<unknown>;
195
+ //#endregion
196
+ //#region src/interruption/InterruptionController.d.ts
197
+ /**
198
+ * InterruptionController — decides whether a barge-in is honored.
199
+ *
200
+ * Gate order (first match wins): disabled → suspended (noise cooldown) →
201
+ * tool running → guard window. The guard window protects the start of a
202
+ * response for a flat duration measured from playback start (mark-echo
203
+ * truth, not generation time). To make the whole first turn uninterruptible,
204
+ * use `deafness.ignoreUserAudioUntilFirstTurnDone` instead.
205
+ *
206
+ * The rate limiter is the noisy-environment defense from production systems:
207
+ * too many barge-ins inside a sliding window suspends interruptions until the
208
+ * current playback ends, optionally injecting an instruction so the agent can
209
+ * say "please move somewhere quieter".
210
+ */
211
+ interface InterruptionRateLimit {
212
+ windowMs: number;
213
+ /** Barge-ins inside the window that trip the limiter. */
214
+ threshold: number;
215
+ /** Injected once, on the rising edge, when the limiter trips. */
216
+ instruction?: string;
217
+ }
218
+ interface InterruptionSettings {
219
+ /** Master switch. Default true. */
220
+ enabled?: boolean;
221
+ /** No-barge window measured from playback start of each response, ms. */
222
+ guardDurationMs?: number;
223
+ /** Apply the guard only to the first response of the call. Default false. */
224
+ firstResponseOnly?: boolean;
225
+ rateLimit?: InterruptionRateLimit;
226
+ }
227
+ type InterruptionBlockCause = 'disabled' | 'guard' | 'rate_limit' | 'tool_running' | 'suspended';
228
+ type InterruptionDecision = {
229
+ allow: true;
230
+ } | {
231
+ allow: false;
232
+ cause: InterruptionBlockCause;
233
+ instruction?: string;
234
+ };
235
+ declare class InterruptionController {
236
+ private readonly settings;
237
+ private readonly now;
238
+ private responseIndex;
239
+ private guardStartedAt;
240
+ private guardResponseId;
241
+ /** The guarded response's audio has not finished (or been flushed) yet. */
242
+ private guardPlaybackOpen;
243
+ /** Responses that began while the guarded one was still playing. */
244
+ private deferredStarts;
245
+ private suspended;
246
+ private bargeInTimestamps;
247
+ constructor(settings?: InterruptionSettings, now?: () => number);
248
+ get isSuspended(): boolean;
249
+ /** A new response started generating: rotate the guard to it. */
250
+ onResponseStarted(responseId: string): void;
251
+ /** Playback of a response reached the caller: the guard clock starts now. */
252
+ onPlaybackStarted(responseId: string): void;
253
+ /** Current playback finished or was cleared: cooldown suspension ends. */
254
+ onPlaybackEnded(): void;
255
+ /**
256
+ * Evaluate a barge-in attempt (user speech while the agent is audible).
257
+ * Every attempt counts toward the rate limiter, including blocked ones.
258
+ */
259
+ evaluate(context: {
260
+ toolRunning: boolean;
261
+ }): InterruptionDecision;
262
+ private guardActive;
263
+ }
264
+ //#endregion
265
+ //#region src/providers/base/reconnect.d.ts
266
+ /** Exponential backoff with optional full jitter, shared by all providers. */
267
+ interface ReconnectPolicy {
268
+ /** Attempts before giving up. 0 disables reconnection. */
269
+ maxAttempts: number;
270
+ initialDelayMs: number;
271
+ maxDelayMs: number;
272
+ /** Full jitter: each delay is uniform in [0, computed]. */
273
+ jitter: boolean;
274
+ }
275
+ declare const DEFAULT_RECONNECT_POLICY: ReconnectPolicy;
276
+ //#endregion
277
+ //#region src/bridge/config.d.ts
278
+ interface GreetingOptions {
279
+ /**
280
+ * `agent-initiates`: the agent speaks first (silence on answer reads as a
281
+ * dropped call). `user-initiates`: wait for the caller.
282
+ */
283
+ mode: 'agent-initiates' | 'user-initiates';
284
+ /** Extra instructions for the opening response. */
285
+ instructions?: string;
286
+ /**
287
+ * Pre-synthesized greeting: burst-written to the caller while the provider
288
+ * connects (~seconds faster to first word). `audio` is 8 kHz μ-law (raw
289
+ * buffer or file path); `text` is what it says, used to keep the model from
290
+ * greeting twice.
291
+ */
292
+ preSynthesized?: {
293
+ audio: Buffer | string;
294
+ text: string;
295
+ };
296
+ }
297
+ interface DeafnessOptions {
298
+ /**
299
+ * Drop caller audio until the agent's first turn finishes playing.
300
+ * Protects the greeting from noisy pickups. Default true — except with
301
+ * `greeting.mode: 'user-initiates'`, where the caller must be heard to
302
+ * start the call at all, so the default flips to false. An explicit true
303
+ * is honored even there, but deafens the call until something else
304
+ * (an idle nudge, a tool) produces the agent's first turn.
305
+ */
306
+ ignoreUserAudioUntilFirstTurnDone?: boolean;
307
+ /** Drop caller audio while a foreground tool is running. Default true. */
308
+ muteDuringToolExecution?: boolean;
309
+ /**
310
+ * Drop caller audio while agent audio is audibly playing (half-duplex).
311
+ * Nothing said while the agent speaks reaches the provider — it is lost,
312
+ * not queued — so its VAD cannot fire mid-playback. (Speech that began
313
+ * just before playback started is already server-side and may still
314
+ * barge in per the interruption settings.) Extreme-noise environments
315
+ * only; prefer `interruptions.enabled: false` when caller speech should
316
+ * still be heard and answered afterwards. Default false.
317
+ */
318
+ muteWhileAgentSpeaking?: boolean;
319
+ }
320
+ interface IdleOptions {
321
+ /** Seconds of caller silence before the first nudge. */
322
+ timeoutSeconds: number;
323
+ /** Nudge instructions, escalated in order. */
324
+ prompts?: string[];
325
+ /** Nudges before giving up. Default = prompts.length. */
326
+ maxNudges?: number;
327
+ /** Goodbye instruction before hanging up. */
328
+ goodbye?: string;
329
+ }
330
+ interface HangupOptions {
331
+ /**
332
+ * Watchdog window: force hangup completion only after the pending goodbye
333
+ * shows no progress — no audio deltas, no response start, no mark echoes —
334
+ * for this long. Evidence re-arms the window, so a goodbye that is still
335
+ * being generated or played is never cut off, on any provider; a dead
336
+ * socket or a model that never says goodbye completes within one or two
337
+ * quiet windows. Default 7000.
338
+ */
339
+ markTimeoutMs: number;
340
+ }
341
+ interface SessionOptions {
342
+ greeting: GreetingOptions;
343
+ interruptions: InterruptionSettings;
344
+ deafness: DeafnessOptions;
345
+ idle?: IdleOptions;
346
+ maxCallDurationSeconds?: number;
347
+ reconnect: ReconnectPolicy;
348
+ hangup: HangupOptions;
349
+ /** Normalized VAD, mapped to the provider's native config. */
350
+ vad?: VadConfig | null;
351
+ /**
352
+ * `afterPlayback` (default): tool results wait until current agent audio
353
+ * finishes playing. `immediate`: send as soon as the tool completes.
354
+ */
355
+ toolResultDelivery: 'afterPlayback' | 'immediate';
356
+ /** Default hold audio for tools that don't specify their own. */
357
+ toolBackgroundAudio?: {
358
+ spec: BackgroundAudioSpec;
359
+ } & BackgroundAudioOptions;
360
+ /**
361
+ * When a handoff target declares a different voice on a provider that
362
+ * cannot change voice mid-session: `keep` (default) keeps the current
363
+ * voice; `reconnect` opens a fresh provider session with the new voice and
364
+ * carries context over (adds a beat of latency).
365
+ */
366
+ handoffVoicePolicy?: 'keep' | 'reconnect';
367
+ /** Hold audio covering the reconnect gap on handoffs that need one. */
368
+ handoffHold?: {
369
+ spec: BackgroundAudioSpec;
370
+ } & BackgroundAudioOptions;
371
+ /** Initial session context KV, available to tools and instructions. */
372
+ context?: Record<string, unknown>;
373
+ /** Handshake bounds for the Twilio start frame. */
374
+ handshake?: {
375
+ timeoutMs?: number;
376
+ maxPreStartMessages?: number;
377
+ };
378
+ }
379
+ declare const DEFAULT_SESSION_OPTIONS: SessionOptions;
380
+ declare function resolveSessionOptions(partial?: Partial<SessionOptions>): SessionOptions;
381
+ interface BuiltinToolsConfig {
382
+ /**
383
+ * Let the agent end the call gracefully: it is asked for a closing line,
384
+ * the goodbye's playout is watched via marks, then the leg completes.
385
+ */
386
+ finishCall?: boolean | {
387
+ description?: string;
388
+ farewellInstruction?: string;
389
+ };
390
+ /** Let the agent transfer the PSTN leg to a human/number. */
391
+ transferCall?: {
392
+ enabled: true;
393
+ /** Fallback when the model doesn't provide a number. */
394
+ defaultPhoneNumber?: string;
395
+ /** Caller id shown to the transferee. Defaults to bridge twilio.callerId. */
396
+ callerId?: string;
397
+ description?: string;
398
+ /** Spoken before transferring (out-of-band instruction). */
399
+ announcement?: string;
400
+ } | false;
401
+ /** Send an SMS via Twilio REST. */
402
+ sendSms?: {
403
+ enabled: true;
404
+ from?: string;
405
+ description?: string;
406
+ } | false;
407
+ /** Send a WhatsApp message via Twilio REST. */
408
+ sendWhatsapp?: {
409
+ enabled: true;
410
+ from?: string;
411
+ description?: string;
412
+ } | false;
413
+ }
414
+ interface TwilioRestOptions {
415
+ accountSid: string;
416
+ authToken: string;
417
+ /** Default caller id for transfers. */
418
+ callerId?: string;
419
+ }
420
+ interface BridgeConfig {
421
+ /** The (root) agent, or a resolver for multi-tenant routing per call. */
422
+ agent: Agent | ((start: TwilioStartEvent) => Agent | Promise<Agent>);
423
+ provider: ProviderFactory;
424
+ session?: Partial<SessionOptions> | ((start: TwilioStartEvent) => Partial<SessionOptions> | Promise<Partial<SessionOptions>>);
425
+ /** Twilio REST credentials — enables clean hangup, transfer, SMS/WhatsApp. */
426
+ twilio?: TwilioRestOptions;
427
+ builtinTools?: BuiltinToolsConfig;
428
+ store?: SessionStore;
429
+ /**
430
+ * Reject unauthorized streams before a session is created. Return false to
431
+ * close with 1008. Typical check: a signed token in customParameters.
432
+ */
433
+ validateConnection?: (start: TwilioStartEvent, request?: IncomingMessage) => boolean | Promise<boolean>;
434
+ logger?: Logger;
435
+ }
436
+ //#endregion
437
+ //#region src/bridge/state.d.ts
438
+ /**
439
+ * Call lifecycle:
440
+ * connecting → active → ending → ended
441
+ * (`awaiting_start` lives in the bridge's handshake, before a session exists.)
442
+ */
443
+ type CallState = 'connecting' | 'active' | 'ending' | 'ended';
444
+ type CallEndReason = 'agent-hangup' | 'caller-hangup' | 'transferred' | 'provider-failed' | 'max-duration' | 'idle-timeout' | 'bridge-closed' | 'error';
445
+ //#endregion
446
+ //#region src/bridge/events.d.ts
447
+ interface CallStartedInfo {
448
+ callSid: string;
449
+ streamSid: string;
450
+ direction: 'inbound' | 'outbound';
451
+ from?: string;
452
+ to?: string;
453
+ customParameters: Record<string, string>;
454
+ }
455
+ interface ToolRunInfo {
456
+ toolCallId: string;
457
+ toolName: string;
458
+ strategy: string;
459
+ agentId: string;
460
+ input?: unknown;
461
+ result?: unknown;
462
+ error?: string;
463
+ durationMs?: number;
464
+ }
465
+ interface ApprovalRequestInfo {
466
+ approvalId: string;
467
+ toolCallId: string;
468
+ toolName: string;
469
+ input: unknown;
470
+ agentId: string;
471
+ expiresAtMs: number;
472
+ }
473
+ interface SessionEventMap {
474
+ 'call.started': (info: CallStartedInfo) => void;
475
+ 'call.ended': (info: {
476
+ reason: CallEndReason;
477
+ durationMs: number;
478
+ usage: UsageInfo;
479
+ }) => void;
480
+ 'call.failed': (error: Error) => void;
481
+ 'provider.connected': () => void;
482
+ 'provider.reconnecting': (info: {
483
+ attempt: number;
484
+ delayMs: number;
485
+ }) => void;
486
+ 'provider.reconnected': () => void;
487
+ 'provider.closed': (info: {
488
+ code?: number;
489
+ reason?: string;
490
+ }) => void;
491
+ /** Generation-side: the model started/finished producing a response. */
492
+ 'agent.speech.started': (info: {
493
+ responseId: string;
494
+ }) => void;
495
+ 'agent.speech.ended': (info: {
496
+ responseId: string;
497
+ }) => void;
498
+ /** Playback-side (mark-confirmed): what the caller actually hears. */
499
+ 'playback.started': (info: {
500
+ responseId: string;
501
+ }) => void;
502
+ 'playback.finished': (info: {
503
+ responseId: string;
504
+ playedMs: number;
505
+ }) => void;
506
+ 'playback.interrupted': (info: {
507
+ responseId: string;
508
+ playedMs: number;
509
+ }) => void;
510
+ 'user.speech.started': () => void;
511
+ 'user.speech.ended': () => void;
512
+ 'transcript.user': (entry: TranscriptEntry) => void;
513
+ 'transcript.agent': (entry: TranscriptEntry) => void;
514
+ 'tool.started': (info: ToolRunInfo) => void;
515
+ 'tool.completed': (info: ToolRunInfo) => void;
516
+ 'tool.failed': (info: ToolRunInfo) => void;
517
+ 'tool.approval.required': (request: ApprovalRequestInfo) => void;
518
+ 'agent.handoff': (info: {
519
+ from: Agent;
520
+ to: Agent;
521
+ reason?: string;
522
+ }) => void;
523
+ interruption: (info: {
524
+ responseId: string;
525
+ playedMs: number;
526
+ }) => void;
527
+ 'interruption.blocked': (info: {
528
+ cause: string;
529
+ }) => void;
530
+ 'background_audio.started': (info: {
531
+ preset?: string;
532
+ }) => void;
533
+ 'background_audio.stopped': (info: {
534
+ preset?: string;
535
+ }) => void;
536
+ dtmf: (info: {
537
+ digit: string;
538
+ }) => void;
539
+ 'usage.updated': (usage: UsageInfo, delta: ProviderUsage) => void;
540
+ error: (error: Error) => void;
541
+ }
542
+ interface BridgeEventMap {
543
+ 'session.started': (session: CallSession) => void;
544
+ 'session.ended': (info: {
545
+ callSid: string;
546
+ reason: CallEndReason;
547
+ }) => void;
548
+ 'connection.rejected': (info: {
549
+ reason: string;
550
+ }) => void;
551
+ error: (error: Error) => void;
552
+ }
553
+ //#endregion
554
+ //#region src/bridge/CallSession.d.ts
555
+ interface CallSessionDeps {
556
+ transport: TwilioMediaTransport;
557
+ start: TwilioStartEvent;
558
+ providerFactory: ProviderFactory;
559
+ agent: Agent;
560
+ options: SessionOptions;
561
+ store: SessionStore;
562
+ logger: Logger;
563
+ builtinTools?: BuiltinToolsConfig;
564
+ rest?: TwilioRestClient;
565
+ restCallerId?: string;
566
+ answeredEarly?: boolean;
567
+ middlewares?: readonly ToolMiddleware[];
568
+ onEnded?: (callSid: string, reason: CallEndReason) => void;
569
+ }
570
+ declare class CallSession extends TypedEmitter<SessionEventMap> {
571
+ readonly callSid: string;
572
+ readonly streamSid: string;
573
+ readonly callInfo: ToolCallInfo;
574
+ readonly context: SessionContext;
575
+ private stateValue;
576
+ private readonly deps;
577
+ private readonly log;
578
+ private readonly tracker;
579
+ private readonly interruptions;
580
+ private readonly usageAccumulator;
581
+ private readonly toolQueue;
582
+ private readonly transcriptEntries;
583
+ private toolset;
584
+ private readonly startedAtMs;
585
+ private readonly interruptedResponses;
586
+ private readonly runningTools;
587
+ private readonly timers;
588
+ private provider;
589
+ private activeAgentValue;
590
+ private generating;
591
+ private currentResponseId;
592
+ private firstTurnDone;
593
+ /**
594
+ * A caller turn that a blocked barge-in swallowed. With bridge-owned
595
+ * interruptions the server no longer auto-responds while the protected
596
+ * response is active, so the bridge answers it after protected playback:
597
+ * 'speaking' → user started during a block; 'committed' → their turn ended
598
+ * (VAD committed it) and deserves a response once playback finishes.
599
+ */
600
+ private blockedUserTurn;
601
+ private greeted;
602
+ private answered;
603
+ private inboundBuffer;
604
+ private reconnectAttempt;
605
+ private reconnecting;
606
+ private pendingHangup;
607
+ private pendingTransfer;
608
+ private endedReason;
609
+ private hangupReason;
610
+ private readonly middlewares;
611
+ private readonly bgAudio;
612
+ /** Deferred tool calls awaiting a real result (execute() or submitToolResult). */
613
+ private readonly deferredPending;
614
+ /** Human-in-the-loop calls awaiting approve/reject. */
615
+ private readonly approvals;
616
+ /** Text turns to inject once the agent finishes speaking (deferred results). */
617
+ private pendingInjections;
618
+ private idleTimer;
619
+ private nudgeCount;
620
+ /** All agents reachable from the root via handoffs, by id. */
621
+ private readonly agents;
622
+ private readonly handoffHistory;
623
+ private handoffInProgress;
624
+ /** Pre-synthesized greeting playout state. */
625
+ private pregreeting;
626
+ constructor(deps: CallSessionDeps);
627
+ get state(): CallState;
628
+ get activeAgent(): Agent;
629
+ get usage(): UsageInfo;
630
+ get transcript(): readonly TranscriptEntry[];
631
+ /** Connect the provider and activate the call. Called by the bridge. */
632
+ begin(): Promise<void>;
633
+ /** The outbound leg was answered (host's Twilio status callback). */
634
+ notifyAnswered(): void;
635
+ sendText(text: string, options?: {
636
+ role?: 'user' | 'system';
637
+ triggerResponse?: boolean;
638
+ }): void;
639
+ updateInstructions(instructions: string): Promise<void>;
640
+ /** Manual barge-in: stop the agent mid-sentence. */
641
+ interrupt(): void;
642
+ /**
643
+ * Gracefully end the call. With `finalMessage`, the agent speaks it first.
644
+ * With `immediate: true`, skip the goodbye and complete now. Resolves when
645
+ * the call has actually ended.
646
+ */
647
+ finishCall(options?: {
648
+ finalMessage?: string;
649
+ immediate?: boolean;
650
+ }): Promise<void>;
651
+ /** Transfer the PSTN leg. Waits for current playback (and announcement). */
652
+ transferTo(phoneNumber: string, options?: {
653
+ callerId?: string;
654
+ announcement?: string;
655
+ }): Promise<void>;
656
+ /** Swap the active agent (swarm handoff). Accepts an Agent or its id. */
657
+ handoffTo(agent: Agent | string): Promise<void>;
658
+ /** Complete a `deferred` tool call from the host side. */
659
+ submitToolResult(toolCallId: string, result: unknown): void;
660
+ /** Start background audio manually (independent of tools). */
661
+ playBackgroundAudio(spec: BackgroundAudioSpec, options?: BackgroundAudioOptions): Promise<void>;
662
+ stopBackgroundAudio(options?: {
663
+ fadeOutMs?: number;
664
+ }): Promise<void>;
665
+ /** Immediate teardown (no goodbye). */
666
+ end(reason?: CallEndReason): Promise<void>;
667
+ private buildToolset;
668
+ private buildProviderInit;
669
+ private wireTransport;
670
+ /** Emit 'error'; when the host attached no listener, log instead of losing it. */
671
+ private emitError;
672
+ private wireProvider;
673
+ private handleInboundMedia;
674
+ private flushInboundBuffer;
675
+ private handleMarkEcho;
676
+ private onPlaybackFinished;
677
+ private handleBargeIn;
678
+ private performInterrupt;
679
+ private handleToolCall;
680
+ /** sync path (also the approved HITL path): hold audio, execute, deliver. */
681
+ private runForegroundTool;
682
+ /** dispatch/deferred body — no hold audio (the model keeps talking). */
683
+ private runDetachedTool;
684
+ /** Shared execution core: abort/timeout, middleware onion, per-tool hooks. */
685
+ private executeToolBody;
686
+ /** Inject a deferred result as a conversation turn (idempotent per call). */
687
+ private completeDeferred;
688
+ /** Complete a deferred tool from the host (webhook, operator console…). */
689
+ private completeDeferredFromHost;
690
+ private requestApproval;
691
+ /** Approve a pending humanInTheLoop tool call (optionally editing input). */
692
+ approveTool(approvalId: string, editedInput?: unknown): void;
693
+ /** Reject a pending humanInTheLoop tool call. */
694
+ rejectTool(approvalId: string, reason?: string): void;
695
+ private acquireHoldAudio;
696
+ /** Inject a text turn now, or after the agent finishes speaking. */
697
+ private injectOrQueueText;
698
+ private raceAbort;
699
+ private deliverToolResult;
700
+ private flushToolQueue;
701
+ private buildToolContext;
702
+ private maybeGreet;
703
+ private armHangup;
704
+ /** Evidence the goodbye is alive (generating or playing) — feeds the watchdog. */
705
+ private noteHangupProgress;
706
+ /**
707
+ * Watchdog: forces completion only after a full quiet window — no deltas, no
708
+ * response start, no mark echoes for `markTimeoutMs`. Evidence re-arms it, so
709
+ * a slow or long goodbye is NEVER truncated mid-playout (wall-clock must not
710
+ * override live mark evidence); a dead socket or a model that never says
711
+ * goodbye still completes within one or two quiet windows.
712
+ */
713
+ private armHangupWatchdog;
714
+ private maybeCompleteHangup;
715
+ private completeHangup;
716
+ private executePendingTransfer;
717
+ private scheduleReconnect;
718
+ private reconnectProvider;
719
+ /** After a reconnect the provider session is blank — restore conversational context. */
720
+ private reinjectHistory;
721
+ private performHandoff;
722
+ /**
723
+ * Burst-write a stored μ-law greeting straight onto the Twilio socket —
724
+ * no pacing loop (Twilio buffers and plays at line rate), so playback
725
+ * starts immediately while the provider session is still being built.
726
+ */
727
+ private playPreGreeting;
728
+ /** Armed whenever the agent goes quiet and we're waiting on the caller. */
729
+ private armIdleTimer;
730
+ private clearIdleTimer;
731
+ private onIdleTimeout;
732
+ private startMaxDurationWatchdog;
733
+ private fail;
734
+ private teardown;
735
+ private saveSnapshot;
736
+ }
737
+ //#endregion
738
+ //#region src/bridge/TwilioRealtimeBridge.d.ts
739
+ declare class TwilioRealtimeBridge extends TypedEmitter<BridgeEventMap> {
740
+ private readonly config;
741
+ private readonly logger;
742
+ private readonly store;
743
+ private readonly rest?;
744
+ private readonly sessionsBySid;
745
+ /** Status callbacks can beat the media stream; remember early answers. */
746
+ private readonly answeredEarly;
747
+ private readonly middlewares;
748
+ private closed;
749
+ constructor(config: BridgeConfig);
750
+ /** Attach one incoming Twilio Media Stream WebSocket. */
751
+ handleConnection(ws: WebSocketLike, request?: IncomingMessage): void;
752
+ /**
753
+ * Register cross-cutting tool middleware (first registered wraps
754
+ * outermost). Applies to sessions created after the call.
755
+ */
756
+ use(middleware: ToolMiddleware): this;
757
+ getSession(callSid: string): CallSession | undefined;
758
+ sessions(): ReadonlyMap<string, CallSession>;
759
+ /**
760
+ * Feed the outbound `answered` signal from your Twilio status callback so
761
+ * agent-initiated greetings fire when the human actually picks up.
762
+ */
763
+ notifyAnswered(callSid: string): void;
764
+ /** Deterministically end every active session and stop accepting new ones. */
765
+ close(): Promise<void>;
766
+ private handshake;
767
+ private onSessionEnded;
768
+ }
769
+ //#endregion
770
+ //#region src/agents/handoff.d.ts
771
+ declare const HANDOFF_MARKER: "__twilioRealtimeAgentsHandoff";
772
+ interface HandoffDirective {
773
+ [HANDOFF_MARKER]: true;
774
+ targetAgentId: string;
775
+ reason?: string;
776
+ }
777
+ declare function isHandoffDirective(value: unknown): value is HandoffDirective;
778
+ declare function handoffToolName(agentId: string): string;
779
+ declare function createHandoffTool(target: Agent): Tool;
780
+ //#endregion
781
+ //#region src/greeting/capture.d.ts
782
+ /**
783
+ * Capture a pre-synthesized greeting from a throwaway realtime session.
784
+ *
785
+ * Why not a TTS API? The realtime voice sets don't fully overlap the TTS
786
+ * voice sets — capturing from a real session guarantees the stored greeting
787
+ * sounds exactly like the live agent. Run this at deploy/config time and
788
+ * store the returned μ-law buffer (it is Twilio wire format; pass it to
789
+ * `greeting.preSynthesized.audio`).
790
+ */
791
+ interface CaptureGreetingOptions {
792
+ apiKey: string;
793
+ /** Exact text the greeting should say. */
794
+ text: string;
795
+ model?: string;
796
+ voice?: string;
797
+ /** OpenAI-compatible realtime endpoint (works for xAI too). */
798
+ baseUrl?: string;
799
+ headers?: Record<string, string>;
800
+ timeoutMs?: number;
801
+ }
802
+ interface CapturedGreeting {
803
+ /** Raw 8 kHz μ-law audio, ready for the Twilio stream. */
804
+ audio: Buffer;
805
+ text: string;
806
+ durationMs: number;
807
+ }
808
+ declare function captureGreetingAudio(options: CaptureGreetingOptions): Promise<CapturedGreeting>;
809
+ //#endregion
810
+ //#region src/tools/json-schema.d.ts
811
+ /**
812
+ * Zod → JSON Schema for provider tool declarations.
813
+ *
814
+ * Zod 4 ships `z.toJSONSchema()` — used when available (zod is a required
815
+ * peer). For Zod 3 (≥3.25) a minimal structural converter covers the subset
816
+ * that makes sense for voice-agent tool parameters: objects, strings,
817
+ * numbers, booleans, enums, literals, arrays, records, unions,
818
+ * optional/nullable/default, descriptions.
819
+ */
820
+ type AnyZod = any;
821
+ declare function zodToJsonSchema(schema: AnyZod): Record<string, unknown>;
822
+ //#endregion
823
+ //#region src/tools/builtins/finishCall.d.ts
824
+ interface FinishCallToolOptions {
825
+ description?: string;
826
+ /** Returned to the model as the tool result's instruction. */
827
+ farewellInstruction?: string;
828
+ }
829
+ /**
830
+ * Graceful hangup: the tool result *instructs* the model to say goodbye (so
831
+ * the farewell is the model's own, in context and language), the session
832
+ * watches the goodbye's playout via marks, and only then completes the leg.
833
+ * A watchdog forces completion if the goodbye never materializes.
834
+ */
835
+ declare function createFinishCallTool(options?: FinishCallToolOptions): Tool<any, any>;
836
+ //#endregion
837
+ //#region src/tools/builtins/transferCall.d.ts
838
+ interface TransferCallToolOptions {
839
+ defaultPhoneNumber?: string;
840
+ callerId?: string;
841
+ description?: string;
842
+ /** Spoken to the caller before the transfer executes. */
843
+ announcement?: string;
844
+ }
845
+ declare function createTransferCallTool(options?: TransferCallToolOptions): Tool<any, any>;
846
+ //#endregion
847
+ //#region src/playback/PlaybackTracker.d.ts
848
+ interface MarkEchoResult {
849
+ kind: 'played' | 'flushed';
850
+ responseId: string;
851
+ playedMs: number;
852
+ /** First audible chunk of this response reached the caller. */
853
+ playbackStarted: boolean;
854
+ /** The last chunk of a completed response finished playing. */
855
+ playbackFinished: boolean;
856
+ }
857
+ declare class PlaybackTracker {
858
+ private epoch;
859
+ private markSeq;
860
+ private readonly marks;
861
+ private readonly responses;
862
+ private readonly now;
863
+ private readonly checkpointIntervalMs;
864
+ constructor(now?: () => number, options?: {
865
+ checkpointIntervalMs?: number;
866
+ });
867
+ /** Is `name` one of ours (as opposed to a host-app or background mark)? */
868
+ static isTrackedMark(name: string): boolean;
869
+ /**
870
+ * Record an outgoing audio chunk. Returns a mark name to interleave after
871
+ * it when a checkpoint is due (first chunk of the response, or
872
+ * checkpointIntervalMs of audio accumulated since the last mark) — null
873
+ * otherwise.
874
+ */
875
+ onAudioSent(responseId: string, chunkMs: number, itemId?: string): string | null;
876
+ /**
877
+ * The response finished generating. If audio accumulated past the last
878
+ * checkpoint, returns a final tail mark that MUST be sent to Twilio (it is
879
+ * what makes `playback.finished` fire); otherwise flags the pending mark
880
+ * covering the full total as final and returns null.
881
+ */
882
+ onGenerationDone(responseId: string): string | null;
883
+ private createMark;
884
+ /** Process a mark echo from Twilio. Returns null for unknown marks. */
885
+ onMarkEcho(name: string): MarkEchoResult | null;
886
+ /**
887
+ * Bump the clear-epoch (call this immediately BEFORE sending `clear`).
888
+ * Every un-echoed mark becomes flushed; responses with unplayed audio are
889
+ * finalized at their current playedMs. Returns those interrupted responses.
890
+ */
891
+ onClear(): Array<{
892
+ responseId: string;
893
+ playedMs: number;
894
+ itemId?: string;
895
+ }>;
896
+ /**
897
+ * Best-estimate of what the caller has heard of `responseId` right now:
898
+ * last confirmed mark plus wall-clock elapsed since, clamped to the total.
899
+ */
900
+ estimatePlayedMs(responseId: string): number;
901
+ /**
902
+ * Live responses with unplayed audio, with their current played-ms
903
+ * estimates — captured BEFORE onClear() freezes them, to feed truncation.
904
+ */
905
+ snapshotActive(): Array<{
906
+ responseId: string;
907
+ itemId?: string;
908
+ estimatedPlayedMs: number;
909
+ }>;
910
+ /** Any response with audio still unplayed (and not flushed)? */
911
+ isPlaybackActive(): boolean;
912
+ itemIdFor(responseId: string): string | undefined;
913
+ playedMsFor(responseId: string): number;
914
+ totalMsFor(responseId: string): number;
915
+ private ensureTrack;
916
+ /** Drop bookkeeping once a response is finished and every mark came home. */
917
+ private maybeForget;
918
+ }
919
+ //#endregion
920
+ export { Agent, type AgentDefinition, type ApprovalRequestInfo, type BackgroundAudioOptions, type BackgroundAudioPreset, type BackgroundAudioSpec, BaseRealtimeProvider, type BridgeConfig, type BridgeEventMap, type BuiltinToolsConfig, type CallEndReason, CallSession, type CallSessionFacade, type CallSnapshot, type CallStartedInfo, type CallState, type CaptureGreetingOptions, type CapturedGreeting, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, type DeafnessOptions, type FinishCallToolOptions, type GreetingOptions, type HandoffDirective, type HangupOptions, type IdleOptions, InMemorySessionStore, type InferSchema, type InterruptionBlockCause, InterruptionController, type InterruptionDecision, type InterruptionRateLimit, type InterruptionSettings, type Logger, type MarkEchoResult, PlaybackTracker, type ProviderAudioDelta, type ProviderCapabilities, type ProviderCloseInfo, type ProviderEvents, type ProviderFactory, type ProviderFactoryContext, type ProviderSessionInit, type ProviderToolCall, type ProviderToolSchema, type ProviderUsage, type ReconnectPolicy, type SendTextOptions, type SendToolResultOptions, SessionContext, type SessionEventMap, type SessionOptions, type SessionStore, type Tool, type ToolCallInfo, type ToolContext, type ToolDecoration, type ToolDefinition, type ToolMiddleware, type ToolRunInfo, type ToolStrategy, type TranscriptEntry, type TransferCallToolOptions, TwilioRealtimeBridge, type TwilioRestOptions, type TwilioStartEvent, type UsageInfo, type VadConfig, type WebSocketLike, type ZodSchemaLike, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };