nexus-ai-pro 1.1.0 → 1.2.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 (141) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +81 -2
  3. package/dist/core/nexus.d.ts +22 -1
  4. package/dist/core/nexus.js +31 -0
  5. package/dist/telephony/index.d.ts +3 -1
  6. package/dist/telephony/index.js +1 -0
  7. package/dist/telephony/manager.d.ts +6 -1
  8. package/dist/telephony/manager.js +59 -0
  9. package/dist/telephony/providers/twilio.d.ts +9 -1
  10. package/dist/telephony/providers/twilio.js +165 -12
  11. package/dist/telephony/realtime-bridge.d.ts +58 -0
  12. package/dist/telephony/realtime-bridge.js +180 -0
  13. package/dist/types/telephony.d.ts +87 -0
  14. package/dist-cjs/agent/loop.js +93 -0
  15. package/dist-cjs/agent/tool.js +50 -0
  16. package/dist-cjs/cache/adapters.js +84 -0
  17. package/dist-cjs/cache/memory-cache.js +106 -0
  18. package/dist-cjs/cache/semantic-cache.js +75 -0
  19. package/dist-cjs/connectors/web.js +401 -0
  20. package/dist-cjs/context/index.js +5 -0
  21. package/dist-cjs/context/window.js +284 -0
  22. package/dist-cjs/core/config-builder.js +203 -0
  23. package/dist-cjs/core/create-nexus.js +184 -0
  24. package/dist-cjs/core/nexus.js +897 -0
  25. package/dist-cjs/core/response-format.js +109 -0
  26. package/dist-cjs/core/secure-stream.js +153 -0
  27. package/dist-cjs/core/streaming.js +35 -0
  28. package/dist-cjs/embeddings/providers.js +65 -0
  29. package/dist-cjs/evals/index.js +24 -0
  30. package/dist-cjs/evals/judge.js +158 -0
  31. package/dist-cjs/evals/metrics.js +171 -0
  32. package/dist-cjs/evals/runner.js +64 -0
  33. package/dist-cjs/hallucination/consistency.js +92 -0
  34. package/dist-cjs/hallucination/factual.js +37 -0
  35. package/dist-cjs/hallucination/knowledge-graph.js +74 -0
  36. package/dist-cjs/hallucination/rag.js +53 -0
  37. package/dist-cjs/hallucination/retrieval.js +78 -0
  38. package/dist-cjs/hallucination/verification.js +139 -0
  39. package/dist-cjs/images/assets.js +351 -0
  40. package/dist-cjs/images/errors.js +90 -0
  41. package/dist-cjs/images/index.js +14 -0
  42. package/dist-cjs/images/manager.js +578 -0
  43. package/dist-cjs/images/mock.js +241 -0
  44. package/dist-cjs/images/openai.js +360 -0
  45. package/dist-cjs/index.js +230 -0
  46. package/dist-cjs/jobs/batch.js +35 -0
  47. package/dist-cjs/jobs/durable-adapters.js +55 -0
  48. package/dist-cjs/jobs/queue.js +61 -0
  49. package/dist-cjs/models/registry.js +52 -0
  50. package/dist-cjs/next/route-handler.js +38 -0
  51. package/dist-cjs/ops/audit-logger.js +51 -0
  52. package/dist-cjs/ops/health.js +71 -0
  53. package/dist-cjs/ops/metrics.js +131 -0
  54. package/dist-cjs/ops/otel-tracing.js +54 -0
  55. package/dist-cjs/ops/rate-limiter.js +38 -0
  56. package/dist-cjs/optimizer/budget.js +93 -0
  57. package/dist-cjs/optimizer/cost.js +37 -0
  58. package/dist-cjs/optimizer/densifier.js +84 -0
  59. package/dist-cjs/optimizer/index.js +65 -0
  60. package/dist-cjs/package.json +3 -0
  61. package/dist-cjs/pipeline/pipeline.js +111 -0
  62. package/dist-cjs/pipeline/types.js +2 -0
  63. package/dist-cjs/providers/anthropic.js +253 -0
  64. package/dist-cjs/providers/azure-openai.js +30 -0
  65. package/dist-cjs/providers/base.js +172 -0
  66. package/dist-cjs/providers/cohere.js +123 -0
  67. package/dist-cjs/providers/deepseek.js +18 -0
  68. package/dist-cjs/providers/errors.js +141 -0
  69. package/dist-cjs/providers/google.js +251 -0
  70. package/dist-cjs/providers/groq.js +54 -0
  71. package/dist-cjs/providers/llamacpp.js +19 -0
  72. package/dist-cjs/providers/lmstudio.js +19 -0
  73. package/dist-cjs/providers/mistral.js +54 -0
  74. package/dist-cjs/providers/ollama.js +163 -0
  75. package/dist-cjs/providers/openai.js +474 -0
  76. package/dist-cjs/providers/openrouter.js +23 -0
  77. package/dist-cjs/providers/type-guards.js +43 -0
  78. package/dist-cjs/rag/file-ingestion.js +60 -0
  79. package/dist-cjs/rag/ingestion.js +52 -0
  80. package/dist-cjs/realtime/conversation.js +414 -0
  81. package/dist-cjs/realtime/errors.js +79 -0
  82. package/dist-cjs/realtime/events.js +43 -0
  83. package/dist-cjs/realtime/id.js +12 -0
  84. package/dist-cjs/realtime/index.js +41 -0
  85. package/dist-cjs/realtime/mock.transport.js +173 -0
  86. package/dist-cjs/realtime/openai-events.js +409 -0
  87. package/dist-cjs/realtime/openai-server.js +143 -0
  88. package/dist-cjs/realtime/openai-webrtc.transport.js +875 -0
  89. package/dist-cjs/realtime/openai-websocket.transport.js +424 -0
  90. package/dist-cjs/realtime/provider.js +64 -0
  91. package/dist-cjs/realtime/session.js +967 -0
  92. package/dist-cjs/realtime/tools.js +364 -0
  93. package/dist-cjs/realtime/transport-utils.js +259 -0
  94. package/dist-cjs/realtime/types.js +2 -0
  95. package/dist-cjs/router/auto-router.js +255 -0
  96. package/dist-cjs/router/failover.js +382 -0
  97. package/dist-cjs/router/index.js +36 -0
  98. package/dist-cjs/router/rules-router.js +72 -0
  99. package/dist-cjs/router/types.js +2 -0
  100. package/dist-cjs/security/index.js +223 -0
  101. package/dist-cjs/security/injection-calibration.js +56 -0
  102. package/dist-cjs/security/injection-detector.js +84 -0
  103. package/dist-cjs/security/input-guard.js +127 -0
  104. package/dist-cjs/security/output-guard.js +238 -0
  105. package/dist-cjs/security/pii-detector.js +70 -0
  106. package/dist-cjs/security/policies.js +65 -0
  107. package/dist-cjs/security/prompt-hardening.js +22 -0
  108. package/dist-cjs/security/schema-validator.js +69 -0
  109. package/dist-cjs/security/semantic-injection-classifier.js +79 -0
  110. package/dist-cjs/security/upload-scanner.js +79 -0
  111. package/dist-cjs/telephony/errors.js +21 -0
  112. package/dist-cjs/telephony/index.js +13 -0
  113. package/dist-cjs/telephony/manager.js +141 -0
  114. package/dist-cjs/telephony/providers/twilio.js +435 -0
  115. package/dist-cjs/telephony/realtime-bridge.js +184 -0
  116. package/dist-cjs/telephony/twiml.js +90 -0
  117. package/dist-cjs/testing/image-provider-conformance.js +328 -0
  118. package/dist-cjs/testing/provider-conformance.js +158 -0
  119. package/dist-cjs/types/agent.js +2 -0
  120. package/dist-cjs/types/config.js +2 -0
  121. package/dist-cjs/types/context-window.js +2 -0
  122. package/dist-cjs/types/images.js +2 -0
  123. package/dist-cjs/types/messages.js +2 -0
  124. package/dist-cjs/types/optimizer.js +2 -0
  125. package/dist-cjs/types/planning.js +2 -0
  126. package/dist-cjs/types/providers.js +604 -0
  127. package/dist-cjs/types/response.js +2 -0
  128. package/dist-cjs/types/security.js +2 -0
  129. package/dist-cjs/types/telephony.js +2 -0
  130. package/dist-cjs/types/voice.js +2 -0
  131. package/dist-cjs/utils/ids.js +7 -0
  132. package/dist-cjs/utils/logger.js +71 -0
  133. package/dist-cjs/utils/tokenizer.js +45 -0
  134. package/dist-cjs/voice/errors.js +21 -0
  135. package/dist-cjs/voice/index.js +10 -0
  136. package/dist-cjs/voice/manager.js +110 -0
  137. package/dist-cjs/voice/providers/openai.js +287 -0
  138. package/dist-cjs/voice/session.js +230 -0
  139. package/dist-cjs/workflow/chains.js +135 -0
  140. package/dist-cjs/workflow/domain.js +118 -0
  141. package/package.json +121 -59
