@vinaverse/client 0.1.0-alpha.5 → 0.1.0-alpha.7

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.
Files changed (35) hide show
  1. package/README.md +67 -7
  2. package/dist/agent/index.cjs +526 -78
  3. package/dist/agent/index.cjs.map +1 -1
  4. package/dist/agent/index.d.cts +2 -2
  5. package/dist/agent/index.d.ts +2 -2
  6. package/dist/agent/index.js +2 -2
  7. package/dist/backend/index.cjs +268 -0
  8. package/dist/backend/index.cjs.map +1 -0
  9. package/dist/backend/index.d.cts +72 -0
  10. package/dist/backend/index.d.ts +72 -0
  11. package/dist/backend/index.js +250 -0
  12. package/dist/backend/index.js.map +1 -0
  13. package/dist/chunk-3BLRAWNC.js +840 -0
  14. package/dist/chunk-3BLRAWNC.js.map +1 -0
  15. package/dist/{chunk-ZWG2TMUL.js → chunk-46D3FSLU.js} +24 -2
  16. package/dist/chunk-46D3FSLU.js.map +1 -0
  17. package/dist/conversation/index.cjs +22 -0
  18. package/dist/conversation/index.cjs.map +1 -1
  19. package/dist/conversation/index.d.cts +2 -2
  20. package/dist/conversation/index.d.ts +2 -2
  21. package/dist/conversation/index.js +2 -2
  22. package/dist/{runtime-DDxwWBBr.d.cts → runtime-C3ZknHdG.d.cts} +139 -20
  23. package/dist/{runtime-B4nKwOTF.d.ts → runtime-CZ1hLvAl.d.ts} +139 -20
  24. package/dist/{store-C7FtS5AY.d.cts → store-7WU1kD9W.d.cts} +31 -1
  25. package/dist/{store-DFk0ayh8.d.ts → store-DrXSPHu6.d.ts} +31 -1
  26. package/dist/testing/index.cjs +38 -1
  27. package/dist/testing/index.cjs.map +1 -1
  28. package/dist/testing/index.d.cts +14 -3
  29. package/dist/testing/index.d.ts +14 -3
  30. package/dist/testing/index.js +40 -3
  31. package/dist/testing/index.js.map +1 -1
  32. package/package.json +21 -2
  33. package/dist/chunk-3WR2SUG2.js +0 -416
  34. package/dist/chunk-3WR2SUG2.js.map +0 -1
  35. package/dist/chunk-ZWG2TMUL.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  export { C as CONTRACT } from '../contract-_NMdPjgN.cjs';
2
2
  import { Q as Question, A as AnswerInput } from '../session-8W3fRNeb.cjs';
3
3
  export { a as Card, b as CardGroup, c as CardImage, d as CardKind, C as CardLink, e as ClientErrorCode, H as HeldCard, f as HeldQuestion, P as Proposal, g as ProposalOutcome, T as ToolLine, V as VinaverseClientError, W as Welcome, i as isKnownCard } from '../session-8W3fRNeb.cjs';
4
- import { C as ConversationStore, a as ConversationOptions, S as SessionRequest, b as ConversationSession } from '../store-C7FtS5AY.cjs';
5
- export { c as CardItem, d as ConversationBanner, e as ConversationPersistence, f as ConversationStatus, g as ConversationView, F as FrameScheduler, M as MAX_TEXT, h as MessageItem, i as MessageState, j as SessionReason, k as StreamingText, l as SystemEvent, m as SystemItem, T as TimelineItem, n as createConversation } from '../store-C7FtS5AY.cjs';
4
+ import { C as ConversationStore, a as ConversationOptions, S as SessionRequest, b as ConversationSession } from '../store-7WU1kD9W.cjs';
5
+ export { c as CardItem, d as ConversationActivity, e as ConversationBanner, f as ConversationPersistence, g as ConversationStatus, h as ConversationView, F as FrameScheduler, M as MAX_TEXT, i as MessageItem, j as MessageState, k as SessionReason, l as StreamingText, m as SystemEvent, n as SystemItem, T as TimelineItem, o as createConversation } from '../store-7WU1kD9W.cjs';
6
6
 
7
7
  type AnswerProblem =
8
8
  /** Nothing chosen or typed, and the question is required (or skipped while required). */
@@ -1,8 +1,8 @@
1
1
  export { C as CONTRACT } from '../contract-_NMdPjgN.js';
2
2
  import { Q as Question, A as AnswerInput } from '../session-8W3fRNeb.js';
3
3
  export { a as Card, b as CardGroup, c as CardImage, d as CardKind, C as CardLink, e as ClientErrorCode, H as HeldCard, f as HeldQuestion, P as Proposal, g as ProposalOutcome, T as ToolLine, V as VinaverseClientError, W as Welcome, i as isKnownCard } from '../session-8W3fRNeb.js';
4
- import { C as ConversationStore, a as ConversationOptions, S as SessionRequest, b as ConversationSession } from '../store-DFk0ayh8.js';
5
- export { c as CardItem, d as ConversationBanner, e as ConversationPersistence, f as ConversationStatus, g as ConversationView, F as FrameScheduler, M as MAX_TEXT, h as MessageItem, i as MessageState, j as SessionReason, k as StreamingText, l as SystemEvent, m as SystemItem, T as TimelineItem, n as createConversation } from '../store-DFk0ayh8.js';
4
+ import { C as ConversationStore, a as ConversationOptions, S as SessionRequest, b as ConversationSession } from '../store-DrXSPHu6.js';
5
+ export { c as CardItem, d as ConversationActivity, e as ConversationBanner, f as ConversationPersistence, g as ConversationStatus, h as ConversationView, F as FrameScheduler, M as MAX_TEXT, i as MessageItem, j as MessageState, k as SessionReason, l as StreamingText, m as SystemEvent, n as SystemItem, T as TimelineItem, o as createConversation } from '../store-DrXSPHu6.js';
6
6
 
