@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 +346 -209
- package/dist/types/media-providers.d.ts +4 -0
- package/dist/types/media-providers.d.ts.map +1 -1
- package/dist/types/media-providers.js +4 -0
- package/dist/types/media-providers.js.map +1 -1
- package/examples/README.md +2 -1
- package/examples/custom-media-providers-script.ts +47 -0
- package/examples/custom-media-providers.ts +15 -31
- package/package.json +1 -1
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 [
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
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
|
-
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
|
|
|
242
|
-
|
|
|
243
|
-
|
|
|
244
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
|
281
|
-
|
|
|
282
|
-
| `
|
|
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
|
-
|
|
313
|
-
|
|
|
314
|
-
|
|
|
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
|
-
|
|
344
|
-
|
|
|
345
|
-
|
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
| `
|
|
354
|
-
| `
|
|
355
|
-
| `
|
|
356
|
-
| `
|
|
357
|
-
| `
|
|
358
|
-
| `
|
|
359
|
-
| `
|
|
360
|
-
| `
|
|
361
|
-
| `
|
|
362
|
-
| `
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
366
|
-
| `
|
|
367
|
-
| `
|
|
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
|
-
|
|
527
|
-
|
|
|
528
|
-
|
|
|
529
|
-
| **
|
|
530
|
-
| **
|
|
531
|
-
| **
|
|
532
|
-
| **
|
|
533
|
-
| **
|
|
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
|
|
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
|
|
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
|
-
|
|
619
|
-
|
|
|
620
|
-
|
|
|
621
|
-
| `
|
|
622
|
-
| `tts.say
|
|
623
|
-
| `tts.
|
|
624
|
-
| `
|
|
625
|
-
| `channel.audio.
|
|
626
|
-
| `channel.audio.
|
|
627
|
-
| `channel.audio.
|
|
628
|
-
| `channel.audio.
|
|
629
|
-
| `channel.audio.
|
|
630
|
-
| `channel.audio.
|
|
631
|
-
| `channel.audio.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
729
|
-
|
|
730
|
-
-
|
|
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
|
-
|
|
769
|
-
|
|
|
770
|
-
|
|
|
771
|
-
| `
|
|
772
|
-
| `
|
|
773
|
-
| `
|
|
774
|
-
| `
|
|
775
|
-
| `
|
|
776
|
-
| `
|
|
777
|
-
| `
|
|
778
|
-
| `
|
|
779
|
-
| `
|
|
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
|
|
786
|
-
`
|
|
787
|
-
|
|
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
|
-
###
|
|
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:
|
|
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:
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
852
|
-
|
|
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(
|
|
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
|
-
-
|
|
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
|
-
|
|
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**
|
|
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
|
-
-
|
|
1027
|
-
-
|
|
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
|
-
|
|
1117
|
-
|
|
|
1118
|
-
|
|
|
1119
|
-
| `
|
|
1120
|
-
| `
|
|
1121
|
-
| `
|
|
1122
|
-
| `
|
|
1123
|
-
| `
|
|
1124
|
-
| `
|
|
1125
|
-
| `
|
|
1126
|
-
| `
|
|
1127
|
-
| `
|
|
1128
|
-
| `
|
|
1129
|
-
| `
|
|
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
|
-
|
|
1138
|
-
|
|
|
1139
|
-
|
|
|
1140
|
-
| `
|
|
1141
|
-
| `
|
|
1142
|
-
| `
|
|
1143
|
-
| `
|
|
1144
|
-
| `
|
|
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
|
-
|
|
1170
|
-
|
|
|
1171
|
-
|
|
|
1172
|
-
| `
|
|
1173
|
-
| `
|
|
1174
|
-
| `
|
|
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
|
|
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
|
-
-
|
|
1239
|
-
-
|
|
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
|
|
1360
|
+
after completion into `PersistedScriptResult.env`.
|
|
1243
1361
|
|
|
1244
1362
|
## Script Lifecycle (`getScriptPhase`)
|
|
1245
1363
|
|
|
1246
|
-
The host always runs your single
|
|
1247
|
-
name (unlike logic-executor Python `run_unit(entry_point=...)`). Use
|
|
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
|
-
|
|
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
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
|
1259
|
-
| `
|
|
1260
|
-
| `
|
|
1261
|
-
| `
|
|
1262
|
-
| `
|
|
1263
|
-
| `
|
|
1264
|
-
| `
|
|
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
|
|
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
|
-
|
|
1358
|
-
|
|
|
1359
|
-
|
|
|
1360
|
-
| `platform.dialog.
|
|
1361
|
-
| `
|
|
1362
|
-
| `
|
|
1363
|
-
| `
|
|
1364
|
-
| `
|
|
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
|
|
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
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
|
1442
|
-
| **
|
|
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
|
|
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
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
|
1691
|
-
|
|
|
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
|
-
|
|
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":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAEvC;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5D,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAEzC,MAAM,IAAI,OAAO,CAAC;IAClB,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IACnC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,QAAQ,IAAI,IAAI,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,IAAI,OAAO,CAAC;IAE7B,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,mBAAmB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAAC;IAErB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,IAAI,IAAI,CAAC;IACd,kBAAkB,CAAC,IAAI,IAAI,CAAC;IAC5B,KAAK,IAAI,IAAI,CAAC;CACf;AAED,8EAA8E;AAC9E,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,MAAM,MAAM,kBAAkB,GAAG,CAC/B,GAAG,EAAE,qBAAqB,EAC1B,MAAM,EAAE,mBAAmB,GAAG,SAAS,KACpC,kBAAkB,CAAC;AAExB,MAAM,MAAM,kBAAkB,GAAG,CAC/B,GAAG,EAAE,qBAAqB,EAC1B,MAAM,EAAE,mBAAmB,GAAG,SAAS,KACpC,kBAAkB,CAAC;AAExB,MAAM,WAAW,wBAAwB;IACvC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;KACzB,KAAK,mBAAmB,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC1C;AAED
|
|
1
|
+
{"version":3,"file":"media-providers.d.ts","sourceRoot":"","sources":["../../src/types/media-providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAEvC;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5D,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAEzC,MAAM,IAAI,OAAO,CAAC;IAClB,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IACnC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,QAAQ,IAAI,IAAI,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,IAAI,OAAO,CAAC;IAE7B,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE,mBAAmB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAAC;IAErB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,IAAI,IAAI,CAAC;IACd,kBAAkB,CAAC,IAAI,IAAI,CAAC;IAC5B,KAAK,IAAI,IAAI,CAAC;CACf;AAED,8EAA8E;AAC9E,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CACpB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE5B,MAAM,MAAM,kBAAkB,GAAG,CAC/B,GAAG,EAAE,qBAAqB,EAC1B,MAAM,EAAE,mBAAmB,GAAG,SAAS,KACpC,kBAAkB,CAAC;AAExB,MAAM,MAAM,kBAAkB,GAAG,CAC/B,GAAG,EAAE,qBAAqB,EAC1B,MAAM,EAAE,mBAAmB,GAAG,SAAS,KACpC,kBAAkB,CAAC;AAExB,MAAM,WAAW,wBAAwB;IACvC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;KACzB,KAAK,mBAAmB,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC1C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,wBAAwB,GAC5B,wBAAwB,CAE1B;AAED,qEAAqE;AACrE,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -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":";;
|
|
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"}
|
package/examples/README.md
CHANGED
|
@@ -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) | `
|
|
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
|
-
*
|
|
2
|
+
* Host-only custom ASR / TTS module.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* outside the
|
|
6
|
-
*
|
|
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
|
|
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(
|
|
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