package/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.2.0] - 2026-08-10
8
+
9
+ ### Added
10
+
11
+ - A `telephony/realtime-bridge` subpath joining a provider media stream to a realtime session:
12
+ inbound-only caller audio forwarding, outbound audio framing in emission order, barge-in that clears
13
+ provider-queued audio before cancelling the response, playback marks that resolve
14
+ `markAudioPlayed()`, stream lifecycle, and custom stream parameters for tenant routing.
15
+ - `twilioRealtimeAudioOptions()` pinning both directions to 8 kHz G.711 mu-law so telephony audio
16
+ reaches the model without transcoding.
17
+ - Call control and usage metering on `TelephonyProvider` and the Twilio adapter: `getCall`, `endCall`,
18
+ and `parseStatusCallback`, surfaced on `NexusAI` as `getCall`, `endCall`, and
19
+ `parseTelephonyStatusCallback`. Status callbacks carry the authoritative billable call duration.
20
+ - Phone-number management on `TelephonyProvider` and the Twilio adapter: `listPhoneNumbers` and
21
+ `updatePhoneNumber`, so an app can point a number at its own voice webhook and status callback.
22
+
23
+ ### Changed
24
+
25
+ - The package now ships parallel ESM and CommonJS builds. Every export subpath gained a `require`
26
+ condition, so NestJS and other `"module": "commonjs"` consumers can load it without a dynamic-import
27
+ shim. Types stay shared between both builds, and the packed-size guards were raised to account for the
28
+ duplicated JS payload.
29
+
30
+ ## [1.1.0] - 2026-07-28
31
+
7
32
  ### Added
8
33
 
9
34
  - An experimental provider-neutral image operation family with portable asset locations,
package/README.md CHANGED
@@ -15,7 +15,9 @@ Use the whole pipeline for production AI features, or turn pieces off when you o
15
15
  npm install nexus-ai-pro
