@oberik/sdk 0.59.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -160,6 +160,9 @@ export interface TokenRequest {
160
160
  allowed_models?: string[];
161
161
  /** Reasoning-effort ceiling. */
162
162
  max_effort?: "minimal" | "low" | "medium" | "high";
163
+ /** Ceiling on how many tool calls one turn may make. Narrows the project's own
164
+ * (Dashboard → LLM & limits) — never widens it. */
165
+ max_tool_iterations?: number;
163
166
  /** Ceiling on how much conversation history is replayed for this token, in tokens.
164
167
  * Narrows the project's own context budget (Dashboard → Context management) — it
165
168
  * can never widen it. Use it to give a cheap tier a shorter memory. */
@@ -1057,6 +1060,15 @@ export interface ChatRequest {
1057
1060
  * the `webhook_tools` capability. Turn it off for a turn that should not touch your
1058
1061
  * systems. */
1059
1062
  enable_webhook_tools?: boolean;
1063
+ /** Let the agent use its own conversation controls on a voice call — put the caller on
1064
+ * hold while something slow runs, then come back. Needs the `voice` capability.
1065
+ *
1066
+ * Nothing to do with whether a call can be OPENED: that is `voice.open()`, and it needs
1067
+ * the same capability. This is the per-turn switch, so a text turn in the same project
1068
+ * can decline the tools. They stay bound either way and refuse with a sentence saying
1069
+ * there is no call — a model that can see a tool it cannot use reasons better than one
1070
+ * whose toolset silently changed shape. */
1071
+ enable_voice_control?: boolean;
1060
1072
  enable_plugins?: boolean;
1061
1073
  /** Which plugins this turn may use, by name. Omit for everything the token can see.
1062
1074
  * Narrowing only — a name that is not visible is absent rather than an error, so a
@@ -2201,6 +2213,875 @@ export interface DownloadOptions {
2201
2213
  onProgress?: (received: number, total: number | null) => void;
2202
2214
  signal?: AbortSignal;
2203
2215
  }
2216
+ /**
2217
+ * Where a call is. Explicit state rather than something inferred from whether audio is
2218
+ * flowing — render it and your UI is honest about what is happening.
2219
+ *
2220
+ * `status` on the `state` event is the same thing in words a support agent can read; use
2221
+ * that rather than mapping these yourself, so a state added later renders correctly instead
2222
+ * of falling through to whichever branch omits it.
2223
+ */
2224
+ export type VoiceState = "listening" | "delegating" | "bridging" | "external_speech" | "canceling" | "holding" | "resuming" | "transferred" | "ended";
2225
+ /** How much of the caller is perceived during a hold.
2226
+ *
2227
+ * `interrupt_only` is the default and almost always right. `off` exists for the rare case
2228
+ * where being interrupted would break something — a caller who says "never mind" into hold
2229
+ * music and is not heard has been ignored by a system that thinks it is helping. */
2230
+ export type VoiceListen = "full" | "interrupt_only" | "dtmf_only" | "off";
2231
+ /** How faithfully the speech layer must realise the model's words.
2232
+ *
2233
+ * `strict` for figures, dates, addresses and confirmation codes: "about eighteen hundred"
2234
+ * is not an acceptable rendering of £1,847.23. `natural` (the default) may adapt punctuation
2235
+ * and disfluencies but never meaning. `free` lets the agent paraphrase, which is right for
2236
+ * small talk and wrong for anything a customer will act on. */
2237
+ export type VoiceFidelity = "strict" | "natural" | "free";
2238
+ export interface VoiceWaitingOptions {
2239
+ /** `"auto"` picks from measured latency: bridge under 2s, an announced wait to 10s, hold
2240
+ * beyond that. Anything else pins the behaviour, which is occasionally what you want for
2241
+ * a flow you have measured yourself. */
2242
+ strategy?: "auto" | "bridge" | "wait" | "hold";
2243
+ /** Named hold media from your project's assets. Omitted gets a default that is
2244
+ * unmistakably "you are on hold and the line is alive" — silence on a hold is
2245
+ * indistinguishable from a dropped call. */
2246
+ hold_music?: string | null;
2247
+ /** Whether the caller may interrupt the agent mid-sentence. Leave it on. */
2248
+ barge_in?: boolean;
2249
+ /** Perception during a hold. */
2250
+ listen_on_hold?: VoiceListen;
2251
+ /** Keep the speech model quiet for the whole wait, not only the beat after it acknowledges.
2252
+ *
2253
+ * A conversational model with the floor and nothing to say does not wait, it talks — and what
2254
+ * it says is invented, because it has no idea what is being looked up. With this on it
2255
+ * acknowledges you once in its own words and then stays silent until you speak, the answer
2256
+ * arrives, or the wait has gone on long enough that the runtime remarks on it (irregularly,
2257
+ * and never with the same line twice running). Off by default: a frontend held quiet cannot
2258
+ * backchannel, and on a fast lookup the silence can feel more abrupt than a murmur. */
2259
+ quiet_while_waiting?: boolean;
2260
+ /** Give the utterance *after* a barge-in longer to finish.
2261
+ *
2262
+ * A caller who cuts in is mid-thought, and mid-thought is where they pause — so at the
2263
+ * ordinary end-of-turn threshold the breath ends the turn, the half-sentence is transcribed,
2264
+ * and the agent answers a question nobody finished asking. On, the detector waits about a
2265
+ * second longer, and only on the turn that was interrupted. Off by default: it is latency on
2266
+ * every interrupted turn to fix a problem on some of them. */
2267
+ patient_bargein?: boolean;
2268
+ /** Keep the answer in flight when you interrupt, instead of abandoning it.
2269
+ *
2270
+ * A barge-in fires 160ms into your speech — before anything can know whether you are asking
2271
+ * something else or just saying "mm-hmm" — so abandoning the turn there loses the answer to a
2272
+ * backchannel. On, the agent stops *speaking* at once and keeps *thinking*; a real new request
2273
+ * still supersedes it. Off by default: interrupt to change the subject and you may hear the
2274
+ * tail of the previous answer arrive behind the new one. */
2275
+ keep_answer_through_bargein?: boolean;
2276
+ /** Keep the agent's own voice out of your transcript when your microphone hears it.
2277
+ *
2278
+ * With an echo path the transcriber attributes the agent's speech to you and the agent then
2279
+ * answers itself — reproduced at 0.5 echo gain, "Thank you for calling Wessex Power" and
2280
+ * "There's an active outage in your area" both came back as things the caller had said. Off by
2281
+ * default: set too strictly the protection costs turns, and not being heard is worse than
2282
+ * being misheard. */
2283
+ guard_asr_echo?: boolean;
2284
+ /** Apply the echo guard to the transcriber's buffer at the onset only, as the detector does.
2285
+ * On is the corrected behaviour; off restores the per-frame exclusion that dropped a caller
2286
+ * who talked over the answer at around the agent's own volume. */
2287
+ onset_echo_only?: boolean;
2288
+ /** How much louder than the agent's own output the caller must be to take the floor. Omit for
2289
+ * the detector's default. Lower admits quieter barge-ins and more of the agent's own echo. */
2290
+ echo_ratio?: number;
2291
+ /** Learn how loud the agent's own voice comes back and lower the barge-in floor to suit the
2292
+ * room. Can only lower it, never raise it, so the worst case is `echo_ratio`'s behaviour. */
2293
+ adaptive_echo?: boolean;
2294
+ /** Hold the speech model silent through every gap, not just while it waits.
2295
+ *
2296
+ * Measured cause of the corruption, the invented facts and the repetition, which are one
2297
+ * bug: 55 words handed over, 175 spoken. The runtime's text is exact; the other 120 are
2298
+ * the model filling gaps it has nothing to say in, and they come out as word salad. This
2299
+ * closes the last of them — at the cost of its own acknowledgement, so it is off by
2300
+ * default. */
2301
+ quiet_unless_answering?: boolean;
2302
+ }
2303
+ export interface VoiceSessionRequest {
2304
+ /** Which voice, from the ones the frontend has. Omit for the project's default. */
2305
+ voice?: string | null;
2306
+ /** Which role to condition it into — `support`, `sales`. Where the frontend supports it. */
2307
+ role?: string | null;
2308
+ /** Extra instructions for the SPEAKING half. Your project's system prompt still governs
2309
+ * the answering half; this is for how it talks, not what it knows. */
2310
+ system?: string | null;
2311
+ /** Which model answers. Bounded by the token's model allow-list, like everywhere else. */
2312
+ model?: string | null;
2313
+ /** Which checkpoint on the deployment's speech host, where it serves more than one — a base
2314
+ * model and a fine-tune of it, say. `voice.frontend()` lists what there is. Not "which
2315
+ * host": there is one per deployment. Asking for one the host does not serve is a 400
2316
+ * rather than a silent fallback, so an A/B cannot end up comparing a model with itself. */
2317
+ voice_model?: string | null;
2318
+ /** Which speech frontend runs this call, overriding the project's setting. Per call for the
2319
+ * same reason the model is: comparing two arrangements means hearing them one after the
2320
+ * other, and a project-level setting alone makes that a round trip through the settings
2321
+ * page between every pair. Refused if the deployment does not serve it. */
2322
+ frontend?: string | null;
2323
+ /** Narrow the tools this call may use, by name. Intersected with what the token already
2324
+ * allows — a call can decline a tool and can never buy one. Omit for everything the token
2325
+ * allows; `[]` means none, which is a different call.
2326
+ *
2327
+ * Worth using. A support line has no business running sandboxed compute, and every tool
2328
+ * the model can see is a tool it can spend a caller's silence on. */
2329
+ allowed_tools?: string[] | null;
2330
+ fidelity?: VoiceFidelity;
2331
+ waiting?: VoiceWaitingOptions;
2332
+ /** Continue an existing conversation, so the caller picks up where they left off. */
2333
+ session_id?: string | null;
2334
+ /** Hard ceiling on this call in seconds. Narrowed by the token's own `max_voice_seconds`
2335
+ * and by the deployment's — never widened. */
2336
+ max_seconds?: number | null;
2337
+ }
2338
+ export interface VoiceSessionOut {
2339
+ id: string;
2340
+ /** The chat session this call's turns land in, so a transcript is readable afterwards. */
2341
+ session_id: string;
2342
+ /** WebSocket URL for audio and events. Relative to the API host. */
2343
+ stream_url: string;
2344
+ frontend: string;
2345
+ voice: string | null;
2346
+ role: string | null;
2347
+ /** The checkpoint serving this call, where the host names them. */
2348
+ voice_model: string;
2349
+ /** Sample rate the frontend generates and expects, in Hz. Send whatever you have; it is
2350
+ * resampled. Play back at this rate. */
2351
+ rate: number;
2352
+ /** Frame period in milliseconds. Informational — the server owns the clock. */
2353
+ frame_ms: number;
2354
+ /** True when the caller is on a channel narrower than the model generates — a phone line.
2355
+ * It will not sound like the browser demo, and that is the telephone network rather than
2356
+ * a setting. Surfaced per call so support can stop guessing. */
2357
+ narrowband: boolean;
2358
+ /** Whether this token may hand the call to a person. */
2359
+ can_transfer: boolean;
2360
+ }
2361
+ /** A live call, as the server reports it. Everything a UI needs in one object. */
2362
+ export interface VoiceSnapshot {
2363
+ state: VoiceState;
2364
+ /** The state in words, for a screen. Computed server-side from what is actually true, so
2365
+ * a new state renders correctly without a client release. */
2366
+ status: string;
2367
+ generation: number | null;
2368
+ epoch: number;
2369
+ listening: VoiceListen;
2370
+ output: "voice" | "hold" | "announcement" | "party" | "silence";
2371
+ queued_ms: number;
2372
+ hold: {
2373
+ id: string;
2374
+ reason: string;
2375
+ elapsed_ms: number;
2376
+ listen: VoiceListen;
2377
+ } | null;
2378
+ transfer: {
2379
+ id: string;
2380
+ target: string;
2381
+ } | null;
2382
+ turns: number;
2383
+ narrowband: boolean;
2384
+ }
2385
+ /**
2386
+ * One frame from a call.
2387
+ *
2388
+ * The register is deliberately flat and every event carries `at_ms` — milliseconds since the
2389
+ * call opened, measured server-side. Relative rather than absolute because every number that
2390
+ * matters here is a difference, and a wall clock makes two traces from two machines
2391
+ * incomparable.
2392
+ */
2393
+ export type VoiceEvent = {
2394
+ event: "session.open";
2395
+ data: {
2396
+ session_id: string;
2397
+ frontend: string;
2398
+ voice: string | null;
2399
+ transport: string;
2400
+ model: string | null;
2401
+ at_ms: number;
2402
+ };
2403
+ } | {
2404
+ event: "session.close";
2405
+ data: {
2406
+ session_id: string;
2407
+ reason: string;
2408
+ duration_ms: number;
2409
+ at_ms: number;
2410
+ };
2411
+ }
2412
+ /** The call moved. `cause` says why, which is what makes a trace readable six states later. */
2413
+ | {
2414
+ event: "state";
2415
+ data: {
2416
+ from: VoiceState;
2417
+ to: VoiceState;
2418
+ cause: string;
2419
+ at_ms: number;
2420
+ };
2421
+ } | {
2422
+ event: "caller.speech_start";
2423
+ data: {
2424
+ at_ms: number;
2425
+ };
2426
+ } | {
2427
+ event: "caller.speech_end";
2428
+ data: {
2429
+ at_ms: number;
2430
+ };
2431
+ } | {
2432
+ event: "caller.transcript";
2433
+ data: {
2434
+ text: string;
2435
+ final: boolean;
2436
+ at_ms: number;
2437
+ };
2438
+ } | {
2439
+ event: "agent.transcript";
2440
+ data: {
2441
+ text: string;
2442
+ at_ms: number;
2443
+ };
2444
+ }
2445
+ /** A request went to the reasoner. `instruction` is what it was asked, which is not
2446
+ * necessarily what the caller said. */
2447
+ | {
2448
+ event: "delegate.start";
2449
+ data: {
2450
+ generation_id: number;
2451
+ instruction: string;
2452
+ source: string;
2453
+ at_ms: number;
2454
+ turn_id: number;
2455
+ revision: number;
2456
+ };
2457
+ }
2458
+ /** A request was abandoned. Nothing from it will be spoken — not "queued and skipped": a
2459
+ * superseded answer cannot reach the caller. */
2460
+ | {
2461
+ event: "delegate.cancel";
2462
+ data: {
2463
+ generation_id: number;
2464
+ reason: string;
2465
+ at_ms: number;
2466
+ };
2467
+ } | {
2468
+ event: "delegate.failed";
2469
+ data: {
2470
+ generation_id: number;
2471
+ detail: string;
2472
+ at_ms: number;
2473
+ };
2474
+ } | {
2475
+ event: "delegate.end";
2476
+ data: {
2477
+ generation_id: number;
2478
+ chars_spoken: number;
2479
+ at_ms: number;
2480
+ };
2481
+ }
2482
+ /** The model's first user-visible character. Thinking and tool-call regions do not count —
2483
+ * those are never spoken, so counting them would make a slow model look fast. */
2484
+ | {
2485
+ event: "reasoner.first_token";
2486
+ data: {
2487
+ generation_id: number;
2488
+ ttft_ms: number;
2489
+ at_ms: number;
2490
+ };
2491
+ } | {
2492
+ event: "reasoner.delta";
2493
+ data: {
2494
+ generation_id: number;
2495
+ text: string;
2496
+ at_ms: number;
2497
+ };
2498
+ } | {
2499
+ event: "reasoner.tool_start";
2500
+ data: {
2501
+ generation_id: number;
2502
+ name: string;
2503
+ id: string;
2504
+ at_ms: number;
2505
+ };
2506
+ } | {
2507
+ event: "reasoner.tool_end";
2508
+ data: {
2509
+ generation_id: number;
2510
+ name: string;
2511
+ id: string;
2512
+ ms: number;
2513
+ at_ms: number;
2514
+ };
2515
+ }
2516
+ /** The filler it chose while the model starts. */
2517
+ | {
2518
+ event: "voice.bridge";
2519
+ data: {
2520
+ generation_id: number;
2521
+ text: string;
2522
+ budget_ms: number;
2523
+ at_ms: number;
2524
+ };
2525
+ }
2526
+ /** From here the caller is hearing the model's words in the frontend's voice. */
2527
+ | {
2528
+ event: "voice.takeover";
2529
+ data: {
2530
+ generation_id: number;
2531
+ buffered_chars: number;
2532
+ at_ms: number;
2533
+ };
2534
+ }
2535
+ /** The caller interrupted and the agent stopped speaking but kept working. Only with
2536
+ * `keep_answer_through_bargein`; followed by `delegate.end`, it is an answer that survived an
2537
+ * interruption instead of being lost to a backchannel. */
2538
+ | {
2539
+ event: "delegate.held";
2540
+ data: {
2541
+ generation_id: number;
2542
+ reason: string;
2543
+ at_ms: number;
2544
+ };
2545
+ }
2546
+ /** How many frames carried a word the model chose rather than one the runtime supplied. A count
2547
+ * over the host's own `forced` flag — no transcription, no constants. */
2548
+ | {
2549
+ event: "voice.gated";
2550
+ data: {
2551
+ kind: string;
2552
+ text: string;
2553
+ at_ms: number;
2554
+ };
2555
+ } | {
2556
+ event: "voice.ungated";
2557
+ data: {
2558
+ kind: string;
2559
+ at_ms: number;
2560
+ };
2561
+ } | {
2562
+ event: "turn.held";
2563
+ data: {
2564
+ reason: string;
2565
+ act: string;
2566
+ wait_ms: number;
2567
+ marked: boolean;
2568
+ at_ms: number;
2569
+ };
2570
+ } | {
2571
+ event: "caller.echoed";
2572
+ data: {
2573
+ text: string;
2574
+ kept: string;
2575
+ at_ms: number;
2576
+ };
2577
+ } | {
2578
+ event: "voice.echo";
2579
+ data: {
2580
+ gain: number;
2581
+ peak: number;
2582
+ floor: number;
2583
+ samples: number;
2584
+ at_ms: number;
2585
+ };
2586
+ } | {
2587
+ event: "voice.own_words";
2588
+ data: {
2589
+ frames: number;
2590
+ forced: number;
2591
+ own: number;
2592
+ sample: string;
2593
+ at_ms: number;
2594
+ };
2595
+ }
2596
+ /** What the agent produced against what the caller actually heard, at the end of a call. The
2597
+ * one instrument that can see speech corruption. `worst_uninterrupted` is the number that
2598
+ * means something — an interrupted turn legitimately scores badly. */
2599
+ | {
2600
+ event: "voice.fidelity";
2601
+ data: {
2602
+ turns: number;
2603
+ unscorable: number;
2604
+ worst: number;
2605
+ worst_uninterrupted: number;
2606
+ spans_intact: boolean;
2607
+ wrote?: string;
2608
+ heard?: string;
2609
+ at_ms: number;
2610
+ };
2611
+ }
2612
+ /** The runtime remarked on a wait that has gone on. Only with `quiet_while_waiting`: the
2613
+ * frontend is held silent for the whole lookup, and this stops the silence reading as a
2614
+ * dropped call. Irregular by design, and never the same line twice running. */
2615
+ | {
2616
+ event: "voice.patience";
2617
+ data: {
2618
+ generation_id: number;
2619
+ text: string;
2620
+ waited_ms: number;
2621
+ at_ms: number;
2622
+ };
2623
+ }
2624
+ /** A grounded answer the frontend never said, spoken as written instead. `free` fidelity bets
2625
+ * that a frontend handed a fact will compose a sentence about it; this is that bet losing. */
2626
+ | {
2627
+ event: "voice.grounding_backstop";
2628
+ data: {
2629
+ generation_id: number;
2630
+ chars: number;
2631
+ waited_ms: number;
2632
+ at_ms: number;
2633
+ };
2634
+ }
2635
+ /** The answer was given to the frontend as grounding rather than as words to say. Only on a
2636
+ * reference-conditioned arrangement: what the caller hears next is the frontend's own
2637
+ * sentence about this fact, not this text read out. */
2638
+ | {
2639
+ event: "voice.conditioned";
2640
+ data: {
2641
+ generation_id: number;
2642
+ chars: number;
2643
+ at_ms: number;
2644
+ };
2645
+ }
2646
+ /** An answer thrown away because its generation was no longer current. On a
2647
+ * reference-conditioned frontend this is the only protection there is — conditioning cannot
2648
+ * be cut off mid-word the way injected text can. */
2649
+ | {
2650
+ event: "voice.dropped";
2651
+ data: {
2652
+ generation_id: number;
2653
+ chars: number;
2654
+ why: string;
2655
+ at_ms: number;
2656
+ };
2657
+ } | {
2658
+ event: "voice.release";
2659
+ data: {
2660
+ generation_id: number;
2661
+ at_ms: number;
2662
+ };
2663
+ }
2664
+ /** The caller barged in. `dropped_chars` is what they did not hear. */
2665
+ | {
2666
+ event: "voice.interrupted";
2667
+ data: {
2668
+ generation_id: number;
2669
+ spoken_chars: number;
2670
+ dropped_chars: number;
2671
+ at_ms: number;
2672
+ };
2673
+ }
2674
+ /** The paced queue ran dry mid-answer — the model is slower than speech. Audible as a
2675
+ * pause inside a sentence, and the number that says whether the bridge was long enough. */
2676
+ | {
2677
+ event: "voice.starved";
2678
+ data: {
2679
+ generation_id: number;
2680
+ ms: number;
2681
+ at_ms: number;
2682
+ };
2683
+ } | {
2684
+ event: "wait.chosen";
2685
+ data: {
2686
+ generation_id: number;
2687
+ mode: "bridge" | "wait" | "hold" | "transfer";
2688
+ predicted_ms: number | null;
2689
+ basis: string;
2690
+ at_ms: number;
2691
+ };
2692
+ } | {
2693
+ event: "hold.requested";
2694
+ data: {
2695
+ hold_id: string;
2696
+ reason: string;
2697
+ mode: string;
2698
+ listen: VoiceListen;
2699
+ max_ms: number;
2700
+ at_ms: number;
2701
+ };
2702
+ } | {
2703
+ event: "hold.entered";
2704
+ data: {
2705
+ hold_id: string;
2706
+ media: string;
2707
+ at_ms: number;
2708
+ };
2709
+ } | {
2710
+ event: "hold.interrupted";
2711
+ data: {
2712
+ hold_id: string;
2713
+ heard: string;
2714
+ at_ms: number;
2715
+ };
2716
+ } | {
2717
+ event: "hold.progress";
2718
+ data: {
2719
+ hold_id: string;
2720
+ text: string;
2721
+ elapsed_ms: number;
2722
+ at_ms: number;
2723
+ };
2724
+ } | {
2725
+ event: "hold.completed";
2726
+ data: {
2727
+ hold_id: string;
2728
+ outcome: "done" | "failed" | "interrupted" | "timeout" | "abandoned";
2729
+ elapsed_ms: number;
2730
+ at_ms: number;
2731
+ };
2732
+ } | {
2733
+ event: "resume.started";
2734
+ data: {
2735
+ hold_id: string;
2736
+ transition: string;
2737
+ speech_hint: string;
2738
+ at_ms: number;
2739
+ };
2740
+ } | {
2741
+ event: "resume.done";
2742
+ data: {
2743
+ hold_id: string;
2744
+ silence_ms: number;
2745
+ at_ms: number;
2746
+ };
2747
+ } | {
2748
+ event: "transfer.requested";
2749
+ data: {
2750
+ transfer_id: string;
2751
+ target: string;
2752
+ context: string;
2753
+ at_ms: number;
2754
+ };
2755
+ } | {
2756
+ event: "transfer.consulting";
2757
+ data: {
2758
+ transfer_id: string;
2759
+ target: string;
2760
+ at_ms: number;
2761
+ };
2762
+ } | {
2763
+ event: "transfer.completed";
2764
+ data: {
2765
+ transfer_id: string;
2766
+ target: string;
2767
+ elapsed_ms: number;
2768
+ at_ms: number;
2769
+ };
2770
+ }
2771
+ /** Nobody took the call. The caller is back with the agent, not on a dead line. */
2772
+ | {
2773
+ event: "transfer.failed";
2774
+ data: {
2775
+ transfer_id: string;
2776
+ target: string;
2777
+ detail: string;
2778
+ at_ms: number;
2779
+ };
2780
+ } | {
2781
+ event: "media.source";
2782
+ data: {
2783
+ source: string;
2784
+ fade_ms: number;
2785
+ at_ms: number;
2786
+ };
2787
+ }
2788
+ /** Audio has actually played out to the caller. The only honest "they heard it" — anything
2789
+ * else is a guess about a buffer that is not yours. */
2790
+ /** A mark was placed where a protected figure finished being spoken, or an answer did.
2791
+ * Call `markPlayed(mark)` once your player has passed it — that confirmation is the only
2792
+ * thing that turns "the balance was sent" into "the balance was heard". */
2793
+ | {
2794
+ event: "media.mark_placed";
2795
+ data: {
2796
+ mark: string;
2797
+ source: string;
2798
+ what: string;
2799
+ at_ms: number;
2800
+ };
2801
+ }
2802
+ /** Your confirmation came back and the server recorded it. */
2803
+ | {
2804
+ event: "media.mark";
2805
+ data: {
2806
+ mark: string;
2807
+ source: string;
2808
+ at_ms: number;
2809
+ };
2810
+ } | {
2811
+ event: "context.injected";
2812
+ data: {
2813
+ keys: string[];
2814
+ at_ms: number;
2815
+ };
2816
+ } | {
2817
+ event: "error";
2818
+ data: {
2819
+ detail: string;
2820
+ where: string;
2821
+ at_ms: number;
2822
+ };
2823
+ }
2824
+ /** What the caller said, kept as context rather than acted on — this frontend decides for
2825
+ * itself when a turn needs the agent. Reported because "the transcript changed nothing" and
2826
+ * "no transcript arrived" are different facts. */
2827
+ | {
2828
+ event: "caller.context";
2829
+ data: {
2830
+ text: string;
2831
+ at_ms: number;
2832
+ };
2833
+ }
2834
+ /** A transcript arrived and this project has them switched off, so it was discarded. */
2835
+ | {
2836
+ event: "caller.transcript_ignored";
2837
+ data: {
2838
+ text: string;
2839
+ at_ms: number;
2840
+ };
2841
+ }
2842
+ /** What the speech model itself said the caller wanted, and whether that needs the agent. The
2843
+ * delegation text coming from the model that *heard* the call rather than a transcript of it. */
2844
+ | {
2845
+ event: "monologue.turn";
2846
+ data: {
2847
+ text: string;
2848
+ delegating: boolean;
2849
+ at_ms: number;
2850
+ };
2851
+ }
2852
+ /** The caller finished a turn and the speech model said nothing at all within the window. No
2853
+ * instruction, so no delegation — reported rather than silent. */
2854
+ | {
2855
+ event: "monologue.empty";
2856
+ data: {
2857
+ waited_ms: number;
2858
+ at_ms: number;
2859
+ };
2860
+ }
2861
+ /** A second delegation for a turn already answered, refused. Two paths can start one; seeing
2862
+ * this is normal with a transcriber, never seeing it means one of them is dead. */
2863
+ | {
2864
+ event: "delegate.duplicate";
2865
+ data: {
2866
+ source: string;
2867
+ instruction: string;
2868
+ at_ms: number;
2869
+ };
2870
+ }
2871
+ /** The selected model thinking. **Never spoken** — carried so a client can show it. */
2872
+ | {
2873
+ event: "reasoner.reasoning";
2874
+ data: {
2875
+ generation_id: number;
2876
+ text: string;
2877
+ at_ms: number;
2878
+ };
2879
+ }
2880
+ /** A caller turn needed nothing looked up, the speech model did not answer it either, and the
2881
+ * runtime spoke rather than leave the caller listening to silence. */
2882
+ | {
2883
+ event: "voice.backstop";
2884
+ data: {
2885
+ waited_ms: number;
2886
+ delegating?: boolean;
2887
+ at_ms: number;
2888
+ };
2889
+ }
2890
+ /** The frontend was given the gap while the agent thought, said nothing audible, and the
2891
+ * runtime covered it. An uncovered gap is a caller who believes the line has dropped. */
2892
+ | {
2893
+ event: "voice.bridge_backstop";
2894
+ data: {
2895
+ generation_id: number;
2896
+ waited_ms: number;
2897
+ at_ms: number;
2898
+ };
2899
+ }
2900
+ /** A mark reached the caller's ear. The client's half of the fidelity guarantee: everything
2901
+ * the server knows is about what it *sent*. */
2902
+ | {
2903
+ event: "media.mark_placed";
2904
+ data: {
2905
+ mark: string;
2906
+ source: string;
2907
+ what: string;
2908
+ at_ms: number;
2909
+ };
2910
+ }
2911
+ /** The application's own line was queued into the frontend's monologue, displacing whatever
2912
+ * of the previous answer had not been spoken yet. */
2913
+ | {
2914
+ event: "speech.injected";
2915
+ data: {
2916
+ chars: number;
2917
+ displaced_chars: number;
2918
+ at_ms: number;
2919
+ };
2920
+ };
2921
+ export type VoiceEventName = VoiceEvent["event"];
2922
+ export interface VoiceHandlers {
2923
+ /** Audio for the caller: 16-bit signed little-endian PCM at `VoiceSessionOut.rate`. */
2924
+ onAudio?: (pcm: ArrayBuffer) => void;
2925
+ /** Every event. Use this for a trace panel; use `on()` for one kind. */
2926
+ onEvent?: (ev: VoiceEvent) => void;
2927
+ /** The call moved. The one handler a UI genuinely needs. */
2928
+ onState?: (snapshot: {
2929
+ from: VoiceState;
2930
+ to: VoiceState;
2931
+ cause: string;
2932
+ }) => void;
2933
+ onError?: (detail: string) => void;
2934
+ /** The socket closed, for any reason. */
2935
+ onClose?: (reason: string) => void;
2936
+ }
2937
+ /**
2938
+ * A live call.
2939
+ *
2940
+ * Audio in with `send`, audio out through `onAudio`, and everything else as events. The
2941
+ * call ends when you `close()` it, when the caller hangs up, or when its ceiling is reached.
2942
+ *
2943
+ * `hold`/`resume`/`transfer` are here as well as being tools the agent can call, because the
2944
+ * decision is sometimes your application's rather than the model's — an app that already
2945
+ * knows the CRM lookup it just triggered takes half a minute should not have to hope the
2946
+ * agent works that out.
2947
+ */
2948
+ export interface VoiceCall {
2949
+ readonly id: string;
2950
+ readonly sessionId: string;
2951
+ readonly rate: number;
2952
+ readonly info: VoiceSessionOut;
2953
+ /** Send caller audio. 16-bit signed little-endian PCM, any rate — it is resampled.
2954
+ *
2955
+ * Any view is accepted, not just `Uint8Array`: a browser's Web Audio path produces an
2956
+ * `Int16Array` and requiring a byte view would make every caller write the same three lines
2957
+ * of reinterpretation. */
2958
+ send(pcm: ArrayBuffer | ArrayBufferView): void;
2959
+ /** Subscribe to one kind of event. Returns an unsubscribe function. */
2960
+ on<K extends VoiceEventName>(name: K, fn: (data: Extract<VoiceEvent, {
2961
+ event: K;
2962
+ }>["data"]) => void): () => void;
2963
+ /** The call as the server sees it, now. */
2964
+ snapshot(): Promise<VoiceSnapshot>;
2965
+ /** Put the caller on hold from your side. */
2966
+ hold(opts?: {
2967
+ reason?: string;
2968
+ listen?: VoiceListen;
2969
+ max_seconds?: number;
2970
+ }): Promise<void>;
2971
+ /** Take them off hold, optionally with a line to come back on. */
2972
+ resume(opts?: {
2973
+ lead?: string;
2974
+ }): Promise<void>;
2975
+ /**
2976
+ * Run something slow behind a hold, and come back when it finishes.
2977
+ *
2978
+ * The primitive you actually want when the slow thing is **yours** — a CRM lookup, a booking
2979
+ * system, a payment confirmation your own backend does. `hold()` and `resume()` are the two
2980
+ * halves; this owns the race between them, which is where the bugs are:
2981
+ *
2982
+ * ```ts
2983
+ * const account = await call.holdUntil(
2984
+ * () => crm.expensiveLookup(customerId),
2985
+ * { reason: "looking up the account", maxSeconds: 90 },
2986
+ * );
2987
+ * if (account.outcome === "done") {
2988
+ * // The caller is already back and has heard "thanks for waiting". Say the answer.
2989
+ * }
2990
+ * ```
2991
+ *
2992
+ * Four things can happen and all four are returned rather than thrown: the work finishes,
2993
+ * the work fails, the ceiling is reached, or **the caller talks their way out of it**. That
2994
+ * last one is why this exists — a caller who says "actually never mind" into thirty seconds
2995
+ * of hold music and is not heard has been hung up on by a system that thinks it is helping,
2996
+ * and getting it right by hand means racing four conditions and cancelling in the right
2997
+ * order.
2998
+ *
2999
+ * A ceiling is enforced whether you pass one or not. A hold with no ceiling is a caller on
3000
+ * hold until they give up, which is the most common way an automated line loses a customer.
3001
+ */
3002
+ holdUntil<T>(work: () => Promise<T>, opts?: {
3003
+ reason?: string;
3004
+ listen?: VoiceListen;
3005
+ maxSeconds?: number;
3006
+ lead?: string;
3007
+ }): Promise<{
3008
+ outcome: "done" | "failed" | "timeout" | "interrupted";
3009
+ result?: T;
3010
+ error?: unknown;
3011
+ }>;
3012
+ /** Hand the call to a person. Needs `voice:transfer`. */
3013
+ transfer(to: string, context: string): Promise<void>;
3014
+ /** Tell the frontend something it should KNOW and not necessarily say — an account tier
3015
+ * that shapes tone, a customer id it must never read out. Distinct from speech on
3016
+ * purpose: conflating the two is what makes an agent recite a reference number. */
3017
+ inject(facts: Record<string, unknown>): Promise<void>;
3018
+ /** Say a line YOU wrote, in the agent's voice. The other half of `inject()`: that one is
3019
+ * "know this", this one is "say this".
3020
+ *
3021
+ * For what only your application knows in the moment — a card that was just declined, an
3022
+ * appointment that has moved, a line compliance requires on every call. **Not for
3023
+ * answers**: an answer is a turn, and it gets a model, a bridge while it is being composed,
3024
+ * and a latency you can measure. Text pushed through here gets none of that.
3025
+ *
3026
+ * It goes through the same machinery the agent's own speech does, so a caller can talk over
3027
+ * it — which is the point. A line the caller cannot interrupt is a line they keep hearing
3028
+ * while they are trying to object to it.
3029
+ *
3030
+ * `interrupt` (default true) **takes the floor**: an answer being spoken is abandoned, and
3031
+ * `displaced_chars` says how much of it. That is right for what this is for — a declined
3032
+ * card outranks the sentence it lands in the middle of. Pass `false` for a disclosure that
3033
+ * should wait; it is refused rather than chopping an answer in half.
3034
+ *
3035
+ * Returns what happened rather than resolving regardless. On hold or mid-transfer the
3036
+ * honest answer is no, and `reason` says which. */
3037
+ say(text: string, opts?: {
3038
+ interrupt?: boolean;
3039
+ }): Promise<{
3040
+ spoken: boolean;
3041
+ chars?: number;
3042
+ displaced_chars?: number;
3043
+ reason?: string;
3044
+ }>;
3045
+ /** The caller started talking. Send this the moment your own voice detection fires —
3046
+ * within a couple of hundred milliseconds — and stop your local playback in the same
3047
+ * breath.
3048
+ *
3049
+ * Waiting for a transcript is waiting for a whole utterance, by which time the agent has
3050
+ * talked over them. This is the client's half of barge-in; the server drops what it had
3051
+ * queued and abandons the answer in flight. */
3052
+ speechStart(): void;
3053
+ /** The caller stopped talking. */
3054
+ speechEnd(): void;
3055
+ /** What the caller said, if your client does its own transcription. Omit it and the
3056
+ * server's frontend supplies one — this is for a deployment that already has an ASR it
3057
+ * trusts, or for driving a call from text. */
3058
+ transcript(text: string, final?: boolean): void;
3059
+ /** A keypad press. On a hold, this is the classic "press 1 to come back". */
3060
+ dtmf(digit: string): void;
3061
+ /** The caller is gone. Distinct from `close()`: this says the *person* left, which is what
3062
+ * ends the conversation; `close()` only drops your connection to it. */
3063
+ hangUp(): void;
3064
+ /** Audio the server sent has finished playing out to the caller.
3065
+ *
3066
+ * Pass the `mark` from a `media.mark_placed` event once your player has played past that
3067
+ * point. Until you do, `verbatim` figures in the call's fidelity report read `heard: null` —
3068
+ * unknown, which is the honest answer, because everything else the server knows is about
3069
+ * what it *sent*.
3070
+ *
3071
+ * Only meaningful if you can actually tell — a browser scheduling PCM cannot, and guessing
3072
+ * is worse than not reporting: a mark confirmed early tells the server a sentence was
3073
+ * heard when it was still queued, which is the one fact a barge-in decision must not get
3074
+ * wrong. */
3075
+ markPlayed(mark: string): void;
3076
+ /** Latencies and counts for this call. The counts matter as much as the timings: an
3077
+ * average over turns that produced audio improves when the system gets worse. */
3078
+ bench(): Promise<{
3079
+ timings: Record<string, unknown>;
3080
+ counts: Record<string, number>;
3081
+ }>;
3082
+ close(): void;
3083
+ readonly closed: boolean;
3084
+ }
2204
3085
  /**
2205
3086
  * Make the arguments match the declared types, as far as that is safe.
2206
3087
  *
@@ -2403,7 +3284,14 @@ export declare class AgentFramework {
2403
3284
  * Mint a narrower token from the current credential — for a backend holding a
2404
3285
  * project key that hands short-lived, per-end-user tokens to its frontend. Every
2405
3286
  * field is intersected/clamped with what the caller already holds, so this can only
2406
- * ever narrow: a restricted token cannot mint a broader one.
3287
+ * ever narrow: a restricted token cannot mint a broader one. A bound the caller holds
3288
+ * applies whether or not you name the field, because an absent claim reads as
3289
+ * *unrestricted*.
3290
+ *
3291
+ * `expires_in` is the one exception, deliberately: it is clamped against the PROJECT's
3292
+ * maximum rather than the caller's remaining lifetime, so a short-lived token can mint
3293
+ * a longer-lived one. It is the same grant for longer, never a wider one — but size a
3294
+ * token's lifetime by what it may do, not by whatever minted it.
2407
3295
  */
2408
3296
  auth: {
2409
3297
  token: (body?: TokenRequest) => Promise<TokenResponse>;
@@ -2731,6 +3619,82 @@ export declare class AgentFramework {
2731
3619
  }) => Promise<SessionOut>;
2732
3620
  };
2733
3621
  };
