@vinaverse/server 0.1.0-alpha.1 → 0.1.0-alpha.10

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.
@@ -15,11 +15,15 @@ interface paths {
15
15
  put?: never;
16
16
  /**
17
17
  * Create an agent session for one subject
18
- * @description A session belongs to a conversation when it names one (`conversation_key`, `conversation_id`, or both) or when its agent runs a guided flow; a flow session that names none starts a new conversation. `Session` then returns its `conversation_id`, and a conversation started without a key can later be resumed by that id. A conversation is scoped to the tenant, the environment, the `subject.id` and the `agent`; `subject.type` is not part of it, so a guest who signs in keeps the conversation.
18
+ * @description A session belongs to a conversation when it names one (`conversation_key`, `conversation_id`, or both) or when its agent runs a guided flow; a flow session that names none starts a new conversation. `Session` then returns its `conversation_id`, and a conversation started without a key can later be resumed by that id. A conversation is scoped to the tenant, the environment, the `subject.id` and the `agent`, and (v1-draft.20) the mode and business scope it started with; `subject.type` itself is not part of it, so a guest who signs in keeps the conversation.
19
19
  * **`business_setup`.** The `business_setup` capability is only for a business's own people: a `subject.type` of `professional`, `host` or `staff`. Asked for any other subject, it is `400 invalid_request` with the pointer `/tool_access/capabilities`.
20
20
  * **`booking_proposals`.** The `booking_proposals` capability is only for a `client`: not a guest, who has no account in the tenant's app to confirm with. Asked for any other subject, it is `400 invalid_request` with the pointer `/tool_access/capabilities`.
21
+ * **Audience** (v1-draft.20). An agent talks only to the subject types of its template (admin-v1 `AgentProfile.audience`): the customer agent to a `client` or `guest`, a business agent to a `professional`, `host` or `staff`, a guided flow to anyone its tenant names. Any other `subject.type` is `400 invalid_request` with the pointer `/subject/type`. The tenant's backend sets the type by the side of its app the person is on, never by the account's roles: a professional booking for themselves is a `client`.
22
+ * **Mode and business scope** (v1-draft.20). `experience_mode` is the agent's side: `customer` for the customer agent, `business` for a business agent. Left out, it follows the agent's template; given, it must match it (`400 invalid_request`, `/experience_mode`). `business_scope_id` names the business the person acts for (a professional's business, a host's place): only with `business` (`400`, `/business_scope_id`), and the tenant's backend derives it from its own records, never from the app. A conversation is bound, when it starts, to the subject, the agent, the mode and the business scope. The mode stands for the side of the app, so a guest who signs in as a client keeps their conversation, while a professional never resumes a customer conversation (the audience rule keeps each side's types apart). Resuming with any of them different is `404 not_found` (step 1 below), never a switch: a person acting for business A never resumes business B's conversation.
21
23
  * **The checks, in this order:** 1. `conversation_id` is given, and is unknown, erased, or belongs to another tenant, environment, `agent` or
22
- * `subject.id`: `404 not_found`.
24
+ * `subject.id`, or (v1-draft.20) was started with another `experience_mode` or `business_scope_id`: `404
25
+ * not_found`. A `conversation_key` names a conversation within the same binding,
26
+ * so the same key under another binding names another conversation.
23
27
  * 2. Both are given, and the key names a different conversation, or none: `409 conversation_mismatch`. 3. The conversation expired: `410 conversation_expired`. With `conversation_key` alone, an expired
24
28
  * conversation instead lets a new one start under the key; the id never starts one silently.
25
29
  * 4. The conversation is completed (its flow was accepted): `409 conversation_completed`. This holds by key too,
@@ -69,6 +73,30 @@ interface paths {
69
73
  patch?: never;
70
74
  trace?: never;
71
75
  };
76
+ "/v1/conversations": {
77
+ parameters: {
78
+ query?: never;
79
+ header?: never;
80
+ path?: never;
81
+ cookie?: never;
82
+ };
83
+ /**
84
+ * A person's past conversations with one agent, newest activity first
85
+ * @description v1-draft.21 (VIN-246): the conversations a history screen lists, for one subject and one agent, on one side of the app (`experience_mode`, and for a business `business_scope_id`, as the conversation was bound when it started). Newest activity first; ties by `id`.
86
+ * **Access.** A service key only, never a client secret: the app asks the tenant's backend, which checks the person and calls this. The key needs `conversations:read` (a key without scopes has every scope) and sees its own tenant and environment only.
87
+ * **What is listed.** Only what the tenant's `transcript_retention` still keeps: a conversation past it, a deleted one (`deleteConversation`) or an erased subject's is never listed, nor one where nothing was said (a session opened and left). A subject Vinaverse holds nothing for gets an empty page, the same as one with no conversations, so the answer never says whether a subject exists.
88
+ * **The title** is deterministic and never the person's words: from the conversation's last search intent (`title_source: intent`, for example "Haircut · Los Angeles", in the subject's locale), else the date it started (`title_source: date`). There is no preview of the messages.
89
+ * **Paging.** `next_cursor` is opaque and signed. It is bound to the filters, the key's tenant and environment, and expires after 24 hours: a cursor changed, used with other filters or expired is `400 invalid_request` at `/cursor`, and the caller starts again without it. An outage is `503 unavailable` (retryable), never an empty page: the app does not take it as "no conversations".
90
+ */
91
+ get: operations["listConversations"];
92
+ put?: never;
93
+ post?: never;
94
+ delete?: never;
95
+ options?: never;
96
+ head?: never;
97
+ patch?: never;
98
+ trace?: never;
99
+ };
72
100
  "/v1/conversations/{conversation_id}": {
73
101
  parameters: {
74
102
  query?: never;
@@ -91,6 +119,42 @@ interface paths {
91
119
  get: operations["getConversation"];
92
120
  put?: never;
93
121
  post?: never;
122
+ /**
123
+ * Delete one of a person's conversations, everywhere
124
+ * @description v1-draft.21 (VIN-246): the person deletes a conversation from their history, through the tenant's backend. A service key with `conversations:write`, for its own tenant and environment; `subject_id` must be the conversation's subject.
125
+ * **What it does, before it answers.** - Ends the conversation's open sessions: their sockets get `session.closed` with `reason: deleted` and
126
+ * close with `4403` (a client that does not know `deleted` reads it as `revoked`). Their status ends as
127
+ * `deleted`.
128
+ * - Blanks every copy of the conversation's messages, cards, screening texts and state (and the guided flow's
129
+ * answers), across both sides of the app, and leaves no title or count behind.
130
+ * - Fences late writes: a turn still running, a webhook retry or a worker job that finishes after the delete
131
+ * records nothing for it (a deletion barrier, kept as long as the longest session could run).
132
+ * - Frees its `conversation_key`: a session with the same key afterwards starts a new conversation.
133
+ * **Answers.** `200` with the number of sessions revoked. An unknown id, another tenant's, environment's or subject's, or one already deleted or erased: `404 not_found`, the same answer for each, so it never says whether the conversation exists. Deleting is not erasing the subject (`eraseSubject`): the person's other conversations and memory stay.
134
+ */
135
+ delete: operations["deleteConversation"];
136
+ options?: never;
137
+ head?: never;
138
+ patch?: never;
139
+ trace?: never;
140
+ };
141
+ "/v1/conversations/{conversation_id}/messages": {
142
+ parameters: {
143
+ query?: never;
144
+ header?: never;
145
+ path?: never;
146
+ cookie?: never;
147
+ };
148
+ /**
149
+ * One past conversation, as one ordered timeline of messages and cards
150
+ * @description v1-draft.21 (VIN-246): what a history screen draws when the person opens a past conversation. A service key with `conversations:read`, for its own tenant and environment; `subject_id` must be the conversation's subject.
151
+ * **The timeline.** One list of messages and the cards their turns drew, in the order they happened, each with an `id` that is stable and unique within the conversation. The first page is the newest; within a page, oldest first; `next_cursor` pages toward older items. A message carries `event_id`, the identifier its live event gave it (as `TranscriptItem.id`: an `assistant` message's `turn_id`, a `person` message's `client_event_id`, a `staff` message's `message_id`), so an app merges what it drew live, or a resumed `session.ready.transcript`, into the fetched history by `role` and `event_id` instead of replacing either. An input the runtime never recorded is not in it, as in the transcript.
152
+ * **Cards from history are read-only.** An app draws their actions disabled: a past slot, price or availability is not checked again, so a tap would act on stale data. The person asks again in the conversation, and a fresh turn draws fresh cards.
153
+ * **What is kept.** Only what the tenant's `transcript_retention` still keeps; a conversation past it is `410 conversation_expired`. An unknown id, another tenant's, environment's or subject's, or a deleted or erased one: `404 not_found`, the same answer for each. A cursor changed, used for another conversation or expired (24 hours) is `400 invalid_request` at `/cursor`. An outage is `503 unavailable`, never an empty timeline.
154
+ */
155
+ get: operations["listConversationMessages"];
156
+ put?: never;
157
+ post?: never;
94
158
  delete?: never;
95
159
  options?: never;
96
160
  head?: never;
@@ -140,7 +204,7 @@ interface paths {
140
204
  put?: never;
141
205
  /**
142
206
  * A fresh client secret for an open session
143
- * @description A service key with the `sessions:write` scope gets a new connect secret for an open session, for an app that missed the connect window or connects from another device (whose connection then replaces the open one, with close code 4409). The previous secret no longer authenticates a new connection or an SSE turn. A socket already open stays open and keeps its session: refreshing never drops the person's live connection. To cut it off, revoke the session (`revokeSession`). An ended session is `409 session_closed`.
207
+ * @description A service key with the `sessions:write` scope gets a new connect secret for an open session, for an app that missed the connect window or connects from another device (whose connection then replaces the open one, with close code 4409). The previous secret no longer authenticates a new connection or an SSE turn. A socket already open stays open and keeps its session: refreshing never drops the person's live connection. To cut it off, revoke the session (`revokeSession`). An ended session is `409 session_closed`. With an optional `Idempotency-Key` (declared in v1-draft.22), a retry within 24 hours answers the secret first issued for that key instead of a newer one.
144
208
  */
145
209
  post: operations["refreshClientSecret"];
146
210
  delete?: never;
@@ -158,7 +222,10 @@ interface paths {
158
222
  };
159
223
  get?: never;
160
224
  put?: never;
161
- /** Run one turn and stream its events as SSE (alternative to the WebSocket) */
225
+ /**
226
+ * Run one turn and stream its events as SSE (alternative to the WebSocket)
227
+ * @description Browsers and WebViews (v1-draft.18): the preflight (`OPTIONS`) is answered for the tenant's client origins of the session's environment only (admin-v1 `ClientOrigins.effective`), with `Access-Control-Allow-Origin` the request's origin (never `*`), `Vary: Origin`, `Access-Control-Allow-Methods: POST`, `Access-Control-Allow-Headers: Authorization, Content-Type` and `Access-Control-Max-Age: 600`; no credentials mode is needed (the client secret is a header). Any other origin, or a session that does not exist, gets no CORS headers. A request whose `Origin` is not one of them is refused with `403` `forbidden` and runs nothing; a request with no `Origin` (a native app, a server) is not checked.
228
+ */
162
229
  post: operations["createTurnSse"];
163
230
  delete?: never;
164
231
  options?: never;
@@ -175,7 +242,10 @@ interface paths {
175
242
  };
176
243
  get?: never;
177
244
  put?: never;
178
- /** Send a card action and stream the resulting turn as SSE */
245
+ /**
246
+ * Send a card action and stream the resulting turn as SSE
247
+ * @description Browsers and WebViews (v1-draft.18), as for `createTurnSse`: the preflight (`OPTIONS`) is answered for the tenant's client origins of the session's environment only (admin-v1 `ClientOrigins.effective`), with `Access-Control-Allow-Origin` the request's origin (never `*`), `Vary: Origin`, `Access-Control-Allow-Methods: POST`, `Access-Control-Allow-Headers: Authorization, Content-Type` and `Access-Control-Max-Age: 600`; no credentials mode is needed (the client secret is a header). Any other origin, or a session that does not exist, gets no CORS headers. A request whose `Origin` is not one of them is refused with `403` `forbidden` and runs nothing; a request with no `Origin` (a native app, a server) is not checked.
248
+ */
179
249
  post: operations["createCardActionSse"];
180
250
  delete?: never;
181
251
  options?: never;
@@ -231,6 +301,26 @@ interface paths {
231
301
  patch?: never;
232
302
  trace?: never;
233
303
  };
304
+ "/v1/media/avatars/{avatar_file}": {
305
+ parameters: {
306
+ query?: never;
307
+ header?: never;
308
+ path?: never;
309
+ cookie?: never;
310
+ };
311
+ /**
312
+ * An agent's avatar image (v1-draft.19), as `Session.agent.avatar_url` names it
313
+ * @description Public: no credential, since an app draws it in an image view. Only an avatar that passed the screen is served, and only while it is an agent's current or previous avatar of the last 30 days; anything else is `404`. The bytes never change under a name (`Cache-Control: public, max-age=31536000, immutable`), and the response is `image/webp` with `X-Content-Type-Options: nosniff` and a sandboxing `Content-Security-Policy`.
314
+ */
315
+ get: operations["getAgentAvatarImage"];
316
+ put?: never;
317
+ post?: never;
318
+ delete?: never;
319
+ options?: never;
320
+ head?: never;
321
+ patch?: never;
322
+ trace?: never;
323
+ };
234
324
  "/v1/media/jobs": {
235
325
  parameters: {
236
326
  query?: never;
@@ -1349,6 +1439,56 @@ interface components {
1349
1439
  conversation_key?: string;
1350
1440
  /** @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. */
1351
1441
  conversation_id?: components["schemas"]["ConversationId"];
1442
+ experience_mode?: components["schemas"]["ExperienceMode"];
1443
+ business_scope_id?: components["schemas"]["BusinessScopeId"];
1444
+ screen_context?: components["schemas"]["ScreenContext"];
1445
+ };
1446
+ /**
1447
+ * @description v1-draft.20, optional on `createSession`: the side of the tenant's app the person is on. `customer` for the customer agent (a `client` or `guest`), `business` for a business agent (a `professional`, `host` or `staff`). Left out, it follows the agent's template; `createSession` describes the binding.
1448
+ * @enum {string}
1449
+ */
1450
+ ExperienceMode: "customer" | "business";
1451
+ /** @description v1-draft.20, optional, with `experience_mode: business` only: the business the person acts for, as the tenant's backend knows it (a professional's business, a host's place). Opaque to Vinaverse; it binds the conversation, so a person acting for two businesses keeps two conversations. It never reaches a model prompt. */
1452
+ BusinessScopeId: string;
1453
+ /** @description v1-draft.20, optional on `createSession`: where the person is in the tenant's app when the session opens, as the tenant's backend vouches for it (it checked that the person may see the entity). It grants nothing: no tool, capability or record. The agent reads it as data in its working notes (never in the cached system prompt, never as something the person said), as a hint: before it states anything about the entity it reads it with a tool, and a tool that refuses means the agent says it cannot see it. A screen change while the chat is open does not open a new session; live updates are a later addition. Never arbitrary page text, URLs, names, amounts or instructions: only these fields, from this vocabulary. */
1454
+ ScreenContext: {
1455
+ /**
1456
+ * @description A coarse screen from a fixed vocabulary; the tenant maps its own screens onto it. `other` when none fits. Customer side: `home`, `explore`, `search_results`, `professional`, `workspace`, `service`, `class`, `checkout`, `booking`, `bookings`, `inbox`, `profile`. Business side: `business_hub`, `setup`, `services`, `hours`, `location`, `gallery`, `spaces`, `space_manage`, `renters`, `payouts`, `plans`, `team`.
1457
+ * @enum {string}
1458
+ */
1459
+ screen: "home" | "explore" | "search_results" | "professional" | "workspace" | "service" | "class" | "checkout" | "booking" | "bookings" | "inbox" | "profile" | "business_hub" | "setup" | "services" | "hours" | "location" | "gallery" | "spaces" | "space_manage" | "renters" | "payouts" | "plans" | "team" | "other";
1460
+ /** @description the object on the screen, as the tenant's read tools name it; `id` is a tool id (letters, digits and `_`, `.`, `:`, `-`), since it reaches the agent's working notes */
1461
+ entity?: {
1462
+ /** @enum {string} */
1463
+ type: "professional" | "workspace" | "service" | "class" | "booking" | "location";
1464
+ id: string;
1465
+ };
1466
+ /** @description the filters the screen shows, typed; free text never */
1467
+ filters?: {
1468
+ service_ids?: string[];
1469
+ category_ids?: string[];
1470
+ area_id?: string;
1471
+ /** Format: date */
1472
+ date_from?: string;
1473
+ /** Format: date */
1474
+ date_to?: string;
1475
+ price_max?: components["schemas"]["Money"];
1476
+ };
1477
+ /** @description the tenant's counter for this person's screen state; a later update never goes back to a lower one */
1478
+ revision: number;
1479
+ };
1480
+ /** @description v1-draft.20: a tap target the app draws under the welcome. Tapping it sends `message` as the person's own text (an `input.text`, or `createTurnSse`), exactly as if they typed it; it starts nothing else. */
1481
+ Chip: {
1482
+ /** @description in Unicode code points */
1483
+ label: string;
1484
+ /** @description in Unicode code points */
1485
+ message: string;
1486
+ };
1487
+ /** @description v1-draft.20: the tenant's greeting for the session's mode, with the chips of the session's screen when its manifest has some for it (else the mode's own), from the connector manifest (admin-v1 `Connector.welcome`), checked when the manifest loads. The app shows it the moment the session is ready: it costs no model call and is not a turn. The agent knows it was shown and does not greet again. */
1488
+ Welcome: {
1489
+ /** @description in Unicode code points */
1490
+ text: string;
1491
+ chips: components["schemas"]["Chip"][];
1352
1492
  };
1353
1493
  ClientSecret: {
1354
1494
  /** @description `vv_cs_` and base64url characters, so the secret is a valid WebSocket subprotocol token in `bearer.<secret>`. */
@@ -1367,6 +1507,11 @@ interface components {
1367
1507
  id: string;
1368
1508
  version: string;
1369
1509
  display_name: string;
1510
+ /**
1511
+ * Format: uri
1512
+ * @description v1-draft.19, optional: the agent's avatar (`getAgentAvatarImage`), a square 512 px WebP with no metadata, re-encoded by Vinaverse from the tenant's upload and passed by its image screen. The URL is immutable (it changes when the image does), so an app may cache the image for as long as it likes; it needs no credential. Absent: no avatar yet, so draw the name's monogram.
1513
+ */
1514
+ avatar_url?: string;
1370
1515
  };
1371
1516
  client_secret: components["schemas"]["ClientSecret"];
1372
1517
  /** Format: uri */
@@ -1381,6 +1526,14 @@ interface components {
1381
1526
  disclosure: string;
1382
1527
  /** @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. */
1383
1528
  conversation_id?: components["schemas"]["ConversationId"];
1529
+ /** @description v1-draft.20, the session's mode, as given or as the agent's template sets it */
1530
+ experience_mode?: components["schemas"]["ExperienceMode"];
1531
+ /** @description v1-draft.20, present when the session acts for a business */
1532
+ business_scope_id?: components["schemas"]["BusinessScopeId"];
1533
+ /** @description v1-draft.20, the screen context the session accepted, when `createSession` gave one */
1534
+ screen_context?: components["schemas"]["ScreenContext"];
1535
+ /** @description v1-draft.20, absent when the tenant's manifest has no welcome for the session's mode */
1536
+ welcome?: components["schemas"]["Welcome"];
1384
1537
  };
1385
1538
  SessionStatus: {
1386
1539
  id: string;
@@ -1391,10 +1544,10 @@ interface components {
1391
1544
  turns: number;
1392
1545
  handoff_ticket_ids?: string[];
1393
1546
  /**
1394
- * @description Why the session ended. `server_restart` is reserved and never appears here: it never ends a session (`docs/realtime-protocol.md` §2).
1547
+ * @description Why the session ended. `server_restart` is reserved and never appears here: it never ends a session (`docs/realtime-protocol.md` §2). `deleted` (v1-draft.21): its conversation was deleted from history.
1395
1548
  * @enum {string}
1396
1549
  */
1397
- end_reason?: "expired" | "revoked" | "limit_reached" | "idle" | "server_restart" | "user_left";
1550
+ end_reason?: "expired" | "revoked" | "limit_reached" | "idle" | "server_restart" | "user_left" | "deleted";
1398
1551
  /** Format: date-time */
1399
1552
  created_at: string;
1400
1553
  /** Format: date-time */
@@ -1443,6 +1596,35 @@ interface components {
1443
1596
  label: string;
1444
1597
  deep_link?: string;
1445
1598
  };
1599
+ /** @description v1-draft.20: one photo of a `professional` or `workspace` card. Https only, from a prefix the tenant registered (admin-v1 `Connector.image_url_prefixes`), compared after the URL is parsed and normalized (no user info, backslash, fragment or dot segment): an image that fails is dropped, never the card. The tenant serves only photos its own moderation approved, at URLs that stay valid as long as the transcript (a card in history draws them again). An image that fails to load draws the name's monogram; the card stays. */
1600
+ CardImage: {
1601
+ /** Format: uri */
1602
+ url: string;
1603
+ width?: number;
1604
+ height?: number;
1605
+ };
1606
+ /** @description v1-draft.20: the cards one turn draws together carry the same `id`. `layout` is a hint: `carousel` draws them in one horizontal row, `list` one under another. A client that does not know `group`, or a `layout` value, draws a list, as before. */
1607
+ CardGroup: {
1608
+ id: string;
1609
+ /** @enum {string} */
1610
+ layout: "list" | "carousel";
1611
+ };
1612
+ /** @description v1-draft.20, one link of a `links` card: one of the tenant's own destinations from its connector manifest (admin-v1 `Connector.help_links`), never a link the model wrote. */
1613
+ CardLink: {
1614
+ /** @description in Unicode code points */
1615
+ label: string;
1616
+ /** @description an https page of the tenant's, or a deep link that passes the deep-link rule (`next_step`) */
1617
+ url: string;
1618
+ /** @enum {string} */
1619
+ purpose: "help" | "support" | "policy" | "contact" | "feedback";
1620
+ };
1621
+ /** @description v1-draft.20, on a `links` card: the tenant's scope policy, at the version in force, that kept the agent from answering, and why. */
1622
+ ScopeProvenance: {
1623
+ policy_id: string;
1624
+ version: string;
1625
+ /** @enum {string} */
1626
+ reason: "off_scope" | "competitor" | "blocked_topic";
1627
+ };
1446
1628
  /**
1447
1629
  * @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.
1448
1630
  * 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
@@ -1462,14 +1644,17 @@ interface components {
1462
1644
  * know.
1463
1645
  * - **It is not restored.** A `next_step` card is in neither `pending_cards` nor `transcript`; after a
1464
1646
  * reconnect the agent shows the step again once it reads the tenant's state.
1647
+ *
1648
+ * **Listing cards** (v1-draft.20). A `professional` or `workspace` card may carry `images` (up to five, the first the hero; `image_url`, when present, is the first one's URL, for clients that predate `images`), `next_available` (the earliest open time the tenant's tool returned; a hint, so a tap on its time reads availability again), `price_from` and a `group`. An app draws one hero image first and the rest on a swipe.
1649
+ * **A `links` card** (v1-draft.20) answers what the tenant's scope keeps the agent from answering: a question off its scope, a comparison with a competitor, a blocked topic. Its `links` are the tenant's own help, support, policy, contact or feedback destinations (`CardLink`), its `provenance` names the scope policy and the reason, and it has no `actions`: each link opens the tenant's page. It belongs to a recorded turn, with the agent's one line around it, never to `input.rejected`. Its `source.tool` is `scope` and its `source.call_id` the turn's id: no tool call made it. An earlier client skips the unknown kind, as it skips any.
1465
1650
  */
1466
1651
  Card: {
1467
1652
  id: string;
1468
1653
  /**
1469
- * @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)
1654
+ * @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), then `links` (v1-draft.20: the tenant's own pages, when its scope keeps the agent from answering)
1470
1655
  * @enum {string}
1471
1656
  */
1472
- kind: "professional" | "workspace" | "service" | "time_slots" | "booking" | "quote" | "policy" | "handoff" | "metric_chart" | "metric_table" | "gap_list" | "cluster" | "claim" | "document" | "question" | "proposal" | "next_step";
1657
+ kind: "professional" | "workspace" | "service" | "time_slots" | "booking" | "quote" | "policy" | "handoff" | "metric_chart" | "metric_table" | "gap_list" | "cluster" | "claim" | "document" | "question" | "proposal" | "next_step" | "links";
1473
1658
  question?: components["schemas"]["Question"];
1474
1659
  proposal?: components["schemas"]["Proposal"];
1475
1660
  ref?: components["schemas"]["CardRef"];
@@ -1477,6 +1662,17 @@ interface components {
1477
1662
  subtitle?: string;
1478
1663
  /** Format: uri */
1479
1664
  image_url?: string;
1665
+ /** @description v1-draft.20, a `professional` or `workspace` card's photos, the first the hero */
1666
+ images?: components["schemas"]["CardImage"][];
1667
+ /**
1668
+ * Format: date-time
1669
+ * @description v1-draft.20, a `professional` or `workspace` card's earliest open time, as the tenant's tool returned it
1670
+ */
1671
+ next_available?: string;
1672
+ group?: components["schemas"]["CardGroup"];
1673
+ /** @description v1-draft.20, a `links` card's links */
1674
+ links?: components["schemas"]["CardLink"][];
1675
+ provenance?: components["schemas"]["ScopeProvenance"];
1480
1676
  rating?: {
1481
1677
  value_ppm: number;
1482
1678
  count: number;
@@ -1520,7 +1716,7 @@ interface components {
1520
1716
  points: {
1521
1717
  /** Format: date */
1522
1718
  t: string;
1523
- /** @description null when suppressed by the k-anonymity floor */
1719
+ /** @description null when suppressed by the k-anonymity floor; on a metric without k_floor, nothing to measure */
1524
1720
  value: number | null;
1525
1721
  suppressed?: boolean;
1526
1722
  }[];
@@ -1538,6 +1734,7 @@ interface components {
1538
1734
  dims: {
1539
1735
  [key: string]: string;
1540
1736
  };
1737
+ /** @description null when suppressed by the metric's k_floor; on a metric without k_floor, nothing to measure */
1541
1738
  values: {
1542
1739
  [key: string]: number | null;
1543
1740
  };
@@ -1582,7 +1779,7 @@ interface components {
1582
1779
  snippet?: string;
1583
1780
  /** Format: date-time */
1584
1781
  updated_at?: string;
1585
- } & (unknown & unknown & unknown);
1782
+ } & (unknown & unknown & unknown & unknown);
1586
1783
  /** @enum {string} */
1587
1784
  ValueUnit: "count" | "ppm" | "micros_usd" | "ms";
1588
1785
  MetricRef: {
@@ -1590,6 +1787,7 @@ interface components {
1590
1787
  label: string;
1591
1788
  unit: components["schemas"]["ValueUnit"];
1592
1789
  definition: string;
1790
+ /** @description The privacy floor: a value built from fewer than this many distinct people is suppressed (`null`). Absent when no floor applies, as on a business's own operational data (Studio's pages, read by the business copilot); a `null` value there means there was nothing to measure, never a hidden count. */
1593
1791
  k_floor?: number;
1594
1792
  };
1595
1793
  /** @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. */
@@ -1822,6 +2020,83 @@ interface components {
1822
2020
  /** Format: date-time */
1823
2021
  at: string;
1824
2022
  };
2023
+ ConversationPage: {
2024
+ data: components["schemas"]["ConversationSummary"][];
2025
+ next_cursor: string | null;
2026
+ };
2027
+ /** @description v1-draft.21: one past conversation as a history screen lists it. No message text: the title is from the conversation's last search intent or its date, never the person's words. */
2028
+ ConversationSummary: {
2029
+ id: components["schemas"]["ConversationId"];
2030
+ agent: string;
2031
+ experience_mode: components["schemas"]["ExperienceMode"];
2032
+ business_scope_id?: components["schemas"]["BusinessScopeId"];
2033
+ title: string;
2034
+ /**
2035
+ * @description `intent`: the last search intent (service and place); `date`: the day it started
2036
+ * @enum {string}
2037
+ */
2038
+ title_source: "intent" | "date";
2039
+ /** Format: date-time */
2040
+ started_at: string;
2041
+ /** Format: date-time */
2042
+ last_activity_at: string;
2043
+ /** @description the messages its timeline holds (cards not counted) */
2044
+ message_count: number;
2045
+ /**
2046
+ * @description `open`: a session can resume it; `ended`: it can only be read
2047
+ * @enum {string}
2048
+ */
2049
+ state: "open" | "ended";
2050
+ };
2051
+ TimelinePage: {
2052
+ data: components["schemas"]["TimelineItem"][];
2053
+ /** @description the next older page; null at the conversation's start */
2054
+ next_cursor: string | null;
2055
+ };
2056
+ /**
2057
+ * @description v1-draft.21: one item of a past conversation, a message or a card its turn drew. `id` is stable and unique within the conversation; a message's `event_id` is its live event's identifier (`TranscriptItem.id`), the key an app merges live and resumed messages by, with `role`. A card is read-only in history.
2058
+ * Unlike `session.ready.transcript`, the history holds a guardrail's refusals: the person's message a guardrail refused and the fixed line said instead of an answer both carry `refusal: true`, so the app draws them as it drew them live (muted, never offered again). A `proposal` card carries `proposal_outcome`, what became of it.
2059
+ */
2060
+ TimelineItem: {
2061
+ /** @constant */
2062
+ type: "message";
2063
+ id: string;
2064
+ /**
2065
+ * @description a client tolerates a role it does not know, as in `TranscriptItem`
2066
+ * @enum {string}
2067
+ */
2068
+ role: "person" | "assistant" | "staff";
2069
+ text: string;
2070
+ /** Format: date-time */
2071
+ at: string;
2072
+ /** @description absent on a `person` message sent without a `client_event_id` */
2073
+ event_id?: string;
2074
+ turn_id?: string;
2075
+ /** @description `assistant` only, present only when true: the reply stopped before it finished */
2076
+ interrupted?: boolean;
2077
+ /**
2078
+ * @description Present only when true: a guardrail refused this turn. On a `person` message, the text it refused (as recorded, card-like digits masked); on an `assistant` message, the fixed line said instead of an answer.
2079
+ * @constant
2080
+ */
2081
+ refusal?: true;
2082
+ } | {
2083
+ /** @constant */
2084
+ type: "card";
2085
+ id: string;
2086
+ turn_id: string;
2087
+ /** Format: date-time */
2088
+ at: string;
2089
+ card: components["schemas"]["Card"];
2090
+ /**
2091
+ * @description A `proposal` card only, always present on one: the outcome the app reported for it (`ui.action` `confirmed`, `declined`, `failed` or `expired`), or `pending` while none was reported. Draw it as settled; a past proposal is never confirmed from history.
2092
+ * @enum {string}
2093
+ */
2094
+ proposal_outcome?: "pending" | "confirmed" | "declined" | "failed" | "expired";
2095
+ };
2096
+ ConversationDeleted: {
2097
+ conversation_id: components["schemas"]["ConversationId"];
2098
+ sessions_revoked: number;
2099
+ };
1825
2100
  /** @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. */
1826
2101
  SlotSelection: {
1827
2102
  id: string;
@@ -2002,6 +2277,10 @@ interface components {
2002
2277
  };
2003
2278
  disclosure: string;
2004
2279
  resumed: boolean;
2280
+ /** @description v1-draft.20, the session's mode (`Session.experience_mode`), so the app keeps one store per mode */
2281
+ experience_mode?: components["schemas"]["ExperienceMode"];
2282
+ /** @description v1-draft.20, present when the session acts for a business */
2283
+ business_scope_id?: components["schemas"]["BusinessScopeId"];
2005
2284
  /**
2006
2285
  * @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.
2007
2286
  * 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):
@@ -2114,10 +2393,10 @@ interface components {
2114
2393
  /** @constant */
2115
2394
  type: "session.closed";
2116
2395
  /**
2117
- * @description `replaced`: a newer session of the same conversation was created (close code `4409`)
2396
+ * @description `replaced`: a newer session of the same conversation was created (close code `4409`). `deleted` (v1-draft.21): the person deleted the conversation from their history (`deleteConversation`, close code `4403`); drop it from the app's store and never resume it. A client that does not know `deleted` reads it as `revoked`, as any unknown reason.
2118
2397
  * @enum {string}
2119
2398
  */
2120
- reason: "expired" | "revoked" | "limit_reached" | "idle" | "server_restart" | "replaced";
2399
+ reason: "expired" | "revoked" | "limit_reached" | "idle" | "server_restart" | "replaced" | "deleted";
2121
2400
  } | {
2122
2401
  /** @constant */
2123
2402
  type: "ui.answer.recorded";
@@ -2696,6 +2975,8 @@ interface components {
2696
2975
  };
2697
2976
  parameters: {
2698
2977
  IdempotencyKey: string;
2978
+ /** @description Accepted, not required (declared in v1-draft.22). Scoped to the tenant and environment: the same key and request within 24 hours answers the first success again (`Idempotent-Replayed: true`); the same key with another request is `409 idempotency_conflict`. */
2979
+ OptionalIdempotencyKey: string;
2699
2980
  MediaJobId: string;
2700
2981
  ConversationId: components["schemas"]["ConversationId"];
2701
2982
  SessionId: string;
@@ -2771,6 +3052,39 @@ interface operations {
2771
3052
  503: components["responses"]["Problem"];
2772
3053
  };
2773
3054
  };
3055
+ listConversations: {
3056
+ parameters: {
3057
+ query: {
3058
+ subject_id: components["schemas"]["SubjectId"];
3059
+ agent: string;
3060
+ /** @description the side of the app; left out, the agent's template's */
3061
+ experience_mode?: components["schemas"]["ExperienceMode"];
3062
+ /** @description the business the person acts for; only with `experience_mode=business` */
3063
+ business_scope_id?: components["schemas"]["BusinessScopeId"];
3064
+ limit?: number;
3065
+ cursor?: string;
3066
+ };
3067
+ header?: never;
3068
+ path?: never;
3069
+ cookie?: never;
3070
+ };
3071
+ requestBody?: never;
3072
+ responses: {
3073
+ /** @description Newest activity first. */
3074
+ 200: {
3075
+ headers: {
3076
+ [name: string]: unknown;
3077
+ };
3078
+ content: {
3079
+ "application/json": components["schemas"]["ConversationPage"];
3080
+ };
3081
+ };
3082
+ 400: components["responses"]["Problem"];
3083
+ 401: components["responses"]["Problem"];
3084
+ 403: components["responses"]["Problem"];
3085
+ 503: components["responses"]["Problem"];
3086
+ };
3087
+ };
2774
3088
  getConversation: {
2775
3089
  parameters: {
2776
3090
  query?: never;
@@ -2797,6 +3111,66 @@ interface operations {
2797
3111
  410: components["responses"]["Problem"];
2798
3112
  };
2799
3113
  };
3114
+ deleteConversation: {
3115
+ parameters: {
3116
+ query: {
3117
+ subject_id: components["schemas"]["SubjectId"];
3118
+ };
3119
+ header?: never;
3120
+ path: {
3121
+ conversation_id: components["parameters"]["ConversationId"];
3122
+ };
3123
+ cookie?: never;
3124
+ };
3125
+ requestBody?: never;
3126
+ responses: {
3127
+ /** @description Deleted. */
3128
+ 200: {
3129
+ headers: {
3130
+ [name: string]: unknown;
3131
+ };
3132
+ content: {
3133
+ "application/json": components["schemas"]["ConversationDeleted"];
3134
+ };
3135
+ };
3136
+ 401: components["responses"]["Problem"];
3137
+ 403: components["responses"]["Problem"];
3138
+ 404: components["responses"]["Problem"];
3139
+ 503: components["responses"]["Problem"];
3140
+ };
3141
+ };
3142
+ listConversationMessages: {
3143
+ parameters: {
3144
+ query: {
3145
+ subject_id: components["schemas"]["SubjectId"];
3146
+ limit?: number;
3147
+ cursor?: string;
3148
+ };
3149
+ header?: never;
3150
+ path: {
3151
+ conversation_id: components["parameters"]["ConversationId"];
3152
+ };
3153
+ cookie?: never;
3154
+ };
3155
+ requestBody?: never;
3156
+ responses: {
3157
+ /** @description The newest page first; oldest first within a page. */
3158
+ 200: {
3159
+ headers: {
3160
+ [name: string]: unknown;
3161
+ };
3162
+ content: {
3163
+ "application/json": components["schemas"]["TimelinePage"];
3164
+ };
3165
+ };
3166
+ 400: components["responses"]["Problem"];
3167
+ 401: components["responses"]["Problem"];
3168
+ 403: components["responses"]["Problem"];
3169
+ 404: components["responses"]["Problem"];
3170
+ 410: components["responses"]["Problem"];
3171
+ 503: components["responses"]["Problem"];
3172
+ };
3173
+ };
2800
3174
  getSession: {
2801
3175
  parameters: {
2802
3176
  query?: never;
@@ -2823,7 +3197,10 @@ interface operations {
2823
3197
  revokeSession: {
2824
3198
  parameters: {
2825
3199
  query?: never;
2826
- header?: never;
3200
+ header?: {
3201
+ /** @description Accepted, not required (declared in v1-draft.22). Scoped to the tenant and environment: the same key and request within 24 hours answers the first success again (`Idempotent-Replayed: true`); the same key with another request is `409 idempotency_conflict`. */
3202
+ "Idempotency-Key"?: components["parameters"]["OptionalIdempotencyKey"];
3203
+ };
2827
3204
  path: {
2828
3205
  session_id: components["parameters"]["SessionId"];
2829
3206
  };
@@ -2841,12 +3218,16 @@ interface operations {
2841
3218
  };
2842
3219
  };
2843
3220
  404: components["responses"]["Problem"];
3221
+ 409: components["responses"]["Problem"];
2844
3222
  };
2845
3223
  };
2846
3224
  refreshClientSecret: {
2847
3225
  parameters: {
2848
3226
  query?: never;
2849
- header?: never;
3227
+ header?: {
3228
+ /** @description Accepted, not required (declared in v1-draft.22). Scoped to the tenant and environment: the same key and request within 24 hours answers the first success again (`Idempotent-Replayed: true`); the same key with another request is `409 idempotency_conflict`. */
3229
+ "Idempotency-Key"?: components["parameters"]["OptionalIdempotencyKey"];
3230
+ };
2850
3231
  path: {
2851
3232
  session_id: components["parameters"]["SessionId"];
2852
3233
  };
@@ -2891,6 +3272,7 @@ interface operations {
2891
3272
  };
2892
3273
  };
2893
3274
  401: components["responses"]["Problem"];
3275
+ 403: components["responses"]["Problem"];
2894
3276
  409: components["responses"]["Problem"];
2895
3277
  429: components["responses"]["RateLimited"];
2896
3278
  };
@@ -2927,6 +3309,7 @@ interface operations {
2927
3309
  "text/event-stream": string;
2928
3310
  };
2929
3311
  };
3312
+ 403: components["responses"]["Problem"];
2930
3313
  };
2931
3314
  };
2932
3315
  createModerationJob: {
@@ -3005,6 +3388,30 @@ interface operations {
3005
3388
  404: components["responses"]["Problem"];
3006
3389
  };
3007
3390
  };
3391
+ getAgentAvatarImage: {
3392
+ parameters: {
3393
+ query?: never;
3394
+ header?: never;
3395
+ path: {
3396
+ /** @description the SHA-256 of the image's bytes, its first 32 hex digits, then `.webp` */
3397
+ avatar_file: string;
3398
+ };
3399
+ cookie?: never;
3400
+ };
3401
+ requestBody?: never;
3402
+ responses: {
3403
+ /** @description the image */
3404
+ 200: {
3405
+ headers: {
3406
+ [name: string]: unknown;
3407
+ };
3408
+ content: {
3409
+ "image/webp": string;
3410
+ };
3411
+ };
3412
+ 404: components["responses"]["Problem"];
3413
+ };
3414
+ };
3008
3415
  listMediaJobs: {
3009
3416
  parameters: {
3010
3417
  query?: {