16
16
  ```
17
17
 
18
- Requires Node.js 22 or newer. `nexus-ai-pro` is ESM-only: use `import` from an ES module rather than CommonJS `require()`.
18
+ Requires Node.js 22 or newer. The package is authored as ESM and ships both an ESM and a CommonJS build,
19
+ so `import` and `require()` both work — including from NestJS and other apps compiled with
20
+ `"module": "commonjs"`. Types are shared between both builds.
19
21
  The main Nexus pipeline is Node-oriented; the isolated realtime subpaths use portable structural
20
22
  interfaces and can be bundled for modern browsers.
21
23
 
@@ -791,6 +793,80 @@ const call = await ai.createCall({
791
793
 
792
794
  For smaller apps, skip Twilio entirely and register your own `TelephonyProvider`.
793
795
 
796
+ ### Phone Agents: Bridging a Call to a Realtime Session
797
+
798
+ A phone agent needs the telephony media stream and a realtime session joined together. The bridge owns
799
+ that audio path — caller audio in, assistant audio out, barge-in, playback marks, and stream lifecycle —
800
+ while session configuration, tools, and authorization stay in application code.
801
+
802
+ ```ts
803
+ import { createTelephonyRealtimeBridge, twilioRealtimeAudioOptions } from 'nexus-ai-pro/telephony/realtime-bridge';
804
+ import { OpenAIWebSocketTransport } from 'nexus-ai-pro/realtime/openai-websocket';
805
+ import { createRealtimeSession } from 'nexus-ai-pro/realtime/session';
806
+
807
+ // One socket per call, from your WebSocket server.
808
+ function handleCallSocket(socket: AppWebSocket) {
809
+ const session = createRealtimeSession({
810
+ provider: 'openai',
811
+ model: 'gpt-realtime',
812
+ transport: new OpenAIWebSocketTransport({ apiKey: process.env.OPENAI_API_KEY!, webSocketFactory }),
813
+ modalities: ['audio'],
814
+ instructions: 'You are a scheduling assistant.',
815
+ tools: [checkAvailability, createBooking],
816
+ // Telephony audio is 8 kHz G.711 mu-law; this passes it through untranscoded.
817
+ audio: twilioRealtimeAudioOptions({ output: { voice: 'alloy' } }),
818
+ });
819
+
820
+ const bridge = createTelephonyRealtimeBridge({
821
+ session,
822
+ telephony,
823
+ send: (message) => socket.send(message.body),
824
+ onStart: (event) => logger.info(`call ${event.callId} started for ${event.parameters?.tenant}`),
825
+ onStop: () => persistTranscript(session.export('json')),
826
+ onError: (error) => logger.error(error),
827
+ });
828
+
829
+ socket.on('message', (raw) => bridge.handleMessage(String(raw)));
830
+ socket.on('close', () => bridge.close());
831
+ }
832
+ ```
833
+
834
+ The bridge feeds only inbound caller audio to the model, so an echoed outbound track never loops back.
835
+ On caller speech it clears audio already queued at the provider and cancels the in-flight response, which
836
+ is what stops the assistant talking over an interruption. Custom `<Parameter>` values from the stream
837
+ instruction are exposed as `bridge.parameters` for tenant and worker routing.
838
+
839
+ Pass `bargeIn: false` to keep queued audio playing, `autoConnect: false` to connect the session yourself,
840
+ and `await bridge.flush()` when you need outstanding sends to settle.
841
+
842
+ ### Call Control and Usage Metering
843
+
844
+ Meter billable time from the provider's own record rather than from stream lifecycle events, which do not
845
+ account for ring time or provider-side teardown:
846
+
847
+ ```ts
848
+ // From your status webhook.
849
+ const status = ai.parseTelephonyStatusCallback('twilio', requestBody);
850
+ if (status?.status === 'completed') {
851
+ await usage.recordCallMinutes({ callId: status.callId, seconds: status.durationSeconds ?? 0 });
852
+ }
853
+
854
+ // Or read it directly, and hang up from a tool.
855
+ const details = await ai.getCall({ callId });
856
+ await ai.endCall({ callId });
857
+ ```
858
+
859
+ ### Pointing a Number at Your Webhook
860
+
861
+ ```ts
862
+ const [number] = await ai.listPhoneNumbers({ phoneNumber: '+15557650000' });
863
+ await ai.updatePhoneNumber({
864
+ id: number.id,
865
+ voiceUrl: 'https://api.example.com/voice/incoming',
866
+ statusCallbackUrl: 'https://api.example.com/voice/status',
867
+ });
868
+ ```
869
+
794
870
  ## Security
795
871
 
796
872
  Security is enabled by default with `standard` mode.
@@ -955,9 +1031,12 @@ import { MockImageProvider } from 'nexus-ai-pro/images/mock';
955
1031
  import { OpenAIImageProvider } from 'nexus-ai-pro/images/openai';
956
1032
  import { createRealtimeSession } from 'nexus-ai-pro/realtime/session';
957
1033
  import { OpenAIWebRTCTransport } from 'nexus-ai-pro/realtime/openai-webrtc';
1034
+ import { TelephonyManager } from 'nexus-ai-pro/telephony';
1035
+ import { TwilioTelephonyProvider } from 'nexus-ai-pro/telephony/twilio';
1036
+ import { createTelephonyRealtimeBridge } from 'nexus-ai-pro/telephony/realtime-bridge';
958
1037
  ```
959
1038
 
960
- Provider SDKs are optional peer dependencies. The package is ESM-only, supports Node.js 22+, and is marked with `sideEffects: false`. Only the entry points listed in the package export map are public; deep imports into `dist` or `src` are unsupported.
1039
+ Provider SDKs are optional peer dependencies. The package ships ESM and CommonJS builds, supports Node.js 22+, and is marked with `sideEffects: false`. Only the entry points listed in the package export map are public; deep imports into `dist`, `dist-cjs`, or `src` are unsupported.
961
1040
 
962
1041
  ## Examples
963
1042
 
@@ -5,7 +5,7 @@ import type { NexusResponse, NexusStream } from '../types/response.js';
5
5
  import type { AgentConfig, AgentResult } from '../types/agent.js';
