@voctiv/agent-sdk 0.2.6 → 0.2.8

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
@@ -89,7 +89,7 @@ export default defineScript(async ({ channel, logger, context }) => {
89
89
  - `channel.audio` controls TTS, raw playback, pre-synthesis, and mixer queues.
90
90
  - `channel.sip` controls SIP state, pre-answer media, DTMF, hold/mute/hangup, outbound calls, and bridging.
91
91
  - `channel.llm` talks to the Omni LLM backend.
92
- - `channel.events` exposes speech, interrupt, termination, and WS data message observables.
92
+ - `channel.events` exposes speech, interrupt, termination, WS data message, and media error observables.
93
93
  - `channel.textInput` injects synthetic ASR results for tests and debug clients.
94
94
 
95
95
  ## SIP And Pre-Answer Media
@@ -194,7 +194,7 @@ Create ASR with `channel.createAsr(config?)`.
194
194
 
195
195
  ```ts
196
196
  const asr = await channel.createAsr({
197
- vendor: 'Y',
197
+ vendor: 'yandex',
198
198
  name: 'main-yandex-key',
199
199
  language: 'ru-RU',
200
200
  vad: {
@@ -217,19 +217,50 @@ const asr = await channel.createAsr({
217
217
  - `speechStart$` / `speechEnd$`: VAD speech boundaries.
218
218
  - `interrupt$`: barge-in / interrupt events where the host supports them.
219
219
  - `vadProbability$`: normalized VAD probability when available.
220
+ - `error$`: runtime errors from the ASR provider (see [Error Handling](#error-handling)).
220
221
  - `pause()` / `resume()` to stop or resume forwarding new audio frames.
221
222
  - `finalize()` to force the current utterance to flush.
222
223
  - `destroy()` to close connector streams and subscriptions.
223
224
 
224
225
  SIP sessions use the call-level telephony VAD when it is available. WS sessions create one VAD/SmartTurn instance for the socket session on the first `createAsr()` call. Headless sessions return an inert ASR handle with empty observables.
225
226
 
226
- If ASR connector creation fails, SIP/WS return a degraded handle. VAD observables still mirror the channel where possible, but no real STT results are emitted.
227
+ If ASR connector creation fails, SIP/WS return a degraded handle. VAD observables still mirror the channel where possible, but no real STT results are emitted. The creation failure is reported on `channel.events.error$`.
227
228
 
228
229
  ## ASR Credentials And Vendors
229
230
 
230
- `AsrConfig.vendor` is an engine hint, for example `"Y"`, `"D"`, `"yandex"`, or `"neuro_v3"`, resolved by the host vendor alias mapping.
231
+ `AsrConfig.vendor` is an engine hint, for example `"yandex"`, `"deepgram"`, `"azure"`, `"elevenlabs"`, or `"neuro_v3"`, resolved by the host vendor alias mapping.
231
232
 
232
- In Voctiv legacy compatibility mode, ASR credentials can be selected by logic-executor `key_storage.name`:
233
+ ### Direct ASR Vendor Parameters
234
+
235
+ Pass vendor-native credentials and settings directly through `AsrConfig.data`. These values are forwarded to the connector as-is and override any defaults or platform-resolved credentials.
236
+
237
+ ```ts
238
+ const asr = await channel.createAsr({
239
+ vendor: 'azure',
240
+ language: 'ru-RU',
241
+ data: {
242
+ subscription_key: 'your-azure-key',
243
+ region: 'swedencentral',
244
+ },
245
+ });
246
+ ```
247
+
248
+ Each vendor connector accepts its native parameter names:
249
+
250
+ | Vendor | Accepted `data` keys |
251
+ | --- | --- |
252
+ | **Azure** | `subscription_key` or `api_key`, `region` |
253
+ | **Yandex** | `api_key` or `token`, `folder_id` |
254
+ | **ElevenLabs** | `api_key` (or `xi_api_key`), `model` |
255
+ | **Deepgram** | `api_key` |
256
+ | **Google** | `email`, `private_key`, `project_id` |
257
+ | **Whisper** | `url`, `rate`, `toFloat` |
258
+
259
+ 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.
260
+
261
+ ### Voctiv Platform ASR Key Selection
262
+
263
+ In Voctiv legacy compatibility mode, ASR credentials can also be selected by logic-executor `key_storage.name`:
233
264
 
234
265
  ```ts
235
266
  const asr = await channel.createAsr({
@@ -238,7 +269,9 @@ const asr = await channel.createAsr({
238
269
  });
239
270
  ```
240
271
 
241
- The runtime looks in `channel.params.authentication_data.legacyAsrKeysByName[name]` for the current dialog agent and company. If `name` is omitted, `channel.params.defaultAsrName` may be used. Vendor-specific overrides go into `data`; primitives are stringified and objects/arrays are JSON-serialized before connector config is built.
272
+ The runtime looks in `channel.params.authentication_data.legacyAsrKeysByName[name]` for the current dialog agent and company. If `name` is omitted, `channel.params.defaultAsrName` may be used.
273
+
274
+ When both `name` (platform key) and explicit `data` are provided, `data` values win — they are applied last and override anything resolved from the platform.
242
275
 
243
276
  ## TTS, Playback, And Mixer Queues
244
277
 
@@ -248,15 +281,22 @@ The runtime looks in `channel.params.authentication_data.legacyAsrKeysByName[nam
248
281
  await channel.audio.say('Please wait while I check that.', {
249
282
  queue: 0,
250
283
  alias: 'main-response',
251
- ttsVendor: 'E',
284
+ ttsVendor: 'elevenlabs',
252
285
  ttsStrategy: 'sentence',
253
286
  ttsConfig: {
254
- voice_id: 'voice-id',
287
+ api_key: 'sk_your-key',
288
+ voice_id: 'bBLRWT6MSWBFAm76ZWXY',
289
+ model_id: 'eleven_turbo_v2_5',
290
+ base_url: 'https://api.eu.residency.elevenlabs.io',
255
291
  output_format: 'pcm_16000',
256
292
  },
257
293
  });
258
294
  ```
259
295
 
296
+ Use full vendor names for `ttsVendor`. Dedicated TTS vendors include `"elevenlabs"`, `"google"`, and `"voctiv"`. The default TTS path can also accept compatible aliases such as `"azure"` or `"neuro_v3"`, depending on how ScriptEngine is configured.
297
+
298
+ Vendor-native parameter names (`api_key`, `voice_id`, `model_id`, `base_url`) are passed directly to the connector and override any platform defaults. See [TTS Credentials And Vendor Parameters](#tts-credentials-and-vendor-parameters) for the full list of accepted keys per vendor.
299
+
260
300
  `channel.audio.play(source, options?)` plays raw audio from a URL/path or a `LegacyPhraseRecord`.
261
301
 
262
302
  ```ts
@@ -301,9 +341,37 @@ channel.audio.stop(2);
301
341
 
302
342
  For sentence-split TTS, queue item aliases are suffixed as `alias-0`, `alias-1`, and so on. Raw `play()` and direct streaming TTS use the alias exactly.
303
343
 
304
- ## TTS Credentials And Saved Phrases
344
+ ## TTS Credentials And Vendor Parameters
345
+
346
+ ### Direct TTS Vendor Parameters
347
+
348
+ 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.
349
+
350
+ ```ts
351
+ await channel.audio.say('Hello!', {
352
+ ttsVendor: 'elevenlabs',
353
+ ttsStrategy: 'streaming',
354
+ ttsConfig: {
355
+ api_key: 'sk_your-elevenlabs-key',
356
+ voice_id: 'bBLRWT6MSWBFAm76ZWXY',
357
+ model_id: 'eleven_turbo_v2_5',
358
+ base_url: 'https://api.eu.residency.elevenlabs.io',
359
+ },
360
+ });
361
+ ```
362
+
363
+ Each TTS vendor connector accepts its native parameter names:
364
+
365
+ | Vendor | Accepted `ttsConfig` keys |
366
+ | --- | --- |
367
+ | **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` |
368
+ | **Voctiv** | `url`, `voice_id`, `language`, `emotion`, `speaking_rate`, `chunk_schedule` |
305
369
 
306
- In Voctiv legacy compatibility mode, TTS credentials can be selected by `PlayOptions.name` or `ttsConfig.name`.
370
+ 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.
371
+
372
+ ### Voctiv Platform TTS Key Selection
373
+
374
+ In Voctiv legacy compatibility mode, TTS credentials can also be selected by `PlayOptions.name` or `ttsConfig.name`.
307
375
 
308
376
  ```ts
309
377
  await channel.audio.say('Здравствуйте!', {
@@ -316,6 +384,8 @@ await channel.audio.say('Здравствуйте!', {
316
384
 
317
385
  The runtime looks in `channel.params.authentication_data.legacyTtsKeysByName[name]`. If `name` is omitted, `channel.params.defaultTtsName` may be used.
318
386
 
387
+ When both `name` (platform key) and explicit `ttsConfig` values are provided, `ttsConfig` values win — they are applied last and override anything resolved from the platform.
388
+
319
389
  `legacySavePhrase` stores synthesized audio under the Voctiv record phrase storage root and inserts phrase metadata so it can later be loaded with `platform.getRecords()`.
320
390
 
321
391
  ```ts
@@ -340,6 +410,72 @@ if (records?.[0]) {
340
410
 
341
411
  This requires legacy compatibility mode, a trusted LE agent id/UUID, TTS cache, and `LEGACY_V3_RECORD_PHRASE_ROOT`.
342
412
 
413
+ ## Error Handling
414
+
415
+ ASR and TTS errors are propagated to the script. Unhandled errors are always logged server-side, but scripts can catch them to react: fall back to a different vendor, notify the caller, or abort the dialog.
416
+
417
+ ### TTS Errors — Promise Rejection
418
+
419
+ `say()` and `play()` reject their promises when TTS/playback fails:
420
+
421
+ ```ts
422
+ try {
423
+ await channel.audio.say('Hello!', {
424
+ ttsVendor: 'elevenlabs',
425
+ ttsConfig: { api_key: 'invalid-key', voice_id: 'abc' },
426
+ });
427
+ } catch (err) {
428
+ logger.error('TTS failed', { error: String(err) });
429
+ await channel.audio.say('Fallback message.'); // try default TTS
430
+ }
431
+ ```
432
+
433
+ ### ASR Errors — `error$` Observable
434
+
435
+ Runtime ASR errors (gRPC disconnect, auth failure, quota exceeded) are emitted on `AsrHandle.error$`:
436
+
437
+ ```ts
438
+ const asr = await channel.createAsr({
439
+ vendor: 'yandex',
440
+ data: { api_key: 'my-key' },
441
+ });
442
+
443
+ asr.error$.subscribe((err) => {
444
+ logger.error('ASR provider error', {
445
+ message: err.message,
446
+ code: err.code,
447
+ vendor: err.vendor,
448
+ });
449
+ });
450
+ ```
451
+
452
+ A degraded handle (returned when connector creation itself failed) has an inert `error$` that never emits — the creation failure is reported on `channel.events.error$` instead.
453
+
454
+ ### Channel Error Stream
455
+
456
+ `channel.events.error$` is a unified stream of all media errors — both ASR and TTS:
457
+
458
+ ```ts
459
+ channel.events.error$.subscribe((err) => {
460
+ logger.warn(`[${err.source}] ${err.message}`, {
461
+ code: err.code,
462
+ vendor: err.vendor,
463
+ });
464
+ });
465
+ ```
466
+
467
+ `MediaError` fields:
468
+
469
+ | Field | Type | Description |
470
+ | --- | --- | --- |
471
+ | `source` | `'asr' \| 'tts' \| 'sip' \| 'channel'` | Which subsystem produced the error. |
472
+ | `message` | `string` | Human-readable description. |
473
+ | `code` | `number?` | HTTP status, gRPC status, or WebSocket close code. |
474
+ | `vendor` | `string?` | Vendor identifier, e.g. `"yandex"`, `"elevenlabs"`, `"azure"`. |
475
+ | `details` | `unknown?` | Arbitrary provider-specific payload. |
476
+
477
+ Subscribing to `error$` is optional. Old scripts that do not subscribe are not affected — the observables simply go unobserved.
478
+
343
479
  ## LLM API
344
480
 
345
481
  `channel.llm` talks to the Omni LLM backend.
@@ -469,6 +605,145 @@ await platform.messaging.send({
469
605
 
470
606
  Outbound messages are transported through legacy Redis streams. `platform.messaging.message$` currently replays the inbound message that started a headless messaging script; it is not a live subscription to all future Redis messages.
471
607
 
608
+ ## Offline / Headless Logic
609
+
610
+ 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.
611
+
612
+ The script entry point is still the same `defineScript()` handler. Detect this mode with `context.headless`:
613
+
614
+ ```ts
615
+ export default defineScript(async ({ channel, context, logger, platform }) => {
616
+ if (!context.headless) {
617
+ channel.sip.answer();
618
+ await channel.audio.say('Hello.');
619
+ return;
620
+ }
621
+
622
+ logger.log('Running offline logic', {
623
+ dialogUuid: context.dialogUuid,
624
+ entryPoint: context.entryPoint,
625
+ });
626
+
627
+ // Offline logic usually works with text, params, env, NLU, LLM, and platform APIs.
628
+ });
629
+ ```
630
+
631
+ Headless sessions can be started by host integrations such as:
632
+
633
+ - a platform dialog queue worker that loads pending dialogs;
634
+ - an inbound messaging worker, usually with `context.entryPoint === 'on_message_api_received'`;
635
+ - an HTTP/API request that asks ScriptEngine to run a script without media.
636
+
637
+ ### What Works In Headless
638
+
639
+ These APIs are available and are the intended tools for offline scripts:
640
+
641
+ - `context.dialogParams`, `context.initialData`, `context.dialogEntity`, and `context.callEntity` for platform data.
642
+ - `context.env$` for persisted per-dialog state.
643
+ - `platform.nlu.extract()` for text NLU when legacy platform compatibility is enabled.
644
+ - `platform.messaging.send()` for outbound messages through the configured platform messaging transport.
645
+ - `platform.call()` for scheduling outbound platform-managed calls.
646
+ - `platform.dialog.entryPoint` and `platform.dialog.result` for updating dialog routing and outcome.
647
+ - `channel.llm.ask()`, `channel.llm.stream()`, and `channel.llm.extract()` for Omni LLM operations.
648
+ - `logger` for structured logs.
649
+
650
+ Audio and telephony APIs are intentionally inert:
651
+
652
+ - `channel.audio.say()`, `play()`, `preload()`, and `presay()` do not play audio and only log warnings.
653
+ - `channel.createAsr()` returns an inert handle with empty observables.
654
+ - `channel.textInput` does not simulate ASR in headless mode.
655
+ - `channel.sip.state` behaves as an already-active synthetic channel, but real SIP actions such as nested calls and bridging are not available.
656
+
657
+ Use headless mode for text and platform workflows. Use SIP or WS sessions when the script needs real audio, ASR, TTS, DTMF, pre-answer media, or bridging.
658
+
659
+ ### Inbound Messaging
660
+
661
+ When a headless script is triggered by an inbound message, the runtime exposes the message as `context.inboundMessage` and also replays it on `platform.messaging.message$`.
662
+
663
+ ```ts
664
+ export default defineScript(async ({ context, platform, logger }) => {
665
+ const inbound = context.inboundMessage;
666
+ const payload = inbound?.payload ?? {};
667
+
668
+ const text =
669
+ typeof payload.text === 'string'
670
+ ? payload.text
671
+ : typeof payload.message === 'string'
672
+ ? payload.message
673
+ : '';
674
+
675
+ logger.log('Inbound message received', {
676
+ src: inbound?.src,
677
+ dst: inbound?.dst,
678
+ channelType: inbound?.channelType,
679
+ text,
680
+ });
681
+
682
+ if (!text.trim()) {
683
+ return { output: { reason: 'empty_message' } };
684
+ }
685
+
686
+ const nlu = await platform.nlu.extract(text, {
687
+ intents: ['support_request', 'callback_request'],
688
+ });
689
+
690
+ await platform.messaging.send({
691
+ src: inbound?.dst ?? 'bot',
692
+ destination: inbound?.src ?? '',
693
+ text: 'Thanks, I received your message.',
694
+ });
695
+
696
+ return {
697
+ output: {
698
+ text,
699
+ nlu,
700
+ },
701
+ };
702
+ });
703
+ ```
704
+
705
+ `context.inboundMessage.payload` is the raw transport payload. Different messaging providers may use different field names (`text`, `message`, `body`, `content`, etc.), so production scripts should normalize the text they need.
706
+
707
+ ### Persisting Offline State
708
+
709
+ Use `context.env$` to keep state between offline runs for the same dialog:
710
+
711
+ ```ts
712
+ const env = context.env$?.getValue() ?? {};
713
+ const messageCount = Number(env.messageCount ?? 0) + 1;
714
+
715
+ context.env$?.next({
716
+ ...env,
717
+ messageCount,
718
+ lastMessageAt: new Date().toISOString(),
719
+ });
720
+ ```
721
+
722
+ Do not return `env` from the script. ScriptEngine snapshots `context.env$` after completion and persists it according to the host integration.
723
+
724
+ ### Combining Voice And Offline In One Script
725
+
726
+ One script can support both live calls and offline messages by branching on `context.headless`:
727
+
728
+ ```ts
729
+ export default defineScript(async ({ channel, context, platform }) => {
730
+ if (context.headless) {
731
+ const text = String(context.inboundMessage?.payload?.text ?? '');
732
+
733
+ if (text.includes('call me')) {
734
+ await platform.call(context.msisdn, {
735
+ entryPoint: 'on_callback',
736
+ });
737
+ }
738
+
739
+ return { output: { handledOffline: true } };
740
+ }
741
+
742
+ channel.sip.answer();
743
+ await channel.audio.say('How can I help you?');
744
+ });
745
+ ```
746
+
472
747
  ## Dialog Context And Persisted Env
473
748
 
474
749
  `context` includes identity, telephony fields, params, routing metadata, and runtime helpers.
@@ -524,9 +799,9 @@ Headless channels are for offline, queue, or messaging sessions:
524
799
  - Audio methods are no-ops that log warnings.
525
800
  - SIP methods are mostly no-ops.
526
801
  - `createAsr()` returns an inert handle.
527
- - LLM and platform APIs still work.
802
+ - LLM, NLU, messaging, platform calls, dialog state, and `env$` still work.
528
803
 
529
- Use `context.headless` to branch when a script must behave differently without a real media channel.
804
+ Use `context.headless` to branch when a script must behave differently without a real media channel. See [Offline / Headless Logic](#offline--headless-logic) for details and examples.
530
805
 
531
806
  ## Text Input For Tests
532
807
 
@@ -554,5 +829,5 @@ npm run build
554
829
  The package exports only the public SDK entry point:
555
830
 
556
831
  ```ts
557
- import { defineScript, type MediaChannel, type AsrHandle } from '@voctiv/agent-sdk';
832
+ import { defineScript, type MediaChannel, type AsrHandle, type MediaError } from '@voctiv/agent-sdk';
558
833
  ```