@oberik/sdk 0.60.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.
- package/dist/cjs/index.js +202 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +954 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +202 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/esm/index.d.ts
CHANGED
|
@@ -1060,6 +1060,15 @@ export interface ChatRequest {
|
|
|
1060
1060
|
* the `webhook_tools` capability. Turn it off for a turn that should not touch your
|
|
1061
1061
|
* systems. */
|
|
1062
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;
|
|
1063
1072
|
enable_plugins?: boolean;
|
|
1064
1073
|
/** Which plugins this turn may use, by name. Omit for everything the token can see.
|
|
1065
1074
|
* Narrowing only — a name that is not visible is absent rather than an error, so a
|
|
@@ -2204,6 +2213,875 @@ export interface DownloadOptions {
|
|
|
2204
2213
|
onProgress?: (received: number, total: number | null) => void;
|
|
2205
2214
|
signal?: AbortSignal;
|
|
2206
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
|
+
}
|
|
2207
3085
|
/**
|
|
2208
3086
|
* Make the arguments match the declared types, as far as that is safe.
|
|
2209
3087
|
*
|
|
@@ -2741,6 +3619,82 @@ export declare class AgentFramework {
|
|
|
2741
3619
|
}) => Promise<SessionOut>;
|
|
2742
3620
|
};
|
|
2743
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;
|
|
2744
3698
|
/** Agent Plugins — the skills this token can reach for, and the ones it may add.
|
|
2745
3699
|
*
|
|
2746
3700
|
* Two sources, one list: what the project published plus anything this end-user
|