6
6
  import type { SpeechRequest, SpeechResponse, TranscriptionRequest, TranscriptionResponse, VoiceProvider, VoiceSessionConfig, VoiceTurnRequest, VoiceTurnResponse } from '../types/voice.js';
7
7
  import type { ImageProvider } from '../types/images.js';
8
- import type { CreateCallRequest, CreateCallResponse, TelephonyMediaStreamEvent, TelephonyOutboundAudioMessage, TelephonyProvider, TelephonyResponseRequest, TelephonyWebhookResponse, TelephonyWebhookValidationRequest } from '../types/telephony.js';
8
+ import type { CreateCallRequest, CreateCallResponse, EndCallRequest, GetCallRequest, ListPhoneNumbersRequest, TelephonyCallDetails, TelephonyMediaStreamEvent, TelephonyOutboundAudioMessage, TelephonyPhoneNumber, TelephonyProvider, TelephonyResponseRequest, TelephonyStatusCallback, TelephonyWebhookResponse, TelephonyWebhookValidationRequest, UpdatePhoneNumberRequest } from '../types/telephony.js';
9
9
  import type { PipelineStep } from '../pipeline/types.js';
10
10
  import type { BaseProvider } from '../providers/base.js';
11
11
  import { type VoiceSession } from '../voice/index.js';
@@ -108,6 +108,27 @@ export declare class NexusAI {
108
108
  event?: 'media' | 'mark' | 'clear';
109
109
  markName?: string;
110
110
  }): TelephonyOutboundAudioMessage;
111
+ /**
112
+ * Reads a call's current provider-side state, including the duration usage metering bills on.
113
+ */
114
+ getCall(request: GetCallRequest): Promise<TelephonyCallDetails>;
115
+ /**
116
+ * Hangs up a live call through the provider's call-control API.
117
+ */
118
+ endCall(request: EndCallRequest): Promise<TelephonyCallDetails>;
119
+ /**
120
+ * Parses a provider status webhook into a normalized record. Prefer this over media-stream
121
+ * lifecycle events when metering usage.
122
+ */
123
+ parseTelephonyStatusCallback(providerName: string, body: string | URLSearchParams | Record<string, string | number | boolean | undefined>): TelephonyStatusCallback | undefined;
124
+ /**
125
+ * Lists phone numbers owned by the provider account.
126
+ */
127
+ listPhoneNumbers(request?: ListPhoneNumbersRequest): Promise<TelephonyPhoneNumber[]>;
128
+ /**
129
+ * Points a phone number at a voice webhook and/or status callback.
130
+ */
131
+ updatePhoneNumber(request: UpdatePhoneNumberRequest): Promise<TelephonyPhoneNumber>;
111
132
  /**
112
133
  * Registers a custom text provider at runtime.
113
134
  */
