@voctiv/agent-sdk 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,18 +70,25 @@ export default defineScript(async ({ channel, logger, context }) => {
70
70
  });
71
71
  ```
72
72
 
73
+
74
+
73
75
  ## Examples
74
76
 
75
- The [`examples/`](./examples/) folder contains copy-paste-ready scripts:
77
+ The `[examples/](./examples/)` folder contains copy-paste-ready scripts:
78
+
79
+
80
+ | Example | Description |
81
+ | --------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
82
+ | [outbound-with-recall.ts](./examples/outbound-with-recall.ts) | Outbound with `recallCount` / `recallDelay` (automatic redial on failure) |
83
+ | [recall-routing-by-attempt.ts](./examples/recall-routing-by-attempt.ts) | Online recall: branch on `context.attempt` (`getScriptPhase` → `'online'`) |
84
+ | [schedule-call-with-defaults.ts](./examples/schedule-call-with-defaults.ts) | `platform.call()` without explicit recall — CMS defaults from `context` |
85
+ | [after-call-continuation.ts](./examples/after-call-continuation.ts) | `onSuccessCall` / `onFailedCall` vs recall (mutually exclusive) |
86
+ | [read-recall-from-params.ts](./examples/read-recall-from-params.ts) | `parseRecallDelaySeconds()` / `parseRecallCount()` on legacy params |
87
+ | [custom-media-providers.ts](./examples/custom-media-providers.ts) | Host-only `mediaProviders` module (`ScriptAsrConnector` / `ScriptTtsConnector`) |
88
+ | [custom-media-providers-script.ts](./examples/custom-media-providers-script.ts) | Sandboxed `defineScript` entry that uses custom vendors (no `ws` import) |
89
+
90
+
76
91
 
77
- | Example | Description |
78
- |---------|-------------|
79
- | [outbound-with-recall.ts](./examples/outbound-with-recall.ts) | Outbound with `recallCount` / `recallDelay` (automatic redial on failure) |
80
- | [recall-routing-by-attempt.ts](./examples/recall-routing-by-attempt.ts) | Online recall: branch on `context.attempt` (`getScriptPhase` → `'online'`) |
81
- | [schedule-call-with-defaults.ts](./examples/schedule-call-with-defaults.ts) | `platform.call()` without explicit recall — CMS defaults from `context` |
82
- | [after-call-continuation.ts](./examples/after-call-continuation.ts) | `onSuccessCall` / `onFailedCall` vs recall (mutually exclusive) |
83
- | [read-recall-from-params.ts](./examples/read-recall-from-params.ts) | `parseRecallDelaySeconds()` / `parseRecallCount()` on legacy params |
84
- | [custom-media-providers.ts](./examples/custom-media-providers.ts) | `defineMediaProviders` + custom ASR/TTS via `createAsr` / `createTts` |
85
92
 
86
93
  ### Quick recall example
87
94
 
@@ -113,19 +120,25 @@ const delaySec = parseRecallDelaySeconds(context.dialogParams?.recall_delay); //
113
120
  const maxAttempts = parseRecallCount(context.dialogParams?.recall_count);
114
121
  ```
115
122
 
123
+
124
+
116
125
  ## What The SDK Contains
117
126
 
118
127
  `defineScript(fn)` marks the default export as the script entry point. It returns the same function and exists to give TypeScript the correct `ScriptContext` shape.
119
128
 