7
7
  type AnswerProblem =
8
8
  /** Nothing chosen or typed, and the question is required (or skipped while required). */
@@ -1,6 +1,6 @@
1
1
  export { CONTRACT } from '../chunk-VSJDVLCP.js';
2
- import { createConversation } from '../chunk-ZWG2TMUL.js';
3
- export { ConversationStore, MAX_TEXT, createConversation, isAllowedLink, isValidLinkPrefix } from '../chunk-ZWG2TMUL.js';
2
+ import { createConversation } from '../chunk-46D3FSLU.js';
3
+ export { ConversationStore, MAX_TEXT, createConversation, isAllowedLink, isValidLinkPrefix } from '../chunk-46D3FSLU.js';
4
4
  export { VinaverseClientError, isKnownCard } from '../chunk-GWLLUOFY.js';
5
5
 
6
6
  // src/conversation/answers.ts
@@ -1,5 +1,5 @@
1
- import { S as SessionRequest, b as ConversationSession, C as ConversationStore, a as ConversationOptions } from './store-C7FtS5AY.cjs';
2
- import { S as Schemas } from './session-8W3fRNeb.cjs';
1
+ import { S as SessionRequest, b as ConversationSession, C as ConversationStore, f as ConversationPersistence, a as ConversationOptions } from './store-7WU1kD9W.cjs';
2
+ import { S as Schemas, k as ClientSecret } from './session-8W3fRNeb.cjs';
3
3
 
4
4
  /** Whose conversation: a person (null for a guest), the app's side, the business acted for, and the thread. */
5
5
  interface AgentKey {
@@ -22,17 +22,47 @@ interface AgentPlace {
22
22
  } | null;
23
23
  readonly filters?: ScreenContext['filters'];
24
24
  }
25
+ /** How the person came in: the kit's parts say `launcher` and `ask`; an app adds `deep_link` or its own words. */
26
+ type AgentEntryPoint = 'launcher' | 'ask' | 'deep_link' | (string & Record<never, never>);
25
27
  /** What the runtime asks the tenant's backend for: a session, for this key, told this place. */
26
28
  interface AgentSessionRequest extends SessionRequest {
27
29
  readonly key: AgentKey;
30
+ /**
31
+ * This session creation's id: the same through its retries, so the backend uses it as the gateway's idempotency key
32
+ * and never opens twice; a new one for every creation (a first open, a resume, continue-here, a rollover). A creation
33
+ * that could not reach the backend keeps it, and its whole request, for the next try, the person's "try again" too,
34
+ * for 24 hours. An `unknown_open` answer is final: not retried under it.
35
+ */
36
+ readonly openId: string;
37
+ /** The runtime's `agent`, for a backend that serves more than one. */
38
+ readonly agent: string | null;
28
39
  readonly place: AgentPlace | null;
29
- /** How the person came in (`button`, `inbox`, …): the app's own words, passed through. */
30
- readonly entryPoint: string | null;
40
+ /**
41
+ * The place's revision (`ScreenContext.revision`): higher than any the session was told before, and lower than any
42
+ * `updateContext` after it, so the API keeps the newest place across a session's rollover.
43
+ */
44
+ readonly placeRevision: number;
45
+ /** How the person came in (`launcher`, `ask`, `deep_link`, the app's own words), passed through. */
46
+ readonly entryPoint: AgentEntryPoint | null;
31
47
  }
32
48
  /** Why there is no session, typed, so an app never needs sentinel errors of its own. */
33
49
  type AgentFailureReason =
34
- /** The conversation asked for is over: not theirs, deleted (404), or past the time it is kept (410). */
50
+ /** The conversation is over (its session's own end, or a refusal with no finer reason). */
35
51
  'ended'
52
+ /** The conversation asked for does not exist for this person (deleted, or never theirs). */
53
+ | 'not_found'
54
+ /** Past the time the conversation is kept. */
55
+ | 'expired'
56
+ /** The conversation belongs to another business, mode or agent than the key's. */
57
+ | 'mismatch'
58
+ /** The conversation's flow is completed; a new one starts when the person asks. */
59
+ | 'completed'
60
+ /** The person may not act for this business or conversation. */
61
+ | 'forbidden'
62
+ /** The server holds a newer screen context than the one told (an update, never a session). */
63
+ | 'conflict'
64
+ /** The backend could not tell whether the session opened: unavailable, never retried, its `openId` kept for support. */
65
+ | 'unknown_open'
36
66
  /** The person's sign-in was refused by the tenant's API. */
37
67
  | 'auth'
38
68
  /** The agent is switched off for this tenant, mode or person. */
@@ -83,11 +113,17 @@ interface AgentStatusRead {
83
113
  interface AgentBackend {
84
114
  /** A session from the tenant's API (it creates it at Vinaverse with its service key), or an `AgentRefusal`. */
85
115
  openSession(request: AgentSessionRequest): Promise<ConversationSession>;
86
- /** A fresh client secret for the same session (before its end). */
87
- refreshSecret?(sessionId: string): Promise<string>;
116
+ /**
117
+ * A fresh client secret for the same session (before its end): a `ClientSecret` `{ value, expires_at }`, or its
118
+ * value. A refusal (`session_closed`: the session is over) goes on with the conversation in a new session.
119
+ */
120
+ refreshSecret?(sessionId: string): Promise<ClientSecret | string>;
88
121
  /**
89
122
  * The person's place changed while the chat is open: the tenant's backend tells Vinaverse
90
- * (`PUT /v1/sessions/{id}/context`). Absent, or refused, the kit offers a question about the new place instead.
123
+ * (`PUT /v1/sessions/{id}/context`). A place cleared (`tell({ place: null })`) comes as `{ screen: 'other' }`.
124
+ * `revision` only grows for a conversation, across its sessions. A conflict (`decodeBackendFailure`'s
125
+ * `context_conflict`, or a refusal with reason `conflict`, with the server's `currentRevision`) is sent once more
126
+ * above it, then said as `moved`. Absent, or refused otherwise, the kit offers a question about the new place.
91
127
  */
92
128
  updateContext?(sessionId: string, place: AgentPlace, revision: number): Promise<void>;
93
129
  /** Whether the agent is offered, and what it can do. Absent: available, nothing extra. */
@@ -100,13 +136,16 @@ interface AgentBackend {
100
136
  }): Promise<void>;
101
137
  /** Sign-out: end the person's open sessions at the tenant's API. */
102
138
  endSessions?(): Promise<void>;
139
+ /** History's delete: the tenant's API deletes the person's conversation (authenticated, theirs only). */
140
+ deleteConversation?(conversationId: string): Promise<void>;
103
141
  }