@@ -443,6 +443,37 @@ export class NexusAI {
443
443
  formatTelephonyAudioMessage(providerName, streamId, payload, options) {
444
444
  return this.telephonyManager.formatAudioMessage(providerName, streamId, payload, options);
445
445
  }
446
+ /**
447
+ * Reads a call's current provider-side state, including the duration usage metering bills on.
448
+ */
449
+ async getCall(request) {
450
+ return this.telephonyManager.getCall(request);
451
+ }
452
+ /**
453
+ * Hangs up a live call through the provider's call-control API.
454
+ */
455
+ async endCall(request) {
456
+ return this.telephonyManager.endCall(request);
457
+ }
458
+ /**
459
+ * Parses a provider status webhook into a normalized record. Prefer this over media-stream
460
+ * lifecycle events when metering usage.
461
+ */
462
+ parseTelephonyStatusCallback(providerName, body) {
463
+ return this.telephonyManager.parseStatusCallback(providerName, body);
464
+ }
465
+ /**
466
+ * Lists phone numbers owned by the provider account.
467
+ */
468
+ async listPhoneNumbers(request) {
469
+ return this.telephonyManager.listPhoneNumbers(request);
470
+ }
471
+ /**
472
+ * Points a phone number at a voice webhook and/or status callback.
473
+ */
474
+ async updatePhoneNumber(request) {
475
+ return this.telephonyManager.updatePhoneNumber(request);
476
+ }
446
477
  /**
447
478
  * Registers a custom text provider at runtime.
448
479
  */
@@ -1,4 +1,6 @@
1
1
  export { TelephonyManager } from './manager.js';
2
2
  export { TelephonyProviderError, TelephonyCapabilityError } from './errors.js';
3
3
  export { createVoiceTwiML } from './twiml.js';
4
- export type { CreateCallRequest, CreateCallResponse, TelephonyAudioEncoding, TelephonyCallDirection, TelephonyCallStatus, TelephonyConfig, TelephonyConnectedEvent, TelephonyDtmfEvent, TelephonyGatherConfig, TelephonyHttpMethod, TelephonyMarkEvent, TelephonyMediaEvent, TelephonyMediaStreamEvent, TelephonyOutboundAudioMessage, TelephonyProvider, TelephonyProviderInfo, TelephonyResponseRequest, TelephonyStartEvent, TelephonyStopEvent, TelephonyStreamConfig, TelephonyStreamMode, TelephonyStreamTrack, TelephonyWebhookResponse, TelephonyWebhookValidationRequest, } from '../types/telephony.js';
4
+ export { createTelephonyRealtimeBridge, twilioRealtimeAudioOptions } from './realtime-bridge.js';
5
+ export type { TelephonyRealtimeBridge, TelephonyRealtimeBridgeOptions } from './realtime-bridge.js';
6
+ export type { CreateCallRequest, CreateCallResponse, EndCallRequest, GetCallRequest, ListPhoneNumbersRequest, TelephonyAudioEncoding, TelephonyCallDetails, TelephonyCallDirection, TelephonyCallStatus, TelephonyConfig, TelephonyConnectedEvent, TelephonyDtmfEvent, TelephonyGatherConfig, TelephonyHttpMethod, TelephonyMarkEvent, TelephonyMediaEvent, TelephonyMediaStreamEvent, TelephonyOutboundAudioMessage, TelephonyPhoneNumber, TelephonyProvider, TelephonyProviderInfo, TelephonyResponseRequest, TelephonyStartEvent, TelephonyStatusCallback, TelephonyStopEvent, TelephonyStreamConfig, TelephonyStreamMode, TelephonyStreamTrack, TelephonyWebhookResponse, TelephonyWebhookValidationRequest, UpdatePhoneNumberRequest, } from '../types/telephony.js';
@@ -1,3 +1,4 @@
1
1
  export { TelephonyManager } from './manager.js';
2
2
  export { TelephonyProviderError, TelephonyCapabilityError } from './errors.js';
3
3
  export { createVoiceTwiML } from './twiml.js';
4
+ export { createTelephonyRealtimeBridge, twilioRealtimeAudioOptions } from './realtime-bridge.js';
@@ -1,4 +1,4 @@
1
- import type { CreateCallRequest, CreateCallResponse, TelephonyConfig, TelephonyMediaStreamEvent, TelephonyOutboundAudioMessage, TelephonyProvider, TelephonyResponseRequest, TelephonyWebhookResponse, TelephonyWebhookValidationRequest } from '../types/telephony.js';
1
+ import type { CreateCallRequest, CreateCallResponse, EndCallRequest, GetCallRequest, ListPhoneNumbersRequest, TelephonyCallDetails, TelephonyConfig, TelephonyMediaStreamEvent, TelephonyOutboundAudioMessage, TelephonyPhoneNumber, TelephonyProvider, TelephonyResponseRequest, TelephonyStatusCallback, TelephonyWebhookResponse, TelephonyWebhookValidationRequest, UpdatePhoneNumberRequest } from '../types/telephony.js';
2
2
  export declare class TelephonyManager {
3
3
  private config;
4
4
  private providers;
@@ -9,6 +9,11 @@ export declare class TelephonyManager {
9
9
  createCall(request: CreateCallRequest): Promise<CreateCallResponse>;
10
10
  createWebhookResponse(request: TelephonyResponseRequest): Promise<TelephonyWebhookResponse>;
11
11
  validateWebhook(request: TelephonyWebhookValidationRequest): Promise<boolean>;
12
+ getCall(request: GetCallRequest): Promise<TelephonyCallDetails>;
13
+ endCall(request: EndCallRequest): Promise<TelephonyCallDetails>;
14
+ parseStatusCallback(providerName: string, body: string | URLSearchParams | Record<string, string | number | boolean | undefined>): TelephonyStatusCallback | undefined;
15
+ listPhoneNumbers(request?: ListPhoneNumbersRequest): Promise<TelephonyPhoneNumber[]>;
16
+ updatePhoneNumber(request: UpdatePhoneNumberRequest): Promise<TelephonyPhoneNumber>;
12
17
  parseMediaStreamEvent(providerName: string, message: string | Record<string, unknown>): TelephonyMediaStreamEvent | undefined;
13
18
  formatAudioMessage(providerName: string, streamId: string, payload: string, options?: {
14
19
  event?: 'media' | 'mark' | 'clear';
@@ -50,6 +50,65 @@ export class TelephonyManager {
50
50
  throw new TelephonyCapabilityError(provider.info.name, 'webhook validation');
51
51
  return provider.validateWebhook(request);
52
52
  }
53
+ async getCall(request) {
54
+ const provider = this.resolveProvider('getCall', request.provider || this.config.defaultProvider);
55
+ if (!provider.getCall)
56
+ throw new TelephonyCapabilityError(provider.info.name, 'call lookup');
57
+ try {
58
+ return await provider.getCall(request);
59
+ }
60
+ catch (error) {
61
+ if (error instanceof TelephonyProviderError)
62
+ throw error;
63
+ throw new TelephonyProviderError(`Telephony call lookup failed for provider "${provider.info.name}"`, provider.info.name, error);
64
+ }
65
+ }
66
+ async endCall(request) {
67
+ const provider = this.resolveProvider('endCall', request.provider || this.config.defaultProvider);
68
+ if (!provider.endCall)
69
+ throw new TelephonyCapabilityError(provider.info.name, 'call control');
70
+ try {
71
+ return await provider.endCall(request);
72
+ }
73
+ catch (error) {
74
+ if (error instanceof TelephonyProviderError)
75
+ throw error;
76
+ throw new TelephonyProviderError(`Telephony call hangup failed for provider "${provider.info.name}"`, provider.info.name, error);
77
+ }
78
+ }
79
+ parseStatusCallback(providerName, body) {
80
+ const provider = this.resolveProvider('parseStatusCallback', providerName);
81
+ if (!provider.parseStatusCallback) {
82
+ throw new TelephonyCapabilityError(provider.info.name, 'status callback parsing');
83
+ }
84
+ return provider.parseStatusCallback(body);
85
+ }
86
+ async listPhoneNumbers(request = {}) {
87
+ const provider = this.resolveProvider('listPhoneNumbers', request.provider || this.config.defaultProvider);
88
+ if (!provider.listPhoneNumbers)
89
+ throw new TelephonyCapabilityError(provider.info.name, 'phone number listing');
90
+ try {
91
+ return await provider.listPhoneNumbers(request);
92
+ }
93
+ catch (error) {
94
+ if (error instanceof TelephonyProviderError)
95
+ throw error;
96
+ throw new TelephonyProviderError(`Telephony phone number listing failed for provider "${provider.info.name}"`, provider.info.name, error);
97
+ }
98
+ }
99
+ async updatePhoneNumber(request) {
100
+ const provider = this.resolveProvider('updatePhoneNumber', request.provider || this.config.defaultProvider);
101
+ if (!provider.updatePhoneNumber)
102
+ throw new TelephonyCapabilityError(provider.info.name, 'phone number updates');
103
+ try {
104
+ return await provider.updatePhoneNumber(request);
105
+ }
106
+ catch (error) {
107
+ if (error instanceof TelephonyProviderError)
108
+ throw error;
109
+ throw new TelephonyProviderError(`Telephony phone number update failed for provider "${provider.info.name}"`, provider.info.name, error);
110
+ }
111
+ }
53
112
  parseMediaStreamEvent(providerName, message) {
54
113
  const provider = this.resolveProvider('parseMediaStreamEvent', providerName);
55
114
  if (!provider.parseMediaStreamEvent)
@@ -1,4 +1,4 @@
1
- import type { CreateCallRequest, CreateCallResponse, TelephonyMediaStreamEvent, TelephonyOutboundAudioMessage, TelephonyProvider, TelephonyProviderInfo, TelephonyResponseRequest, TelephonyWebhookResponse, TelephonyWebhookValidationRequest } from '../../types/telephony.js';
1
+ import type { CreateCallRequest, CreateCallResponse, EndCallRequest, GetCallRequest, ListPhoneNumbersRequest, TelephonyCallDetails, TelephonyMediaStreamEvent, TelephonyOutboundAudioMessage, TelephonyPhoneNumber, TelephonyProvider, TelephonyProviderInfo, TelephonyResponseRequest, TelephonyStatusCallback, TelephonyWebhookResponse, TelephonyWebhookValidationRequest, UpdatePhoneNumberRequest } from '../../types/telephony.js';
2
2
  export interface TwilioTelephonyProviderConfig {
3
3
  accountSid?: string;
4
4
  authToken?: string;
@@ -12,11 +12,19 @@ export declare class TwilioTelephonyProvider implements TelephonyProvider {
12
12
  createCall(request: CreateCallRequest): Promise<CreateCallResponse>;
13
13
  createWebhookResponse(request: TelephonyResponseRequest): Promise<TelephonyWebhookResponse>;
14
14
  validateWebhook(request: TelephonyWebhookValidationRequest): boolean;
15
+ getCall(request: GetCallRequest): Promise<TelephonyCallDetails>;
16
+ endCall(request: EndCallRequest): Promise<TelephonyCallDetails>;
17
+ parseStatusCallback(body: string | URLSearchParams | Record<string, string | number | boolean | undefined>): TelephonyStatusCallback | undefined;
18
+ listPhoneNumbers(request?: ListPhoneNumbersRequest): Promise<TelephonyPhoneNumber[]>;
19
+ updatePhoneNumber(request: UpdatePhoneNumberRequest): Promise<TelephonyPhoneNumber>;
15
20
  parseMediaStreamEvent(message: string | Record<string, unknown>): TelephonyMediaStreamEvent | undefined;
16
21
  formatAudioMessage(streamId: string, payload: string, options?: {
17
22
  event?: 'media' | 'mark' | 'clear';
18
23
  markName?: string;
19
24
  }): TelephonyOutboundAudioMessage;
25
+ private request;
26
+ private toCallDetails;
27
+ private toPhoneNumber;
20
28
  private requireConfig;
21
29
  private fetchImpl;
22
30
  private baseUrl;
@@ -12,14 +12,14 @@ export class TwilioTelephonyProvider {
12
12
  mediaStreams: true,
13
13
  bidirectionalStreams: true,
14
14
  webhookValidation: true,
15
+ callControl: true,
16
+ phoneNumbers: true,
15
17
  },
16
18
  };
17
19
  constructor(config = {}) {
18
20
  this.config = config;
19
21
  }
20
22
  async createCall(request) {
21
- const accountSid = this.requireConfig('accountSid');
22
- const authToken = this.requireConfig('authToken');
23
23
  const body = new URLSearchParams();
24
24
  body.set('To', request.to);
25
25
  body.set('From', request.from);
@@ -49,20 +49,12 @@ export class TwilioTelephonyProvider {
49
49
  if (!body.has('Url') && !body.has('Twiml') && !body.has('ApplicationSid')) {
50
50
  throw new TelephonyProviderError('Twilio calls require webhookUrl, twiml, mediaStreamUrl, or applicationSid', 'twilio');
51
51
  }
52
- const response = await this.fetchImpl()(`${this.baseUrl()}/Accounts/${accountSid}/Calls.json`, {
52
+ const raw = await this.request('/Calls.json', {
53
53
  method: 'POST',
54
54
  body,
55
- headers: {
56
- authorization: `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString('base64')}`,
57
- 'content-type': 'application/x-www-form-urlencoded',
58
- },
59
55
  signal: request.signal,
56
+ errorPrefix: 'Twilio call request failed',
60
57
  });
61
- if (!response.ok) {
62
- const message = await response.text().catch(() => response.statusText);
63
- throw new TelephonyProviderError(`Twilio call request failed: ${response.status} ${message}`, 'twilio');
64
- }
65
- const raw = (await response.json());
66
58
  return {
67
59
  callId: stringValue(raw.sid) || '',
68
60
  providerUsed: 'twilio',
@@ -98,6 +90,85 @@ export class TwilioTelephonyProvider {
98
90
  const expected = createHmac('sha1', token).update(payload).digest('base64');
99
91
  return safeEqual(signature, expected);
100
92
  }
93
+ async getCall(request) {
94
+ const raw = await this.request(`/Calls/${encodeURIComponent(request.callId)}.json`, {
95
+ method: 'GET',
96
+ signal: request.signal,
97
+ errorPrefix: 'Twilio call lookup failed',
98
+ });
99
+ return this.toCallDetails(raw, request.callId);
100
+ }
101
+ async endCall(request) {
102
+ const body = new URLSearchParams();
103
+ body.set('Status', request.status || 'completed');
104
+ const raw = await this.request(`/Calls/${encodeURIComponent(request.callId)}.json`, {
105
+ method: 'POST',
106
+ body,
107
+ signal: request.signal,
108
+ errorPrefix: 'Twilio call hangup failed',
109
+ });
110
+ return this.toCallDetails(raw, request.callId);
111
+ }
112
+ parseStatusCallback(body) {
113
+ const params = toParamRecord(body);
114
+ const callId = params.CallSid || params.callSid || params.sid;
115
+ if (!callId)
116
+ return undefined;
117
+ // Twilio sends CallDuration on completed-call callbacks and Duration on recording callbacks.
118
+ const durationSeconds = numberValue(params.CallDuration ?? params.Duration ?? params.duration);
119
+ return {
120
+ providerUsed: 'twilio',
121
+ callId,
122
+ status: normalizeStatus(params.CallStatus || params.status),
123
+ direction: normalizeDirection(params.Direction || params.direction),
124
+ from: params.From || params.from,
125
+ to: params.To || params.to,
126
+ durationSeconds,
127
+ // Twilio only sets SipResponseCode/ErrorCode when a call failed to complete normally.
128
+ endedReason: params.ErrorCode || params.SipResponseCode || undefined,
129
+ raw: params,
130
+ };
131
+ }
132
+ async listPhoneNumbers(request = {}) {
133
+ const query = new URLSearchParams();
134
+ if (request.phoneNumber)
135
+ query.set('PhoneNumber', request.phoneNumber);
136
+ if (request.pageSize !== undefined)
137
+ query.set('PageSize', String(request.pageSize));
138
+ const suffix = query.size ? `?${query.toString()}` : '';
139
+ const raw = await this.request(`/IncomingPhoneNumbers.json${suffix}`, {
140
+ method: 'GET',
141
+ signal: request.signal,
142
+ errorPrefix: 'Twilio phone number lookup failed',
143
+ });
144
+ const list = raw.incoming_phone_numbers;
145
+ if (!Array.isArray(list))
146
+ return [];
147
+ return list.map((entry) => this.toPhoneNumber(objectValue(entry)));
148
+ }
149
+ async updatePhoneNumber(request) {
150
+ const body = new URLSearchParams();
151
+ if (request.friendlyName !== undefined)
152
+ body.set('FriendlyName', request.friendlyName);
153
+ if (request.voiceUrl !== undefined)
154
+ body.set('VoiceUrl', request.voiceUrl);
155
+ if (request.voiceMethod !== undefined)
156
+ body.set('VoiceMethod', request.voiceMethod);
157
+ if (request.statusCallbackUrl !== undefined)
158
+ body.set('StatusCallback', request.statusCallbackUrl);
159
+ if (request.statusCallbackMethod !== undefined)
160
+ body.set('StatusCallbackMethod', request.statusCallbackMethod);
161
+ if (!body.size) {
162
+ throw new TelephonyProviderError('Twilio phone number update requires at least one field to change', 'twilio');
163
+ }
164
+ const raw = await this.request(`/IncomingPhoneNumbers/${encodeURIComponent(request.id)}.json`, {
165
+ method: 'POST',
166
+ body,
167
+ signal: request.signal,
168
+ errorPrefix: 'Twilio phone number update failed',
169
+ });
170
+ return this.toPhoneNumber(raw);
171
+ }
101
172
  parseMediaStreamEvent(message) {
102
173
  const data = typeof message === 'string' ? JSON.parse(message) : message;
103
174
  const event = stringValue(data.event);
@@ -180,6 +251,59 @@ export class TwilioTelephonyProvider {
180
251
  : JSON.stringify({ event: 'clear', streamSid: streamId });
181
252
  return { event, streamId, body };
182
253
  }
254
+ async request(path, options) {
255
+ const accountSid = this.requireConfig('accountSid');
256
+ const authToken = this.requireConfig('authToken');
257
+ const response = await this.fetchImpl()(`${this.baseUrl()}/Accounts/${accountSid}${path}`, {
258
+ method: options.method,
259
+ body: options.body,
260
+ headers: {
261
+ authorization: `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString('base64')}`,
262
+ ...(options.body ? { 'content-type': 'application/x-www-form-urlencoded' } : {}),
263
+ },
264
+ signal: options.signal,
265
+ });
266
+ if (!response.ok) {
267
+ const message = await response.text().catch(() => response.statusText);
268
+ throw new TelephonyProviderError(`${options.errorPrefix}: ${response.status} ${message}`, 'twilio');
269
+ }
270
+ return (await response.json());
271
+ }
272
+ toCallDetails(raw, fallbackCallId) {
273
+ return {
274
+ callId: stringValue(raw.sid) || fallbackCallId,
275
+ providerUsed: 'twilio',
276
+ status: normalizeStatus(stringValue(raw.status)),
277
+ direction: normalizeDirection(stringValue(raw.direction)),
278
+ from: stringValue(raw.from),
279
+ to: stringValue(raw.to),
280
+ durationSeconds: numberValue(raw.duration),
281
+ startedAt: stringValue(raw.start_time),
282
+ endedAt: stringValue(raw.end_time),
283
+ price: numberValue(raw.price),
284
+ priceUnit: stringValue(raw.price_unit),
285
+ raw,
286
+ };
287
+ }
288
+ toPhoneNumber(raw) {
289
+ const capabilities = objectValue(raw.capabilities);
290
+ return {
291
+ id: stringValue(raw.sid) || '',
292
+ providerUsed: 'twilio',
293
+ phoneNumber: stringValue(raw.phone_number) || '',
294
+ friendlyName: stringValue(raw.friendly_name),
295
+ voiceUrl: stringValue(raw.voice_url),
296
+ voiceMethod: normalizeHttpMethod(stringValue(raw.voice_method)),
297
+ statusCallbackUrl: stringValue(raw.status_callback),
298
+ statusCallbackMethod: normalizeHttpMethod(stringValue(raw.status_callback_method)),
299
+ capabilities: {
300
+ voice: capabilities.voice === true,
301
+ sms: capabilities.sms === true,
302
+ mms: capabilities.mms === true,
303
+ },
304
+ raw,
305
+ };
306
+ }
183
307
  requireConfig(key) {
184
308
  const value = this.config[key];
185
309
  if (!value)
@@ -247,6 +371,35 @@ function normalizeStatus(status) {
247
371
  }
248
372
  return 'unknown';
249
373
  }
374
+ function normalizeDirection(direction) {
375
+ if (!direction)
376
+ return undefined;
377
+ // Twilio reports outbound calls as `outbound-api` or `outbound-dial` depending on how they started.
378
+ return direction.startsWith('outbound') ? 'outbound' : 'inbound';
379
+ }
380
+ function normalizeHttpMethod(method) {
381
+ if (!method)
382
+ return undefined;
383
+ return method.toUpperCase() === 'GET' ? 'GET' : 'POST';
384
+ }
385
+ function toParamRecord(body) {
386
+ const params = {};
387
+ if (body instanceof URLSearchParams) {
388
+ for (const [key, value] of body.entries())
389
+ params[key] = value;
390
+ }
391
+ else if (typeof body === 'string') {
392
+ for (const [key, value] of new URLSearchParams(body).entries())
393
+ params[key] = value;
394
+ }
395
+ else if (body && typeof body === 'object') {
396
+ for (const [key, value] of Object.entries(body)) {
397
+ if (value !== undefined)
398
+ params[key] = String(value);
399
+ }
400
+ }
401
+ return params;
402
+ }
250
403
  function normalizeTrack(track) {
251
404
  if (track === 'outbound' || track === 'outbound_track')
252
405
  return 'outbound';
@@ -0,0 +1,58 @@
1
+ import type { RealtimeSession } from '../realtime/session.js';
2
+ import type { RealtimeAudioOptions } from '../realtime/types.js';
3
+ import type { TelephonyDtmfEvent, TelephonyOutboundAudioMessage, TelephonyStartEvent, TelephonyStopEvent } from '../types/telephony.js';
4
+ import type { TelephonyManager } from './manager.js';
5
+ /**
6
+ * Audio settings that match what Twilio Media Streams actually send and accept: 8 kHz G.711 µ-law.
7
+ *
8
+ * Passing these to a realtime session lets the provider consume and produce telephony audio directly,
9
+ * so the bridge forwards payloads without resampling or transcoding. Overriding them means the app
10
+ * becomes responsible for converting every frame in both directions.
11
+ */
12
+ export declare function twilioRealtimeAudioOptions(overrides?: RealtimeAudioOptions): RealtimeAudioOptions;
13
+ export interface TelephonyRealtimeBridgeOptions {
14
+ /** The realtime session driving the call. The bridge does not create it, so tools and instructions stay app-owned. */
15
+ session: RealtimeSession;
16
+ telephony: TelephonyManager;
17
+ /** Registered provider name. Defaults to `twilio`. */
18
+ provider?: string;
19
+ /** Sends one framed message back over the provider's media socket. Awaited, so ordering is preserved. */
20
+ send(message: TelephonyOutboundAudioMessage): void | Promise<void>;
21
+ /** Connect the session on the provider's `start` event. Defaults to true. */
22
+ autoConnect?: boolean;
23
+ /**
24
+ * On caller speech, discard audio already buffered at the provider and cancel the in-flight response.
25
+ * Defaults to true. Without it the assistant keeps talking over the caller for as long as the
26
+ * provider has audio queued.
27
+ */
28
+ bargeIn?: boolean;
29
+ /** Disconnect the session when the provider's stream stops. Defaults to true. */
30
+ autoDisconnect?: boolean;
31
+ onStart?(event: TelephonyStartEvent): void | Promise<void>;
32
+ onStop?(event: TelephonyStopEvent): void | Promise<void>;
33
+ onDtmf?(event: TelephonyDtmfEvent): void | Promise<void>;
34
+ /** Receives errors from message handling and outbound sends, which are never thrown at the socket. */
35
+ onError?(error: unknown): void;
36
+ }
37
+ export interface TelephonyRealtimeBridge {
38
+ /** Provider call identifier, available once the stream has started. */
39
+ readonly callId: string | undefined;
40
+ readonly streamId: string | undefined;
41
+ /** Custom `<Parameter>` values from the provider's stream instruction. */
42
+ readonly parameters: Record<string, string>;
43
+ readonly closed: boolean;
44
+ /** Feeds one raw media-socket message through the bridge. Safe to call before `start`. */
45
+ handleMessage(message: string | Record<string, unknown>): Promise<void>;
46
+ /** Resolves once every queued outbound message has been sent. Send failures surface via `onError`. */
47
+ flush(): Promise<void>;
48
+ /** Detaches session listeners and, unless disabled, disconnects the session. Idempotent. */
49
+ close(): Promise<void>;
50
+ }
51
+ /**
52
+ * Connects a provider media stream to a realtime session: caller audio in, assistant audio out,
53
+ * barge-in, playback marks, and stream lifecycle.
54
+ *
55
+ * The bridge owns only the audio path. Session configuration, tools, authorization, and any
56
+ * post-call persistence stay with the application.
57
+ */
58
+ export declare function createTelephonyRealtimeBridge(options: TelephonyRealtimeBridgeOptions): TelephonyRealtimeBridge;