120
- `defineMediaProviders(def)` marks a named `mediaProviders` export for custom ASR/TTS factories (trusted packages only). See [Custom ASR / TTS Providers](#custom-asr--tts-providers).
129
+ `defineMediaProviders(def)` marks a named `mediaProviders` export for custom ASR/TTS factories.
130
+ Ship it in a **sibling** module (`media-providers/`), not in the sandboxed script entry — see
131
+ [Custom ASR / TTS Providers](#custom-asr--tts-providers).
121
132
 
122
133
  **Three different “context” names:**
123
134
 
124
- | Name | Meaning |
125
- | --- | --- |
126
- | `ScriptContext` | Top-level injection: `{ channel, logger, context, platform }` |
127
- | `context` (`ScriptDialogContext`) | Dialog identity, params, routing snapshot, `env$`, headless — see [Dialog Context](#dialog-context-and-persisted-env) |
128
- | `options.context` on `platform.nlu.extract` | Opaque NLU disambiguation string/JSON**not** dialog context |
135
+
136
+ | Name | Meaning |
137
+ | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
138
+ | `ScriptContext` | Top-level injection: `{ channel, logger, context, platform }` |
139
+ | `context` (`ScriptDialogContext`) | Dialog identity, params, routing snapshot, `env$`, headless see [Dialog Context](#dialog-context-and-persisted-env) |
140
+ | `options.context` on `platform.nlu.extract` | Opaque NLU disambiguation string/JSON — **not** dialog context |
141
+
129
142
 
130
143
  `ScriptContext` is the top-level object passed to a script:
131
144
 
@@ -146,6 +159,8 @@ const maxAttempts = parseRecallCount(context.dialogParams?.recall_count);
146
159
  - `channel.events` exposes speech, interrupt, termination, WS data message, and media error observables.
147
160
  - `channel.textInput` injects synthetic ASR results for tests and debug clients.
148
161
 
162
+
163
+
149
164
  ## SIP And Pre-Answer Media
150
165
 
151
166
  SIP sessions expose call state through `channel.sip.state`, `state$`, `progress$`, `early$`, and `answered$`.
@@ -157,6 +172,8 @@ The important states are:
157
172
  - `active`: final 200 OK has been received or sent.
158
173
  - `terminated`: the call ended and no more audio is possible.
159
174
 
175
+
176
+
160
177
  ### How Pre-Answer Works
161
178
 
162
179
  Pre-answer means the SIP media path is open before the call is finally answered with `200 OK`. In this state the caller can already hear TTS, the script can already receive audio for ASR, and DTMF can be exchanged.
@@ -234,14 +251,16 @@ carrier routing, diversion chains, and vendor SDP attributes.
234
251
 
235
252
  #### When to use what
236
253
 
237
- | Need | API | When |
238
- | --- | --- | --- |
239
- | Routing / identity / locale from the **inbound INVITE** (`Diversion`, `X-Trunk-Id`, `X-Neuro-UUID`, `X-language`, …) | `channel.sip.inviteSipHeaders` | Call **start** only (snapshot) |
240
- | Peer signal **during** the call (e.g. mid-call language in INFO body) | `channel.sip.sipInfo$` | After subscribe; each INFO |
241
- | DTMF digits | `channel.sip.dtmf$` | Prefer over parsing INFO |
242
- | SDP / codec / connection | `remoteSdp` / `getRemoteSdpDetails()` | May change (183 / 200 / re-INVITE) |
243
- | SIP response code / phrase (180, 486, ) | `sipSignal$` | Low-level; **no** response headers |
244
- | Live SIP headers on 200 / re-INVITE / BYE | | **Not exposed** |
254
+
255
+ | Need | API | When |
256
+ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ---------------------------------- |
257
+ | Routing / identity / locale from the **inbound INVITE** (`Diversion`, `X-Trunk-Id`, `X-Neuro-UUID`, `X-language`, ) | `channel.sip.inviteSipHeaders` | Call **start** only (snapshot) |
258
+ | Peer signal **during** the call (e.g. mid-call language in INFO body) | `channel.sip.sipInfo$` | After subscribe; each INFO |
259
+ | DTMF digits | `channel.sip.dtmf$` | Prefer over parsing INFO |
260
+ | SDP / codec / connection | `remoteSdp` / `getRemoteSdpDetails()` | May change (183 / 200 / re-INVITE) |
261
+ | SIP response code / phrase (180, 486, …) | `sipSignal$` | Low-level; **no** response headers |
262
+ | Live SIP headers on 200 / re-INVITE / BYE | — | **Not exposed** |
263
+
245
264
 
246
265
  Decision guide:
247
266
 
@@ -260,10 +279,12 @@ Outbound INVITE headers you **send** go through `platform.call({ protoAdditional
260
279
  Snapshot of SIP headers from an **inbound INVITE** at call setup. Includes standard and
261
280
  extension headers (`Diversion`, `P-Asserted-Identity`, `X-Trunk-Id`, `X-Neuro-UUID`, `X-language`, …).
262
281
 
263
- | Property | Updates during call? |
264
- | --- | --- |
282
+
283
+ | Property | Updates during call? |
284
+ | ------------------ | ---------------------------------------------------------------------------------- |
265
285
  | `inviteSipHeaders` | **No** — INVITE snapshot only; outbound B-legs / WS / headless usually `undefined` |
266
286
 
287
+
267
288
  ```ts
268
289
  const h = channel.sip.inviteSipHeaders;
269
290
  const diversion = h?.Diversion; // string | string[] when multiple hops
@@ -276,10 +297,12 @@ Header names match what the host stack exposes (case-sensitive). Duplicate heade
276
297
 
277
298
  #### Remote SDP (`remoteSdp`, `getRemoteSdpDetails()`)
278
299
 
279
- | Property / method | Updates during call? |
280
- | --- | --- |
281
- | `remoteSdp` | **Yes** — latest negotiated remote SDP (INVITE, 183, 200, re-INVITE) |
282
- | `getRemoteSdpDetails()` | **Yes** — re-parses current `remoteSdp` on each call |
300
+
301
+ | Property / method | Updates during call? |
302
+ | ----------------------- | -------------------------------------------------------------------- |
303
+ | `remoteSdp` | **Yes** — latest negotiated remote SDP (INVITE, 183, 200, re-INVITE) |
304
+ | `getRemoteSdpDetails()` | **Yes** — re-parses current `remoteSdp` on each call |
305
+
283
306
 
284
307
  ```ts
285
308
  const raw = channel.sip.remoteSdp;
@@ -309,11 +332,13 @@ Events before subscribe are not replayed.
309
332
 
310
333
  #### Relating streams to SDP
311
334
 
312
- | Stream | `sdp` field |
313
- | --- | --- |
314
- | `progress$` | Present when a 1xx response carries SDP (e.g. 183 early media) |
335
+
336
+ | Stream | `sdp` field |
337
+ | ------------ | ------------------------------------------------------------------------- |
338
+ | `progress$` | Present when a 1xx response carries SDP (e.g. 183 early media) |
315
339
  | `sipSignal$` | Present only on callbacks that include SDP; often empty on final `active` |
316
340
 
341
+
317
342
  For the **persisted** negotiated SDP, use `remoteSdp` / `getRemoteSdpDetails()`, not only
318
343
  the per-event `sdp` on `sipSignal$`.
319
344
 
@@ -340,31 +365,33 @@ unnoticed. Guard them with `context.headless` or `channel.type` when a script ru
340
365
  SIP-only unless noted. WS/headless: most methods are no-ops and `state` behaves as synthetic
341
366
  `active`, except `makeCall()` and `bridge()`, which throw.
342
367
 
343
- | Member | Description |
344
- | --- | --- |
345
- | `state` | Sync getter: `idle` \| `ringing` \| `early` \| `active` \| `holding` \| `terminated` |
346
- | `isAnswered` | `true` after 200 OK (outbound received / inbound sent via `answer()`) |
347
- | `state$` | Emits on every state transition |
348
- | `progress$` | SIP 1xx provisional responses (`SipProgressEvent`) |
349
- | `early$` | Emits once when RTP is up before final answer |
350
- | `answered$` | Emits once on 200 OK |
351
- | `dtmf$` | Remote DTMF digits (`DtmfEvent`: `digit`, `duration`) |
352
- | `sipInfo$` | Mid-call SIP INFO (`contentType` + `body` only; INFO headers not exposed) |
353
- | `sipSignal$` | Low-level stack events (`statusCode` / `statusPhrase` / optional `sdp`; no headers) |
354
- | `remoteSdp` | Latest negotiated remote SDP body; updates when new SDP arrives |
355
- | `inviteSipHeaders` | Start-of-call inbound INVITE header snapshot; does not update |
356
- | `getRemoteSdpDetails()` | Parse `remoteSdp` `ParsedSdpDetails` (session + `a=` attributes) |
357
- | `sendProgress()` | Inbound: send 183 Session Progress `early` |
358
- | `waitForEarly()` | Await `early` or `active` (Promise) |
359
- | `waitForAnswer()` | Await final 200 OK (Promise) |
360
- | `answer()` | Inbound: send final 200 OK → `active` |
361
- | `sendDtmf(digit, duration?)` | Send DTMF tone |
362
- | `sendInfo(contentType, body)` | Send SIP INFO |
363
- | `hold()` / `unhold()` | SIP hold |
364
- | `mute()` / `unmute()` | Suppress local outgoing audio |
365
- | `hangup()` | Terminate call |
366
- | `makeCall(opts)` | Outbound B-leg (`MediaChannel`); `sipUri` or `msisdn` |
367
- | `bridge(other)` | Cross-connect two SIP calls; returns teardown `() => void` |
368
+
369
+ | Member | Description |
370
+ | ----------------------------- | ----------------------------------------------------------------------------------- |
371
+ | `state` | Sync getter: `idle` | `ringing` | `early` | `active` | `holding` | `terminated` |
372
+ | `isAnswered` | `true` after 200 OK (outbound received / inbound sent via `answer()`) |
373
+ | `state$` | Emits on every state transition |
374
+ | `progress$` | SIP 1xx provisional responses (`SipProgressEvent`) |
375
+ | `early$` | Emits once when RTP is up before final answer |
376
+ | `answered$` | Emits once on 200 OK |
377
+ | `dtmf$` | Remote DTMF digits (`DtmfEvent`: `digit`, `duration`) |
378
+ | `sipInfo$` | Mid-call SIP INFO (`contentType` + `body` only; INFO headers not exposed) |
379
+ | `sipSignal$` | Low-level stack events (`statusCode` / `statusPhrase` / optional `sdp`; no headers) |
380
+ | `remoteSdp` | Latest negotiated remote SDP body; updates when new SDP arrives |
381
+ | `inviteSipHeaders` | Start-of-call inbound INVITE header snapshot; does not update |
382
+ | `getRemoteSdpDetails()` | Parse `remoteSdp` `ParsedSdpDetails` (session + `a=` attributes) |
383
+ | `sendProgress()` | Inbound: send 183 Session Progress → `early` |
384
+ | `waitForEarly()` | Await `early` or `active` (Promise) |
385
+ | `waitForAnswer()` | Await final 200 OK (Promise) |
386
+ | `answer()` | Inbound: send final 200 OK → `active` |
387
+ | `sendDtmf(digit, duration?)` | Send DTMF tone |
388
+ | `sendInfo(contentType, body)` | Send SIP INFO |
389
+ | `hold()` / `unhold()` | SIP hold |
390
+ | `mute()` / `unmute()` | Suppress local outgoing audio |
391
+ | `hangup()` | Terminate call |
392
+ | `makeCall(opts)` | Outbound B-leg (`MediaChannel`); `sipUri` or `msisdn` |
393
+ | `bridge(other)` | Cross-connect two SIP calls; returns teardown `() => void` |
394
+
368
395
 
369
396
  Prefer `dtmf$` over `sipInfo$` for DTMF. Prefer `state$` / `early$` / `answered$` over raw
370
397
  `sipSignal$` for call lifecycle. Use `remoteSdp` / `getRemoteSdpDetails()` for negotiated
@@ -523,20 +550,22 @@ const asr = await channel.createAsr({
523
550
 
524
551
  Each vendor connector accepts its native parameter names:
525
552
 
526
- | Vendor | Accepted `data` keys |
527
- | --- | --- |
528
- | **Azure** | `subscription_key` or `api_key`, `region` |
529
- | **Yandex** | `api_key` or `token`, `folder_id` |
530
- | **ElevenLabs** | `api_key` (or `xi_api_key`), `model` |
531
- | **Deepgram** | `api_key` |
532
- | **Google** | `email`, `private_key`, `project_id` |
533
- | **Whisper** | `url`, `rate`, `toFloat` |
553
+
554
+ | Vendor | Accepted `data` keys |
555
+ | -------------- | ----------------------------------------- |
556
+ | **Azure** | `subscription_key` or `api_key`, `region` |
557
+ | **Yandex** | `api_key` or `token`, `folder_id` |
558
+ | **ElevenLabs** | `api_key` (or `xi_api_key`), `model` |
559
+ | **Deepgram** | `api_key` |
560
+ | **Google** | `email`, `private_key`, `project_id` |
561
+ | **Whisper** | `url`, `rate`, `toFloat` |
562
+
534
563
 
535
564
  All vendors also accept the env-style names (`AZURE_SPEECH_KEY`, `ELEVENLABS_API_KEY`, etc.) for backwards compatibility, but vendor-native names are checked first and are preferred.
536
565
 
537
566
  ### Platform ASR Key Selection
538
567
 
539
- When platform credential catalogs are enabled, select ASR keys by **`name`**:
568
+ When platform credential catalogs are enabled, select ASR keys by `name`:
540
569
 
541
570
  ```ts
542
571
  const asr = await channel.createAsr({
@@ -551,7 +580,7 @@ When both `name` (platform key) and explicit `data` are provided, `data` values
551
580
 
552
581
  ## TTS, Playback, And Mixer Queues
553
582
 
554
- All audio playback goes through **`channel.audio`** (`ChannelAudio`). There are no top-level
583
+ All audio playback goes through `channel.audio` (`ChannelAudio`). There are no top-level
555
584
  `channel.say()` / `channel.play()` shortcuts on `MediaChannel`.
556
585
 
557
586
  Create a reusable TTS session with `channel.createTts(config?)` — same pattern as `createAsr`.
@@ -615,20 +644,22 @@ terminate the Observable via **error** (`MediaError`), and are also mirrored on
615
644
  You can still use `channel.audio.say(..., { tts })` if you prefer the channel API; a matching
616
645
  pre-warmed session is also reused when vendor+config align.
617
646
 
618
- | Method | Purpose |
619
- | --- | --- |
620
- | `channel.createTts(config?)` | Pre-warm a TTS connector / streaming socket; returns `TtsHandle` |
621
- | `tts.say(textOrObservable, options?)` | Synthesize via the handle's cached connection (`Promise`) |
622
- | `tts.say$(textOrObservable, options?)` | Same path with per-utterance status events (`Observable`) |
623
- | `tts.presay(text, options?)` | Pre-synthesize into the host TTS cache via the handle |
624
- | `channel.audio.say(textOrObservable, options?)` | Synthesize text with TTS and play on a mixer queue |
625
- | `channel.audio.play(source, options?)` | Play raw audio (URL, path, or platform phrase record) |
626
- | `channel.audio.presay(text, options?)` | Pre-synthesize TTS into the host cache (no playback) |
627
- | `channel.audio.preload(source, options?)` | Decode/warm a raw audio source (no playback) |
628
- | `channel.audio.queue(index)` | Per-queue control handle (`MixerQueueControl`) |
629
- | `channel.audio.remove(alias, queue?)` | Remove one queued item by alias |
630
- | `channel.audio.stop(queue)` | Clear a queue **and** abort in-flight sentence TTS for it |
631
- | `channel.audio.stopAll()` | Clear every queue (WS clients also get an audio interrupt) |
647
+
648
+ | Method | Purpose |
649
+ | ----------------------------------------------- | ---------------------------------------------------------------- |
650
+ | `channel.createTts(config?)` | Pre-warm a TTS connector / streaming socket; returns `TtsHandle` |
651
+ | `tts.say(textOrObservable, options?)` | Synthesize via the handle's cached connection (`Promise`) |
652
+ | `tts.say$(textOrObservable, options?)` | Same path with per-utterance status events (`Observable`) |
653
+ | `tts.presay(text, options?)` | Pre-synthesize into the host TTS cache via the handle |
654
+ | `channel.audio.say(textOrObservable, options?)` | Synthesize text with TTS and play on a mixer queue |
655
+ | `channel.audio.play(source, options?)` | Play raw audio (URL, path, or platform phrase record) |
656
+ | `channel.audio.presay(text, options?)` | Pre-synthesize TTS into the host cache (no playback) |
657
+ | `channel.audio.preload(source, options?)` | Decode/warm a raw audio source (no playback) |
658
+ | `channel.audio.queue(index)` | Per-queue control handle (`MixerQueueControl`) |
659
+ | `channel.audio.remove(alias, queue?)` | Remove one queued item by alias |
660
+ | `channel.audio.stop(queue)` | Clear a queue **and** abort in-flight sentence TTS for it |
661
+ | `channel.audio.stopAll()` | Clear every queue (WS clients also get an audio interrupt) |
662
+
632
663
 
633
664
  `channel.audio.say(textOrObservable, options?)` synthesizes text and plays it through the mixer.
634
665
 
@@ -661,6 +692,8 @@ await channel.audio.play('/opt/prompts/welcome.wav', {
661
692
  });
662
693
  ```
663
694
 
695
+
696
+
664
697
  ### Pre-synthesis And Preload
665
698
 
666
699
  `channel.audio.presay(text, options?)` runs TTS ahead of time and stores PCM in the host TTS
@@ -708,26 +741,28 @@ When using an `Observable<string>` input, WS clients also receive text progress
708
741
 
709
742
  ### Mixer Queues
710
743
 
711
- The mixer has queues **`0` through `4`**. Use separate queues for main speech, earcons, hold
744
+ The mixer has queues `0` **through** `4`. Use separate queues for main speech, earcons, hold
712
745
  music, or background audio so barge-in on one queue does not cut unrelated audio.
713
746
 
714
- Obtain a per-queue handle with **`channel.audio.queue(index)`** (`MixerQueueControl`):
747
+ Obtain a per-queue handle with `channel.audio.queue(index)` (`MixerQueueControl`):
715
748
 
716
- | Member | Description |
717
- | --- | --- |
718
- | `index` | Queue index **0–4** |
719
- | `volume` | Linear gain **0.0–1.0** for the entire queue (get/set) |
720
- | `itemStarted$` | Emits item **`alias`** when playback starts |
721
- | `itemFinished$` | Emits **`alias`** when an item finishes, is removed, or is skipped by clear |
722
- | `queueEmpty$` | Emits when the queue is empty after all PCM has been mixed out |
723
- | `remove(alias)` | Drop one item on this queue |
724
- | `clear()` | Drop all items on this queue (does **not** abort in-flight TTS generation) |
725
749
 
726
- Top-level helpers on **`channel.audio`**:
750
+ | Member | Description |
751
+ | --------------- | -------------------------------------------------------------------------- |
752
+ | `index` | Queue index **0–4** |
753
+ | `volume` | Linear gain **0.0–1.0** for the entire queue (get/set) |
754
+ | `itemStarted$` | Emits item `alias` when playback starts |
755
+ | `itemFinished$` | Emits `alias` when an item finishes, is removed, or is skipped by clear |
756
+ | `queueEmpty$` | Emits when the queue is empty after all PCM has been mixed out |
757
+ | `remove(alias)` | Drop one item on this queue |
758
+ | `clear()` | Drop all items on this queue (does **not** abort in-flight TTS generation) |
759
+
727
760
 
728
- - **`remove(alias, queue?)`** — when `queue` is omitted, searches all five queues; when set, only that queue is checked.
729
- - **`stop(queue)`** — same as `clear()` **plus** aborts in-flight sentence TTS for that queue.
730
- - **`stopAll()`** — `stop()` on every queue; WS clients also receive an audio interrupt signal.
761
+ Top-level helpers on `channel.audio`:
762
+
763
+ - `remove(alias, queue?)`when `queue` is omitted, searches all five queues; when set, only that queue is checked.
764
+ - `stop(queue)` — same as `clear()` **plus** aborts in-flight sentence TTS for that queue.
765
+ - `stopAll()` — `stop()` on every queue; WS clients also receive an audio interrupt signal.
731
766
 
732
767
  ```ts
733
768
  const tts = channel.audio.queue(0);
@@ -765,34 +800,85 @@ For sentence-split TTS, queue item aliases are suffixed as `alias-0`, `alias-1`,
765
800
 
766
801
  Shared by `say()`, `play()`, and (where noted) `presay()`:
767
802
 
768
- | Field | Type | Applies to | Description |
769
- | --- | --- | --- | --- |
770
- | `queue` | `number?` | `say`, `play` | Mixer queue **0–4** (default **0**). |
771
- | `alias` | `string?` | `say`, `play` | Stable item id for `remove()` and queue events. |
772
- | `loop` | `boolean?` | `say`, `play` | Restart after finish until stopped/removed. |
773
- | `loopDelayMs` | `number?` | `say`, `play` | Silence between loop iterations. |
774
- | `volume` | `number?` | `say`, `play` | Sets **whole queue** gain **0.0–1.0** (not per-item). |
775
- | `ttsStrategy` | `TtsStrategy?` | `say`, `presay` | `sentence` \| `streaming` \| `full`. |
776
- | `ttsVendor` | `TtsVendor?` | `say`, `presay` | Override `channel.params.ttsVendor`. |
777
- | `name` | `string?` | `say`, `presay` | Platform TTS credential **`name`** (key catalog selector). |
778
- | `ttsConfig` | `Record<string, unknown>?` | `say`, `presay` | Vendor params; `name` key is stripped before send. |
779
- | `cache` | `true \| CacheOptions?` | `say`, `presay` | TTS file cache; optional platform phrase persist. |
803
+
804
+ | Field | Type | Applies to | Description |
805
+ | ------------- | -------------------------- | --------------- | ------------------------------------------------------ |
806
+ | `queue` | `number?` | `say`, `play` | Mixer queue **0–4** (default **0**). |
807
+ | `alias` | `string?` | `say`, `play` | Stable item id for `remove()` and queue events. |
808
+ | `loop` | `boolean?` | `say`, `play` | Restart after finish until stopped/removed. |
809
+ | `loopDelayMs` | `number?` | `say`, `play` | Silence between loop iterations. |
810
+ | `volume` | `number?` | `say`, `play` | Sets **whole queue** gain **0.0–1.0** (not per-item). |
811
+ | `ttsStrategy` | `TtsStrategy?` | `say`, `presay` | `sentence` | `streaming` | `full`. |
812
+ | `ttsVendor` | `TtsVendor?` | `say`, `presay` | Override `channel.params.ttsVendor`. |
813
+ | `name` | `string?` | `say`, `presay` | Platform TTS credential `name` (key catalog selector). |
814
+ | `ttsConfig` | `Record<string, unknown>?` | `say`, `presay` | Vendor params; `name` key is stripped before send. |
815
+ | `cache` | `true | CacheOptions?` | `say`, `presay` | TTS file cache; optional platform phrase persist. |
816
+
780
817
 
781
818
  `play()` ignores `tts*` and `cache` for raw audio. `preload()` only accepts `cache` overrides.
782
819
 
783
820
  ## Custom ASR / TTS Providers
784
821
 
785
- Trusted logic packages can ship their own ASR/TTS engines next to `defineScript`. The host
786
- `require`s a static **`export const mediaProviders`** **outside** the script sandbox (same
787
- process privileges as the API). Use this only for packages you trust.
822
+ Trusted logic packages can ship their own ASR/TTS engines. The host loads
823
+ `export const mediaProviders` with a normal Node `require` **outside** the script VM
824
+ (same privileges as the API). Use this only for packages you trust.
788
825
 
789
826
  Mid-script `registerAsr` / `registerTts` is **not** supported — declare vendors at module load.
790
827
 
791
- ### Export shape
828
+ ### Package layout (required for connectors that use `ws` / Node APIs)
829
+
830
+ Put providers in a **sibling module**. Do **not** re-export them from the sandboxed entry
831
+ (`dist/index.js`). The script worker evaluates the entry under a restricted VM: there is
832
+ **no** global `process`, and loading `ws` (or similar) inside that graph fails even when
833
+ `net` / `tls` are allowlisted.
834
+
835
+ Recommended layout after `tsc`:
836
+
837
+ ```text
838
+ dist/
839
+ index.js ← defineScript only (sandboxed)
840
+ media-providers/
841
+ index.js ← export const mediaProviders (host-only)
842
+ my-asr.js
843
+ my-tts.js
844
+ ```
845
+
846
+ Host resolution order for `mediaProviders`:
847
+
848
+ 1. `<entryDir>/media-providers/index.js`
849
+ 2. `<entryDir>/media-providers.js`
850
+ 3. `<scriptRoot>/media-providers/…` and `dist/media-providers/…` fallbacks
851
+ 4. the script entry itself (backward compatible — avoid for `ws`-based connectors)
852
+
853
+ Sandboxed script code may import **lightweight** helpers from the providers package
854
+ (e.g. vendor id constants) if those files do **not** `require('ws')` / touch `process`.
855
+ Never import the connector classes or `mediaProviders` index from the entry.
856
+
857
+ Examples:
858
+
859
+ - Host module: [`examples/custom-media-providers.ts`](./examples/custom-media-providers.ts)
860
+ - Script entry: [`examples/custom-media-providers-script.ts`](./examples/custom-media-providers-script.ts)
861
+
862
+ ### Contracts (implement the interfaces)
863
+
864
+ There are no abstract base classes — implement the SDK interfaces so TypeScript checks
865
+ the full surface and IDEs autocomplete correctly:
866
+
867
+ | Interface | Role |
868
+ | --- | --- |
869
+ | `ScriptAsrConnector` | Custom STT connector returned by an `asr` factory |
870
+ | `ScriptTtsConnector` | Unified batch + optional streaming TTS connector |
871
+ | `MediaConnectorContext` | `id`, flattened `config`, `dialogUuid`, `debug$` passed into factories |
872
+ | `MediaProviderShared` | Optional per-dialog object from `createShared` |
873
+
874
+ `defineMediaProviders` is an identity helper for typing. Factories must return objects that
875
+ satisfy those interfaces (classes with `implements` recommended).
876
+
877
+ ### Export shape (host module)
792
878
 
793
879
  ```ts
880
+ // media-providers/index.ts — host-only (may use ws, https, process.env, …)
794
881
  import {
795
- defineScript,
796
882
  defineMediaProviders,
797
883
  type MediaConnectorContext,
798
884
  type MediaProviderShared,
@@ -802,6 +888,9 @@ import {
802
888
  import { Subject } from 'rxjs';
803
889
  import { Readable } from 'stream';
804
890
 
891
+ class MyAsr implements ScriptAsrConnector { /* … */ }
892
+ class MyTts implements ScriptTtsConnector { /* … */ }
893
+
805
894
  export const mediaProviders = defineMediaProviders({
806
895
  // Optional: one object per dialog, shared by ASR + TTS factories
807
896
  createShared: (ctx) => ({
@@ -815,17 +904,24 @@ export const mediaProviders = defineMediaProviders({
815
904
  'my-tts': (ctx, shared) => new MyTts(ctx, shared),
816
905
  },
817
906
  });
907
+ ```
908
+
909
+ ```ts
910
+ // index.ts — sandboxed entry (do NOT import ./media-providers or ws)
911
+ import { defineScript } from '@voctiv/agent-sdk';
818
912
 
819
913
  export default defineScript(async ({ channel }) => {
914
+ // Credentials: pass via createAsr/createTts `data` (or platform/channel params).
915
+ // Do not rely on process.env here — `process` is not defined in the script VM.
820
916
  const asr = await channel.createAsr({
821
917
  vendor: 'my-asr',
822
918
  language: 'ru-RU',
823
- data: { api_key: process.env.MY_ASR_KEY! },
919
+ data: { api_key: String(channel.params.api_key ?? '') },
824
920
  });
825
921
  const tts = await channel.createTts({
826
922
  vendor: 'my-tts',
827
923
  data: {
828
- api_key: process.env.MY_TTS_KEY!,
924
+ api_key: String(channel.params.api_key ?? ''),
829
925
  voice_id: '…',
830
926
  output_format: 'pcm_16000', // preferred for telephony
831
927
  },
@@ -836,22 +932,19 @@ export default defineScript(async ({ channel }) => {
836
932
  ttsStrategy: 'streaming', // used when supportsStreaming() === true
837
933
  });
838
934
 
839
- tts.say$('One. Two.', { alias: 'reply' }).subscribe((ev) => {
935
+ tts.say$('One. Two.', { alias: 'reply' }).subscribe((_ev) => {
840
936
  // queued | speaking | done | cancelled — same events as builtin TTS
841
937
  });
842
938
 
843
- // Reuse the warm handle:
844
- await channel.audio.say('Again', { tts });
939
+ await channel.audio.say('Again', { tts }); // warm handle reuse
845
940
 
846
941
  asr.destroy();
847
942
  tts.destroy();
848
943
  });
849
944
  ```
850
945
 
851
- `defineMediaProviders` is an identity helper for typing. The host looks up
852
- `module.exports.mediaProviders` (or `default.mediaProviders`) on the script entry file.
853
-
854
- Full stub: [`examples/custom-media-providers.ts`](./examples/custom-media-providers.ts).
946
+ Host-side factories **may** read `process.env` (e.g. `ELEVENLABS_API_KEY`) because they run
947
+ outside the VM. Prefer also accepting the same keys via `ctx.config` from `data`.
855
948
 
856
949
  ### Implement ASR (`ScriptAsrConnector`)
857
950
 
@@ -925,7 +1018,10 @@ class MyTts implements ScriptTtsConnector {
925
1018
  return true; // or false for HTTP-only
926
1019
  }
927
1020
 
928
- async textToSpeechStream(rawtext: string, _ctx?: { previousText?: string; nextText?: string }) {
1021
+ async textToSpeechStream(
1022
+ rawtext: string,
1023
+ _ctx?: { previousText?: string; nextText?: string },
1024
+ ) {
929
1025
  // Return PCM (preferred) or MP3/OGG. Hint format via createTts data:
930
1026
  // output_format: 'pcm_16000' | 'mp3_…' or audioFormat: 'pcm' | 'mp3'
931
1027
  return Readable.from([/* bytes */]);
@@ -946,12 +1042,12 @@ class MyTts implements ScriptTtsConnector {
946
1042
  ### Using custom vendors in the script
947
1043
 
948
1044
  ```ts
949
- const asr = await channel.createAsr({ vendor: 'my-asr', data: { … } });
950
- const tts = await channel.createTts({ vendor: 'my-tts', data: { … } });
1045
+ const asr = await channel.createAsr({ vendor: 'my-asr', data: { /* */ } });
1046
+ const tts = await channel.createTts({ vendor: 'my-tts', data: { /* */ } });
951
1047
 
952
1048
  await tts.say('Hi', { alias: 'greet' });
953
1049
  await tts.presay('Warm cache');
954
- tts.say$('Next', { alias: 'reply', ttsStrategy: 'streaming' }).subscribe(…);
1050
+ tts.say$('Next', { alias: 'reply', ttsStrategy: 'streaming' }).subscribe(/* */);
955
1051
 
956
1052
  await channel.audio.say('Reuse', { tts }); // same warm session
957
1053
  ```
@@ -970,14 +1066,24 @@ Vendor keys must be **multi-character** names. Builtin single-letter codes
970
1066
  Call `createAsr` / `createTts` early to warm sockets. Repeated `say` with the same handle (or
971
1067
  matching fingerprint) must **not** open a new TCP/WS per utterance.
972
1068
 
1069
+ The host also loads the same `mediaProviders` module into the **pipeline worker** process
1070
+ (separate `require`) when `PIPELINE_WORKERS` is enabled — keep the module side-effect free
1071
+ aside from exporting factories.
1072
+
973
1073
  ### Security
974
1074
 
975
1075
  - Host `require` of `mediaProviders` runs with API privileges — **trusted packages only**.
976
- - Entry path is restricted to the script root (path traversal denied).
1076
+ - Keep connector/`ws` code out of the sandboxed entry so the VM never evaluates it.
1077
+ - The script VM does **not** expose `process` (no `process.env` in `defineScript`).
1078
+ - Entry / provider paths are restricted to the script root (path traversal denied).
977
1079
  - Optional host allowlists may further restrict which packages may export providers.
978
1080
 
1081
+
1082
+
979
1083
  ## TTS Credentials And Vendor Parameters
980
1084
 
1085
+
1086
+
981
1087
  ### Direct TTS Vendor Parameters
982
1088
 
983
1089
  Pass vendor-native credentials and settings directly through `PlayOptions.ttsConfig`. These values are forwarded to the TTS connector as-is and override any defaults or platform-resolved credentials.
@@ -997,10 +1103,12 @@ await channel.audio.say('Hello!', {
997
1103
 
998
1104
  Each TTS vendor connector accepts its native parameter names:
999
1105
 
1000
- | Vendor | Accepted `ttsConfig` keys |
1001
- | --- | --- |
1106
+
1107
+ | Vendor | Accepted `ttsConfig` keys |
1108
+ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1002
1109
  | **ElevenLabs** | `api_key` (or `xi_api_key`), `voice_id`, `model_id` (or `model`), `base_url`, `output_format`, `language_code`, `voice_settings_stability`, `voice_settings_similarity_boost`, `voice_settings_style`, `voice_settings_speed` |
1003
- | **Voctiv** | `url`, `voice_id`, `language`, `emotion`, `speaking_rate`, `chunk_schedule` |
1110
+ | **Voctiv** | `url`, `voice_id`, `language`, `emotion`, `speaking_rate`, `chunk_schedule` |
1111
+
1004
1112
 
1005
1113
  All vendors also accept the env-style names (`ELEVENLABS_API_KEY`, `ELEVENLABS_VOICE_ID`, etc.) for backwards compatibility, but vendor-native names are checked first and are preferred.
1006
1114
 
@@ -1023,8 +1131,8 @@ When both `name` (platform key) and explicit `ttsConfig` values are provided, `t
1023
1131
 
1024
1132
  `cache` enables TTS result caching for `say()` and `presay()`:
1025
1133
 
1026
- - **`cache: true`** — read/write host TTS file cache.
1027
- - **`cache: { phraseName, flag?, language? }`** — TTS cache **plus** persist as a platform phrase so `platform.getRecords()` can retrieve the audio later.
1134
+ - `cache: true` — read/write host TTS file cache.
1135
+ - `cache: { phraseName, flag?, language? }` — TTS cache **plus** persist as a platform phrase so `platform.getRecords()` can retrieve the audio later.
1028
1136
 
1029
1137
  ```ts
1030
1138
  // Cache only (no platform persist):
@@ -1074,6 +1182,8 @@ try {
1074
1182
  }
1075
1183
  ```
1076
1184
 
1185
+
1186
+
1077
1187
  ### ASR Errors — `error$` Observable
1078
1188
 
1079
1189
  Runtime ASR errors (gRPC disconnect, auth failure, quota exceeded) are emitted on `AsrHandle.error$`:
@@ -1113,20 +1223,22 @@ channel.events.error$.subscribe((err) => {
1113
1223
 
1114
1224
  `MediaError` fields:
1115
1225
 
1116
- | Field | Type | Description |
1117
- | --- | --- | --- |
1118
- | `source` | `'asr' \| 'tts' \| 'sip' \| 'channel' \| 'llm'` | Which subsystem produced the error. |
1119
- | `phase` | `'create' \| 'start' \| 'stream' \| 'playback' \| 'finalize' \| 'destroy'?` | Lifecycle phase where the error happened. |
1120
- | `operation` | `string?` | Public SDK operation, e.g. `createAsr`, `createTts`, `audio.say`, or `audio.play`. |
1121
- | `recoverable` | `boolean?` | Whether the runtime can keep the session alive after this error. |
1122
- | `handleId` | `string?` | ASR handle id when the error belongs to a recognizer instance. |
1123
- | `queue` | `number?` | Mixer queue index when the error belongs to an audio operation. |
1124
- | `alias` | `string?` | Queue item alias when the error belongs to playback. |
1125
- | `message` | `string` | Human-readable description. |
1126
- | `code` | `number \| string?` | HTTP status, gRPC status, provider code, or WebSocket close code. |
1127
- | `vendor` | `string?` | Vendor identifier, e.g. `"yandex"`, `"elevenlabs"`, `"azure"`. |
1128
- | `details` | `unknown?` | Arbitrary provider-specific payload. |
1129
- | `cause` | `unknown?` | Original underlying error when available. |
1226
+
1227
+ | Field | Type | Description |
1228
+ | ------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
1229
+ | `source` | `'asr' | 'tts' | 'sip' | 'channel' | 'llm'` | Which subsystem produced the error. |
1230
+ | `phase` | `'create' | 'start' | 'stream' | 'playback' | 'finalize' | 'destroy'?` | Lifecycle phase where the error happened. |
1231
+ | `operation` | `string?` | Public SDK operation, e.g. `createAsr`, `createTts`, `audio.say`, or `audio.play`. |
1232
+ | `recoverable` | `boolean?` | Whether the runtime can keep the session alive after this error. |
1233
+ | `handleId` | `string?` | ASR handle id when the error belongs to a recognizer instance. |
1234
+ | `queue` | `number?` | Mixer queue index when the error belongs to an audio operation. |
1235
+ | `alias` | `string?` | Queue item alias when the error belongs to playback. |
1236
+ | `message` | `string` | Human-readable description. |
1237
+ | `code` | `number | string?` | HTTP status, gRPC status, provider code, or WebSocket close code. |
1238
+ | `vendor` | `string?` | Vendor identifier, e.g. `"yandex"`, `"elevenlabs"`, `"azure"`. |
1239
+ | `details` | `unknown?` | Arbitrary provider-specific payload. |
1240
+ | `cause` | `unknown?` | Original underlying error when available. |
1241
+
1130
1242
 
1131
1243
  Subscribing to `error$` is optional. Old scripts that do not subscribe are not affected — the observables simply go unobserved.
1132
1244
 
@@ -1134,14 +1246,16 @@ Subscribing to `error$` is optional. Old scripts that do not subscribe are not a
1134
1246
 
1135
1247
  `channel.events` exposes session-level observables that are **not** tied to a single ASR handle:
1136
1248
 
1137
- | Observable | Emits when |
1138
- | --- | --- |
1139
- | `speechStart$` | User started speaking (VAD, socket event, or synthetic text input). |
1140
- | `speechEnd$` | User stopped speaking (VAD end, ASR final, or synthetic text input). |
1141
- | `interrupt$` | Barge-in: user speech interrupted bot audio (may be inert without VAD). |
1142
- | `terminated$` | Session ending hangup, WS disconnect, or `channel.destroy()`. |
1143
- | `message$` | Structured WS data messages (`DataMessage`: `{ event, payload }`). |
1144
- | `error$` | Unified media/runtime errors (see [Error Handling](#error-handling)). |
1249
+
1250
+ | Observable | Emits when |
1251
+ | -------------- | ----------------------------------------------------------------------- |
1252
+ | `speechStart$` | User started speaking (VAD, socket event, or synthetic text input). |
1253
+ | `speechEnd$` | User stopped speaking (VAD end, ASR final, or synthetic text input). |
1254
+ | `interrupt$` | Barge-in: user speech interrupted bot audio (may be inert without VAD). |
1255
+ | `terminated$` | Session ending hangup, WS disconnect, or `channel.destroy()`. |
1256
+ | `message$` | Structured WS data messages (`DataMessage`: `{ event, payload }`). |
1257
+ | `error$` | Unified media/runtime errors (see [Error Handling](#error-handling)). |
1258
+
1145
1259
 
1146
1260
  ```ts
1147
1261
  channel.events.speechStart$.subscribe(() => {
@@ -1166,12 +1280,14 @@ create ASR or want one subscription for the whole channel).
1166
1280
 
1167
1281
  `channel.llm` talks to the Omni LLM backend.
1168
1282
 
1169
- | Method | Returns | Description |
1170
- | --- | --- | --- |
1171
- | `ask(message, options?)` | `Promise<string>` | Single-shot completion (consumes SSE stream). |
1172
- | `stream(message, options?)` | `Observable<LlmStreamChunk>` | Token/chunk stream; use `chunk.content` for TTS. |
1173
- | `extract(options?)` | `Promise<Record<string, any>>` | Structured extraction via Omni extract API. |
1174
- | `makePersistentStream(options?)` | `PersistentLlmStreamHandle` | Long-lived stream for multi-turn chat. |
1283
+
1284
+ | Method | Returns | Description |
1285
+ | -------------------------------- | ------------------------------ | ------------------------------------------------ |
1286
+ | `ask(message, options?)` | `Promise<string>` | Single-shot completion (consumes SSE stream). |
1287
+ | `stream(message, options?)` | `Observable<LlmStreamChunk>` | Token/chunk stream; use `chunk.content` for TTS. |
1288
+ | `extract(options?)` | `Promise<Record<string, any>>` | Structured extraction via Omni extract API. |
1289
+ | `makePersistentStream(options?)` | `PersistentLlmStreamHandle` | Long-lived stream for multi-turn chat. |
1290
+
1175
1291
 
1176
1292
  Common `LlmOptions`: `dialogUuid`, `agentUuid`, `role`, `hidden`, `name` (LLM speaker label — **not**
1177
1293
  the TTS credential `name`), `payload`, `debug`, `agentAliasFilter`, `currentAgentAlias`.
@@ -1225,9 +1341,11 @@ chat.send('And my last payment date?');
1225
1341
  chat.disconnect();
1226
1342
  ```
1227
1343
 
1344
+
1345
+
1228
1346
  ## Script Return Value
1229
1347
 
1230
- Scripts may return `void` or a **`ScriptResult`**:
1348
+ Scripts may return `void` or a `ScriptResult`:
1231
1349
 
1232
1350
  ```ts
1233
1351
  return {
@@ -1235,33 +1353,35 @@ return {
1235
1353
  };
1236
1354
  ```
1237
1355
 
1238
- - **`output`** — stored in dialog stats / host persistence. This is **not** the LE `dialog.result` lifecycle column — that is `platform.dialog.result` (see [Dialog State](#dialog-state)).
1239
- - **`error`** — optional; usually auto-populated on crash, but scripts may set it explicitly.
1356
+ - `output` — stored in dialog stats / host persistence. This is **not** the LE `dialog.result` lifecycle column — that is `platform.dialog.result` (see [Dialog State](#dialog-state)).
1357
+ - `error` — optional; usually auto-populated on crash, but scripts may set it explicitly.
1240
1358
 
1241
1359
  **Do not** return `env` from the script. Persist state via `context.env$`; the runtime snapshots it
1242
- after completion into **`PersistedScriptResult.env`**.
1360
+ after completion into `PersistedScriptResult.env`.
1243
1361
 
1244
1362
  ## Script Lifecycle (`getScriptPhase`)
1245
1363
 
1246
- The host always runs your single **`defineScript` export** — there is no separate runtime entry per
1247
- name (unlike logic-executor Python `run_unit(entry_point=...)`). Use **`getScriptPhase(context)`**
1364
+ The host always runs your single `defineScript` **export** — there is no separate runtime entry per
1365
+ name (unlike logic-executor Python `run_unit(entry_point=...)`). Use `getScriptPhase(context)`
1248
1366
  to tell *why* the script is running now: live call, pre-call queue, post-call continuation,
1249
1367
  messaging, etc.
1250
1368
 
1251
- **`context.headless === true` only means “no live SIP/media”.** It does **not** tell you whether
1369
+ `context.headless === true` **only means “no live SIP/media”.** It does **not** tell you whether
1252
1370
  the run is before or after a call. For that, use `getScriptPhase`.
1253
1371
 
1254
1372
  The return value is the `ScriptPhase` union — one of the phases in the table below.
1255
1373
 
1256
- | Phase | `context.headless` | When | Typical `context.entryPoint` |
1257
- |-------|-------------------|------|--------------------------------|
1258
- | `before_call` | `true` | Dialog queue / bulk outbound **before** the first platform call (often schedules `platform.call`) | empty, `main`, `default` |
1259
- | `online` | `false` | Live SIP session (inbound, outbound, **and automatic recall redials**) | any (ignored for phase) |
1260
- | `after_call_success` | `true` | Headless run **after** a successful call | `on_success_call`, `after_call_success`, `on_done_call` |
1261
- | `after_call_failed` | `true` | Headless run **after** failed attempts (when `onFailedCall` was configured) | `on_failed_call`, `after_call_failed` |
1262
- | `messaging` | `true` | Inbound message triggered the run | `on_message_api_received`, or `context.inboundMessage` set |
1263
- | `recall` | `true` | Headless recall leg (legacy `entry_point`) | `on_recall`, `recall` |
1264
- | `headless_other` | `true` | Any other headless run with a custom `entry_point` | your custom name |
1374
+
1375
+ | Phase | `context.headless` | When | Typical `context.entryPoint` |
1376
+ | -------------------- | ------------------ | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
1377
+ | `before_call` | `true` | Dialog queue / bulk outbound **before** the first platform call (often schedules `platform.call`) | empty, `main`, `default` |
1378
+ | `online` | `false` | Live SIP session (inbound, outbound, **and automatic recall redials**) | any (ignored for phase) |
1379
+ | `after_call_success` | `true` | Headless run **after** a successful call | `on_success_call`, `after_call_success`, `on_done_call` |
1380
+ | `after_call_failed` | `true` | Headless run **after** failed attempts (when `onFailedCall` was configured) | `on_failed_call`, `after_call_failed` |
1381
+ | `messaging` | `true` | Inbound message triggered the run | `on_message_api_received`, or `context.inboundMessage` set |
1382
+ | `recall` | `true` | Headless recall leg (legacy `entry_point`) | `on_recall`, `recall` |
1383
+ | `headless_other` | `true` | Any other headless run with a custom `entry_point` | your custom name |
1384
+
1265
1385
 
1266
1386
  **Automatic recall** (`recallCount` + `recallDelay`) creates new **online** SIP legs with an
1267
1387
  incremented `context.attempt`. Branch with `(context.attempt ?? 0) > 0`, **not** `phase === 'recall'`.
@@ -1315,6 +1435,8 @@ export default defineScript(async ({ channel, context, logger, platform }) => {
1315
1435
  });
1316
1436
  ```
1317
1437
 
1438
+
1439
+
1318
1440
  ## Platform API
1319
1441
 
1320
1442
  `platform` exposes platform operations.
@@ -1340,28 +1462,32 @@ Platform APIs (`platform.nlu`, `platform.call`, dialog writes, messaging, phrase
1340
1462
 
1341
1463
  #### What `platform.dialog.result` is
1342
1464
 
1343
- `platform.dialog.result` maps to the PostgreSQL column **`dialog.result`**: the **lifecycle status** of the dialog entity in the CMS / offline queue (in queue, in progress, closed). It is **not** the outcome of a SIP leg.
1465
+ `platform.dialog.result` maps to the PostgreSQL column `dialog.result`: the **lifecycle status** of the dialog entity in the CMS / offline queue (in queue, in progress, closed). It is **not** the outcome of a SIP leg.
1466
+
1467
+
1468
+ | Value | Meaning |
1469
+ | --------- | -------------------------------------------------------------------------------- |
1470
+ | `null` | Often after-call continuation: dialog re-enters queue-api, then becomes `queued` |
1471
+ | `queued` | In the offline queue, not yet claimed |
1472
+ | `pending` | In progress (live SIP/WS or claimed queue row) |
1473
+ | `done` | Dialog closed successfully (terminal for the pipeline) |
1474
+ | `error` | Dialog closed with a logic/runtime error |
1344
1475
 
1345
- | Value | Meaning |
1346
- | --- | --- |
1347
- | `null` | Often after-call continuation: dialog re-enters queue-api, then becomes `queued` |
1348
- | `queued` | In the offline queue, not yet claimed |
1349
- | `pending` | In progress (live SIP/WS or claimed queue row) |
1350
- | `done` | Dialog closed successfully (terminal for the pipeline) |
1351
- | `error` | Dialog closed with a logic/runtime error |
1352
1476
 
1353
1477
  The **host** also moves these statuses (live session → `pending`; shutdown without continuation → often `done` / `error`; automatic recall → `pending`; after-call chain → `null` + `entry_point` in params). Scripts set `platform.dialog.result` when they want to **explicitly** fix the LE dialog status (commonly `'done'` in a headless after-call handler). That does **not** replace `channel.sip.hangup()`.
1354
1478
 
1355
1479
  #### Do not confuse
1356
1480
 
1357
- | API | Layer | Does | Does not |
1358
- | --- | --- | --- | --- |
1359
- | `platform.dialog.result` | LE `dialog` row | Lifecycle status (`done`, `pending`, …) | Hang up SIP; equal `call.result`; equal `ScriptResult.output` |
1360
- | `platform.dialog.entryPoint` | LE `dialog.params` | Persist routing hint for later headless/queue runs | Change SIP state; select another script export |
1361
- | `context.entryPoint` | `ScriptDialogContext` | **Snapshot** of `entry_point` at script start (`getScriptPhase`) | Persist if you assign it write via `platform.dialog.entryPoint` |
1362
- | `return { output }` / `ScriptResult` | Script return | dialog_stats / host dump | LE `dialog.result` column |
1363
- | `channel.sip.hangup()` / `terminated$` | Media leg | End or observe SIP/WS media | Set `platform.dialog.result` by itself |
1364
- | `call.result` (LE call row) | Per-call | SIP terminal code/phrase for CMS logs | Same as `dialog.result` |
1481
+
1482
+ | API | Layer | Does | Does not |
1483
+ | -------------------------------------- | --------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------- |
1484
+ | `platform.dialog.result` | LE `dialog` row | Lifecycle status (`done`, `pending`, …) | Hang up SIP; equal `call.result`; equal `ScriptResult.output` |
1485
+ | `platform.dialog.entryPoint` | LE `dialog.params` | Persist routing hint for later headless/queue runs | Change SIP state; select another script export |
1486
+ | `context.entryPoint` | `ScriptDialogContext` | **Snapshot** of `entry_point` at script start (`getScriptPhase`) | Persist if you assign it write via `platform.dialog.entryPoint` |
1487
+ | `return { output }` / `ScriptResult` | Script return | dialog_stats / host dump | LE `dialog.result` column |
1488
+ | `channel.sip.hangup()` / `terminated$` | Media leg | End or observe SIP/WS media | Set `platform.dialog.result` by itself |
1489
+ | `call.result` (LE call row) | Per-call | SIP terminal code/phrase for CMS logs | Same as `dialog.result` |
1490
+
1365
1491
 
1366
1492
  There is no `context.result` field — read/write dialog lifecycle only through `platform.dialog.result`.
1367
1493
 
@@ -1406,7 +1532,7 @@ await platform.call('+12025551234', {
1406
1532
  });
1407
1533
  ```
1408
1534
 
1409
- When the scheduled call connects, the host runs your **`defineScript` export** again. Branch inside that handler if needed (there is no separate runtime entry per name, unlike logic-executor Python `run_unit(entry_point=...)`).
1535
+ When the scheduled call connects, the host runs your `defineScript` **export** again. Branch inside that handler if needed (there is no separate runtime entry per name, unlike logic-executor Python `run_unit(entry_point=...)`).
1410
1536
 
1411
1537
  Use `dateEnd` to define the latest time when the call is still useful. If the platform cannot place the call before that deadline, it can skip the attempt.
1412
1538
 
@@ -1436,21 +1562,23 @@ After a failed **outbound** call the platform must choose **one** failure-handli
1436
1562
  `recallCount` + `recallDelay` and `onFailedCall` answer the same question in different ways, so
1437
1563
  they are **mutually exclusive** on `platform.call()` (logic-executor `nn.call` parity).
1438
1564
 
1439
- | Strategy | `platform.call()` options | What happens on failure | Next script run |
1440
- |----------|---------------------------|-------------------------|-----------------|
1441
- | **Automatic recall** | `recallCount` + `recallDelay` (both required on the `call` row) | Host schedules another outbound `call` after `recallDelay`; bumps `dialog.params.attempt` | **Online** SIP leg — same `defineScript`, branch on `context.attempt` |
1442
- | **After-call continuation** | `onFailedCall` (and optionally `onSuccessCall`) | Host sets `dialog.params.entry_point` to the handler name and re-queues the dialog | **Headless** run — `getScriptPhase(context)` `after_call_failed` |
1565
+
1566
+ | Strategy | `platform.call()` options | What happens on failure | Next script run |
1567
+ | --------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
1568
+ | **Automatic recall** | `recallCount` + `recallDelay` (both required on the `call` row) | Host schedules another outbound `call` after `recallDelay`; bumps `dialog.params.attempt` | **Online** SIP leg same `defineScript`, branch on `context.attempt` |
1569
+ | **After-call continuation** | `onFailedCall` (and optionally `onSuccessCall`) | Host sets `dialog.params.entry_point` to the handler name and re-queues the dialog | **Headless** run — `getScriptPhase(context)` → `after_call_failed` |
1570
+
1443
1571
 
1444
1572
  **Why not both?** Recall is fully platform-driven (dialer redials without running your script between
1445
1573
  attempts). `onFailedCall` is script-driven (your handler decides logging, CRM, manual retry, etc.).
1446
- If both were written to `call.params`, shutdown would be ambiguous. This host therefore **keeps
1447
- `onFailedCall` and drops recall** at schedule time so the outbound still starts and the after-call
1574
+ If both were written to `call.params`, shutdown would be ambiguous. This host therefore **keeps**
1575
+ `onFailedCall` **and drops recall** at schedule time so the outbound still starts and the after-call
1448
1576
  branch runs on failure. (If both somehow land on an existing `call.params` row, recall still wins
1449
1577
  at shutdown — avoid writing both.)
1450
1578
 
1451
1579
  **Do not** pass `onFailedCall` together with `recallCount` and `recallDelay` in the same
1452
1580
  `platform.call()` invocation. If both are present (script options, dialog params, or CMS
1453
- defaults), the host **keeps `onFailedCall` and drops recall** so the call still schedules —
1581
+ defaults), the host **keeps** `onFailedCall` **and drops recall** so the call still schedules —
1454
1582
  after-call continuation wins over automatic redial. Prefer configuring only one strategy
1455
1583
  explicitly.
1456
1584
 
@@ -1486,7 +1614,7 @@ export default defineScript(async ({ channel, context, logger }) => {
1486
1614
  See [examples/recall-routing-by-attempt.ts](./examples/recall-routing-by-attempt.ts).
1487
1615
 
1488
1616
  When you omit recall options, the host fills them from `context.recallCount` / `context.recallDelay`
1489
- (effective values for the current dialog) **only when `onFailedCall` is not configured** for that
1617
+ (effective values for the current dialog) **only when** `onFailedCall` **is not configured** for that
1490
1618
  scheduled call. Those values fall back to CMS agent contact-rules
1491
1619
  (`context.agent?.recallCount` / `context.agent?.recallDelay`, legacy `nn.get_recall_count()` /
1492
1620
  `nn.get_recall_delay()`). See [Failed outbound: automatic recall vs after-call continuation](#failed-outbound-automatic-recall-vs-after-call-continuation).
@@ -1500,6 +1628,8 @@ Other scheduling options:
1500
1628
  - `entryPoint` (optional): stored as `entry_point` in call params for LE DB compatibility. The host still runs the same `defineScript` export; use `context.entryPoint` only if **you** branch on it inside the handler.
1501
1629
  - `protoAdditional`: extra protocol-level parameters, such as SIP headers expected by your telephony setup.
1502
1630
 
1631
+
1632
+
1503
1633
  ### Messaging
1504
1634
 
1505
1635
  ```ts
@@ -1517,9 +1647,9 @@ await platform.messaging.send({
1517
1647
  Offline, or headless, sessions run a script without a live SIP call, WebSocket audio stream, RTP pipeline, ASR, or TTS playback. They are used for platform-driven background logic, queued dialog processing, and messaging events.
1518
1648
 
1519
1649
  The script entry point is still the same `defineScript()` handler. Detect offline mode with
1520
- `context.headless`, then use **`getScriptPhase(context)`** to distinguish pre-call queue runs
1650
+ `context.headless`, then use `getScriptPhase(context)` to distinguish pre-call queue runs
1521
1651
  (`before_call`) from post-call continuations (`after_call_success` / `after_call_failed`). See
1522
- [Script Lifecycle (`getScriptPhase`)](#script-lifecycle-getscriptphase).
1652
+ [Script Lifecycle (](#script-lifecycle-getscriptphase)`getScriptPhase`[)](#script-lifecycle-getscriptphase).
1523
1653
 
1524
1654
  ```ts
1525
1655
  import { defineScript, getScriptPhase } from '@voctiv/agent-sdk';
@@ -1662,6 +1792,8 @@ export default defineScript(async ({ channel, context, platform }) => {
1662
1792
  });
1663
1793
  ```
1664
1794
 
1795
+
1796
+
1665
1797
  ## Dialog Context And Persisted Env
1666
1798
 
1667
1799
  `context` includes identity, telephony fields, params, routing metadata, and runtime helpers.
@@ -1681,14 +1813,18 @@ const counter = await context.agent?.env?.<number>('visitCount');
1681
1813
  await context.agent?.env?.('visitCount', 42, { expire: 30 });
1682
1814
  ```
1683
1815
 
1816
+
1817
+
1684
1818
  #### Recall settings (agent defaults vs effective)
1685
1819
 
1686
1820
  Recall behavior uses **two layers** on `context`:
1687
1821
 
1688
- | Layer | Fields | Source | Use when |
1689
- |-------|--------|--------|----------|
1690
- | Agent defaults | `context.agent?.recallCount`, `context.agent?.recallDelay` | CMS contact-rules (`agent.recall_count`, `agent.delay` → seconds) | Compare with CMS settings; legacy `nn.get_recall_count()` / `get_recall_delay()` parity |
1691
- | Effective for this run | `context.recallCount`, `context.recallDelay` | `dialog.params` / `call.params`, then agent defaults | Read in script; pass explicitly to `platform.call()` when using **automatic recall** |
1822
+
1823
+ | Layer | Fields | Source | Use when |
1824
+ | ---------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
1825
+ | Agent defaults | `context.agent?.recallCount`, `context.agent?.recallDelay` | CMS contact-rules (`agent.recall_count`, `agent.delay` seconds) | Compare with CMS settings; legacy `nn.get_recall_count()` / `get_recall_delay()` parity |
1826
+ | Effective for this run | `context.recallCount`, `context.recallDelay` | `dialog.params` / `call.params`, then agent defaults | Read in script; pass explicitly to `platform.call()` when using **automatic recall** |
1827
+
1692
1828
 
1693
1829
  Precedence for effective values: **dialog/call params** (`recall_count`, `recall_delay`) **>** agent CMS defaults.
1694
1830
 
@@ -1794,7 +1930,7 @@ Headless channels are for offline, queue, or messaging sessions:
1794
1930
 
1795
1931
  - Audio methods are no-ops that log warnings.
1796
1932
  - SIP methods are no-ops, except `makeCall()` and `bridge()`, which throw: there is no real leg to
1797
- create, and a silent no-op would hide the mistake.
1933
+ create, and a silent no-op would hide the mistake.
1798
1934
  - `createAsr()` returns an inert handle whose observables complete immediately.
1799
1935
  - `createTts()` returns an inert handle (no vendor connection is opened).
1800
1936
  - LLM, NLU, messaging, platform calls, dialog state, and `env$` still work.
@@ -1802,7 +1938,7 @@ Headless channels are for offline, queue, or messaging sessions:
1802
1938
  Use `context.headless` plus `getScriptPhase(context)` when a script must behave differently without a
1803
1939
  real media channel or across pre-call / post-call headless runs. See
1804
1940
  [Offline / Headless Logic](#offline--headless-logic) and
1805
- [Script Lifecycle (`getScriptPhase`)](#script-lifecycle-getscriptphase).
1941
+ [Script Lifecycle (](#script-lifecycle-getscriptphase)`getScriptPhase`[)](#script-lifecycle-getscriptphase).
1806
1942
 
1807
1943
  ## Text Input For Tests
1808
1944
 
@@ -1824,3 +1960,4 @@ The package ships as CommonJS with TypeScript declarations. Import from `@voctiv
1824
1960
  ```ts
1825
1961
  import { defineScript, type MediaChannel, type AsrHandle, type MediaError } from '@voctiv/agent-sdk';
1826
1962
  ```
1963
+
@@ -68,8 +68,12 @@ export interface MediaProvidersDefinition {
68
68
  /**
69
69
  * Mark a named export as custom media providers (identity helper).
70
70
  *
71
+ * Export from a sibling module (`media-providers/index.ts`), not from the
72
+ * sandboxed script entry. The host loads this outside the VM.
73
+ *
71
74
  * @example
72
75
  * ```ts
76
+ * // media-providers/index.ts (host-only)
73
77
  * export const mediaProviders = defineMediaProviders({
74
78
  * createShared: (ctx) => new MySession(ctx),
75
79
  * asr: { 'my-asr': (ctx, shared) => new MyAsr(ctx, shared) },
@@ -1 +1 @@
1
- {"version":3,"file":"media-providers.d.ts","sourceRoot":"","sources":["../../src/types/media-providers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"media-providers.d.ts","sourceRoot":"","sources":["../../src/types/media-providers.ts"],"names":[],"mappings":"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"}
@@ -4,8 +4,12 @@ exports.defineMediaProviders = defineMediaProviders;
4
4
  /**
5
5
  * Mark a named export as custom media providers (identity helper).
6
6
  *
7
+ * Export from a sibling module (`media-providers/index.ts`), not from the
8
+ * sandboxed script entry. The host loads this outside the VM.
9
+ *
7
10
  * @example
8
11
  * ```ts
12
+ * // media-providers/index.ts (host-only)
9
13
  * export const mediaProviders = defineMediaProviders({
10
14
  * createShared: (ctx) => new MySession(ctx),
11
15
  * asr: { 'my-asr': (ctx, shared) => new MyAsr(ctx, shared) },
@@ -1 +1 @@
1
- {"version":3,"file":"media-providers.js","sourceRoot":"","sources":["../../src/types/media-providers.ts"],"names":[],"mappings":";;AAmGA,oDAIC;AAhBD;;;;;;;;;;;GAWG;AACH,SAAgB,oBAAoB,CAClC,GAA6B;IAE7B,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"media-providers.js","sourceRoot":"","sources":["../../src/types/media-providers.ts"],"names":[],"mappings":";;AAuGA,oDAIC;AApBD;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,oBAAoB,CAClC,GAA6B;IAE7B,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -11,7 +11,8 @@ Each file exports one `defineScript` handler. The host **always** invokes that s
11
11
  | [schedule-call-with-defaults.ts](./schedule-call-with-defaults.ts) | `platform.call()` using CMS defaults from `context` |
12
12
  | [after-call-continuation.ts](./after-call-continuation.ts) | `onSuccessCall` / `onFailedCall` + `getScriptPhase()` |
13
13
  | [read-recall-from-params.ts](./read-recall-from-params.ts) | `parseRecallDelaySeconds()` / `parseRecallCount()` |
14
- | [custom-media-providers.ts](./custom-media-providers.ts) | `defineMediaProviders` + `createAsr` / `createTts` / `say$` |
14
+ | [custom-media-providers.ts](./custom-media-providers.ts) | Host-only `mediaProviders` (`implements ScriptAsrConnector` / `ScriptTtsConnector`) |
15
+ | [custom-media-providers-script.ts](./custom-media-providers-script.ts) | Sandboxed `defineScript` using custom vendors (no `ws` / no `process.env`) |
15
16
 
16
17
  ## Recall flow
17
18
 
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Sandboxed script entry that uses custom mediaProviders vendors.
3
+ *
4
+ * Pair with `custom-media-providers.ts` deployed as `dist/media-providers/index.js`.
5
+ * This file must stay free of `ws` / connector imports — the script VM has no `process`.
6
+ */
7
+ import { defineScript } from '@voctiv/agent-sdk';
8
+
9
+ export default defineScript(async ({ channel, logger }) => {
10
+ channel.sip.answer();
11
+
12
+ // Pass credentials via `data` (or channel/platform params). Do not use process.env
13
+ // in the sandboxed entry — `process` is not defined in the script VM.
14
+ // Host-side factories in media-providers/ may still read process.env.
15
+ const apiKey = String(
16
+ (channel.params as Record<string, unknown>).api_key ?? '',
17
+ );
18
+
19
+ const asr = await channel.createAsr({
20
+ vendor: 'my-asr',
21
+ language: 'ru-RU',
22
+ data: { api_key: apiKey },
23
+ });
24
+ const tts = await channel.createTts({
25
+ vendor: 'my-tts',
26
+ data: {
27
+ api_key: apiKey,
28
+ voice_id: '…',
29
+ output_format: 'pcm_16000',
30
+ },
31
+ });
32
+
33
+ await tts.say('Hello from a custom TTS connector.', { alias: 'greet' });
34
+
35
+ tts.say$('One. Two.', { alias: 'reply', ttsStrategy: 'streaming' }).subscribe({
36
+ next: (ev) => {
37
+ logger.log('tts utterance', { state: ev.state, alias: ev.itemAlias });
38
+ },
39
+ error: (err) => logger.error('tts failed', err),
40
+ });
41
+
42
+ await channel.audio.say('Reuse handle', { tts });
43
+
44
+ asr.destroy();
45
+ tts.destroy();
46
+ channel.sip.hangup();
47
+ });
@@ -1,22 +1,25 @@
1
1
  /**
2
- * Custom ASR / TTS from a trusted logic package.
2
+ * Host-only custom ASR / TTS module.
3
3
  *
4
- * Export `mediaProviders` next to `defineScript`. The host loads that export
5
- * outside the sandbox and wires vendors into `createAsr` / `createTts` with the
6
- * same warm TCP/WS reuse as built-in engines.
4
+ * Ship as `dist/media-providers/index.js` next to the sandboxed script entry.
5
+ * The host `require`s this file outside the script VM (full Node: `ws`, `process`, …).
6
+ *
7
+ * Do **not** re-export `mediaProviders` from `dist/index.js` — the sandbox has no
8
+ * `process`, so loading `ws` there fails. Keep `defineScript` in a separate file
9
+ * (see `custom-media-providers-script.ts`).
7
10
  *
8
11
  * Batch and streaming TTS share one class (`supportsStreaming()` + streaming
9
- * lifecycle methods). Do not register a separate streaming map.
12
+ * lifecycle). Do not register a separate streaming map.
10
13
  */
11
14
  import { Readable } from 'stream';
12
15
  import { Subject } from 'rxjs';
13
16
  import {
14
- defineScript,
15
17
  defineMediaProviders,
16
18
  type MediaConnectorContext,
17
19
  type MediaProviderShared,
18
20
  type ScriptAsrConnector,
19
21
  type ScriptTtsConnector,
22
+ type ScriptTtsSynthesisContext,
20
23
  } from '@voctiv/agent-sdk';
21
24
 
22
25
  type SharedSession = MediaProviderShared & {
@@ -38,7 +41,8 @@ class EchoAsr implements ScriptAsrConnector {
38
41
  }
39
42
 
40
43
  send(_audio: ArrayBufferLike): void {
41
- // Forward PCM to your vendor here.
44
+ // Forward PCM (S16LE mono 16 kHz) to your vendor here.
45
+ void this.ctx;
42
46
  }
43
47
 
44
48
  speech(_active: boolean): void {}
@@ -71,7 +75,10 @@ class BatchOrStreamTts implements ScriptTtsConnector {
71
75
  return false;
72
76
  }
73
77
 
74
- async textToSpeechStream(rawtext: string): Promise<Readable> {
78
+ async textToSpeechStream(
79
+ rawtext: string,
80
+ _ctx?: ScriptTtsSynthesisContext,
81
+ ): Promise<Readable> {
75
82
  void this.ctx;
76
83
  void this.shared;
77
84
  // Replace with HTTP keep-alive synthesis that returns a PCM stream.
@@ -103,26 +110,3 @@ export const mediaProviders = defineMediaProviders({
103
110
  new BatchOrStreamTts(ctx, shared as SharedSession),
104
111
  },
105
112
  });
106
-
107
- export default defineScript(async ({ channel, logger }) => {
108
- channel.sip.answer();
109
-
110
- const asr = await channel.createAsr({ vendor: 'my-asr' });
111
- const tts = await channel.createTts({ vendor: 'my-tts' });
112
-
113
- await tts.say('Hello from a custom TTS connector.');
114
-
115
- tts.say$('One. Two.', { alias: 'reply' }).subscribe({
116
- next: (ev) => {
117
- logger.log('tts utterance', { state: ev.state, alias: ev.itemAlias });
118
- },
119
- error: (err) => logger.error('tts failed', err),
120
- });
121
-
122
- // Same warm handle via audio.say:
123
- await channel.audio.say('Reuse handle', { tts });
124
-
125
- asr.destroy();
126
- tts.destroy();
127
- channel.sip.hangup();
128
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voctiv/agent-sdk",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Voctiv TypeScript agent SDK: defineScript and platform types for the voice/dialog scripting runtime.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "",