@vinaverse/client 0.0.0-stage → 0.1.0-alpha.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.
@@ -0,0 +1,2028 @@
1
+ declare const CONTRACT: {
2
+ readonly api: "v1-draft.17";
3
+ readonly commit: "fe4fb7d2945e1ced2bce7d44e85a4b28005e46df";
4
+ };
5
+
6
+ /** A frame as received or sent: its `type`, and whatever else the contract gives it. */
7
+ interface Frame {
8
+ readonly type: string;
9
+ readonly [field: string]: unknown;
10
+ }
11
+ /** A card as the app holds it. `locked` lists the question ids (the card's, or a `rate_each` row's) drawn locked. */
12
+ interface HeldCard {
13
+ readonly id: string;
14
+ readonly kind: string;
15
+ readonly card: Readonly<Record<string, unknown>>;
16
+ readonly question?: HeldQuestion;
17
+ readonly locked: readonly string[];
18
+ /** The turn that drew the card, when known: `ui.action` names it with the card's `id`. */
19
+ readonly turnId?: string;
20
+ /** A proposal's first reported outcome (`confirmed`, `declined`, `failed`, `expired`); later ones change nothing. */
21
+ readonly outcome?: string | null;
22
+ /** Arrival order, shared with messages: a timeline sorts both by it. Moves when the card is drawn again. */
23
+ readonly seq: number;
24
+ }
25
+ interface HeldQuestion {
26
+ readonly question_id: string;
27
+ readonly kind: string;
28
+ readonly state: string;
29
+ readonly answer?: unknown;
30
+ readonly row_answers?: readonly {
31
+ question_id: string;
32
+ answer: unknown;
33
+ }[];
34
+ readonly rows?: readonly {
35
+ question_id: string;
36
+ }[];
37
+ readonly [field: string]: unknown;
38
+ }
39
+ interface Message {
40
+ readonly role: 'assistant' | 'person' | 'staff' | (string & {});
41
+ readonly text: string;
42
+ /**
43
+ * Stable for the message's life, to key it in a list: a person's text by its `client_event_id`, an agent's reply by
44
+ * its turn (`a:<turn_id>`), a staff reply by its `message_id` (`s:<id>`), a guardrail refusal by the input it answers
45
+ * (`r:<client_event_id>`), a transcript item by its server `id` as the live key it matches, or by its time and role
46
+ * (`t:<at>:<role>`) from a server that sends no `id`.
47
+ */
48
+ readonly key: string;
49
+ /** Arrival order, shared with cards: a timeline sorts both by it. */
50
+ readonly seq: number;
51
+ /**
52
+ * A reply cut off after some of it streamed; what streamed stands (§6): `blocked` (`output_blocked`), `interrupted`
53
+ * (`cancelled`, as when a person from the business takes over, or the connection dropped), `failed` (any other code).
54
+ */
55
+ readonly partial?: 'blocked' | 'interrupted' | 'failed';
56
+ /** A person's text recorded during a takeover (`input.received`): the business reads it; no agent turn runs. */
57
+ readonly received?: true;
58
+ /** The agent's reply is a guardrail refusal (`input.rejected` `guardrail`, its `message`). */
59
+ readonly refusal?: true;
60
+ }
61
+ /** A person from the business is replying (`takeover.started`); `by` is their role as the person sees it. */
62
+ interface Takeover {
63
+ readonly by: string | null;
64
+ readonly at: string;
65
+ }
66
+ /** The turn's text streamed so far (`message.delta`), until its `message.completed`. */
67
+ interface Streaming {
68
+ readonly turnId: string;
69
+ readonly text: string;
70
+ /** Taken at the turn's first delta, and kept by its message: a reply sorts before the cards its turn drew after it. */
71
+ readonly seq: number;
72
+ }
73
+ interface Connection {
74
+ readonly name: string;
75
+ readonly state: 'open' | 'closed';
76
+ readonly code?: number;
77
+ readonly reason?: string;
78
+ }
79
+ interface ToolLine {
80
+ readonly turn_id: string;
81
+ readonly label: string;
82
+ readonly state: string;
83
+ }
84
+ /**
85
+ * Whether and how the conversation goes on after a connection ends:
86
+ * - `allowed`: reconnect to the same session;
87
+ * - `user_action_only`: the session was revoked or replaced (the conversation continued elsewhere): resume only when
88
+ * the person asks;
89
+ * - `new_session_on_next_action`: the session ended (idle, expired, limit_reached) but the conversation did not; its
90
+ * secret is spent. On the person's next action, the app has its backend create a new session for the same
91
+ * conversation and calls `resume()`.
92
+ */
93
+ type Reconnect = 'allowed' | 'user_action_only' | 'new_session_on_next_action';
94
+ /** The engine's whole state, for an app to draw. */
95
+ interface ConversationState {
96
+ readonly sessionId: string | null;
97
+ readonly conversationId: string | null;
98
+ readonly connection: Connection | null;
99
+ /** How the conversation goes on after the connection ends (`Reconnect`). */
100
+ readonly reconnect: Reconnect;
101
+ readonly messages: readonly Message[];
102
+ /** Every card held, oldest first. */
103
+ readonly cards: readonly HeldCard[];
104
+ /** The newest question card still open: the one to answer. */
105
+ readonly openQuestion: string | null;
106
+ /** Events sent and neither acknowledged nor refused, by `client_event_id`: resend them, same id, after a reconnect. */
107
+ readonly unacknowledged: readonly Frame[];
108
+ /** Ids refused for now (`turn_in_progress`, `screen_unavailable`): send again with the same id. */
109
+ readonly resend: readonly string[];
110
+ /**
111
+ * Texts and answers refused with `limit_reached`: never resent in this session (it would get the same refusal);
112
+ * resent, same id, on the next session's `session.ready`. Kept apart from `resend` (conformance README, `held`). A
113
+ * refused action is dropped: a new session cannot act on an earlier session's card.
114
+ */
115
+ readonly held: readonly string[];
116
+ /** Typed messages whose refusal stands, by `client_event_id`: shown as not sent, never resent. */
117
+ readonly refused: Readonly<Record<string, string>>;
118
+ /** The last `flow.completed`: `ok`, `refused` or `failed` (an unknown result reads as `failed`). */
119
+ readonly flow: {
120
+ readonly flow: unknown;
121
+ readonly result: 'ok' | 'refused' | 'failed';
122
+ } | null;
123
+ /** The turn in progress. */
124
+ readonly turn: string | null;
125
+ /** The last state of each tool line (the last 100). */
126
+ readonly tools: readonly ToolLine[];
127
+ readonly handoffs: readonly unknown[];
128
+ /** The codes of `error` and `turn.failed` events (the last 100). */
129
+ readonly errors: readonly string[];
130
+ /**
131
+ * While `reconnect` is `new_session_on_next_action`: the `client_event_id`s held for the next session (unacknowledged,
132
+ * or refused for this session's turn limit). Draw them as "sends when you continue", not as failed. Empty otherwise.
133
+ */
134
+ readonly heldForNextSession: readonly string[];
135
+ /** Seconds left when `session.expiring` arrived (WebSocket only, about a minute before the end); null otherwise. */
136
+ readonly expiring: number | null;
137
+ /** The turn's text streamed so far, until its `message.completed` (or its end, which keeps it as a message). */
138
+ readonly streaming: Streaming | null;
139
+ /** While a person from the business replies (§8): from `takeover.started` to `takeover.ended`. */
140
+ readonly takeover: Takeover | null;
141
+ /** Every refusal's code and `message` (`input.rejected`), by `client_event_id`; `refused` lists those that stand. */
142
+ readonly rejections: Readonly<Record<string, {
143
+ code: string;
144
+ message: string;
145
+ }>>;
146
+ /** The AI disclosure `session.ready` carries: always shown, never removed. Null until the first one. */
147
+ readonly disclosure: string | null;
148
+ /** `session.ready`'s `limits.turns_left`, when given. */
149
+ readonly turnsLeft: number | null;
150
+ }
151
+ declare class ConversationEngine {
152
+ private sessionId;
153
+ private conversationId;
154
+ private connection;
155
+ private reconnect;
156
+ private messages;
157
+ private cards;
158
+ private unacknowledged;
159
+ private resend;
160
+ private held;
161
+ private refused;
162
+ private flow;
163
+ private turn;
164
+ private tools;
165
+ private handoffs;
166
+ private errors;
167
+ private expiring;
168
+ private streaming;
169
+ private takeover;
170
+ private rejections;
171
+ private disclosure;
172
+ private turnsLeft;
173
+ /** The next arrival number for a message or a card. */
174
+ private seq;
175
+ /** The cards drawn before a resumed flow's transcript replaced them: proposal outcomes carry over. */
176
+ private replaced;
177
+ /** client_event_id → the first event the app sent under it, until it is settled. */
178
+ private readonly inputs;
179
+ /** The tenant's backend created a session (and named its conversation). */
180
+ sessionCreated(sessionId: string, conversationId?: string | null): void;
181
+ /** A connection opened: the user's own action, or an allowed reconnect, so it lifts a hold on reconnecting. */
182
+ connected(name: string): void;
183
+ /** A connection closed with its code. `4403` (revoked) and `4409` (replaced) hold reconnecting for a user action. */
184
+ closed(name: string, code: number): void;
185
+ /** The app was killed: what it drew is gone; what it sent and never saw acknowledged it kept, to resend. */
186
+ killed(): void;
187
+ sent(event: Frame): void;
188
+ received(event: Frame): void;
189
+ private ready;
190
+ private hold;
191
+ private recorded;
192
+ private rejected;
193
+ private inputRejected;
194
+ /** No longer to resend, nor held for the next session (sent again, or settled). */
195
+ private unqueue;
196
+ /** An input acknowledged or refused for good: nothing more is resent under its id. */
197
+ private settle;
198
+ /** Adds a message once: a key already held (a frame seen twice) changes nothing. */
199
+ private say;
200
+ /** The turn's streamed text, kept as the agent's message when the turn ends without its `message.completed`. */
201
+ private cutOff;
202
+ private error;
203
+ /**
204
+ * An input that could not be delivered, or whose retries are spent (`screen_unavailable` after the last try): it is
205
+ * no longer resent. A typed message is shown as not sent, with `code`; an answer's question unlocks so the person
206
+ * can answer again; a proposal outcome it reported is taken back, so it can be reported again.
207
+ */
208
+ giveUp(clientEventId: string, code: string): void;
209
+ /** A turn the app stopped where no event will say so (an SSE turn cancelled): none is in progress. */
210
+ turnEnded(): void;
211
+ /** The event first sent under a `client_event_id`, to resend it unchanged. */
212
+ input(clientEventId: string): Frame | undefined;
213
+ state(): ConversationState;
214
+ }
215
+
216
+ /** Why the client refused to do something; `code` is stable, `message` is for people. */
217
+ type ClientErrorCode =
218
+ /** A service key (`vv_sk_`) was given: it belongs on the tenant's backend, never in an app. */
219
+ 'service_key'
220
+ /** The value is not a client secret (`vv_cs_` and at least 16 base64url characters). */
221
+ | 'invalid_secret'
222
+ /** The action needs the WebSocket (answers to question cards); the session runs over SSE. */
223
+ | 'needs_websocket'
224
+ /** The session was closed by the app, or ended for good (replaced, revoked, expired). */
225
+ | 'session_ended'
226
+ /** An SSE request failed: `status` holds the HTTP status. */
227
+ | 'http_error'
228
+ /**
229
+ * The session ended but the conversation did not (idle, expired, its turn limit): its secret is spent. Have your
230
+ * backend create a new session for the conversation and call `resume()` with it.
231
+ */
232
+ | 'needs_new_session';
233
+ declare class VinaverseClientError extends Error {
234
+ readonly name = "VinaverseClientError";
235
+ readonly code: ClientErrorCode;
236
+ readonly status: number | undefined;
237
+ constructor(code: ClientErrorCode, message: string, status?: number);
238
+ }
239
+
240
+ interface components {
241
+ schemas: {
242
+ Problem: {
243
+ /** Format: uri */
244
+ type: string;
245
+ title: string;
246
+ status: number;
247
+ /** @enum {string} */
248
+ code: "invalid_request" | "unauthenticated" | "forbidden" | "feature_unavailable" | "not_found" | "idempotency_conflict" | "session_closed" | "payload_too_large" | "unprocessable_media" | "rate_limited" | "internal" | "unavailable" | "conversation_mismatch" | "conversation_expired" | "conversation_completed";
249
+ detail?: string;
250
+ request_id: string;
251
+ retryable: boolean;
252
+ /** @description field-level problems for invalid_request */
253
+ errors?: {
254
+ /** @description JSON Pointer into the request */
255
+ pointer: string;
256
+ message: string;
257
+ }[];
258
+ };
259
+ Money: {
260
+ /** @description minor units */
261
+ amount: number;
262
+ currency: string;
263
+ };
264
+ MicroMoney: {
265
+ /** @description millionths of the currency unit */
266
+ amount_micros: number;
267
+ currency: string;
268
+ };
269
+ SubjectId: string;
270
+ /** @enum {string} */
271
+ SubjectType: "client" | "professional" | "host" | "guest" | "staff";
272
+ Subject: {
273
+ id: components["schemas"]["SubjectId"];
274
+ type: components["schemas"]["SubjectType"];
275
+ /**
276
+ * @example en-US
277
+ * @example es-US
278
+ * @example pt-BR
279
+ */
280
+ locale?: string;
281
+ /** @example America/New_York */
282
+ timezone?: string;
283
+ approximate_location?: {
284
+ lat: number;
285
+ lng: number;
286
+ /** @enum {string} */
287
+ precision: "city" | "neighborhood";
288
+ };
289
+ };
290
+ /** @enum {string} */
291
+ Feature: "assistant" | "moderation_image" | "moderation_video" | "moderation_text" | "screening" | "document_check" | "insights" | "suggestions";
292
+ /**
293
+ * @description `business_setup`: grants the business's own read tools and its `propose` tools (`tool-provider-v1.yaml`, `x-effect: propose`), for the `business_setup` agent; never a write tool. Only a subject of type `professional`, `host` or `staff` may be granted it. `booking_proposals`: grants the tenant's `propose` tools for a client's bookings (a booking, a reschedule, a cancellation the client confirms in the tenant's app), for the customer agent; never a write tool. Only a subject of type `client` may be granted it (`docs/agents-guardrails-suggestions-writes.md` §3.4). These two are the only capabilities that carry `propose` tools: `create_proposal` and every tenant `propose` tool require one of them. `flows`: grants the guided-flow endpoints (`tool-provider-v1.yaml`, `/flows/*`). It names no flow and no record: the tenant limits the `tool_access` token itself to one flow and one record. `my_orders`: grants reading the customer's own kits and orders from a styling-kit retailer (`tool-provider-v1.yaml`, `list_my_kits`, `get_my_kit`, `list_my_orders`, `get_my_order`), read only. Like `my_bookings`, it is meant for a signed-in `client`, and the tenant answers for the token's customer only.
294
+ * @enum {string}
295
+ */
296
+ Capability: "search" | "availability" | "quotes" | "my_bookings" | "policies" | "handoff" | "flows" | "business_setup" | "booking_proposals" | "my_orders";
297
+ ToolAccess: {
298
+ token: string;
299
+ /** Format: date-time */
300
+ expires_at: string;
301
+ capabilities: components["schemas"]["Capability"][];
302
+ };
303
+ /** @description `candidates`, or `tool_access` alone (tool mode). With `candidates`, the tools are not read. */
304
+ SuggestionsRequest: {
305
+ /** @enum {string} */
306
+ surface: "explore";
307
+ /** @description a `client` or a `guest`; a guest has no history */
308
+ subject: components["schemas"]["Subject"] & {
309
+ /** @enum {unknown} */
310
+ type?: "client" | "guest";
311
+ };
312
+ /**
313
+ * @description whose guardrails apply
314
+ * @default customer_assistant
315
+ * @example customer_assistant
316
+ */
317
+ agent: string;
318
+ context?: {
319
+ place?: {
320
+ /** @example Midtown, Atlanta */
321
+ label?: string;
322
+ lat: number;
323
+ lng: number;
324
+ };
325
+ /**
326
+ * Format: date-time
327
+ * @description the person's local time, with its offset
328
+ */
329
+ local_time?: string;
330
+ /** @description the filters the person has on, by name */
331
+ filters?: {
332
+ [key: string]: string;
333
+ };
334
+ };
335
+ candidates?: components["schemas"]["SuggestionCandidate"][];
336
+ /**
337
+ * @description fewer good suggestions than this is an empty list
338
+ * @default 3
339
+ */
340
+ min: number;
341
+ /**
342
+ * @description at least `min`
343
+ * @default 5
344
+ */
345
+ max: number;
346
+ /** @description Optional. Without `candidates`, the agent reads the tenant's `read` tools with it (tool mode). */
347
+ tool_access?: components["schemas"]["ToolAccess"];
348
+ } | unknown | unknown;
349
+ SuggestionCandidate: {
350
+ /**
351
+ * @example pro:66f1a2
352
+ * @example search:service:silk press
353
+ */
354
+ ref: string;
355
+ /**
356
+ * @description what `ref` names
357
+ * @enum {string}
358
+ */
359
+ kind: "professional" | "space" | "class" | "service" | "search";
360
+ title: string;
361
+ /** @description Open: what the tenant knows about the candidate (price, rating, distance, next open time, whether the subject booked it before…). A key the agent does not know is ignored. `why` uses only these facts. */
362
+ facts?: {
363
+ [key: string]: unknown;
364
+ };
365
+ };
366
+ SuggestionsResponse: {
367
+ decision_id: string;
368
+ /** @description between `min` and `max`, or empty */
369
+ suggestions: components["schemas"]["Suggestion"][];
370
+ /** @description true when any suggestion used the subject's own history; such a response is cached only per tenant, environment and `subject.id`. Always false for a guest. */
371
+ personalized: boolean;
372
+ usage: components["schemas"]["SuggestionsUsage"];
373
+ };
374
+ Suggestion: {
375
+ /** @description one of the request's candidates (in tool mode, `<kind>:<id>` from a tool result); `null` only for `ask` */
376
+ ref: string | null;
377
+ /**
378
+ * @description `open`: open the candidate. `search`: run its search. `ask`: open the agent with `question` in the composer, unsent.
379
+ * @enum {string}
380
+ */
381
+ action: "open" | "search" | "ask";
382
+ /** @description in Unicode code points */
383
+ title: string;
384
+ /** @description a plain reason taken from the facts sent; in Unicode code points */
385
+ why: string;
386
+ /** @description `ask` only; in Unicode code points */
387
+ question?: string;
388
+ } & unknown;
389
+ SuggestionsUsage: {
390
+ /** @constant */
391
+ unit: "request";
392
+ quantity: number;
393
+ cost: components["schemas"]["MicroMoney"];
394
+ };
395
+ SessionCreate: {
396
+ /** @example customer_assistant */
397
+ agent: string;
398
+ subject: components["schemas"]["Subject"];
399
+ tool_access: components["schemas"]["ToolAccess"];
400
+ limits?: {
401
+ max_turns?: number;
402
+ max_duration_seconds?: number;
403
+ };
404
+ context?: {
405
+ [key: string]: string;
406
+ };
407
+ /**
408
+ * @description Optional. An opaque key the tenant chooses: printable ASCII, no spaces, at most 128 characters. It names a conversation within the tenant, the environment, the `subject.id` and the `agent`. A new session with the same key resumes that conversation; `createSession` describes resuming and its checks, the same whether a conversation is named by key or by `conversation_id`.
409
+ * One session per conversation is open at a time. Creating a new one closes the older open session: its socket gets `session.closed` with reason `replaced` and close code `4409`, and the session ends as `revoked` (so in `SessionStatus` a replacement looks like a revoke). When two creations for the same conversation race, the later one wins. Conversation state lasts as long as the tenant's `transcript_retention`, counted from the conversation's creation, and never beyond the tenant's `conversation_max_days` (at most 30) from it, however often it is resumed. The key never reaches a model prompt, and logs keep only a hash of it.
410
+ */
411
+ conversation_key?: string;
412
+ /** @description Optional. Resumes the conversation with this id (`Session.conversation_id`, `ConversationState`), as an alternative to `conversation_key` or together with it; `createSession` describes resuming and the order of its checks. */
413
+ conversation_id?: components["schemas"]["ConversationId"];
414
+ };
415
+ ClientSecret: {
416
+ /** @description `vv_cs_` and base64url characters, so the secret is a valid WebSocket subprotocol token in `bearer.<secret>`. */
417
+ value: string;
418
+ /**
419
+ * Format: date-time
420
+ * @description The end of the connect window (120 s after issue): the secret must open a connection (over SSE: run its first turn) by then. Once used, it is bound to the session until the session ends or a new secret is issued (`refreshClientSecret`), so the app reconnects with the same secret (and sends it on every SSE turn) without asking for a new one.
421
+ */
422
+ expires_at: string;
423
+ };
424
+ Session: {
425
+ id: string;
426
+ /** @enum {string} */
427
+ status: "open";
428
+ agent: {
429
+ id: string;
430
+ version: string;
431
+ display_name: string;
432
+ };
433
+ client_secret: components["schemas"]["ClientSecret"];
434
+ /** Format: uri */
435
+ realtime_url: string;
436
+ /** Format: uri */
437
+ turns_url: string;
438
+ /**
439
+ * Format: date-time
440
+ * @description When the session ends: the earliest of the session's own `limits.max_duration_seconds` (when `createSession` sets it), the agent's and the environment's longest session, and `tool_access.expires_at`. A short-lived tool token shortens the session. On the WebSocket, `session.expiring` comes 60 s before, then `session.closed` with `expired`.
441
+ */
442
+ expires_at: string;
443
+ disclosure: string;
444
+ /** @description The conversation this session belongs to, the same for every session of it, however it was resumed (by `conversation_key` or `conversation_id`). Present whenever the session belongs to a conversation: when the request named one, and for every guided-flow session. It is absent only for a session with no conversation, and optional in the schema so clients built on earlier drafts keep working. */
445
+ conversation_id?: components["schemas"]["ConversationId"];
446
+ };
447
+ SessionStatus: {
448
+ id: string;
449
+ /** @enum {string} */
450
+ status: "open" | "closed" | "revoked" | "expired";
451
+ subject_id: components["schemas"]["SubjectId"];
452
+ agent_version?: string;
453
+ turns: number;
454
+ handoff_ticket_ids?: string[];
455
+ /**
456
+ * @description Why the session ended. `server_restart` is reserved and never appears here: it never ends a session (`docs/realtime-protocol.md` §2).
457
+ * @enum {string}
458
+ */
459
+ end_reason?: "expired" | "revoked" | "limit_reached" | "idle" | "server_restart" | "user_left";
460
+ /** Format: date-time */
461
+ created_at: string;
462
+ /** Format: date-time */
463
+ closed_at?: string;
464
+ usage?: components["schemas"]["Usage"];
465
+ };
466
+ TurnInput: {
467
+ client_event_id?: string;
468
+ text: string;
469
+ };
470
+ Usage: {
471
+ /** @enum {string} */
472
+ unit: "turn" | "image" | "video_second" | "text_item" | "screening_call" | "document_page";
473
+ quantity: number;
474
+ input_tokens?: number;
475
+ output_tokens?: number;
476
+ cost: components["schemas"]["MicroMoney"];
477
+ };
478
+ UsageReport: {
479
+ period: string;
480
+ features: {
481
+ feature: components["schemas"]["Feature"];
482
+ quantity: number;
483
+ cost: components["schemas"]["MicroMoney"];
484
+ /** @description null when the feature has no monthly cap */
485
+ cap: components["schemas"]["MicroMoney"] | null;
486
+ /** @enum {string} */
487
+ state: "enabled" | "disabled";
488
+ }[];
489
+ };
490
+ CardRef: {
491
+ /** @enum {string} */
492
+ type: "professional" | "workspace" | "service" | "booking" | "quote" | "policy" | "slot_list" | "ticket" | "metric_series" | "metric_table" | "gap_list" | "intent_cluster" | "memory_claim" | "knowledge_snippet";
493
+ id: string;
494
+ };
495
+ CardAction: {
496
+ /**
497
+ * @description `confirmed`, `declined`, `failed` and `expired` are reserved for proposal outcomes, never a card action
498
+ * @example open
499
+ * @example show_times
500
+ * @example show_more
501
+ * @example get_quote
502
+ * @example contact_support
503
+ */
504
+ action: string;
505
+ label: string;
506
+ deep_link?: string;
507
+ };
508
+ /**
509
+ * @description `ref` is present on every customer-agent card (professional … handoff); copilot cards (metric_chart … document) may omit it, since a metric, cluster or claim is not a tenant object. A `question` card (guided flows) carries `question`, has no `actions` (the person answers with `ui.answer`), uses `title` as a short label for the question, and its `source` names the tool call that fetched the flow's definition.
510
+ * A `next_step` card is a step the person finishes on the tenant's own screen (payouts, a plan purchase, photos). It has a `title` (at most 80 code points), a `body` (at most 300) and exactly one action: `action: open`, with a `label` (at most 40) and a `deep_link`. It may omit `ref`, and its `source` names the tool call the step came from. - **Nothing is written.** Tapping the action opens `deep_link` in the tenant's app. The app sends no
511
+ * `ui.action`, nor `createCardActionSse`, for it.
512
+ * - **The link passes the deep-link rule** (`admin-v1.yaml`, `Connector.deep_link_prefixes`). It must be a
513
+ * whole string value in the result of the tenant tool call that `source` names (`source.call_id`, the
514
+ * tenant's `get_setup_state` for example), in the same session. A substring, or any other tool's result,
515
+ * does not count, and the model never composes a link. It must start with a prefix the tenant registered
516
+ * for the session's environment, compared code point for code point. And what follows the prefix must be
517
+ * on an allowlist: path segments of `A`–`Z`, `a`–`z`, `0`–`9`, `.`, `_`, `~` and `-`, each with a character
518
+ * that is not `.`, then optionally a query of those characters, `=`, `&` and `%20`–`%7E` escapes other
519
+ * than `%25`, `%2F`, `%3A` and `%5C`. No `%` in the path, no fragment, nothing else. Query values are
520
+ * data: the app never navigates to one.
521
+ * - **A card whose link fails is dropped.** The model is told the step could not be shown, so the agent
522
+ * does not point to a card, and the connector is flagged in Studio (`Connector.health`).
523
+ * - **The app also checks** the page the link names on its own side, and drops a link to a page it does not
524
+ * know.
525
+ * - **It is not restored.** A `next_step` card is in neither `pending_cards` nor `transcript`; after a
526
+ * reconnect the agent shows the step again once it reads the tenant's state.
527
+ */
528
+ Card: {
529
+ id: string;
530
+ /**
531
+ * @description customer-agent kinds, then business-copilot kinds (Studio), then `question` (guided flows), then `proposal` (a change the person confirms or declines in the tenant's app), then `next_step` (a step the person finishes on the tenant's own screen)
532
+ * @enum {string}
533
+ */
534
+ kind: "professional" | "workspace" | "service" | "time_slots" | "booking" | "quote" | "policy" | "handoff" | "metric_chart" | "metric_table" | "gap_list" | "cluster" | "claim" | "document" | "question" | "proposal" | "next_step";
535
+ question?: components["schemas"]["Question"];
536
+ proposal?: components["schemas"]["Proposal"];
537
+ ref?: components["schemas"]["CardRef"];
538
+ title: string;
539
+ subtitle?: string;
540
+ /** Format: uri */
541
+ image_url?: string;
542
+ rating?: {
543
+ value_ppm: number;
544
+ count: number;
545
+ };
546
+ price_from?: components["schemas"]["Money"];
547
+ price?: components["schemas"]["Money"];
548
+ /** Format: date-time */
549
+ starts_at?: string;
550
+ /** @description A `time_slots` card's open times, as the tenant's tool returned them: each with its `start` and `end` (its length), and the card's `subtitle` names the professional or space when the tool result does. Each has an `id` (v1-draft.17): the app may draw the slots as choices, and a tap sends `ui.action` `select_slot` with that `id` and the card's `source.call_id` (`SlotSelection`), over SSE through `createCardActionSse`. A slot without an `id` (an earlier runtime) is drawn read-only. */
551
+ slots?: {
552
+ /** @description Opaque, unique within the card, the same for the same slot of the same tool result. Never parse it. */
553
+ id?: string;
554
+ /** Format: date-time */
555
+ start: string;
556
+ /** Format: date-time */
557
+ end: string;
558
+ }[];
559
+ status?: string;
560
+ /** @description policy excerpt, handoff text, or what a `next_step` is for */
561
+ body?: string;
562
+ badges?: string[];
563
+ actions: components["schemas"]["CardAction"][];
564
+ source: {
565
+ tool: string;
566
+ call_id: string;
567
+ /** @description optional; the tool arguments that produced this card (shown by Studio as the source query) */
568
+ arguments?: Record<string, unknown>;
569
+ };
570
+ metric?: components["schemas"]["MetricRef"];
571
+ /** @enum {string} */
572
+ grain?: "day" | "week";
573
+ period?: {
574
+ /** Format: date */
575
+ from: string;
576
+ /** Format: date */
577
+ to: string;
578
+ };
579
+ series?: {
580
+ key: string;
581
+ label: string;
582
+ points: {
583
+ /** Format: date */
584
+ t: string;
585
+ /** @description null when suppressed by the k-anonymity floor */
586
+ value: number | null;
587
+ suppressed?: boolean;
588
+ }[];
589
+ }[];
590
+ dimensions?: {
591
+ key: string;
592
+ label: string;
593
+ }[];
594
+ columns?: {
595
+ key: string;
596
+ label: string;
597
+ unit: components["schemas"]["ValueUnit"];
598
+ }[];
599
+ rows?: {
600
+ dims: {
601
+ [key: string]: string;
602
+ };
603
+ values: {
604
+ [key: string]: number | null;
605
+ };
606
+ }[];
607
+ /** @description gap_list entries */
608
+ items?: {
609
+ service: string;
610
+ area: string;
611
+ demand: number;
612
+ supply: number;
613
+ zero_result_share_ppm: number;
614
+ trend_ppm?: number | null;
615
+ }[];
616
+ label?: string;
617
+ count?: number;
618
+ share_ppm?: number;
619
+ /** @enum {string} */
620
+ outcome?: "zero_results" | "withheld" | "handoff" | "mixed";
621
+ /** @description redacted */
622
+ examples?: string[];
623
+ claim?: {
624
+ id: string;
625
+ statement: string;
626
+ /** @enum {string} */
627
+ status: "candidate" | "confirmed" | "rejected";
628
+ evidence: {
629
+ label: string;
630
+ call_id: string;
631
+ }[];
632
+ /** Format: date-time */
633
+ proposed_at: string;
634
+ decided_by?: string | null;
635
+ /** Format: date-time */
636
+ decided_at?: string | null;
637
+ };
638
+ document?: {
639
+ title: string;
640
+ section?: string;
641
+ url?: string;
642
+ version?: string;
643
+ };
644
+ snippet?: string;
645
+ /** Format: date-time */
646
+ updated_at?: string;
647
+ } & (unknown & unknown & unknown);
648
+ /** @enum {string} */
649
+ ValueUnit: "count" | "ppm" | "micros_usd" | "ms";
650
+ MetricRef: {
651
+ name: string;
652
+ label: string;
653
+ unit: components["schemas"]["ValueUnit"];
654
+ definition: string;
655
+ k_floor?: number;
656
+ };
657
+ /** @description Links a span of the reply text to the card or tool call it came from. Offsets are in Unicode code points (not bytes, not UTF-16 units), start inclusive, end exclusive. */
658
+ Citation: {
659
+ start: number;
660
+ end: number;
661
+ card_id?: string;
662
+ call_id?: string;
663
+ };
664
+ /** @description The tenant's stable ref for a question (or a `rate_each` row), echoed in answers. */
665
+ QuestionId: string;
666
+ /** @enum {string} */
667
+ QuestionKind: "single_choice" | "multi_choice" | "image_choice" | "yes_no" | "list_choice" | "rate_each" | "short_text" | "long_text" | "date" | "number" | "scale" | "photo";
668
+ QuestionOption: {
669
+ /** @description The tenant's stable ref for this choice. Answers carry it, never the label. */
670
+ value: string;
671
+ label: string;
672
+ /**
673
+ * Format: uri
674
+ * @description on the tenant's CDN; required for `image_choice`
675
+ */
676
+ image_url?: string;
677
+ description?: string;
678
+ };
679
+ /**
680
+ * @description One question of a guided flow, as the app renders it natively. The tenant's definition supplies every field except `prompt` (the agent's phrasing), `progress`, `state`, `answer` and `row_answers`. Option `value`s are the tenant's refs: the server rejects any value that is not on the card. For `rate_each`, each row is its own question with its own `question_id`, and `options` are the columns. The newest `open` question card is the one to answer. An `answered` card is drawn locked with its `answer`. A `superseded` card was replaced by a newer one and takes no answer.
681
+ * A `rate_each` card has no single `answer`. Its answered rows are in `row_answers`, on any card where at least one row is answered, in `pending_cards` too. Each answered row is drawn locked, and the others stay open. The card is `answered` only when every row has an answer. A row of a question that is not `required` may be answered `skipped`.
682
+ */
683
+ Question: {
684
+ question_id: components["schemas"]["QuestionId"];
685
+ kind: components["schemas"]["QuestionKind"];
686
+ /** @description the question as the agent phrased it */
687
+ prompt: string;
688
+ /** @description optional, from the definition */
689
+ help?: string;
690
+ /** @description when false the person may skip it (`answer.skipped`) */
691
+ required: boolean;
692
+ /** @description The choices, and for `rate_each` the columns. Choice kinds and `rate_each` need at least 2 and at most 12; `yes_no` needs exactly 2; `list_choice` allows up to 500. */
693
+ options?: components["schemas"]["QuestionOption"][];
694
+ /** @description fewest selections (`multi_choice`, `image_choice`) */
695
+ min?: number;
696
+ /** @description most selections (`multi_choice`, `image_choice`) */
697
+ max?: number;
698
+ /**
699
+ * @description offers a free-text "Other", answered with `other_text`
700
+ * @default false
701
+ */
702
+ allows_other: boolean;
703
+ /**
704
+ * @description With `allows_other` only: the longest "Other" text (`other_text`), in Unicode code points; 500 when absent. A longer `ui.answer` is rejected with `invalid_value` at any length, past 500 too (`AnswerInput`), and its `message` names the limit, so an app that predates this field can say why. Other text the agent takes from a typed message is held to it too: the runtime asks for a shorter one and never submits a longer one. The app enforces it as the person types.
705
+ * @default 500
706
+ */
707
+ other_max_length: number;
708
+ /** @description an exclusive choice, answered as `values` with its `value` alone */
709
+ none_of_the_above?: {
710
+ value: string;
711
+ label: string;
712
+ };
713
+ /** @description `rate_each` only: each row is rated on the `options` scale and answered on its own `question_id` */
714
+ rows?: {
715
+ question_id: components["schemas"]["QuestionId"];
716
+ label: string;
717
+ /** Format: uri */
718
+ image_url?: string;
719
+ }[];
720
+ /** @description `short_text` (max_length at most 200) and `long_text` (at most 2000) */
721
+ text?: {
722
+ max_length: number;
723
+ };
724
+ number?: {
725
+ min: number;
726
+ max: number;
727
+ step: number;
728
+ /**
729
+ * @example cm
730
+ * @example kg
731
+ * @example years
732
+ */
733
+ unit?: string;
734
+ };
735
+ /** @description optional bounds, inclusive */
736
+ date?: {
737
+ /** Format: date */
738
+ min?: string;
739
+ /** Format: date */
740
+ max?: string;
741
+ };
742
+ scale?: {
743
+ min: number;
744
+ max: number;
745
+ step: number;
746
+ min_label: string;
747
+ max_label: string;
748
+ };
749
+ /** @description The app uploads the photo to the tenant (never through Vinaverse) and answers with the `upload_ref` the tenant issued. */
750
+ photo?: {
751
+ max_bytes: number;
752
+ mime_types: string[];
753
+ };
754
+ /** @description `total` is an estimate when the flow branches */
755
+ progress?: {
756
+ step: number;
757
+ total: number;
758
+ };
759
+ /** @enum {string} */
760
+ state: "open" | "answered" | "superseded";
761
+ /** @description the recorded answer, when `state` is `answered`; never on `rate_each` (see `row_answers`) */
762
+ answer?: components["schemas"]["QuestionAnswer"];
763
+ /** @description `rate_each` only: the rows answered so far, each on its row's `question_id`, and each row at most once. A row is answered with one column (`values` holding one value, never `other_text`) or, when the question is not `required`, `skipped`. */
764
+ row_answers?: components["schemas"]["RowAnswer"][];
765
+ } & (unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown & unknown);
766
+ /** @description A `rate_each` row's answer: one column (`values` holding one value, never `other_text`) or `skipped`. */
767
+ RowAnswer: {
768
+ question_id: components["schemas"]["QuestionId"];
769
+ answer: components["schemas"]["QuestionAnswer"] & ({
770
+ values: unknown;
771
+ } | unknown) & unknown;
772
+ };
773
+ ConversationId: string;
774
+ /**
775
+ * @description Where one conversation stands, for the tenant backend that created its sessions. It is the `data` of the conversation webhooks and the response of `getConversation`. The backend keeps a durable copy, so it still has the answers after Vinaverse's state ends.
776
+ * **Personal data.** It never carries the transcript or the `conversation_key`, and nothing about the person beyond the opaque `subject_id` except what the person gave the flow. The answers and remarks (`text`, `other_text`, `upload_ref`, the verbatim remarks) are personal data the person gave the tenant's flow; handle them as such. The object is closed: no other field is ever added to it without a contract change.
777
+ * **`sequence`** is one counter per conversation, shared by the webhooks and `getConversation`. It goes up by one on every change to any field, `session_id` included, and on nothing else; `last_activity_at` is the time of that change. Each step produces exactly one webhook, so a gap means a delivery still to come. Two states with the same `sequence` are identical. A terminal event (`completed`, `expired`) always takes a new, higher `sequence`.
778
+ * **Receiving.** Delivery is at least once and in no guaranteed order. Keep, per `conversation_id`, the state with the highest `sequence`, and ignore an equal or lower one. A state with `answers_omitted: true` updates every other field, and never replaces `answers` and `remarks` already held. Merge it, then read the full state with `getConversation`.
779
+ * **Size.** A webhook body is at most 64 KB: 65,536 bytes of the UTF-8 request body, envelope included. When the state would make it larger, the webhook leaves out `answers` and `remarks` and sets `answers_omitted: true`.
780
+ */
781
+ ConversationState: {
782
+ conversation_id: components["schemas"]["ConversationId"];
783
+ subject_id: components["schemas"]["SubjectId"];
784
+ /** @description the agent profile the conversation runs under */
785
+ agent: string;
786
+ /** @description the guided flow's name; null outside a guided flow */
787
+ flow: string | null;
788
+ /** @description the flow definition version the answers follow; null outside a guided flow */
789
+ version: number | null;
790
+ /**
791
+ * @description `completed`: submitted and accepted. `failed`: the last submission is not known to be accepted (`flow.completed failed`); a later retry, in this session or a later one, can still complete it. `expired`: retention or `conversation_max_days` ended it.
792
+ * @enum {string}
793
+ */
794
+ status: "in_progress" | "completed" | "failed" | "expired";
795
+ sequence: number;
796
+ progress: {
797
+ /** @description questions answered, a `rate_each` row counting as one */
798
+ answered: number;
799
+ /** @description an estimate when the flow branches; null outside a guided flow */
800
+ total_estimate: number | null;
801
+ };
802
+ /** @description the open question, or null when none is open (finished, between questions, or no flow) */
803
+ pending_question_id: components["schemas"]["QuestionId"] | null;
804
+ /** @description The answers recorded so far, in the order they were given. */
805
+ answers?: components["schemas"]["ConversationAnswer"][];
806
+ /** @description By `question_id`, extra things the person volunteered; never answers. */
807
+ remarks?: {
808
+ [key: string]: string;
809
+ };
810
+ /** @description true when a webhook left out `answers` and `remarks` to stay within 64 KB; never in `getConversation` */
811
+ answers_omitted?: boolean;
812
+ /**
813
+ * Format: date-time
814
+ * @description the time of the change that produced this `sequence`
815
+ */
816
+ last_activity_at: string;
817
+ /** @description the latest session of the conversation */
818
+ session_id: string;
819
+ } & unknown;
820
+ /** @description One recorded answer: `{ question_id, answer }`, or for `rate_each` `{ question_id, row_answers }`. */
821
+ ConversationAnswer: {
822
+ question_id: components["schemas"]["QuestionId"];
823
+ answer: components["schemas"]["QuestionAnswer"];
824
+ } | {
825
+ question_id: components["schemas"]["QuestionId"];
826
+ row_answers: components["schemas"]["RowAnswer"][];
827
+ };
828
+ /** @description Exactly one of: `values` (option values, validated against the card; one for single-choice kinds), `other_text` (the free-text "Other", when `allows_other`), `text`, `number`, `date` (YYYY-MM-DD), `upload_ref` (`photo`) or `skipped` (only when the question is not `required`). The one combination is `values` with `other_text`, for a `single_choice` or `multi_choice` question that `allows_other`: the person picked options and also typed an "Other". On any other question it is refused (`not_allowed`). */
829
+ QuestionAnswer: {
830
+ values: string[];
831
+ } | {
832
+ other_text: string;
833
+ } | {
834
+ values: string[];
835
+ other_text: string;
836
+ } | {
837
+ text: string;
838
+ } | {
839
+ number: number;
840
+ } | {
841
+ /** Format: date */
842
+ date: string;
843
+ } | {
844
+ upload_ref: string;
845
+ } | {
846
+ /** @constant */
847
+ skipped: true;
848
+ };
849
+ /** @description What `ui.answer` carries: a `QuestionAnswer`, or the same with an "Other" longer than 500. An `other_text` longer than the question's `other_max_length` is answered `ui.answer.rejected` with `invalid_value`, at any length, and its `message` names the limit, so a person always learns it. It is never recorded, so recorded and submitted answers keep the `QuestionAnswer` bound. */
850
+ AnswerInput: components["schemas"]["QuestionAnswer"] | {
851
+ other_text: string;
852
+ } | {
853
+ values: string[];
854
+ other_text: string;
855
+ };
856
+ /**
857
+ * @description A message, oldest first: `{ role: assistant | person | staff, text, at }` (`staff`: a reply from a person from the business during a takeover, text only, never a name; v1-draft.15 widened the enum, so a client must tolerate a role it does not know), or an answered question card as `{ role: card, card, at }` (its `question.state` is `answered`, with its `answer`, or for `rate_each` its `row_answers`).
858
+ * A `person` message is the person's own text as the runtime recorded it, card-like digits masked; a card tap, an answer or a proposal's outcome is not a message, and neither is an input the runtime never recorded (one refused with `input.rejected`, held at the turn limit, or a guardrail's refusal and its reply): the app keeps its own copy of those. An `assistant` message with `interrupted: true` (v1-draft.17) is a reply that stopped before it finished: its turn ended with `turn.failed` after it had streamed (a takeover, `input.cancel`, `output_blocked`, an outage), and the text is what was streamed. Draw it as cut off. A client that does not know the field draws it as any reply.
859
+ * **Identity (v1-draft.17).** A message carries `id`, the identifier the live events gave it, so an app reconciles the transcript with what it drew instead of drawing it again: an `assistant` message's `turn_id` (`message.delta`, `message.completed`), a `person` message's `client_event_id` (`input.text`; absent when the app sent none), a `staff` message's `message_id` (`message.staff`). Unique per role within a conversation. A message of a turn also carries `turn_id`, the person's text and the agent's reply alike, so the cards the turn drew stay anchored after its text; a message sent during a takeover has none.
860
+ */
861
+ TranscriptItem: {
862
+ /** @enum {string} */
863
+ role: "assistant" | "person" | "staff";
864
+ text: string;
865
+ /** Format: date-time */
866
+ at: string;
867
+ /** @description `assistant` only, present only when true: the reply stopped before it finished */
868
+ interrupted?: boolean;
869
+ /** @description v1-draft.17: the live event's identifier for this message (`turn_id`, `client_event_id` or `message_id`, by role); absent on a `person` message sent without a `client_event_id` */
870
+ id?: string;
871
+ /** @description v1-draft.17; the turn the message belongs to, when it belongs to one */
872
+ turn_id?: string;
873
+ } | {
874
+ /** @constant */
875
+ role: "card";
876
+ card: components["schemas"]["Card"] & {
877
+ /** @constant */
878
+ kind: "question";
879
+ question: {
880
+ /** @constant */
881
+ state: "answered";
882
+ };
883
+ };
884
+ /** Format: date-time */
885
+ at: string;
886
+ };
887
+ /** @description A time slot the person picked on a `time_slots` card (v1-draft.17, `docs/realtime-protocol.md` §6): the slot's `id`, and the `call_id` of the tool call the card came from (the card's `source.call_id`), so the runtime checks the pick against the evidence it holds for that call. Both are copied from the card; never built by the app. */
888
+ SlotSelection: {
889
+ id: string;
890
+ call_id: string;
891
+ };
892
+ /**
893
+ * @description A change the agent proposed through a `propose` tool. The tenant holds it as pending; nothing has changed yet, and Vinaverse never applies it. The app shows `title`, `summary` and each field's `before` and `after`, with its own Confirm and Decline buttons (`confirm_mode: tenant_app`; the card has no `actions`). - **Confirm or decline** happens in the tenant's app, against the tenant's backend, with the person's own
894
+ * token, before `expires_at`. The tenant applies the change on confirm.
895
+ * - **Then** the app sends `ui.action` with the card's `turn_id` and `card_id`, a `client_event_id`, and the
896
+ * outcome as `action`. The runtime gives that to the model as an event, never as a message from the person,
897
+ * and the agent carries on from it.
898
+ * - `confirmed`: the tenant's backend applied the change;
899
+ * - `declined`: the person declined it;
900
+ * - `failed`: the person confirmed but the tenant did not apply it (it refused the change, or the proposal
901
+ * had expired);
902
+ * - `expired`: the tenant says `expires_at` passed with no answer.
903
+ * - **The tenant is the source of truth, and only an outcome the app reports is final.** At `expires_at` the
904
+ * runtime tells the model, provisionally, that the proposal was not confirmed, so the model never waits on it.
905
+ * This counts for nothing. A `confirmed`, `declined` or `failed` the app reports later is accepted, wins,
906
+ * and is fed to the model as a correction.
907
+ * - **The first app-reported outcome per card is final.** A repeat of that same outcome changes nothing. A
908
+ * second, conflicting outcome, or an outcome on a card that is not a `proposal`, is answered with `error`
909
+ * (`code: proposal_outcome_rejected`, with the `card_id` and `client_event_id`), and nothing reaches the
910
+ * model.
911
+ * - **`status`** is the tenant's state when the card was drawn. A card restored after a reconnect
912
+ * (`session.ready.pending_cards`) still waits for its outcome.
913
+ * - **Only a `pending` proposal is actionable.** The app draws Confirm and Decline only for
914
+ * `proposal.status: pending`. A `propose` result that is not `pending` (a reused key that met an expired,
915
+ * declined, confirmed or superseded proposal) is never drawn as an actionable card. The runtime advances the
916
+ * key's attempt counter and calls once more with the new key, or, if that one is not `pending` either, gives
917
+ * the `status` to the model as the outcome.
918
+ * - **`edit_deep_link`** opens the tenant's own screen for the change, to edit it there. It is held to the
919
+ * deep-link rule (`admin-v1.yaml`, `Connector.deep_link_prefixes`), the same check as a `next_step` link.
920
+ * The runtime removes an `edit_deep_link` that fails, and the card is still sent. With no prefix registered,
921
+ * nothing passes.
922
+ *
923
+ * - **A booking proposal** (topic `booking`, `booking_reschedule` or `booking_cancellation`) carries the fields a
924
+ * client must see under fixed keys (`BookingFields`): what is booked and with whom, when, and the price; for a
925
+ * reschedule, the booking (unchanged) and its time before and after; for a cancellation, the booking and its
926
+ * time (both unchanged), and any fee or refund. It expires no later than the booking starts, and a
927
+ * reschedule no later than the earlier of its current and its new start
928
+ * (`docs/agents-guardrails-suggestions-writes.md` §3.4).
929
+ *
930
+ * Each field `key` appears once. `expires_at` is at most 7 days after the proposal was created. A field whose `before` equals its `after` does not change: show its value once.
931
+ */
932
+ Proposal: {
933
+ /** @description the tenant's proposal id */
934
+ id: string;
935
+ /**
936
+ * @description The tenant's current state of the proposal. Only `pending` can still be confirmed. The tenant is the source of truth, and `confirmed` means the change was applied.
937
+ * @enum {string}
938
+ */
939
+ status: "pending" | "confirmed" | "declined" | "expired" | "superseded";
940
+ /**
941
+ * @example business_address
942
+ * @example opening_hours
943
+ * @example services
944
+ * @example booking
945
+ * @example booking_reschedule
946
+ * @example booking_cancellation
947
+ */
948
+ topic: string;
949
+ title: string;
950
+ summary: string;
951
+ fields: components["schemas"]["ProposalField"][];
952
+ /**
953
+ * Format: date-time
954
+ * @description after it the tenant declines the proposal on its own
955
+ */
956
+ expires_at: string;
957
+ /** @description a link into the tenant's app; removed by the runtime when it fails the deep-link rule (see `edit_deep_link` above) */
958
+ edit_deep_link?: string;
959
+ /** @enum {string} */
960
+ confirm_mode: "tenant_app";
961
+ } & (unknown & unknown & unknown & unknown);
962
+ /** @description The fields of a booking proposal (topic `booking`, `booking_reschedule` or `booking_cancellation`), whose keys are fixed so every app draws a booking card the same way (`docs/agents-guardrails-suggestions-writes.md` §3.4): `service`, `professional`, `space`, `booking`, `location`, `note` and `reason` are `text`, `services` is a `list`, `time` is a `time_range`, `place` an `address`, and `price`, `deposit`, `fee`, `cancellation_fee` and `refund` are `money_cents`. */
963
+ BookingFields: (unknown & unknown & unknown & unknown & unknown)[];
964
+ /** @description `before` is the current value, absent or null when the proposal creates it, and equal to `after` when the field does not change but says what the change is about (the booking a reschedule moves); `before` and `after` take the shape `type` names. */
965
+ ProposalField: {
966
+ key: string;
967
+ label: string;
968
+ /**
969
+ * @description `money_cents` is an amount in the currency's minor units (ISO 4217: cents for USD, yen for JPY, fils for BHD), never negative. `time_range` is one dated range whose `end` is after its `start`. `weekly_hours` is recurring opening hours, by weekday, in the business's time zone.
970
+ * @enum {string}
971
+ */
972
+ type: "text" | "money_cents" | "time_range" | "address" | "list" | "weekly_hours";
973
+ before?: unknown;
974
+ after: unknown;
975
+ } & (unknown & unknown & unknown & unknown & unknown & unknown);
976
+ ProposalMoney: {
977
+ /** @description in the currency's minor units (ISO 4217) */
978
+ amount: number;
979
+ currency: string;
980
+ };
981
+ /** @description Recurring opening hours. A day absent from `entries` is closed; a day may have several entries (a lunch break), which never overlap. `end` may be `24:00` (closing at midnight), and `00:00`–`24:00` is open all day. An `end` earlier than `start` runs past midnight into the next day (a bar's `22:00`–`02:00`), and that spill counts against the next day's entries: `fri 22:00`–`02:00` and `sat 01:00`–`09:00` overlap. `end` never equals `start`. */
982
+ WeeklyHours: {
983
+ /**
984
+ * @description an IANA time zone name
985
+ * @example America/New_York
986
+ */
987
+ time_zone: string;
988
+ entries: {
989
+ /** @enum {string} */
990
+ day: "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
991
+ start: string;
992
+ end: string;
993
+ }[];
994
+ };
995
+ TimeRange: {
996
+ /** Format: date-time */
997
+ start: string;
998
+ /** Format: date-time */
999
+ end: string;
1000
+ };
1001
+ PostalAddress: {
1002
+ line1: string;
1003
+ line2?: string;
1004
+ city: string;
1005
+ region?: string;
1006
+ postal_code?: string;
1007
+ /** @description ISO 3166-1 alpha-2 */
1008
+ country: string;
1009
+ };
1010
+ TextList: string[];
1011
+ /** @description What the app sends. **The turn limit narrows the typed message's deduplication rule below**, which lists `limit_reached` among the refusals a resend gets again: that holds in the same session only. A text or an answer refused with `input.rejected` `limit_reached` is not recorded. On a new session of the conversation it is checked as new: it runs, as a turn of that session, or, if its turn had already run, gets `duplicate` (`input.rejected` for a text, `ui.answer.recorded` with `duplicate: true` for an answer) (`docs/realtime-protocol.md` §2, a session out of turns). */
1012
+ ClientEvent: {
1013
+ /** @constant */
1014
+ type: "session.auth";
1015
+ client_secret: string;
1016
+ } | {
1017
+ /** @constant */
1018
+ type: "input.text";
1019
+ client_event_id?: string;
1020
+ text: string;
1021
+ } | {
1022
+ /** @constant */
1023
+ type: "input.cancel";
1024
+ turn_id: string;
1025
+ } | {
1026
+ /** @constant */
1027
+ type: "playback.fenced";
1028
+ turn_id: string;
1029
+ heard_chars: number;
1030
+ } | ({
1031
+ /** @constant */
1032
+ type: "ui.action";
1033
+ client_event_id?: string;
1034
+ turn_id: string;
1035
+ card_id: string;
1036
+ /**
1037
+ * @description On a `proposal` card: `confirmed`, `declined`, `failed` or `expired` (`Proposal`), sent after the outcome in the tenant's app. It tells the session; it changes nothing.
1038
+ * On a `time_slots` card: `select_slot` (v1-draft.17), with `slot`, the person's pick. It is checked against this session's evidence: the card must be one this session drew from the tool call `slot.call_id` names, and the slot one that call returned; anything else gets `error` (`invalid_action`) and reaches nothing. An accepted pick starts a turn: `turn.started` echoes the `client_event_id`, and it is deduplicated as `input.text` is (a resend whose turn ran gets `input.rejected` `duplicate`). It is a tap on a card: refused with `input.rejected` `turn_in_progress` during a turn or a takeover, never held at the turn limit, and never sent on another session.
1039
+ */
1040
+ action: string;
1041
+ slot?: components["schemas"]["SlotSelection"];
1042
+ } & (unknown & unknown)) | {
1043
+ /** @constant */
1044
+ type: "ui.answer";
1045
+ /** @description the action id, unique per answer */
1046
+ client_event_id: string;
1047
+ card_id: string;
1048
+ question_id: components["schemas"]["QuestionId"];
1049
+ answer: components["schemas"]["AnswerInput"];
1050
+ } | {
1051
+ /** @constant */
1052
+ type: "ping";
1053
+ };
1054
+ /** @description What the server sends on the WebSocket, and as SSE events of a turn. `session.expiring` is WebSocket only: sent once per connection, 60 s before `Session.expires_at`, never for the client secret; `session.closed` with `expired` follows at the end (`docs/realtime-protocol.md` §1). What the app does after each `session.closed` reason is in §2 of the same document; `server_restart` is reserved, nothing sends it today, and it never ends a session. */
1055
+ ServerEvent: {
1056
+ /** @constant */
1057
+ type: "session.ready";
1058
+ session: components["schemas"]["SessionStatus"];
1059
+ limits: {
1060
+ max_turns?: number;
1061
+ turns_left?: number;
1062
+ /** Format: date-time */
1063
+ expires_at?: string;
1064
+ };
1065
+ disclosure: string;
1066
+ resumed: boolean;
1067
+ /**
1068
+ * @description Optional. The last messages of the conversation, oldest first, text only, so the app can redraw it after a kill or a reconnect. Older context is not sent.
1069
+ * A guided flow's resumed `session.ready` always carries it. Any other session's (a chat; v1-draft.17) carries it when `resumed` is true and there is something to restore, whether the app reconnects to the same session or the backend created a new session of the conversation: - the conversation's messages across its sessions (the session's own when it has no conversation):
1070
+ * the person's texts, the agent's replies (`interrupted` when cut off) and the staff replies of a
1071
+ * takeover; the last 20, and none older than 7 days;
1072
+ * - only what the tenant's `transcript_retention` still keeps: under `none`, only the open session's
1073
+ * own messages; never anything of a subject erased since (`eraseSubject`).
1074
+ *
1075
+ * When present, it is the server's view: redraw the messages from it, including any the app missed while disconnected. When absent, the server holds nothing to restore: keep what the app holds. It is sent only on the connection the client secret opened, so it reaches no one the session does not.
1076
+ */
1077
+ transcript?: components["schemas"]["TranscriptItem"][];
1078
+ /**
1079
+ * @description Optional. The open question card, if there is one (at most one); render it unlocked. For `rate_each`, its `row_answers` say which rows are already answered: draw those locked. Also the `proposal` cards still awaiting an outcome from the app, so the app can report `confirmed`, `declined`, `failed` or `expired` with its `turn_id` and `card_id` after a reconnect. A proposal card awaits until the app reports an outcome, or until 7 days after its `expires_at`. When more than 10 await, the 10 newest are restored. An older one still takes an outcome if the app has its ids.
1080
+ * Every item carries `turn_id`, the turn that drew the card. `ui.action` needs it with the card's `id`, so a proposal card restored after a reconnect or an app kill can still be reported.
1081
+ */
1082
+ pending_cards?: (components["schemas"]["Card"] & {
1083
+ /** @description the turn that drew the card; `ui.action` names it with the card's `id` */
1084
+ turn_id: string;
1085
+ } & ({
1086
+ /** @constant */
1087
+ kind: "question";
1088
+ question: {
1089
+ /** @constant */
1090
+ state: "open";
1091
+ };
1092
+ } | {
1093
+ /** @constant */
1094
+ kind: "proposal";
1095
+ proposal: {
1096
+ /** @constant */
1097
+ status: "pending";
1098
+ };
1099
+ }))[];
1100
+ } | {
1101
+ /** @constant */
1102
+ type: "turn.started";
1103
+ turn_id: string;
1104
+ client_event_id?: string;
1105
+ } | {
1106
+ /** @constant */
1107
+ type: "tool.status";
1108
+ turn_id: string;
1109
+ label: string;
1110
+ /** @enum {string} */
1111
+ state: "running" | "done" | "failed";
1112
+ } | {
1113
+ /** @constant */
1114
+ type: "message.delta";
1115
+ turn_id: string;
1116
+ /** @description One or more whole sentences. Each sentence passes the leak check, and the tenant's blocked-topic filter, before it is sent. */
1117
+ text: string;
1118
+ } | {
1119
+ /** @constant */
1120
+ type: "ui.cards";
1121
+ turn_id: string;
1122
+ cards: components["schemas"]["Card"][];
1123
+ } | {
1124
+ /** @constant */
1125
+ type: "message.completed";
1126
+ turn_id: string;
1127
+ text: string;
1128
+ /** @description optional; spans of `text` with their source card or tool call */
1129
+ citations?: components["schemas"]["Citation"][];
1130
+ } | {
1131
+ /** @constant */
1132
+ type: "handoff.created";
1133
+ turn_id: string;
1134
+ ticket: {
1135
+ id: string;
1136
+ /** @description the tenant's phrase completing "they'll reply {expected_response}" (`within one business day by email`), never a sentence of its own */
1137
+ expected_response: string;
1138
+ };
1139
+ } | {
1140
+ /** @constant */
1141
+ type: "turn.completed";
1142
+ turn_id: string;
1143
+ usage: components["schemas"]["Usage"];
1144
+ } | {
1145
+ /** @constant */
1146
+ type: "turn.failed";
1147
+ turn_id: string;
1148
+ /**
1149
+ * @description Whatever the code, the sentences the turn already streamed (`message.delta`) stand: they are the reply recorded, marked interrupted (`TranscriptItem.interrupted`, v1-draft.17), so the app keeps them, drawn as cut off, and adds `message` after them. Includes: - `output_blocked`: a sentence of the reply would have leaked (the prompt, keys, other subjects' data),
1150
+ * so it was never sent, and the turn ended (`retryable: false`). A sentence that only touches a
1151
+ * blocked topic is dropped instead, and the turn goes on.
1152
+ * - `cancelled`: the turn was stopped: by `input.cancel`, because the person left, or because a person
1153
+ * from the business took the conversation over (`takeover.started` follows, and nothing of the turn
1154
+ * comes after it).
1155
+ * - `tool_result_invalid`: a `propose` tool returned something that is not a `Proposal`. No card is
1156
+ * shown, and the connector is flagged.
1157
+ */
1158
+ code: string;
1159
+ message: string;
1160
+ retryable: boolean;
1161
+ } | {
1162
+ /** @constant */
1163
+ type: "input.rejected";
1164
+ client_event_id?: string;
1165
+ /**
1166
+ * @description `duplicate`: the same `client_event_id` was already handled in this conversation, and its turn ran; do not resend it. `turn_in_progress`: send the input again, with the same `client_event_id`, after `turn.completed`. `screen_unavailable`: the chat-safety screen could not check the input, so nothing ran and the model never saw it, and nothing was recorded or remembered. Resend the same input with the same `client_event_id` after a short backoff (for example 2 s, doubling, three or four tries); if it is still refused, show it as not sent, with `message`, and let the person send it again. It replaces, for this case, the retryable `error` with `code: screen_unavailable` that earlier runtimes sent; an older server still sends that `error`. An app that does not know the code treats it as any unknown `input.rejected` code: it does not resend, and shows `message`. `guardrail`: the message was refused before the model saw it, by one of the agent's guardrails (a blocked topic, or outside its scope under `strict`) or by the platform floor (an instruction override or persona hijack); `message` is the tenant's `refusal_message` (or a platform default in the subject's locale): show it as the agent's reply, and do not resend.
1167
+ * @enum {string}
1168
+ */
1169
+ code: "screened" | "too_long" | "turn_in_progress" | "limit_reached" | "duplicate" | "guardrail" | "screen_unavailable";
1170
+ message: string;
1171
+ } | {
1172
+ /** @constant */
1173
+ type: "session.expiring";
1174
+ seconds_left: number;
1175
+ } | {
1176
+ /** @constant */
1177
+ type: "session.closed";
1178
+ /**
1179
+ * @description `replaced`: a newer session of the same conversation was created (close code `4409`)
1180
+ * @enum {string}
1181
+ */
1182
+ reason: "expired" | "revoked" | "limit_reached" | "idle" | "server_restart" | "replaced";
1183
+ } | {
1184
+ /** @constant */
1185
+ type: "ui.answer.recorded";
1186
+ card_id: string;
1187
+ question_id: components["schemas"]["QuestionId"];
1188
+ client_event_id: string;
1189
+ duplicate: boolean;
1190
+ } | {
1191
+ /** @constant */
1192
+ type: "ui.answer.rejected";
1193
+ card_id: string;
1194
+ question_id: components["schemas"]["QuestionId"];
1195
+ client_event_id: string;
1196
+ /** @enum {string} */
1197
+ code: "invalid_value" | "too_many" | "too_few" | "card_superseded" | "not_allowed" | "client_event_id_reused";
1198
+ message?: string;
1199
+ } | ({
1200
+ /** @constant */
1201
+ type: "flow.completed";
1202
+ /** @example intake-survey */
1203
+ flow: string;
1204
+ /** @enum {string} */
1205
+ result: "ok" | "refused" | "failed";
1206
+ /**
1207
+ * @description For `refused`, the tenant's refusal code, as it sent it: `REQUIRED_MISSING`, `UNKNOWN_VALUE`, `TOO_LONG`, `VERSION_NOT_ACCEPTED`, `TOO_MANY`, `TOO_FEW`, `BAD_DATE`, `UPLOAD_NOT_FOUND`, `OUT_OF_RANGE`, or a newer one. Show it as it is; the agent has already asked again.
1208
+ * @example REQUIRED_MISSING
1209
+ * @example unavailable
1210
+ */
1211
+ code?: string;
1212
+ /** @description `failed` only */
1213
+ retryable?: boolean;
1214
+ } & (unknown & unknown & unknown)) | {
1215
+ /** @constant */
1216
+ type: "error";
1217
+ code: string;
1218
+ message: string;
1219
+ retryable: boolean;
1220
+ /** @description the card the refused event named (`proposal_outcome_rejected`) */
1221
+ card_id?: string;
1222
+ /** @description the refused event's `client_event_id` */
1223
+ client_event_id?: string;
1224
+ } | {
1225
+ /** @constant */
1226
+ type: "pong";
1227
+ } | {
1228
+ /** @constant */
1229
+ type: "takeover.started";
1230
+ /** Format: date-time */
1231
+ at: string;
1232
+ /** @description the replier's role as the person sees it, never a name (e.g. "the team") */
1233
+ by?: string;
1234
+ } | {
1235
+ /** @constant */
1236
+ type: "takeover.ended";
1237
+ /** Format: date-time */
1238
+ at: string;
1239
+ } | {
1240
+ /** @constant */
1241
+ type: "message.staff";
1242
+ message_id: string;
1243
+ text: string;
1244
+ /** Format: date-time */
1245
+ at: string;
1246
+ } | {
1247
+ /** @constant */
1248
+ type: "input.received";
1249
+ client_event_id: string;
1250
+ };
1251
+ /**
1252
+ * @description The tenant's binding lists its surfaces; unknown surfaces are rejected.
1253
+ * @example listing_photo
1254
+ * @example gallery_photo
1255
+ * @example profile_photo
1256
+ * @example service_photo
1257
+ * @example class_photo
1258
+ * @example listing_video
1259
+ * @example bio
1260
+ * @example listing_text
1261
+ * @example business_name
1262
+ * @example service_name
1263
+ * @example chat_message
1264
+ * @example review
1265
+ */
1266
+ Surface: string;
1267
+ Media: {
1268
+ /** Format: uri */
1269
+ url: string;
1270
+ /** Format: date-time */
1271
+ url_expires_at?: string;
1272
+ /** @enum {string} */
1273
+ content_type: "image/jpeg" | "image/png" | "image/webp" | "image/heic" | "video/mp4" | "video/quicktime" | "video/webm";
1274
+ bytes?: number;
1275
+ sha256?: string;
1276
+ };
1277
+ ModerationItemInput: {
1278
+ ref: string;
1279
+ surface: components["schemas"]["Surface"];
1280
+ /** @enum {string} */
1281
+ kind: "image" | "video" | "text";
1282
+ subject: components["schemas"]["Subject"];
1283
+ media?: components["schemas"]["Media"];
1284
+ text?: string;
1285
+ context?: {
1286
+ [key: string]: string;
1287
+ };
1288
+ options?: {
1289
+ /** @default false */
1290
+ assess_quality: boolean;
1291
+ };
1292
+ } & ({
1293
+ /** @enum {unknown} */
1294
+ kind?: "image" | "video";
1295
+ } | {
1296
+ /** @constant */
1297
+ kind?: "text";
1298
+ });
1299
+ ModerationJobCreate: {
1300
+ /** @default default */
1301
+ policy: string;
1302
+ items: components["schemas"]["ModerationItemInput"][];
1303
+ };
1304
+ Reason: {
1305
+ /**
1306
+ * @example contact.off_platform
1307
+ * @example sexual.nudity
1308
+ * @example quality.not_a_space
1309
+ */
1310
+ code: string;
1311
+ /** @enum {string} */
1312
+ severity: "info" | "review" | "hold" | "never";
1313
+ evidence?: {
1314
+ /** @enum {string} */
1315
+ kind?: "visual" | "text_in_image" | "text" | "speech" | "metadata";
1316
+ /** @description redacted excerpt, at most 120 chars */
1317
+ excerpt?: string;
1318
+ /** @description normalized box, integers in ppm of width/height */
1319
+ region?: {
1320
+ x?: number;
1321
+ y?: number;
1322
+ w?: number;
1323
+ h?: number;
1324
+ } | null;
1325
+ timestamp_ms?: number | null;
1326
+ };
1327
+ explanation?: string;
1328
+ };
1329
+ /** @description A vv-decision/0 decision record as exposed to tenants. */
1330
+ Decision: {
1331
+ id: string;
1332
+ /** @enum {string} */
1333
+ domain: "image_safety" | "video_safety" | "text_safety" | "chat_safety" | "document_check";
1334
+ domain_version: string;
1335
+ domain_hash?: string;
1336
+ /** @enum {string} */
1337
+ class: "clean" | "review" | "hold" | "never";
1338
+ abstained: boolean;
1339
+ reasons: components["schemas"]["Reason"][];
1340
+ distribution_ppm?: {
1341
+ [key: string]: number;
1342
+ } | null;
1343
+ /** @enum {string} */
1344
+ calibration: "absent" | "calibrated";
1345
+ threshold_policy?: string;
1346
+ policy_version: string;
1347
+ model: {
1348
+ id: string;
1349
+ provider: string;
1350
+ revision?: string;
1351
+ };
1352
+ prompt_version?: string;
1353
+ audit: {
1354
+ sampled: boolean;
1355
+ probability_ppm: number;
1356
+ };
1357
+ };
1358
+ Quality: {
1359
+ /** @enum {string} */
1360
+ rating: "good" | "fair" | "poor";
1361
+ issues: ("dark" | "blurry" | "low_resolution" | "tilted" | "cluttered" | "watermark" | "heavy_filter" | "not_a_space" | "duplicate_in_listing")[];
1362
+ is_space: boolean;
1363
+ note?: string;
1364
+ };
1365
+ VideoFindings: {
1366
+ duration_ms: number;
1367
+ frames_analyzed: number;
1368
+ transcript_language?: string | null;
1369
+ segments: {
1370
+ start_ms: number;
1371
+ end_ms: number;
1372
+ reasons: components["schemas"]["Reason"][];
1373
+ /** @description timestamps of the frames supporting visual reasons; the tenant seeks its own video */
1374
+ frame_timestamps_ms: number[];
1375
+ note?: string;
1376
+ }[];
1377
+ summary_for_reviewer: string;
1378
+ };
1379
+ ModerationItem: {
1380
+ id: string;
1381
+ ref: string;
1382
+ surface: components["schemas"]["Surface"];
1383
+ /** @enum {string} */
1384
+ kind: "image" | "video" | "text";
1385
+ /** @enum {string} */
1386
+ status: "queued" | "processing" | "completed" | "failed";
1387
+ decision?: components["schemas"]["Decision"];
1388
+ quality?: components["schemas"]["Quality"];
1389
+ video?: components["schemas"]["VideoFindings"];
1390
+ error?: components["schemas"]["Problem"];
1391
+ /** Format: date-time */
1392
+ completed_at?: string;
1393
+ processing_ms?: number;
1394
+ usage?: components["schemas"]["Usage"];
1395
+ };
1396
+ ModerationJob: {
1397
+ id: string;
1398
+ /** @enum {string} */
1399
+ status: "queued" | "processing" | "completed" | "partially_failed";
1400
+ items: components["schemas"]["ModerationItem"][];
1401
+ estimated_completion_seconds?: {
1402
+ [key: string]: number;
1403
+ };
1404
+ };
1405
+ ModerationJobSummary: {
1406
+ id: string;
1407
+ /** @enum {string} */
1408
+ status: "completed" | "partially_failed";
1409
+ counts: {
1410
+ clean?: number;
1411
+ review?: number;
1412
+ hold?: number;
1413
+ never?: number;
1414
+ failed?: number;
1415
+ };
1416
+ };
1417
+ /** @description A destination configured in Studio (an S3-compatible bucket and a write-only key). */
1418
+ DestinationId: string;
1419
+ /** @description `poster` and `captions` write where `transcode` writes unless they name a destination. Outside the mock environment a step that writes needs a destination. An image takes only `moderate`. */
1420
+ MediaSteps: {
1421
+ moderate: {
1422
+ /** @default default */
1423
+ policy: string;
1424
+ };
1425
+ transcode?: {
1426
+ /** @enum {string} */
1427
+ profile: "tour_hls_v1";
1428
+ destination?: components["schemas"]["DestinationId"];
1429
+ };
1430
+ /** @description One JPEG plus a 6-second silent preview MP4. `auto` picks a sharp, well-lit frame. */
1431
+ poster?: {
1432
+ /** @default auto */
1433
+ at: "auto" | number;
1434
+ destination?: components["schemas"]["DestinationId"];
1435
+ };
1436
+ /** @description WebVTT from the transcript the moderation step makes. */
1437
+ captions?: {
1438
+ languages: string[];
1439
+ destination?: components["schemas"]["DestinationId"];
1440
+ };
1441
+ };
1442
+ MediaJobCreate: {
1443
+ ref: string;
1444
+ surface: components["schemas"]["Surface"];
1445
+ subject: components["schemas"]["Subject"];
1446
+ media: components["schemas"]["Media"];
1447
+ steps: components["schemas"]["MediaSteps"];
1448
+ /**
1449
+ * @description Orders work inside the tenant's own turn; never ahead of another tenant's fair share.
1450
+ * @default normal
1451
+ * @enum {string}
1452
+ */
1453
+ priority: "high" | "normal" | "low";
1454
+ context?: {
1455
+ [key: string]: string;
1456
+ };
1457
+ /**
1458
+ * @description Reprocess even when a dedupe match exists.
1459
+ * @default false
1460
+ */
1461
+ force: boolean;
1462
+ };
1463
+ /**
1464
+ * @description `waiting`: for another step; `skipped`: not run because a step it needs failed or the verdict was `never`.
1465
+ * @enum {string}
1466
+ */
1467
+ MediaStepStatus: "waiting" | "queued" | "processing" | "completed" | "failed" | "skipped" | "cancelled";
1468
+ MediaJobStep: {
1469
+ status: components["schemas"]["MediaStepStatus"];
1470
+ progress_percent: number;
1471
+ /** @enum {string} */
1472
+ stage?: "fetching" | "analyzing" | "transcoding" | "uploading";
1473
+ /** @description `moderate`, when completed */
1474
+ decision?: components["schemas"]["Decision"];
1475
+ video?: components["schemas"]["VideoFindings"];
1476
+ error?: components["schemas"]["Problem"];
1477
+ /** Format: date-time */
1478
+ completed_at?: string;
1479
+ };
1480
+ MediaOutput: {
1481
+ /** @enum {string} */
1482
+ step: "transcode" | "poster" | "captions";
1483
+ /**
1484
+ * @example hls_master
1485
+ * @example hls_720p
1486
+ * @example mp4_fallback
1487
+ * @example poster
1488
+ * @example preview
1489
+ * @example captions
1490
+ */
1491
+ role?: string;
1492
+ /** @description Object key in the tenant's bucket, under `<prefix>/<job id>/` */
1493
+ key: string;
1494
+ destination_id?: components["schemas"]["DestinationId"];
1495
+ content_type: string;
1496
+ bytes: number;
1497
+ sha256: string;
1498
+ duration_ms?: number;
1499
+ width?: number;
1500
+ height?: number;
1501
+ language?: string;
1502
+ };
1503
+ MediaJob: {
1504
+ id: string;
1505
+ ref: string;
1506
+ surface: components["schemas"]["Surface"];
1507
+ /** @enum {string} */
1508
+ kind: "image" | "video";
1509
+ subject: {
1510
+ id: components["schemas"]["SubjectId"];
1511
+ type: components["schemas"]["SubjectType"];
1512
+ };
1513
+ /** @enum {string} */
1514
+ status: "queued" | "processing" | "completed" | "failed" | "cancelled";
1515
+ /** @enum {string} */
1516
+ stage: "waiting" | "fetching" | "analyzing" | "transcoding" | "uploading" | "done";
1517
+ progress_percent: number;
1518
+ /** @enum {string} */
1519
+ priority: "high" | "normal" | "low";
1520
+ /** @description While queued, the job's place among this tenant's waiting work in its first lane. */
1521
+ queue_position: number | null;
1522
+ /**
1523
+ * Format: date-time
1524
+ * @description From the current queue and measured step durations; null once the job has ended.
1525
+ */
1526
+ estimated_completion_at: string | null;
1527
+ /** @description One entry per requested step. */
1528
+ steps: {
1529
+ moderate?: components["schemas"]["MediaJobStep"];
1530
+ transcode?: components["schemas"]["MediaJobStep"];
1531
+ poster?: components["schemas"]["MediaJobStep"];
1532
+ captions?: components["schemas"]["MediaJobStep"];
1533
+ };
1534
+ /** @description Set when the job completes (empty after a `never` verdict); null before. */
1535
+ outputs: components["schemas"]["MediaOutput"][] | null;
1536
+ error?: components["schemas"]["Problem"];
1537
+ /** @description 1, plus one per retry from Studio's dead letters */
1538
+ attempts: number;
1539
+ context: {
1540
+ [key: string]: string;
1541
+ };
1542
+ /** Format: date-time */
1543
+ created_at: string;
1544
+ /** Format: date-time */
1545
+ completed_at?: string;
1546
+ };
1547
+ MediaJobCreated: components["schemas"]["MediaJob"] & {
1548
+ /** @description `202` only: the expected wait is past the tenant's soft limit. */
1549
+ delayed?: boolean;
1550
+ /** @description `200` only: the earlier job with the same content, steps and policy version. */
1551
+ deduplicated?: boolean;
1552
+ };
1553
+ MediaJobProgress: {
1554
+ id: string;
1555
+ ref: string;
1556
+ /** @enum {string} */
1557
+ status: "processing";
1558
+ /** @enum {string} */
1559
+ stage: "fetching" | "analyzing" | "transcoding" | "uploading";
1560
+ progress_percent: number;
1561
+ /** Format: date-time */
1562
+ estimated_completion_at: string | null;
1563
+ };
1564
+ MediaJobStepCompleted: components["schemas"]["MediaJob"] & {
1565
+ /** @enum {string} */
1566
+ completed_step: "moderate" | "transcode" | "poster" | "captions";
1567
+ };
1568
+ MediaProfile: {
1569
+ /** @example tour_hls_v1 */
1570
+ id: string;
1571
+ description: string;
1572
+ /** @enum {string} */
1573
+ container: "hls";
1574
+ segment_seconds?: number;
1575
+ video_codec?: string;
1576
+ audio_codec?: string;
1577
+ renditions: {
1578
+ name: string;
1579
+ height: number;
1580
+ video_kbps?: number;
1581
+ audio_kbps?: number;
1582
+ }[];
1583
+ fallback?: {
1584
+ container?: string;
1585
+ height?: number;
1586
+ faststart?: boolean;
1587
+ };
1588
+ };
1589
+ ScreeningRequest: {
1590
+ /** @default default */
1591
+ policy: string;
1592
+ surface: components["schemas"]["Surface"];
1593
+ subject: components["schemas"]["Subject"];
1594
+ text: string;
1595
+ context?: {
1596
+ conversation_ref?: string;
1597
+ booking_exists?: boolean;
1598
+ previous_messages?: {
1599
+ /** @enum {string} */
1600
+ author: "self" | "other";
1601
+ text: string;
1602
+ }[];
1603
+ };
1604
+ };
1605
+ ScreeningResponse: {
1606
+ decision: components["schemas"]["Decision"];
1607
+ /** @enum {string} */
1608
+ action: "allow" | "nudge" | "hold" | "block";
1609
+ nudge?: {
1610
+ /** @enum {string} */
1611
+ audience: "author" | "recipient" | "both";
1612
+ text: string;
1613
+ };
1614
+ };
1615
+ FeedbackRecord: {
1616
+ decision_id?: string;
1617
+ turn_id?: string;
1618
+ /** @enum {string} */
1619
+ outcome: "approved" | "rejected" | "edited" | "escalated" | "user_appealed" | "appeal_upheld" | "appeal_denied" | "thumbs_up" | "thumbs_down";
1620
+ /** @enum {string} */
1621
+ final_class?: "clean" | "review" | "hold" | "never";
1622
+ reason_codes?: string[];
1623
+ reviewer?: {
1624
+ /** @enum {string} */
1625
+ role?: "admin" | "moderator" | "support" | "end_user";
1626
+ ref?: string;
1627
+ };
1628
+ /** Format: date-time */
1629
+ decided_at: string;
1630
+ note?: string;
1631
+ } & (unknown | unknown);
1632
+ FeedbackBatch: {
1633
+ records: components["schemas"]["FeedbackRecord"][];
1634
+ };
1635
+ WebhookEnvelope: {
1636
+ /** @description evt_…; deduplicate on it */
1637
+ id: string;
1638
+ /** @enum {string} */
1639
+ type: "moderation.item.completed" | "moderation.job.completed" | "document_check.completed" | "session.completed" | "subject.erased" | "usage.threshold_reached" | "feature.state_changed" | "media.job.queued" | "media.job.progress" | "media.job.step.completed" | "media.job.completed" | "media.job.failed" | "media.job.cancelled" | "conversation.updated" | "conversation.completed" | "conversation.failed" | "conversation.expired";
1640
+ /** Format: date-time */
1641
+ created_at: string;
1642
+ /** @enum {string} */
1643
+ environment: "mock" | "development" | "production";
1644
+ data: Record<string, unknown>;
1645
+ };
1646
+ /** @enum {string} */
1647
+ DocumentType: "driver_license" | "state_id" | "business_license" | "cosmetology_license" | "insurance_card" | "insurance_certificate";
1648
+ DocumentCheckCreate: {
1649
+ ref: string;
1650
+ document_type: components["schemas"]["DocumentType"];
1651
+ subject: components["schemas"]["Subject"];
1652
+ pages: {
1653
+ /** @enum {string} */
1654
+ side: "front" | "back" | "page";
1655
+ media: {
1656
+ /** Format: uri */
1657
+ url: string;
1658
+ /** Format: date-time */
1659
+ url_expires_at?: string;
1660
+ /** @enum {string} */
1661
+ content_type: "image/jpeg" | "image/png" | "image/webp" | "image/heic" | "application/pdf";
1662
+ bytes?: number;
1663
+ sha256?: string;
1664
+ };
1665
+ }[];
1666
+ expected?: {
1667
+ full_name?: string | null;
1668
+ business_name?: string | null;
1669
+ region?: string | null;
1670
+ };
1671
+ };
1672
+ DocumentFlag: {
1673
+ /** @enum {string} */
1674
+ code: "expired" | "expires_soon" | "name_mismatch" | "business_name_mismatch" | "region_mismatch" | "wrong_document_type" | "unreadable" | "incomplete" | "photo_of_screen" | "possible_edit" | "barcode_text_mismatch";
1675
+ /** @enum {string} */
1676
+ severity: "info" | "review";
1677
+ detail?: string;
1678
+ /** @description for *_mismatch: true when normalisation found them equivalent */
1679
+ matched?: boolean;
1680
+ /** @enum {string} */
1681
+ signal_strength?: "weak" | "moderate" | "strong";
1682
+ };
1683
+ DocumentCheck: {
1684
+ id: string;
1685
+ ref: string;
1686
+ /** @enum {string} */
1687
+ status: "queued" | "processing" | "completed" | "failed";
1688
+ document_type_detected?: string | null;
1689
+ /** @description null after 24 hours; the date of birth is never returned */
1690
+ fields?: {
1691
+ full_name?: string | null;
1692
+ document_number?: string | null;
1693
+ issuer?: string | null;
1694
+ region?: string | null;
1695
+ license_class?: string | null;
1696
+ /** Format: date */
1697
+ issued_on?: string | null;
1698
+ /** Format: date */
1699
+ expires_on?: string | null;
1700
+ date_of_birth_present?: boolean | null;
1701
+ business_name?: string | null;
1702
+ insured_name?: string | null;
1703
+ policy_number?: string | null;
1704
+ carrier?: string | null;
1705
+ /** Format: date */
1706
+ coverage_end?: string | null;
1707
+ } | null;
1708
+ field_sources?: {
1709
+ [key: string]: "barcode" | "text_recognition" | "visual";
1710
+ } | null;
1711
+ flags?: components["schemas"]["DocumentFlag"][];
1712
+ decision?: components["schemas"]["Decision"];
1713
+ model?: {
1714
+ id?: string;
1715
+ provider?: string;
1716
+ };
1717
+ error?: components["schemas"]["Problem"];
1718
+ /** Format: date-time */
1719
+ completed_at?: string;
1720
+ /** Format: date-time */
1721
+ fields_expire_at?: string;
1722
+ processing_ms?: number;
1723
+ };
1724
+ /** @description Common fields; the per-type fields and their strictness come from the event catalogue's JSON Schemas (contract §13.2). */
1725
+ Event: {
1726
+ event_id: string;
1727
+ /** @enum {string} */
1728
+ type: "search.performed" | "listing.viewed" | "booking.requested" | "booking.confirmed" | "booking.completed" | "booking.no_show" | "booking.cancelled" | "signup.completed" | "listing.submitted" | "listing.approved" | "listing.rejected" | "lead.created" | "lead.contacted" | "lead.converted" | "review.posted";
1729
+ /** Format: date-time */
1730
+ occurred_at: string;
1731
+ subject_id: components["schemas"]["SubjectId"];
1732
+ subject_type: components["schemas"]["SubjectType"];
1733
+ } & {
1734
+ [key: string]: unknown;
1735
+ };
1736
+ };
1737
+ responses: {
1738
+ /** @description Problem details (RFC 9457) */
1739
+ Problem: {
1740
+ headers: {
1741
+ "Vinaverse-Request-Id": components["headers"]["RequestId"];
1742
+ [name: string]: unknown;
1743
+ };
1744
+ content: {
1745
+ "application/problem+json": components["schemas"]["Problem"];
1746
+ };
1747
+ };
1748
+ /** @description Too many requests */
1749
+ RateLimited: {
1750
+ headers: {
1751
+ "Retry-After"?: number;
1752
+ [name: string]: unknown;
1753
+ };
1754
+ content: {
1755
+ "application/problem+json": components["schemas"]["Problem"];
1756
+ };
1757
+ };
1758
+ };
1759
+ parameters: {
1760
+ IdempotencyKey: string;
1761
+ MediaJobId: string;
1762
+ ConversationId: components["schemas"]["ConversationId"];
1763
+ SessionId: string;
1764
+ };
1765
+ requestBodies: never;
1766
+ headers: {
1767
+ /** @description Vinaverse-Request-Id */
1768
+ RequestId: string;
1769
+ };
1770
+ pathItems: never;
1771
+ }
1772
+
1773
+ type Schemas = components['schemas'];
1774
+ type ClientEvent = Schemas['ClientEvent'];
1775
+ type ServerEvent = Schemas['ServerEvent'];
1776
+ type ServerEventType = ServerEvent['type'];
1777
+ type ServerEventOf<T extends ServerEventType> = Extract<ServerEvent, {
1778
+ type: T;
1779
+ }>;
1780
+ type Card = Schemas['Card'];
1781
+ type CardKind = Card['kind'];
1782
+ type Question = Schemas['Question'];
1783
+ type AnswerInput = Schemas['AnswerInput'];
1784
+ type Proposal = Schemas['Proposal'];
1785
+ type TranscriptItem = Schemas['TranscriptItem'];
1786
+ type ClientSecret = Schemas['ClientSecret'];
1787
+ type TurnInput = Schemas['TurnInput'];
1788
+ /** The session as createSession returns it to the tenant's backend, which passes this part to the app. */
1789
+ type SessionInfo = Pick<Schemas['Session'], 'id' | 'realtime_url' | 'turns_url' | 'conversation_id'> & {
1790
+ client_secret: ClientSecret | string;
1791
+ };
1792
+ /** The outcomes an app reports on a proposal card with `ui.action`. */
1793
+ type ProposalOutcome = 'confirmed' | 'declined' | 'failed' | 'expired';
1794
+ /** Whether this client knows how to read a card of this kind; skip any other card (§5). */
1795
+ declare function isKnownCard(card: {
1796
+ kind: string;
1797
+ }): card is Card;
1798
+
1799
+ type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
1800
+ /** One SSE request: where and with which secret, and the `fetch` to use. */
1801
+ interface SseTarget {
1802
+ /** The session's `turns_url` (`…/v1/sessions/{session_id}/turns`). */
1803
+ readonly turnsUrl: string;
1804
+ readonly secret: string;
1805
+ readonly fetch?: FetchLike;
1806
+ readonly signal?: AbortSignal;
1807
+ }
1808
+ /** Run one turn from a typed message (`createTurnSse`), yielding its events as they arrive. */
1809
+ declare function turnSse(target: SseTarget, input: {
1810
+ text: string;
1811
+ client_event_id?: string;
1812
+ }): AsyncGenerator<Frame>;
1813
+ /** Send a card action, such as a proposal outcome, and stream the resulting turn (`createCardActionSse`). */
1814
+ declare function cardActionSse(target: SseTarget, turnId: string, input: {
1815
+ card_id: string;
1816
+ action: string;
1817
+ client_event_id?: string;
1818
+ }): AsyncGenerator<Frame>;
1819
+ /** Frames from an SSE text stream: blank-line separated events, `data:` lines joined; comments and non-JSON skipped. */
1820
+ declare function sseFrames(chunks: AsyncIterable<string>): AsyncGenerator<Frame>;
1821
+
1822
+ /** The part of a WebSocket the session uses: the browser, React Native and Node 22+ ones all fit. */
1823
+ interface WebSocketLike {
1824
+ readonly readyState: number;
1825
+ send(data: string): void;
1826
+ close(code?: number, reason?: string): void;
1827
+ onopen: ((event: unknown) => void) | null;
1828
+ onmessage: ((event: {
1829
+ data: unknown;
1830
+ }) => void) | null;
1831
+ onclose: ((event: {
1832
+ code: number;
1833
+ reason?: string;
1834
+ }) => void) | null;
1835
+ onerror: ((event: unknown) => void) | null;
1836
+ }
1837
+ /**
1838
+ * Opens a WebSocket. `headers` is set only with `auth: 'header'`. The default factory passes them as React Native's
1839
+ * WebSocket takes them (its third argument) or, on Node 22+, as its WebSocket does (`{ protocols, headers }`); a
1840
+ * browser's WebSocket cannot send headers, so browsers use the subprotocol.
1841
+ */
1842
+ type WebSocketFactory = (url: string, protocols: string[], headers?: Record<string, string>) => WebSocketLike;
1843
+ /** How the client secret reaches the server (§1); the secret never goes in the URL. */
1844
+ type AuthMode = 'header' | 'subprotocol' | 'first_frame';
1845
+ type SessionStatus =
1846
+ /** Opening a connection, or waiting for `session.ready`. */
1847
+ 'connecting'
1848
+ /** `session.ready` arrived: events flow. */
1849
+ | 'ready'
1850
+ /** Waiting to reconnect after the connection dropped. */
1851
+ | 'reconnecting'
1852
+ /** Disconnected; connects again on the next send or `connect()`. */
1853
+ | 'idle'
1854
+ /** Running turns over SSE, because the WebSocket could not be opened (twice); `connect()` tries it again. */
1855
+ | 'sse'
1856
+ /**
1857
+ * The session ended. `state.reconnect` says what next: `new_session_on_next_action` after it ended idle, expired
1858
+ * or at its limit (have your backend create a new session for the conversation on the person's next action, then
1859
+ * `resume()` it; what they send meanwhile is held and resent there), or `user_action_only` (the conversation
1860
+ * continued elsewhere, or the session was revoked: resume only when the person asks).
1861
+ */
1862
+ | 'ended'
1863
+ /** The client secret was refused (4401) and no fresh one could be had. */
1864
+ | 'unauthorized'
1865
+ /** Closed by the app. */
1866
+ | 'closed';
1867
+ interface SessionOptions {
1868
+ /** The session the tenant's backend created: its `id`, `realtime_url`, `turns_url` and `client_secret`. */
1869
+ readonly session: SessionInfo;
1870
+ /**
1871
+ * A fresh client secret for the session named, from the tenant's backend (refreshClientSecret is a service-key
1872
+ * call), when the server refuses the current one (4401): for example after the first connection's window (two
1873
+ * minutes from issue on the current gateway) passed. It is asked for the session in use; an answer that arrives
1874
+ * after `resume()` moved to another session, or after `close()`, is dropped. Without it, a refused secret ends in
1875
+ * `unauthorized`.
1876
+ */
1877
+ readonly refreshSecret?: (sessionId: string) => Promise<ClientSecret | string>;
1878
+ /**
1879
+ * Default `subprotocol` (`vinaverse.v1, bearer.<secret>`). `header` where the WebSocket takes headers: React
1880
+ * Native and Node 22+ with the default factory, or your own `webSocket`.
1881
+ */
1882
+ readonly auth?: AuthMode;
1883
+ /** Default: the global WebSocket. */
1884
+ readonly webSocket?: WebSocketFactory;
1885
+ /** Default: the global fetch. Used by the SSE fallback. */
1886
+ readonly fetch?: FetchLike;
1887
+ /** Fall back to SSE turns when the WebSocket cannot be opened. Default true. */
1888
+ readonly sseFallback?: boolean;
1889
+ /** Delays before reconnecting after a drop, in ms; the last one repeats. Default 1 s doubling to 30 s. */
1890
+ readonly reconnectDelays?: readonly number[];
1891
+ /** Attempts to reconnect before going `idle`; they count again once a connection stayed up 30 s. Default 8. */
1892
+ readonly maxReconnects?: number;
1893
+ /** Delays before resending an input refused with `screen_unavailable`, in ms. Default 2 s, 4 s, 8 s. */
1894
+ readonly screenRetryDelays?: readonly number[];
1895
+ /** The `ping` interval while in the foreground, in ms. Default 25 s. */
1896
+ readonly pingInterval?: number;
1897
+ /** New `client_event_id`s. Default: random, `ce_` and 22 characters. */
1898
+ readonly createId?: () => string;
1899
+ /** An engine to continue, for example one an app kept across a restart. */
1900
+ readonly engine?: ConversationEngine;
1901
+ }
1902
+ interface SessionEvents {
1903
+ /**
1904
+ * Every event the server sends, `message.delta` included. A type this client does not know arrives too (§5), so a
1905
+ * `switch` on `type` keeps a default branch.
1906
+ */
1907
+ event: ServerEvent;
1908
+ /** The conversation's state changed. */
1909
+ state: ConversationState;
1910
+ status: SessionStatus;
1911
+ /**
1912
+ * Something failed that no call returned: an input that could not be delivered (it is also shown as not sent in
1913
+ * the state), an SSE turn, or getting a fresh secret.
1914
+ */
1915
+ error: VinaverseClientError;
1916
+ }
1917
+ type Listener<T> = (value: T) => void;
1918
+ declare class VinaverseSession {
1919
+ readonly engine: ConversationEngine;
1920
+ private info;
1921
+ private secret;
1922
+ private readonly options;
1923
+ private socket;
1924
+ /** The engine's name for the connection in `socket`, once it opened. */
1925
+ private opened;
1926
+ private connection;
1927
+ private ready;
1928
+ private current;
1929
+ private everOpened;
1930
+ private firstFailures;
1931
+ private attempts;
1932
+ /** The reason of the `session.closed` that preceded the close, on the current connection. */
1933
+ private closeReason;
1934
+ private stableTimer;
1935
+ /** SSE requests in flight. */
1936
+ private readonly streams;
1937
+ private reconnectTimer;
1938
+ private pingTimer;
1939
+ private foreground;
1940
+ private refreshed;
1941
+ /** client_event_id → why it waits: after the turn, or after a backoff. */
1942
+ private readonly waiting;
1943
+ /** client_event_id → the session that refused it for its turn limit: resent only on another session. */
1944
+ private readonly heldForSession;
1945
+ private readonly screenTries;
1946
+ private readonly screenTimers;
1947
+ private readonly listeners;
1948
+ constructor(options: SessionOptions);
1949
+ /** The engine names the session, and keeps the conversation it already knew when this one does not say. */
1950
+ private named;
1951
+ get status(): SessionStatus;
1952
+ get state(): ConversationState;
1953
+ on<K extends keyof SessionEvents>(name: K, listener: Listener<SessionEvents[K]>): () => void;
1954
+ /**
1955
+ * Connect, or reconnect: the user's own action, so it also resumes after the conversation continued elsewhere
1956
+ * or the secret was refused.
1957
+ */
1958
+ connect(): void;
1959
+ private start;
1960
+ /**
1961
+ * Continue the conversation on a new session the tenant's backend created for it (by `conversation_key` or
1962
+ * `conversation_id`): its `session.ready` brings the transcript and pending cards, and what was never
1963
+ * acknowledged is resent there. A user action, like `connect()`.
1964
+ */
1965
+ resume(session: SessionInfo): void;
1966
+ /** Close for good: the connection, any SSE turn still streaming, and every timer. */
1967
+ close(): void;
1968
+ /** Ping only while the app is in the foreground (every 25 s); in the background the connection may idle out. */
1969
+ setForeground(foreground: boolean): void;
1970
+ private open;
1971
+ private afterClose;
1972
+ /** After a 4401: one fresh secret for the session in use, unless it moved on or was closed meanwhile. */
1973
+ private refresh;
1974
+ private scheduleReconnect;
1975
+ private cancelReconnect;
1976
+ private fallBack;
1977
+ /** Close the current connection, if any, without reacting to its close event. */
1978
+ private drop;
1979
+ private forget;
1980
+ private frame;
1981
+ private receive;
1982
+ private refusedForNow;
1983
+ private resendUnacknowledged;
1984
+ /** What a turn limit held, on a session other than the one that refused it. */
1985
+ private resendHeldForSession;
1986
+ private resendWaiting;
1987
+ private resend;
1988
+ /** Keep an input for a later session: unacknowledged, so the next `session.ready` (after resume()) sends it. */
1989
+ private hold;
1990
+ /** The session ended: retries waiting on a timer would go nowhere, so their inputs are held for the next session. */
1991
+ private holdRetries;
1992
+ /** Send a typed message; returns its `client_event_id`. Over SSE it runs the turn there. */
1993
+ sendText(text: string): string;
1994
+ /** Answer a question card (or a `rate_each` row, by the row's `question_id`); returns the `client_event_id`. */
1995
+ answer(cardId: string, questionId: string, answer: AnswerInput): string;
1996
+ /**
1997
+ * Report a proposal's outcome after the tenant's backend answered (§6): `confirmed`, `declined`, `failed` or
1998
+ * `expired`. The first outcome per card is final. Returns the `client_event_id`.
1999
+ */
2000
+ reportOutcome(cardId: string, outcome: ProposalOutcome): string;
2001
+ /** A card action (`ui.action`), named by the card's `id`; the card's turn is taken from the state. */
2002
+ action(cardId: string, action: string): string;
2003
+ /** Stop the turn in progress: `input.cancel` on the WebSocket; over SSE, the turn's request is stopped. */
2004
+ cancel(turnId: string): void;
2005
+ /** Ended idle: inputs are held for the new session that `resume()` brings. */
2006
+ private awaitingNewSession;
2007
+ private live;
2008
+ /**
2009
+ * Every input goes through here: recorded as sent (unacknowledged until the server answers it), then over SSE,
2010
+ * or on the WebSocket when the session is ready, or held and resent once `session.ready` arrives.
2011
+ */
2012
+ private deliver;
2013
+ private transmit;
2014
+ /** One input over SSE: a typed message or a card action; an answer cannot go, and is shown as not sent. */
2015
+ private overSse;
2016
+ private isUnacknowledged;
2017
+ private inFlight;
2018
+ private stopSse;
2019
+ private sseTarget;
2020
+ private newId;
2021
+ private startPing;
2022
+ private stopPing;
2023
+ private setStatus;
2024
+ private emitState;
2025
+ private emit;
2026
+ }
2027
+
2028
+ export { type AnswerInput as A, turnSse as B, type ClientSecret as C, type FetchLike as F, type HeldCard as H, type Message as M, type ProposalOutcome as P, type Question as Q, type Reconnect as R, type SessionInfo as S, type ToolLine as T, VinaverseClientError as V, type WebSocketFactory as W, type SessionOptions as a, CONTRACT as b, type Card as c, type CardKind as d, type ClientErrorCode as e, type HeldQuestion as f, type Proposal as g, type AuthMode as h, isKnownCard as i, type ClientEvent as j, type Connection as k, ConversationEngine as l, type ConversationState as m, type Frame as n, type ServerEvent as o, type ServerEventOf as p, type ServerEventType as q, type SessionEvents as r, type SessionStatus as s, type SseTarget as t, type TranscriptItem as u, type TurnInput as v, VinaverseSession as w, type WebSocketLike as x, cardActionSse as y, sseFrames as z };