3622
+ /**
3623
+ * Live voice calls.
3624
+ *
3625
+ * `open()` creates the call and connects the audio socket; everything after that is
3626
+ * `send()` for microphone audio, `onAudio` for what to play, and events for what is
3627
+ * happening. The token is the same end-user JWT everything else uses, and the turns
3628
+ * behind the call are ordinary turns — same capability gate, same tool allow-list, same
3629
+ * per-customer cost attribution.
3630
+ *
3631
+ * Needs a `WebSocket` global: present in browsers and in Node 22+. On older Node, pass
3632
+ * one in as `webSocket` — the SDK deliberately has no dependencies, so it will not bring
3633
+ * an implementation of its own.
3634
+ */
3635
+ voice: {
3636
+ open: (body?: VoiceSessionRequest, handlers?: VoiceHandlers, opts?: {
3637
+ webSocket?: unknown;
3638
+ }) => Promise<VoiceCall>;
3639
+ /** Reconnect to a call that is already open — a page reload, a second screen watching.
3640
+ *
3641
+ * Audio goes to whoever is connected; events go to everyone. So a supervisor can watch
3642
+ * a call without taking it over, which is what a call-centre floor actually needs. */
3643
+ attach: (id: string, handlers?: VoiceHandlers, opts?: {
3644
+ webSocket?: unknown;
3645
+ }) => Promise<VoiceCall>;
3646
+ /** Every call this token can see. */
3647
+ list: () => Promise<VoiceSnapshot[]>;
3648
+ /** What the frontend can do — the voices and roles a project may ask for. Read it rather
3649
+ * than hard-coding a list: asking for a voice the host does not have fails the call at
3650
+ * `open()`, which is a worse place to find out than a dropdown. */
3651
+ frontend: () => Promise<{
3652
+ name: string;
3653
+ rate: number;
3654
+ frame_ms: number;
3655
+ voices: string[];
3656
+ roles: string[];
3657
+ /** The checkpoints this host serves. Empty means it serves one and does not name it —
3658
+ * and then `voice_model` on `open()` is refused rather than quietly ignored. */
3659
+ models: string[];
3660
+ default_model: string;
3661
+ /** What language each voice speaks, and the one used when the project names none. The
3662
+ * frontend is the authority here: a voice is trained in a language, so this is what
3663
+ * decides which phrasebook a call gets. */
3664
+ voice_languages: Record<string, string>;
3665
+ default_language: string;
3666
+ /** Whether the host will speak words it did not generate (a grounded answer read out as
3667
+ * written), and whether an answer can be handed to it as context instead. A frontend
3668
+ * with neither can only be bridged to, which changes what a slow lookup sounds like. */
3669
+ supports_injection: boolean;
3670
+ supports_context: boolean;
3671
+ max_concurrent: number;
3672
+ in_use: number;
3673
+ /** The other speech arrangements this deployment can reach, if any. More than one means
3674
+ * a call may name which runs it — see `frontend` on `open()`. One or none means the
3675
+ * deployment has a single host and the choice does not exist. */
3676
+ frontends: string[];
3677
+ /** Whether an answer reaches the model as grounding rather than as words to say. Told
3678
+ * apart from `supports_injection` because a host can take a reference and still refuse
3679
+ * to be told what to say, and the two produce different failures. */
3680
+ conditions_on_reference: boolean;
3681
+ /** Whether the selected model decides for itself when a turn needs the agent, and says so
3682
+ * with a directive in its own monologue. A checkpoint with a native retrieval trigger, or
3683
+ * one carrying a trained adapter, does; without either the runtime's router decides
3684
+ * instead. Worth reading rather than inferring from behaviour: the two arrangements fail
3685
+ * in different ways, and knowing which you are looking at is most of the diagnosis. */
3686
+ emits_directives: boolean;
3687
+ /** Which of the host's models decide for themselves, by name. Per model, because it is a
3688
+ * property of the weights: a host serving a checkpoint and an adapter trained on top of it
3689
+ * answers differently for each, and `emits_directives` alone described the process. */
3690
+ decides_by_model: Record<string, boolean>;
3691
+ /** Whether the host transcribes the caller. Context for the request, never a trigger —
3692
+ * a transcript that also triggers races the model's own monologue and loses, because the
3693
+ * monologue is ready the moment the frontend stops talking. */
3694
+ provides_transcripts: boolean;
3695
+ }>;
3696
+ };
3697
+ private connectVoice;
2734
3698
  /** Agent Plugins — the skills this token can reach for, and the ones it may add.
2735
3699
  *
2736
3700
  * Two sources, one list: what the project published plus anything this end-user