@pyai/sdk 0.2.0 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
- * @pyai/sdk official TypeScript/JavaScript client for the PyAI API.
2
+ * @pyai/sdk, official TypeScript/JavaScript client for the PyAI API.
3
3
  *
4
4
  * Thin, dependency-free wrapper over the public OpenAI-compatible surface at
5
5
  * https://api.pyai.com (contract: https://api.pyai.com/openapi.json). Runs in
6
- * the browser and Node 22+ (uses global fetch + WebSocket). Keys are opaque
7
- * never parsed.
6
+ * the browser and Node 22+ (uses global fetch + WebSocket). Keys are opaque, * never parsed.
8
7
  */
9
8
  export interface PyAIOptions {
10
9
  /** A pyai_live_ or pyai_test_ key. */
@@ -48,7 +47,7 @@ export interface TranscriptionJob {
48
47
  }
49
48
  /**
50
49
  * Output container/codec for `audio.speech`. This is the **exact** set the
51
- * server accepts on `POST /v1/audio/speech` any other value is rejected with
50
+ * server accepts on `POST /v1/audio/speech`, any other value is rejected with
52
51
  * `400 unsupported_format`. The default (when `response_format` is omitted) is
53
52
  * `mp3`. Omit `sample_rate` for the engine's native 24 kHz (`g711_*` is always
54
53
  * 8 kHz).
@@ -80,20 +79,20 @@ export interface SpeechParams {
80
79
  model?: string;
81
80
  /**
82
81
  * Output container/codec, resampled+encoded server-side. One of
83
- * {@link SpeechFormat} anything else is a `400 unsupported_format`. Omit for
82
+ * {@link SpeechFormat}, anything else is a `400 unsupported_format`. Omit for
84
83
  * the default of `mp3`.
85
84
  *
86
- * `g711_ulaw`/`g711_alaw` return raw 8 kHz mono G.711 the bytes Twilio/SIP
85
+ * `g711_ulaw`/`g711_alaw` return raw 8 kHz mono G.711, the bytes Twilio/SIP
87
86
  * media streams expect, so you can hand the response straight to a telephony
88
87
  * frame without a client-side resampler or μ-law encoder. `sample_rate` is
89
88
  * forced to 8000 for those (omit it, or set exactly 8000). `pcm` is raw,
90
89
  * headerless int16 LE mono at `sample_rate`. `mp3`/`opus` are buffered (not
91
- * chunk-streamed) use them with `speech`, not `speechStream`.
90
+ * chunk-streamed), use them with `speech`, not `speechStream`.
92
91
  */
93
92
  response_format?: SpeechFormat;
94
93
  /**
95
94
  * Output sample rate in Hz. One of {@link SpeechSampleRate}
96
- * (8000/16000/24000/48000) anything else is a `400`. Omit for the engine's
95
+ * (8000/16000/24000/48000), anything else is a `400`. Omit for the engine's
97
96
  * native 24 kHz; `g711_*` is always 8000 (forced). Set `8000`/`16000` for
98
97
  * telephony pipelines, most often with `response_format: "pcm"`.
99
98
  */
@@ -106,8 +105,7 @@ export interface SpeechParams {
106
105
  */
107
106
  seed?: number;
108
107
  /**
109
- * Sampling temperature (lower = more deterministic). Forward-compatible
110
- * honored once the engine supports it, otherwise ignored.
108
+ * Sampling temperature (lower = more deterministic). Forward-compatible, * honored once the engine supports it, otherwise ignored.
111
109
  */
112
110
  temperature?: number;
113
111
  }
@@ -121,13 +119,52 @@ export interface CreateJobParams {
121
119
  webhook_url?: string;
122
120
  }
123
121
  export interface RealtimeOptions {
124
- /** "omni" (agentic, needs agentId) or "flow" (voice duplex). Default "omni". */
122
+ /** "omni" (agentic voice) or "flow" (legacy voice duplex). Default "omni". */
125
123
  product?: "omni" | "flow";
126
- /** Required for omni: the agent to drive. */
124
+ /**
125
+ * Optional opaque tag echoed to your `kb_endpoint` and recorded on the call.
126
+ * Omni is zero-state: the session is authorized by the key's org, so there is
127
+ * nothing to create first and this tag is never required.
128
+ */
129
+ sessionLabel?: string;
130
+ /**
131
+ * @deprecated Use {@link sessionLabel}. Kept for back-compat, emitted as the
132
+ * `agent_id` query alias, which the gateway still accepts. Ignored if
133
+ * `sessionLabel` is set.
134
+ */
127
135
  agentId?: string;
128
136
  /** Extra query params (e.g. format, rate). */
129
137
  query?: Record<string, string>;
130
138
  }
139
+ /** Parameters for minting an ephemeral browser Omni session token (server-side). */
140
+ export interface OmniSessionParams {
141
+ /**
142
+ * Browser origins (scheme://host[:port]) the minted token may connect from.
143
+ * Required and non-empty, a browser token must be origin-locked. `*` is not
144
+ * allowed.
145
+ */
146
+ allowedOrigins: string[];
147
+ /** Token lifetime in seconds. Default 60 server-side; max 600. Keep it short. */
148
+ ttlSeconds?: number;
149
+ /** Optional opaque tag echoed back to your `kb_endpoint` and recorded on the call. */
150
+ sessionLabel?: string;
151
+ }
152
+ /**
153
+ * The result of {@link PyAI.omni}.createSession, a short-lived credential safe
154
+ * to hand to the browser. Use {@link OmniSession.token} as the WebSocket
155
+ * subprotocol `pyai-key.<token>` against {@link OmniSession.url}.
156
+ */
157
+ export interface OmniSession {
158
+ object?: "omni.session";
159
+ /** The ephemeral session token. Short-lived + origin-locked; browser-safe. */
160
+ token: string;
161
+ /** Token expiry, Unix epoch milliseconds. */
162
+ expires_at: number;
163
+ /** The Omni realtime WebSocket URL to connect to. */
164
+ url: string;
165
+ /** Echoed back when supplied on the request. */
166
+ session_label?: string;
167
+ }
131
168
  /** Frame `type`s emitted by the Hear streaming-STT WebSocket. */
132
169
  export declare const HearFrameType: {
133
170
  /** Eager live hypothesis for the current utterance. */
@@ -138,6 +175,8 @@ export declare const HearFrameType: {
138
175
  readonly SpeechFinal: "speech_final";
139
176
  /** Corrected, full-context transcript following `speech_final`. */
140
177
  readonly Final: "final";
178
+ /** Final billed-usage summary, emitted just before a graceful close. */
179
+ readonly Usage: "usage";
141
180
  /** Server-side fault frame. */
142
181
  readonly Error: "error";
143
182
  };
@@ -156,7 +195,7 @@ export declare const WSCloseCode: {
156
195
  export type WSCloseCode = (typeof WSCloseCode)[keyof typeof WSCloseCode];
157
196
  /**
158
197
  * Stable, machine-readable error `code`s (the documented contract). Branch on
159
- * these. The set is treated as open `PyAIError.code` stays `string` so a
198
+ * these. The set is treated as open, `PyAIError.code` stays `string`, so a
160
199
  * new server code never breaks the build, but the known ones are named here.
161
200
  */
162
201
  export declare const ErrorCode: {
@@ -203,15 +242,30 @@ export interface HearFinalFrame {
203
242
  /** Present only with Cue grounding enabled (top KB passages). */
204
243
  grounding?: HearGroundingPassage[];
205
244
  }
245
+ /** Final billed-usage summary (`usage`), emitted just before a graceful close
246
+ * so realtime spend can be reconciled in-band (a realtime WS carries no
247
+ * `x-pyai-units` response header). Best-effort: absent if there was no billable
248
+ * audio or the close was abnormal. */
249
+ export interface HearUsageFrame {
250
+ type: "usage";
251
+ /** `hear` for plain streaming, `cue` when grounding was enabled. */
252
+ product: "hear" | "cue";
253
+ /** The billed meter (`hear.requests` or `cue.minutes`). */
254
+ meter: string;
255
+ /** Summed active-speech audio billed for the session, in seconds. */
256
+ audio_seconds: number;
257
+ /** The same quantity in minutes. */
258
+ minutes: number;
259
+ }
206
260
  /** Server fault frame (`error`). */
207
261
  export interface HearErrorFrame {
208
262
  type: "error";
209
263
  code?: string;
210
264
  message: string;
211
265
  }
212
- export type HearFrame = HearPartialFrame | HearFinalFrame | HearErrorFrame;
266
+ export type HearFrame = HearPartialFrame | HearFinalFrame | HearUsageFrame | HearErrorFrame;
213
267
  /**
214
- * Minimal structural WebSocket matches both the browser/Node global
268
+ * Minimal structural WebSocket, matches both the browser/Node global
215
269
  * `WebSocket` and the `ws` package, and lets tests inject a mock.
216
270
  */
217
271
  export interface WebSocketLike {
@@ -240,12 +294,32 @@ export interface HearStreamOptions {
240
294
  encoding?: "pcm16" | "opus";
241
295
  /** Emit eager partial hypotheses. Default true server-side. */
242
296
  interimResults?: boolean;
297
+ /**
298
+ * Format spoken numbers as digits in the transcript (e.g. "one two three" →
299
+ * "123"). Useful for voice agents that read back phone numbers, codes, and
300
+ * amounts. Default false (spoken form). Forwards `?numerals=true` on the URL.
301
+ */
302
+ numerals?: boolean;
303
+ /**
304
+ * Turn-segmentation tuning: trailing-pause (ms, 50-2000) that ends an
305
+ * utterance. Forwards `?endpointing_ms=`, clamped + honored once the engine
306
+ * supports it; a no-op when omitted. Drive end-of-turn yourself with
307
+ * {@link HearStream.commit} for full control today.
308
+ */
309
+ endpointingMs?: number;
243
310
  /**
244
311
  * Enable Cue knowledge-base grounding: sends `{type:"config",grounding:true}`
245
312
  * on open, after which `speech_final`/`final` frames carry a `grounding`
246
313
  * array. Bills a single `cue.minutes` line instead of the Hear rate.
247
314
  */
248
315
  grounding?: boolean;
316
+ /** Cue: number of KB passages to retrieve per turn (1-20, default 3). */
317
+ groundingK?: number;
318
+ /** Cue: drop passages scoring below this (0-1, default 0 = keep all). */
319
+ groundingMinScore?: number;
320
+ /** Cue: max ms to wait for retrieval at the final before failing open to
321
+ * `grounding: []` (50-2000, default 450). */
322
+ groundingTimeoutMs?: number;
249
323
  /** Extra query params merged onto the connect URL. */
250
324
  query?: Record<string, string>;
251
325
  /** Fired once the socket opens (after the optional grounding config). */
@@ -254,6 +328,8 @@ export interface HearStreamOptions {
254
328
  onPartial?: (frame: HearPartialFrame) => void;
255
329
  /** Fired on `speech_final` / `final`. */
256
330
  onFinal?: (frame: HearFinalFrame) => void;
331
+ /** Fired on the final `usage` summary (in-band realtime reconciliation). */
332
+ onUsage?: (frame: HearUsageFrame) => void;
257
333
  /** Fired on an `error` frame or a transport-level error. */
258
334
  onError?: (err: HearErrorFrame | Error) => void;
259
335
  /** Fired when the socket closes (code per `WSCloseCode`). */
@@ -284,6 +360,220 @@ export declare class HearStream {
284
360
  /** Current WebSocket readyState. */
285
361
  get readyState(): number;
286
362
  }
363
+ /**
364
+ * A live AMD (answering-machine-detection) stream. The wire is Twilio's Media
365
+ * Streams protocol, so if you're already on Twilio the usual path is one line of
366
+ * TwiML pointing `<Stream url="wss://api.pyai.com/v1/amd/stream">` at PyAI, no
367
+ * SDK needed. This helper is for server-side clients that fork the media
368
+ * themselves: send Twilio `start`/`media`/`stop` frames with {@link AmdStream.send}
369
+ * (or raw μ-law audio with {@link AmdStream.sendAudio}) and receive the `amd`
370
+ * decision via `onDecision`. Construct via `pyai.amd.stream()`.
371
+ */
372
+ export declare class AmdStream {
373
+ private readonly ws;
374
+ private readonly opts;
375
+ private closed;
376
+ constructor(url: string, subprotocol: string, opts: AmdStreamOptions);
377
+ private handleMessage;
378
+ /** Send a Twilio Media Streams control/media frame (JSON) or a raw string. */
379
+ send(frame: string | Record<string, unknown>): void;
380
+ /** Send a raw audio chunk (G.711 μ-law 8 kHz for the Twilio-native path). */
381
+ sendAudio(chunk: ArrayBufferLike | ArrayBufferView | Blob): void;
382
+ /** Close the socket. */
383
+ close(code?: number, reason?: string): void;
384
+ /** The underlying socket (escape hatch for advanced use). */
385
+ get socket(): WebSocketLike;
386
+ /** Current WebSocket readyState. */
387
+ get readyState(): number;
388
+ }
389
+ /**
390
+ * Event names on Omni **server → client** frames. ⚠️ Note the asymmetry:
391
+ * inbound frames are keyed on `event`, but your **outbound** control frames
392
+ * (`configure`, `dtmf`, …) are keyed on `type`. {@link OmniConnection} handles
393
+ * both sides for you; this map is for matching frames in `onEvent`.
394
+ */
395
+ export declare const OmniEvent: {
396
+ /** Handshake; advertises protocol version + audio formats. */
397
+ readonly Hello: "hello";
398
+ /** Ack for your `configure` frame (echoes the resolved `voice_id`). */
399
+ readonly Configured: "configured";
400
+ /** Session is live; includes the resolved agent + audio caps. */
401
+ readonly SessionStarted: "session_started";
402
+ /** Turn boundary (user/assistant speaking). */
403
+ readonly Turn: "turn";
404
+ /** Incremental/final transcript text. */
405
+ readonly Transcript: "transcript";
406
+ /** User interrupted; assistant audio is being cut. */
407
+ readonly BargeIn: "barge_in";
408
+ /** Alias for barge-in on some engine builds. */
409
+ readonly Flush: "flush";
410
+ /** Engine requests a client-loop tool invocation. */
411
+ readonly ToolCall: "tool_call";
412
+ /** Session is closing; see close code. */
413
+ readonly SessionEnd: "session_end";
414
+ /** Server fault frame. */
415
+ readonly Error: "error";
416
+ };
417
+ export type OmniEvent = (typeof OmniEvent)[keyof typeof OmniEvent];
418
+ /** A server → client Omni frame. Keyed on `event` (not `type`); open for forward-compat. */
419
+ export interface OmniServerFrame {
420
+ event: string;
421
+ [k: string]: unknown;
422
+ }
423
+ /** A binary agent-audio chunk delivered to {@link OmniConnectOptions.onAudio}. */
424
+ export type OmniAudioChunk = ArrayBuffer | ArrayBufferView | Blob;
425
+ export interface OmniToolDef {
426
+ name: string;
427
+ description?: string;
428
+ parameters?: Record<string, unknown>;
429
+ /** When set, engine-POST mode. Omit for client-loop (default). */
430
+ endpoint?: string;
431
+ }
432
+ export interface OmniToolCallFrame {
433
+ type: "tool_call";
434
+ call_id: string;
435
+ name: string;
436
+ arguments?: Record<string, unknown>;
437
+ }
438
+ /**
439
+ * The agent config sent as the `configure` control frame. These are the wire
440
+ * (snake_case) fields the engine reads; it's an **open bag**, so forward-compat
441
+ * fields (e.g. `greeting`, or `model_tier` once the engine acks it) pass
442
+ * straight through. The SDK supplies the `{"type":"configure"}` envelope for
443
+ * you, which is the whole point: a hand-rolled `{"event":"configure"}` is
444
+ * acked but silently dropped, leaving the agent with no brain and zero turns.
445
+ */
446
+ export interface OmniConfigure {
447
+ /** Voice to speak with (stock / clone / designed id). */
448
+ voice_id?: string;
449
+ /** System prompt / role + instructions for the agent. */
450
+ persona?: string;
451
+ /** Customer-hosted URL the engine calls per turn for grounding. */
452
+ kb_endpoint?: string;
453
+ /** Bearer the engine presents to `kb_endpoint`. */
454
+ kb_token?: string;
455
+ /**
456
+ * Session language, end to end (recognition, reasoning, voice). Also
457
+ * settable on the agent profile (`language` on `POST /v1/agents`), which
458
+ * applies automatically when connecting with `session_label={agent_id}`;
459
+ * an inline value here wins for the session. Default `en`. Fail-safe: an
460
+ * unknown/not-yet-enabled language falls back to `en` (the `configured`
461
+ * ack carries `language_active` + `language_fallback: true`), the call
462
+ * proceeds and bills as what was served. Per-language availability is
463
+ * staged, see the Language support reference.
464
+ */
465
+ language?: "en" | "fr" | "es" | "de" | "hi";
466
+ /** Function calling definitions (client-loop when `endpoint` is omitted). */
467
+ tools?: OmniToolDef[];
468
+ /** Forward-compatible: any other key the engine honors. */
469
+ [k: string]: unknown;
470
+ }
471
+ export interface OmniConnectOptions {
472
+ /** Optional opaque per-session tag echoed to your `kb_endpoint`. Omni is zero-state. */
473
+ sessionLabel?: string;
474
+ /**
475
+ * Ephemeral browser token from `omni.createSession`. When set it is used as
476
+ * the WS subprotocol (`pyai-key.<token>`) instead of the client's secret key,
477
+ * so a page never holds a secret key.
478
+ */
479
+ token?: string;
480
+ /** Connect-URL sample rate: 24000 browser, 16000 wideband telephony, 8000 G.711/Twilio. */
481
+ rate?: 24000 | 16000 | 8000;
482
+ /** Connect-URL audio format. Default `pcm16`. */
483
+ format?: "pcm16";
484
+ /**
485
+ * Agent config sent as `{"type":"configure",...}` the instant the socket
486
+ * opens. Omit to send it yourself later via {@link OmniConnection.configure}.
487
+ */
488
+ configure?: OmniConfigure;
489
+ /** Extra query params merged onto the connect URL. */
490
+ query?: Record<string, string>;
491
+ /** Fired once the socket opens (after the optional auto-configure). */
492
+ onOpen?: () => void;
493
+ /** Fired for each binary agent-audio chunk, play it out as it arrives. */
494
+ onAudio?: (chunk: OmniAudioChunk) => void;
495
+ /** Fired on the `hello` handshake frame. */
496
+ onHello?: (frame: OmniServerFrame) => void;
497
+ /** Fired on the `configured` ack. */
498
+ onConfigured?: (frame: OmniServerFrame) => void;
499
+ /** Fired on `session_started`. */
500
+ onSessionStarted?: (frame: OmniServerFrame) => void;
501
+ /** Fired on `turn` boundaries. */
502
+ onTurn?: (frame: OmniServerFrame) => void;
503
+ /** Fired on `transcript` text frames. */
504
+ onTranscript?: (frame: OmniServerFrame) => void;
505
+ /** Fired on `barge_in` / `flush` (user interrupted). */
506
+ onBargeIn?: (frame: OmniServerFrame) => void;
507
+ /** Fired when the engine requests a client-loop tool invocation. */
508
+ onToolCall?: (frame: OmniToolCallFrame) => void;
509
+ /** Fired on `session_end`. */
510
+ onSessionEnd?: (frame: OmniServerFrame) => void;
511
+ /** Fired on EVERY JSON frame (including unknown/forward-compat ones). */
512
+ onEvent?: (frame: OmniServerFrame) => void;
513
+ /** Fired on an `error` frame or a transport-level error. */
514
+ onError?: (err: OmniServerFrame | Error) => void;
515
+ /** Fired when the socket closes (code per {@link WSCloseCode}). */
516
+ onClose?: (code: number, reason: string) => void;
517
+ /** Injectable WebSocket constructor (defaults to the global). */
518
+ webSocket?: WebSocketCtor;
519
+ }
520
+ /**
521
+ * A live Omni agentic-voice session over `/v1/omni`. Hides the wire protocol's
522
+ * frame-key asymmetry: it sends control frames keyed on `type` (`configure`,
523
+ * `dtmf`) and parses server frames keyed on `event`, so you cannot trip the #1
524
+ * Omni integration bug (mirroring the server's `event` key on outbound, which
525
+ * is silently dropped). Construct via `pyai.omni.connect()`.
526
+ *
527
+ * @example
528
+ * const omni = pyai.omni.connect({
529
+ * rate: 16000,
530
+ * configure: { voice_id: "stock_emma_en_gb", persona: "You are a receptionist." },
531
+ * onAudio: (chunk) => speaker.write(chunk),
532
+ * onTranscript: (f) => console.log(f.text),
533
+ * });
534
+ * omni.sendAudio(pcm16Chunk); // stream caller audio continuously
535
+ */
536
+ export declare class OmniConnection {
537
+ private readonly ws;
538
+ private readonly opts;
539
+ private closed;
540
+ /** Serializes async Blob reads in {@link sendAudio} so frames stay ordered. */
541
+ private blobTail;
542
+ constructor(url: string, subprotocol: string, opts: OmniConnectOptions);
543
+ private handleMessage;
544
+ private dispatchFrame;
545
+ /**
546
+ * Send (or update) the agent config. Always emitted as
547
+ * `{"type":"configure", ...}`, the correct key. (A hand-rolled
548
+ * `{"event":"configure"}` is acked but silently dropped by the engine.)
549
+ */
550
+ configure(cfg: OmniConfigure): void;
551
+ /**
552
+ * Stream a chunk of caller audio (PCM16 LE mono at the negotiated rate) as a
553
+ * `0x01`-prefixed frame. The engine demuxes on the first byte and has no
554
+ * default branch, so an untagged chunk is dropped with no error.
555
+ */
556
+ sendAudio(chunk: ArrayBufferLike | ArrayBufferView | Blob): void;
557
+ /** Send DTMF digits as a `{"type":"dtmf"}` control frame. */
558
+ sendDtmf(digits: string): void;
559
+ /** Reply to a client-loop {@link OmniEvent.ToolCall}. */
560
+ toolResult(callId: string, payload: {
561
+ result?: unknown;
562
+ error?: string;
563
+ }): void;
564
+ /**
565
+ * Send an arbitrary control frame for forward-compat control types the SDK
566
+ * does not model yet. Reminder: client → server frames are keyed on `type`,
567
+ * never `event`.
568
+ */
569
+ send(frame: Record<string, unknown>): void;
570
+ /** Close the session. */
571
+ close(code?: number, reason?: string): void;
572
+ /** The underlying socket (escape hatch for advanced use). */
573
+ get socket(): WebSocketLike;
574
+ /** Current WebSocket readyState. */
575
+ get readyState(): number;
576
+ }
287
577
  /** Shape of `GET /v1/me`. Fields are best-effort / forward-compatible. */
288
578
  export interface MeResponse {
289
579
  object?: string;
@@ -378,7 +668,7 @@ export interface TraceBargeMetrics {
378
668
  }
379
669
  /**
380
670
  * Known timeline-turn roles. Left open (see {@link TraceTimelineTurn.role}) so a
381
- * new server-side role never breaks the build branch defensively.
671
+ * new server-side role never breaks the build, branch defensively.
382
672
  */
383
673
  export type TraceTimelineRole = "agent" | "caller" | "system" | "tool";
384
674
  /** One turn of a per-call operational timeline (eval scorecard-v1). */
@@ -402,18 +692,18 @@ export interface TraceTimelineTurn {
402
692
  }
403
693
  /**
404
694
  * Aggregate per-call quality metrics (eval scorecard-v1). All optional and
405
- * forward-compatible present once the engine emits them.
695
+ * forward-compatible, present once the engine emits them.
406
696
  */
407
697
  export interface QualityMetrics {
408
- /** Word error rate vs. reference transcript, 01 (lower is better). */
698
+ /** Word error rate vs. reference transcript, 0-1 (lower is better). */
409
699
  wer?: number;
410
700
  /** Representative time-to-first-audio across turns, ms. */
411
701
  ttfb_ms?: number;
412
702
  /** 95th-percentile end-to-end turn latency, ms. */
413
703
  turn_p95_ms?: number;
414
- /** Barge-in recovery rate, 01. */
704
+ /** Barge-in recovery rate, 0-1. */
415
705
  barge_recovery?: number;
416
- /** Task-success score, 01. */
706
+ /** Task-success score, 0-1. */
417
707
  task_success?: number;
418
708
  /** Composite voice-agent quality index (engine-defined scale). */
419
709
  vaqi?: number;
@@ -551,6 +841,76 @@ export interface RecapCallTriggerInput {
551
841
  customer_name?: string;
552
842
  crm_fields?: Record<string, unknown>;
553
843
  }
844
+ /** PyAI's richer answered-by vocabulary (superset of Twilio's enum). */
845
+ export type AmdAnsweredBy = "human" | "voicemail" | "live_voicemail" | "screening" | "ivr" | "human_gatekeeper" | "sit_invalid" | "fax" | "silence" | "unknown";
846
+ /** Twilio's `AnsweredBy` enum, echoed for drop-in migration parity. */
847
+ export type AmdTwilioAnsweredBy = "human" | "machine_start" | "machine_end_beep" | "machine_end_silence" | "machine_end_other" | "fax" | "unknown";
848
+ export interface AmdConfigInput {
849
+ /** Operating point on the ROC curve, [0,1]. Near 0 = human-safe (never hang up
850
+ * on a person); near 1 = fire `machine` fast. A per-call TwiML `<Parameter>`
851
+ * overrides this. */
852
+ aggressiveness?: number;
853
+ /** Signed POST target for `amd.call.completed` events (https). */
854
+ webhookUrl?: string | null;
855
+ }
856
+ export interface AmdConfig {
857
+ object?: "amd.config";
858
+ aggressiveness?: number;
859
+ webhook_url?: string | null;
860
+ updated_at?: number;
861
+ }
862
+ export interface AmdCallSummary {
863
+ object?: "amd.call";
864
+ call_id: string;
865
+ session_label?: string | null;
866
+ status?: "completed" | "failed";
867
+ answered_by?: AmdAnsweredBy;
868
+ /** Twilio-enum projection of `answered_by` for drop-in routing parity. */
869
+ answered_by_twilio?: string | null;
870
+ confidence?: number | null;
871
+ /** Latency from answer to decision, in ms. */
872
+ decision_ms?: number | null;
873
+ created_at?: number;
874
+ }
875
+ export interface AmdCall extends AmdCallSummary {
876
+ /** Human-readable evidence, e.g. "machine phrase: 'leave a message' @1.2s". */
877
+ reason?: string | null;
878
+ aggressiveness?: number | null;
879
+ started_at?: number | null;
880
+ meta?: Record<string, unknown> | null;
881
+ error?: string | null;
882
+ }
883
+ /** A mid-call AMD decision event pushed on the stream (and to the webhook). */
884
+ export interface AmdDecisionEvent {
885
+ event?: "amd";
886
+ call_id?: string;
887
+ answered_by?: AmdAnsweredBy;
888
+ answered_by_twilio?: string | null;
889
+ confidence?: number | null;
890
+ decision_ms?: number | null;
891
+ reason?: string | null;
892
+ [k: string]: unknown;
893
+ }
894
+ export interface AmdStreamOptions {
895
+ /** Per-call operating point override (0-1), sent on the connect URL. */
896
+ aggressiveness?: number;
897
+ /** Opaque per-call tag, echoed back on the decision. */
898
+ sessionLabel?: string;
899
+ /** Extra query params merged onto the connect URL. */
900
+ query?: Record<string, string>;
901
+ /** Fired once the socket opens. */
902
+ onOpen?: () => void;
903
+ /** Fired when PyAI emits the `amd` decision event mid-call. */
904
+ onDecision?: (event: AmdDecisionEvent) => void;
905
+ /** Fired on any other JSON frame (forward-compatible). */
906
+ onMessage?: (frame: Record<string, unknown>) => void;
907
+ /** Fired on a transport-level error. */
908
+ onError?: (err: Error) => void;
909
+ /** Fired when the socket closes. */
910
+ onClose?: (code: number, reason: string) => void;
911
+ /** Injectable WebSocket constructor (defaults to the global). */
912
+ webSocket?: WebSocketCtor;
913
+ }
554
914
  export declare class PyAI {
555
915
  private readonly apiKey;
556
916
  private readonly baseURL;
@@ -628,7 +988,7 @@ export declare class PyAI {
628
988
  };
629
989
  /**
630
990
  * Introspect the calling key: scopes, environment, and limits. Useful for a
631
- * preflight/doctor check. (New route; older deployments may 404 handle it.)
991
+ * preflight/doctor check. (New route; older deployments may 404, handle it.)
632
992
  */
633
993
  me: () => Promise<MeResponse>;
634
994
  clones: {
@@ -695,7 +1055,7 @@ export declare class PyAI {
695
1055
  }) => Promise<ListResponse<TraceViolation>>;
696
1056
  };
697
1057
  findings: {
698
- /** List Tier-2 (async semantic) findings advisory, non-blocking. Scope `trace:read`. */
1058
+ /** List Tier-2 (async semantic) findings, advisory, non-blocking. Scope `trace:read`. */
699
1059
  list: (params?: {
700
1060
  checkId?: string;
701
1061
  action?: "flag" | "preempt_next" | "escalate";
@@ -744,12 +1104,61 @@ export declare class PyAI {
744
1104
  trigger: (callId: string, input: RecapCallTriggerInput) => Promise<RecapCallSummary>;
745
1105
  };
746
1106
  };
1107
+ amd: {
1108
+ config: {
1109
+ /** The org's AMD operating point + webhook. Scope `amd:configure`. */
1110
+ get: () => Promise<AmdConfig>;
1111
+ /** Set the account-default `aggressiveness` (0-1) and webhook. Scope `amd:configure`. */
1112
+ set: (input: AmdConfigInput) => Promise<AmdConfig>;
1113
+ };
1114
+ calls: {
1115
+ /** Recent AMD decisions, newest first. Scope `amd:read`. */
1116
+ list: (params?: {
1117
+ limit?: number;
1118
+ cursor?: string;
1119
+ sessionLabel?: string;
1120
+ }) => Promise<ListResponse<AmdCallSummary>>;
1121
+ /** The full decision (answered_by, reason, …) for one call. Scope `amd:read`. */
1122
+ get: (callId: string) => Promise<AmdCall>;
1123
+ };
1124
+ /**
1125
+ * Open a live AMD stream over `/v1/amd/stream` (Twilio Media Streams
1126
+ * protocol). Server-side helper for forking media yourself; the common
1127
+ * Twilio path is one line of TwiML, no SDK. Scope `amd:detect`.
1128
+ */
1129
+ stream: (opts?: AmdStreamOptions) => AmdStream;
1130
+ };
1131
+ omni: {
1132
+ /**
1133
+ * Mint an ephemeral, origin-locked Omni session token a browser can use to
1134
+ * open ONE realtime session **directly**, the public/private split for
1135
+ * realtime. **Call this from your server** with a secret key holding
1136
+ * `omni:session`; never ship the secret key to a page. Hand the returned
1137
+ * `token` to the browser, which connects with
1138
+ * `new WebSocket(session.url, ["pyai-key." + session.token])`. The token
1139
+ * expires after `ttlSeconds` (default 60s) and only works from
1140
+ * `allowedOrigins`. Scope `omni:session`.
1141
+ */
1142
+ createSession: (params: OmniSessionParams) => Promise<OmniSession>;
1143
+ /**
1144
+ * Open a live Omni agentic-voice session over `/v1/omni`. Returns an
1145
+ * {@link OmniConnection} that handles the wire protocol's frame-key
1146
+ * asymmetry for you, it sends `type`-keyed control frames (`configure`,
1147
+ * `dtmf`) and parses `event`-keyed server frames, so you can't trip the #1
1148
+ * Omni integration bug. Omni is zero-state: nothing to create first; the
1149
+ * agent's behavior travels in the `configure` frame. Pass `token` (from
1150
+ * `createSession`) to connect from a browser without the secret key.
1151
+ */
1152
+ connect: (opts?: OmniConnectOptions) => OmniConnection;
1153
+ };
747
1154
  /** Build the realtime WebSocket URL for the chosen product. */
748
1155
  realtimeURL(opts?: RealtimeOptions): string;
749
1156
  /** The subprotocol that carries the key on a WS upgrade (browser-safe auth). */
750
1157
  realtimeSubprotocol(): string;
751
1158
  /** Build the Hear streaming-STT WebSocket URL (`/v1/audio/transcriptions/stream`). */
752
1159
  hearStreamURL(opts?: HearStreamOptions): string;
1160
+ /** Build the AMD detection WebSocket URL (`/v1/amd/stream`). */
1161
+ amdStreamURL(opts?: AmdStreamOptions): string;
753
1162
  /**
754
1163
  * Open a realtime WebSocket. Uses the global WebSocket (browser, Node 22+).
755
1164
  * The key travels as a subprotocol so it works from the browser without