104
142
  type AgentRating = 'up' | 'down';
105
143
  type AgentFailure = 'ended' | 'unreachable' | 'unavailable';
106
- type AgentEventType = 'session_opened' | 'session_resumed' | 'session_retried' | 'session_failed' | 'conversation_ended' | 'context_updated' | 'rated' | 'chip_tapped' | 'card_action' | 'target_opened' | 'link_opened' | 'launcher_shown' | 'launcher_opened' | 'launcher_greeted' | 'greeting_closed';
144
+ type AgentEventType = 'session_opened' | 'session_resumed' | 'session_retried' | 'session_failed' | 'conversation_ended' | 'context_updated' | 'rated' | 'chip_tapped' | 'card_action' | 'target_opened' | 'link_opened' | 'launcher_shown' | 'launcher_opened' | 'launcher_greeted' | 'greeting_closed' | 'message_sent' | 'reply_completed' | 'card_shown';
107
145
  /** For the app's analytics: what happened, for which conversation, with nothing the person said. */
108
146
  interface AgentEvent {
109
147
  readonly type: AgentEventType;
148
+ /** The conversation, as a hash of its key (`hashKey`): stable, and never the person's id. */
110
149
  readonly key?: string;
111
150
  readonly sessionId?: string;
112
151
  readonly failure?: AgentFailure;
@@ -116,8 +155,8 @@ interface AgentEvent {
116
155
  readonly rating?: AgentRating;
117
156
  readonly cardId?: string;
118
157
  readonly kind?: string;
119
- /** Where the person opened from (`entryPoint`, the screen). */
120
- readonly from?: string;
158
+ /** Where the person opened from (`launcher`, `ask`, `deep_link`, the app's own words). */
159
+ readonly from?: AgentEntryPoint;
121
160
  }
122
161
  /** What a UI draws around the store: why there is no session, whether the person moved, the thumbs given. */
123
162
  interface AgentConversationState {
@@ -129,6 +168,13 @@ interface AgentConversationState {
129
168
  readonly moved: boolean;
130
169
  readonly ratings: ReadonlyMap<string, AgentRating>;
131
170
  readonly sessionId: string | null;
171
+ /**
172
+ * The conversation the key holds: the one it continues before a session opens, then its session's. Null for a new
173
+ * one not yet opened. History's delete compares against it (`deleteConversation` does).
174
+ */
175
+ readonly conversationId: string | null;
176
+ /** The open the backend could not settle (`unknown_open`), for support; null otherwise. */
177
+ readonly openId: string | null;
132
178
  }
133
179
  interface AgentConversation {
134
180
  /** The key as a string, for the app's own maps and the events. */
@@ -142,18 +188,26 @@ interface AgentConversation {
142
188
  */
143
189
  show(open?: {
144
190
  readonly place?: AgentPlace | null;
145
- readonly entryPoint?: string | null;
191
+ readonly entryPoint?: AgentEntryPoint | null;
146
192
  }): () => void;
147
- /** The place and the way in changed while a view is attached: told live where the backend can, else on the next session. */
193
+ /**
194
+ * The place and the way in changed while a view is attached: told live where the backend can, else on the next
195
+ * session. `undefined` keeps what was told; `null` clears it.
196
+ */
148
197
  tell(open: {
149
198
  readonly place?: AgentPlace | null;
150
- readonly entryPoint?: string | null;
199
+ readonly entryPoint?: AgentEntryPoint | null;
151
200
  }): void;
152
201
  getState(): AgentConversationState;
153
202
  subscribe(listener: () => void): () => void;
154
203
  /** Asks for a session again, after the backend could not be reached. */
155
204
  retry(): void;
156
- /** A thumbs on a whole reply; a change of mind only is sent again. */
205
+ /**
206
+ * Whether a reply can be rated: one this runtime saw given live. A reply restored from the device or a resumed
207
+ * transcript cannot be, until the API rates a turn by its conversation; the kits draw no thumbs for it.
208
+ */
209
+ canRate(turnId: string): boolean;
210
+ /** A thumbs on a whole reply, against the session it was given in; a change of mind only is sent again. */
157
211
  rate(turnId: string, rating: AgentRating): void;
158
212
  /**
159
213
  * Sends the person's `text` once in this conversation's life (a chip tapped twice, a retried tap). It is shown as
@@ -172,6 +226,11 @@ interface AgentClock {
172
226
  }
173
227
  interface AgentRuntimeOptions {
174
228
  readonly backend: AgentBackend;
229
+ /**
230
+ * The agent this runtime talks to (its id at Vinaverse). It is part of every key, so two agents sharing the device's
231
+ * storage (`conversation.persistence`) never share a conversation. Default: none (one agent per storage namespace).
232
+ */
233
+ readonly agent?: string;
175
234
  /** The store's own options (locale, time zone, deep-link prefixes, transport, persistence, closeInBackground…). */
176
235
  readonly conversation?: Omit<ConversationOptions, 'getSession' | 'refreshSecret' | 'clock'>;
177
236
  /** Default: 500, 1500, 3500 ms, for errors with `retryable: true`; applied to `openSession` and `refreshSecret`. */
@@ -189,6 +248,19 @@ interface AgentRuntimeOptions {
189
248
  */
190
249
  readonly insecureAddresses?: AgentInsecureAddresses;
191
250
  readonly onEvent?: (event: AgentEvent) => void;
251
+ /**
252
+ * Where the launcher remembers it greeted a person (the app's preferences store). Given here, the launcher reads and
253
+ * writes it through the runtime (`greetings`), a sign-out fences it, and `erase()` removes it.
254
+ */
255
+ readonly greetingStorage?: ConversationPersistence;
256
+ /**
257
+ * Keys an earlier version of the app or the kit wrote that no index lists, for a storage with neither `keys` nor
258
+ * `removePrefix`: removed by every sign-out from `conversation.persistence`, and by `erase()` from `greetingStorage`
259
+ * too. The migration that makes their removal certain on such a storage.
260
+ */
261
+ readonly legacyKeys?: readonly string[];
262
+ /** Where the runtime's warnings go (the app's logger, crash reporter); default `console.warn`. */
263
+ readonly onWarning?: (message: string) => void;
192
264
  }
193
265
  /** The legs whose session address may be plaintext: `realtime` (the WebSocket), `turns` (HTTP). */
194
266
  interface AgentInsecureAddresses {
@@ -205,15 +277,43 @@ interface AgentRuntime {
205
277
  peek(key: AgentKey): AgentConversation;
206
278
  /** A new conversation for the key from the person's next opening; the old one is let go and forgotten. */
207
279
  startNew(key: AgentKey): void;
208
- /** Closes the key's conversation now and drops every reference to it; the next opening continues it in a new session. */
280
+ /**
281
+ * Closes the key's conversation now; the next opening continues it in a new session. What the device kept of it and
282
+ * the conversation it continues stay: `forget` removes them.
283
+ */
209
284
  dispose(key: AgentKey): void;
285
+ /**
286
+ * Closes the key's conversation and forgets it on this device: the conversation it continued and the transcript kept
287
+ * by `persistence`. History's "delete" calls it after the tenant's backend deleted the conversation.
288
+ */
289
+ forget(key: AgentKey): void;
290
+ /** Ends every conversation no view shows that is past its session's end. The kits call it when the app comes back. */
291
+ sweep(): void;
292
+ /**
293
+ * History's delete: the backend deletes the conversation, then the device forgets it, only where the key still
294
+ * continues that conversation (an older history entry never forgets the key's newer one).
295
+ */
296
+ deleteConversation(key: AgentKey, conversationId: string): Promise<void>;
297
+ /**
298
+ * The person's erasure on this device: a sign-out, plus the launcher's greeting records (`greetingStorage`) and the
299
+ * analytics salt, every key this version and alpha.5 or alpha.6 wrote. Keys no index lists (an earlier version's, a
300
+ * write lost with its process) are found only when the storage has `keys` or `removePrefix`.
301
+ */
302
+ erase(): Promise<void>;
303
+ /** The launcher's greeting storage, under the sign-out fence; null without `greetingStorage`. */
304
+ readonly greetings: ConversationPersistence | null;
210
305
  /**
211
306
  * The key goes on with this conversation (history's "Continue"): pinned for the key and opened through the backend's
212
307
  * resume. One that is gone (404, 410: the backend's `AgentRefusal('ended')`) is said as `ended`, never replaced by a
213
308
  * new one silently.
214
309
  */
215
310
  continue(key: AgentKey, conversationId: string): AgentConversation;
216
- /** Every conversation closed and forgotten (what the device kept too), and the backend told to end the sessions. */
311
+ /**
312
+ * Every conversation closed and forgotten, and everything this runtime kept on the device (every transcript it ever
313
+ * wrote under `conversation.persistenceKey`, even of conversations already closed), the status and the name; then
314
+ * the backend is told to end the sessions at the same time. A late answer from before (a session, a status, a write)
315
+ * changes nothing. Kept on purpose: the launcher's greeting records and the analytics salt (`erase` takes them).
316
+ */
217
317
  signOut(): Promise<void>;
218
318
  /** The agent's status, read from the backend at most every `statusStaleMs`. */
219
319
  status(): Promise<AgentStatus>;
@@ -225,8 +325,27 @@ interface AgentRuntime {
225
325
  /** An event from a kit or the app, passed to `onEvent`. */
226
326
  emit(event: AgentEvent): void;
227
327
  }
228
- /** The key as a string: one conversation per person, mode, business and thread. */
229
- declare function agentKey(key: AgentKey): string;
328
+ /**
329
+ * Where the launcher remembers that it greeted a person in a mode, the key its storage sees. Encoded so a guest
330
+ * (`null`) is never a person whose id is "guest".
331
+ */
332
+ declare function greetedKey(subject: string | null, mode: string): string;
333
+ /**
334
+ * A conversation's key as analytics sees it: SHA-256 of the install's salt and the key, 128 bits in hex. Stable per
335
+ * install (the runtime keeps the salt in the app's storage until `erase()`), never the person's id, and not
336
+ * recomputable from a guessed id without the salt.
337
+ */
338
+ declare function hashKey(key: string, salt: string): string;
339
+ /**
340
+ * The key as a string: one conversation per agent, person, mode, business and thread. Encoded as a JSON array, so no
341
+ * value can pass for another (a guest is `null`, never a subject named "guest"; a `|` in an id is just a character).
342
+ */
343
+ declare function agentKey(key: AgentKey, agent?: string | null): string;
230
344
  declare function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime;
345
+ /**
346
+ * An `AgentRefusal`, also one made by another copy of this package (the mock in `@vinaverse/client/testing`, the
347
+ * backend helpers in `@vinaverse/client/backend`, a second install): recognised by its name and kind, not its class.
348
+ */
349
+ declare function isRefusal(error: unknown): error is AgentRefusal;
231
350
 
232
- export { type AgentBackend as A, type AgentClock as a, type AgentConversation as b, type AgentConversationState as c, type AgentEvent as d, type AgentEventType as e, type AgentFailure as f, type AgentFailureReason as g, type AgentInsecureAddresses as h, type AgentKey as i, type AgentPlace as j, type AgentRating as k, AgentRefusal as l, type AgentRetryPolicy as m, type AgentRuntime as n, type AgentRuntimeOptions as o, type AgentSessionRequest as p, type AgentStatus as q, type AgentStatusRead as r, agentKey as s, createAgentRuntime as t };
351
+ export { type AgentBackend as A, type AgentClock as a, type AgentConversation as b, type AgentConversationState as c, type AgentEntryPoint as d, type AgentEvent as e, type AgentEventType as f, type AgentFailure as g, type AgentFailureReason as h, type AgentInsecureAddresses as i, type AgentKey as j, type AgentPlace as k, type AgentRating as l, AgentRefusal as m, type AgentRetryPolicy as n, type AgentRuntime as o, type AgentRuntimeOptions as p, type AgentSessionRequest as q, type AgentStatus as r, type AgentStatusRead as s, agentKey as t, createAgentRuntime as u, greetedKey as v, hashKey as w, isRefusal as x };
@@ -1,5 +1,5 @@
1
- import { S as SessionRequest, b as ConversationSession, C as ConversationStore, a as ConversationOptions } from './store-DFk0ayh8.js';
2
- import { S as Schemas } from './session-8W3fRNeb.js';
1
+ import { S as SessionRequest, b as ConversationSession, C as ConversationStore, f as ConversationPersistence, a as ConversationOptions } from './store-DrXSPHu6.js';
2
+ import { S as Schemas, k as ClientSecret } from './session-8W3fRNeb.js';
3
3
 
4
4
  /** Whose conversation: a person (null for a guest), the app's side, the business acted for, and the thread. */
5
5
  interface AgentKey {
@@ -22,17 +22,47 @@ interface AgentPlace {
22
22
  } | null;
23
23
  readonly filters?: ScreenContext['filters'];
24
24
  }
25
+ /** How the person came in: the kit's parts say `launcher` and `ask`; an app adds `deep_link` or its own words. */
26
+ type AgentEntryPoint = 'launcher' | 'ask' | 'deep_link' | (string & Record<never, never>);
25
27
  /** What the runtime asks the tenant's backend for: a session, for this key, told this place. */
26
28
  interface AgentSessionRequest extends SessionRequest {
27
29
  readonly key: AgentKey;
30
+ /**
31
+ * This session creation's id: the same through its retries, so the backend uses it as the gateway's idempotency key
32
+ * and never opens twice; a new one for every creation (a first open, a resume, continue-here, a rollover). A creation
33
+ * that could not reach the backend keeps it, and its whole request, for the next try, the person's "try again" too,
34
+ * for 24 hours. An `unknown_open` answer is final: not retried under it.
35
+ */
36
+ readonly openId: string;
37
+ /** The runtime's `agent`, for a backend that serves more than one. */
38
+ readonly agent: string | null;
28
39
  readonly place: AgentPlace | null;
29
- /** How the person came in (`button`, `inbox`, …): the app's own words, passed through. */
30
- readonly entryPoint: string | null;
40
+ /**
41
+ * The place's revision (`ScreenContext.revision`): higher than any the session was told before, and lower than any
42
+ * `updateContext` after it, so the API keeps the newest place across a session's rollover.
43
+ */
44
+ readonly placeRevision: number;
45
+ /** How the person came in (`launcher`, `ask`, `deep_link`, the app's own words), passed through. */
46
+ readonly entryPoint: AgentEntryPoint | null;
31
47
  }
32
48
  /** Why there is no session, typed, so an app never needs sentinel errors of its own. */
33
49
  type AgentFailureReason =
34
- /** The conversation asked for is over: not theirs, deleted (404), or past the time it is kept (410). */
50
+ /** The conversation is over (its session's own end, or a refusal with no finer reason). */
35
51
  'ended'
52
+ /** The conversation asked for does not exist for this person (deleted, or never theirs). */
53
+ | 'not_found'
54
+ /** Past the time the conversation is kept. */
55
+ | 'expired'
56
+ /** The conversation belongs to another business, mode or agent than the key's. */
57
+ | 'mismatch'
58
+ /** The conversation's flow is completed; a new one starts when the person asks. */
59
+ | 'completed'
60
+ /** The person may not act for this business or conversation. */
61
+ | 'forbidden'
62
+ /** The server holds a newer screen context than the one told (an update, never a session). */
63
+ | 'conflict'
64
+ /** The backend could not tell whether the session opened: unavailable, never retried, its `openId` kept for support. */
65
+ | 'unknown_open'
36
66
  /** The person's sign-in was refused by the tenant's API. */
37
67
  | 'auth'
38
68
  /** The agent is switched off for this tenant, mode or person. */
@@ -83,11 +113,17 @@ interface AgentStatusRead {
83
113
  interface AgentBackend {
84
114
  /** A session from the tenant's API (it creates it at Vinaverse with its service key), or an `AgentRefusal`. */
85
115
  openSession(request: AgentSessionRequest): Promise<ConversationSession>;
86
- /** A fresh client secret for the same session (before its end). */
87
- refreshSecret?(sessionId: string): Promise<string>;
116
+ /**
117
+ * A fresh client secret for the same session (before its end): a `ClientSecret` `{ value, expires_at }`, or its
118
+ * value. A refusal (`session_closed`: the session is over) goes on with the conversation in a new session.
119
+ */
120
+ refreshSecret?(sessionId: string): Promise<ClientSecret | string>;
88
121
  /**
89
122
  * The person's place changed while the chat is open: the tenant's backend tells Vinaverse
90
- * (`PUT /v1/sessions/{id}/context`). Absent, or refused, the kit offers a question about the new place instead.
123
+ * (`PUT /v1/sessions/{id}/context`). A place cleared (`tell({ place: null })`) comes as `{ screen: 'other' }`.
124
+ * `revision` only grows for a conversation, across its sessions. A conflict (`decodeBackendFailure`'s
125
+ * `context_conflict`, or a refusal with reason `conflict`, with the server's `currentRevision`) is sent once more
126
+ * above it, then said as `moved`. Absent, or refused otherwise, the kit offers a question about the new place.
91
127
  */
92
128
  updateContext?(sessionId: string, place: AgentPlace, revision: number): Promise<void>;
93
129
  /** Whether the agent is offered, and what it can do. Absent: available, nothing extra. */
@@ -100,13 +136,16 @@ interface AgentBackend {
100
136
  }): Promise<void>;
101
137
  /** Sign-out: end the person's open sessions at the tenant's API. */
102
138
  endSessions?(): Promise<void>;
139
+ /** History's delete: the tenant's API deletes the person's conversation (authenticated, theirs only). */
140
+ deleteConversation?(conversationId: string): Promise<void>;
103
141
  }
104
142
  type AgentRating = 'up' | 'down';
105
143
  type AgentFailure = 'ended' | 'unreachable' | 'unavailable';
106
- type AgentEventType = 'session_opened' | 'session_resumed' | 'session_retried' | 'session_failed' | 'conversation_ended' | 'context_updated' | 'rated' | 'chip_tapped' | 'card_action' | 'target_opened' | 'link_opened' | 'launcher_shown' | 'launcher_opened' | 'launcher_greeted' | 'greeting_closed';
144
+ type AgentEventType = 'session_opened' | 'session_resumed' | 'session_retried' | 'session_failed' | 'conversation_ended' | 'context_updated' | 'rated' | 'chip_tapped' | 'card_action' | 'target_opened' | 'link_opened' | 'launcher_shown' | 'launcher_opened' | 'launcher_greeted' | 'greeting_closed' | 'message_sent' | 'reply_completed' | 'card_shown';
107
145
  /** For the app's analytics: what happened, for which conversation, with nothing the person said. */
108
146
  interface AgentEvent {
109
147
  readonly type: AgentEventType;
148
+ /** The conversation, as a hash of its key (`hashKey`): stable, and never the person's id. */
110
149
  readonly key?: string;
111
150
  readonly sessionId?: string;
112
151
  readonly failure?: AgentFailure;
@@ -116,8 +155,8 @@ interface AgentEvent {
116
155
  readonly rating?: AgentRating;
117
156
  readonly cardId?: string;
118
157
  readonly kind?: string;
119
- /** Where the person opened from (`entryPoint`, the screen). */
120
- readonly from?: string;
158
+ /** Where the person opened from (`launcher`, `ask`, `deep_link`, the app's own words). */
159
+ readonly from?: AgentEntryPoint;
121
160
  }
122
161
  /** What a UI draws around the store: why there is no session, whether the person moved, the thumbs given. */
123
162
  interface AgentConversationState {
@@ -129,6 +168,13 @@ interface AgentConversationState {
129
168
  readonly moved: boolean;
130
169
  readonly ratings: ReadonlyMap<string, AgentRating>;
131
170
  readonly sessionId: string | null;
171
+ /**
172
+ * The conversation the key holds: the one it continues before a session opens, then its session's. Null for a new
173
+ * one not yet opened. History's delete compares against it (`deleteConversation` does).
174
+ */
175
+ readonly conversationId: string | null;
176
+ /** The open the backend could not settle (`unknown_open`), for support; null otherwise. */
177
+ readonly openId: string | null;
132
178
  }
133
179
  interface AgentConversation {
134
180
  /** The key as a string, for the app's own maps and the events. */
@@ -142,18 +188,26 @@ interface AgentConversation {
142
188
  */
143
189
  show(open?: {
144
190
  readonly place?: AgentPlace | null;
145
- readonly entryPoint?: string | null;
191
+ readonly entryPoint?: AgentEntryPoint | null;
146
192
  }): () => void;
147
- /** The place and the way in changed while a view is attached: told live where the backend can, else on the next session. */
193
+ /**
194
+ * The place and the way in changed while a view is attached: told live where the backend can, else on the next
195
+ * session. `undefined` keeps what was told; `null` clears it.
196
+ */
148
197
  tell(open: {
149
198
  readonly place?: AgentPlace | null;
150
- readonly entryPoint?: string | null;
199
+ readonly entryPoint?: AgentEntryPoint | null;
151
200
  }): void;
152
201
  getState(): AgentConversationState;
153
202
  subscribe(listener: () => void): () => void;
154
203
  /** Asks for a session again, after the backend could not be reached. */
155
204
  retry(): void;
156
- /** A thumbs on a whole reply; a change of mind only is sent again. */
205
+ /**
206
+ * Whether a reply can be rated: one this runtime saw given live. A reply restored from the device or a resumed
207
+ * transcript cannot be, until the API rates a turn by its conversation; the kits draw no thumbs for it.
208
+ */
209
+ canRate(turnId: string): boolean;
210
+ /** A thumbs on a whole reply, against the session it was given in; a change of mind only is sent again. */
157
211
  rate(turnId: string, rating: AgentRating): void;
158
212
  /**
159
213
  * Sends the person's `text` once in this conversation's life (a chip tapped twice, a retried tap). It is shown as
@@ -172,6 +226,11 @@ interface AgentClock {
172
226
  }
173
227
  interface AgentRuntimeOptions {
174
228
  readonly backend: AgentBackend;
229
+ /**
230
+ * The agent this runtime talks to (its id at Vinaverse). It is part of every key, so two agents sharing the device's
231
+ * storage (`conversation.persistence`) never share a conversation. Default: none (one agent per storage namespace).
232
+ */
233
+ readonly agent?: string;
175
234
  /** The store's own options (locale, time zone, deep-link prefixes, transport, persistence, closeInBackground…). */
176
235
  readonly conversation?: Omit<ConversationOptions, 'getSession' | 'refreshSecret' | 'clock'>;
177
236
  /** Default: 500, 1500, 3500 ms, for errors with `retryable: true`; applied to `openSession` and `refreshSecret`. */
@@ -189,6 +248,19 @@ interface AgentRuntimeOptions {
189
248
  */
190
249
  readonly insecureAddresses?: AgentInsecureAddresses;
191
250
  readonly onEvent?: (event: AgentEvent) => void;
251
+ /**
252
+ * Where the launcher remembers it greeted a person (the app's preferences store). Given here, the launcher reads and
253
+ * writes it through the runtime (`greetings`), a sign-out fences it, and `erase()` removes it.
254
+ */
255
+ readonly greetingStorage?: ConversationPersistence;
256
+ /**
257
+ * Keys an earlier version of the app or the kit wrote that no index lists, for a storage with neither `keys` nor
258
+ * `removePrefix`: removed by every sign-out from `conversation.persistence`, and by `erase()` from `greetingStorage`
259
+ * too. The migration that makes their removal certain on such a storage.
260
+ */
261
+ readonly legacyKeys?: readonly string[];
262
+ /** Where the runtime's warnings go (the app's logger, crash reporter); default `console.warn`. */
263
+ readonly onWarning?: (message: string) => void;
192
264
  }
193
265
  /** The legs whose session address may be plaintext: `realtime` (the WebSocket), `turns` (HTTP). */
194
266
  interface AgentInsecureAddresses {
@@ -205,15 +277,43 @@ interface AgentRuntime {
205
277
  peek(key: AgentKey): AgentConversation;
206
278
  /** A new conversation for the key from the person's next opening; the old one is let go and forgotten. */
207
279
  startNew(key: AgentKey): void;
208
- /** Closes the key's conversation now and drops every reference to it; the next opening continues it in a new session. */
280
+ /**
281
+ * Closes the key's conversation now; the next opening continues it in a new session. What the device kept of it and
282
+ * the conversation it continues stay: `forget` removes them.
283
+ */
209
284
  dispose(key: AgentKey): void;
285
+ /**
286
+ * Closes the key's conversation and forgets it on this device: the conversation it continued and the transcript kept
287
+ * by `persistence`. History's "delete" calls it after the tenant's backend deleted the conversation.
288
+ */
289
+ forget(key: AgentKey): void;
290
+ /** Ends every conversation no view shows that is past its session's end. The kits call it when the app comes back. */
291
+ sweep(): void;
292
+ /**
293
+ * History's delete: the backend deletes the conversation, then the device forgets it, only where the key still
294
+ * continues that conversation (an older history entry never forgets the key's newer one).
295
+ */
296
+ deleteConversation(key: AgentKey, conversationId: string): Promise<void>;
297
+ /**
298
+ * The person's erasure on this device: a sign-out, plus the launcher's greeting records (`greetingStorage`) and the
299
+ * analytics salt, every key this version and alpha.5 or alpha.6 wrote. Keys no index lists (an earlier version's, a
300
+ * write lost with its process) are found only when the storage has `keys` or `removePrefix`.
301
+ */
302
+ erase(): Promise<void>;
303
+ /** The launcher's greeting storage, under the sign-out fence; null without `greetingStorage`. */
304
+ readonly greetings: ConversationPersistence | null;
210
305
  /**
211
306
  * The key goes on with this conversation (history's "Continue"): pinned for the key and opened through the backend's
212
307
  * resume. One that is gone (404, 410: the backend's `AgentRefusal('ended')`) is said as `ended`, never replaced by a
213
308
  * new one silently.
214
309
  */
215
310
  continue(key: AgentKey, conversationId: string): AgentConversation;
216
- /** Every conversation closed and forgotten (what the device kept too), and the backend told to end the sessions. */
311
+ /**
312
+ * Every conversation closed and forgotten, and everything this runtime kept on the device (every transcript it ever
313
+ * wrote under `conversation.persistenceKey`, even of conversations already closed), the status and the name; then
314
+ * the backend is told to end the sessions at the same time. A late answer from before (a session, a status, a write)
315
+ * changes nothing. Kept on purpose: the launcher's greeting records and the analytics salt (`erase` takes them).
316
+ */
217
317
  signOut(): Promise<void>;
218
318
  /** The agent's status, read from the backend at most every `statusStaleMs`. */
219
319
  status(): Promise<AgentStatus>;
@@ -225,8 +325,27 @@ interface AgentRuntime {
225
325
  /** An event from a kit or the app, passed to `onEvent`. */
226
326
  emit(event: AgentEvent): void;
227
327
  }
228
- /** The key as a string: one conversation per person, mode, business and thread. */
229
- declare function agentKey(key: AgentKey): string;
328
+ /**
329
+ * Where the launcher remembers that it greeted a person in a mode, the key its storage sees. Encoded so a guest
330
+ * (`null`) is never a person whose id is "guest".
331
+ */
332
+ declare function greetedKey(subject: string | null, mode: string): string;
333
+ /**
334
+ * A conversation's key as analytics sees it: SHA-256 of the install's salt and the key, 128 bits in hex. Stable per
335
+ * install (the runtime keeps the salt in the app's storage until `erase()`), never the person's id, and not
336
+ * recomputable from a guessed id without the salt.
337
+ */
338
+ declare function hashKey(key: string, salt: string): string;
339
+ /**
340
+ * The key as a string: one conversation per agent, person, mode, business and thread. Encoded as a JSON array, so no
341
+ * value can pass for another (a guest is `null`, never a subject named "guest"; a `|` in an id is just a character).
342
+ */
343
+ declare function agentKey(key: AgentKey, agent?: string | null): string;
230
344
  declare function createAgentRuntime(options: AgentRuntimeOptions): AgentRuntime;
345
+ /**
346
+ * An `AgentRefusal`, also one made by another copy of this package (the mock in `@vinaverse/client/testing`, the
347
+ * backend helpers in `@vinaverse/client/backend`, a second install): recognised by its name and kind, not its class.
348
+ */
349
+ declare function isRefusal(error: unknown): error is AgentRefusal;
231
350
 
232
- export { type AgentBackend as A, type AgentClock as a, type AgentConversation as b, type AgentConversationState as c, type AgentEvent as d, type AgentEventType as e, type AgentFailure as f, type AgentFailureReason as g, type AgentInsecureAddresses as h, type AgentKey as i, type AgentPlace as j, type AgentRating as k, AgentRefusal as l, type AgentRetryPolicy as m, type AgentRuntime as n, type AgentRuntimeOptions as o, type AgentSessionRequest as p, type AgentStatus as q, type AgentStatusRead as r, agentKey as s, createAgentRuntime as t };
351
+ export { type AgentBackend as A, type AgentClock as a, type AgentConversation as b, type AgentConversationState as c, type AgentEntryPoint as d, type AgentEvent as e, type AgentEventType as f, type AgentFailure as g, type AgentFailureReason as h, type AgentInsecureAddresses as i, type AgentKey as j, type AgentPlace as k, type AgentRating as l, AgentRefusal as m, type AgentRetryPolicy as n, type AgentRuntime as o, type AgentRuntimeOptions as p, type AgentSessionRequest as q, type AgentStatus as r, type AgentStatusRead as s, agentKey as t, createAgentRuntime as u, greetedKey as v, hashKey as w, isRefusal as x };
@@ -179,6 +179,15 @@ interface ConversationPersistence {
179
179
  getItem(key: string): string | null | Promise<string | null>;
180
180
  setItem(key: string, value: string): void | Promise<void>;
181
181
  removeItem(key: string): void | Promise<void>;
182
+ /**
183
+ * Optional: the keys under a prefix. With it (or `removePrefix`), a sign-out and an erasure are guaranteed to find every
184
+ * key kept, ones no index lists too (an earlier version's, a write lost with its process); without either, only the
185
+ * keys this version indexed and the shapes it can derive from them. AsyncStorage and MMKV: `getAllKeys()` filtered
186
+ * by the prefix; web `localStorage`: `Object.keys(localStorage)` filtered.
187
+ */
188
+ keys?(prefix: string): readonly string[] | Promise<readonly string[]>;
189
+ /** Optional: removes every key under a prefix at once (used instead of `keys` where it cannot reach a kept record). */
190
+ removePrefix?(prefix: string): void | Promise<void>;
182
191
  }
183
192
  interface ConversationOptions {
184
193
  /**
@@ -217,7 +226,24 @@ interface ConversationOptions {
217
226
  readonly clock?: {
218
227
  now(): number;
219
228
  };
229
+ /**
230
+ * What the person and the agent did live, for analytics (never what a transcript or the device brought back): the
231
+ * person sent a text, a reply finished, a card was drawn. Carries no text.
232
+ */
233
+ readonly onActivity?: (activity: ConversationActivity) => void;
220
234
  }
235
+ /** One live step of the conversation, without its words. */
236
+ type ConversationActivity = {
237
+ readonly type: 'message_sent';
238
+ readonly id: string;
239
+ } | {
240
+ readonly type: 'reply_completed';
241
+ readonly turnId: string;
242
+ } | {
243
+ readonly type: 'card_shown';
244
+ readonly cardId: string;
245
+ readonly kind: string;
246
+ };
221
247
  /** A text is at most 2000 code points (`TurnInput`). */
222
248
  declare const MAX_TEXT = 2000;
223
249
  declare class ConversationStore {
@@ -346,6 +372,10 @@ declare class ConversationStore {
346
372
  /** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
347
373
  private rolled;
348
374
  private statusChanged;
375
+ /** The app's analytics hears of a step; what it throws is reported (`onError`), never in the way of a send or a frame. */
376
+ private tellActivity;
377
+ /** The live frames analytics hears of: a reply finished, cards drawn. */
378
+ private activity;
349
379
  private event;
350
380
  /** The person's text that started a turn, while its message is on screen. */
351
381
  private turnText;
@@ -381,4 +411,4 @@ declare class ConversationStore {
381
411
  /** One conversation with an agent, for any UI: see `ConversationStore`. */
382
412
  declare function createConversation(options: ConversationOptions): ConversationStore;
383
413
 
384
- export { ConversationStore as C, type FrameScheduler as F, MAX_TEXT as M, type SessionRequest as S, type TimelineItem as T, type ConversationOptions as a, type ConversationSession as b, type CardItem as c, type ConversationBanner as d, type ConversationPersistence as e, type ConversationStatus as f, type ConversationView as g, type MessageItem as h, type MessageState as i, type SessionReason as j, type StreamingText as k, type SystemEvent as l, type SystemItem as m, createConversation as n };
414
+ export { ConversationStore as C, type FrameScheduler as F, MAX_TEXT as M, type SessionRequest as S, type TimelineItem as T, type ConversationOptions as a, type ConversationSession as b, type CardItem as c, type ConversationActivity as d, type ConversationBanner as e, type ConversationPersistence as f, type ConversationStatus as g, type ConversationView as h, type MessageItem as i, type MessageState as j, type SessionReason as k, type StreamingText as l, type SystemEvent as m, type SystemItem as n, createConversation as o };