@vinaverse/client 0.0.0-stage → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,93 @@
1
- # Temporary Holding Version
1
+ # @vinaverse/client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The realtime session for apps: a conversation with an agent over the WebSocket, with SSE as the fallback, that
4
+ reconnects and resumes, resends what was never acknowledged, and gives typed events and cards. Headless: it holds
5
+ the conversation's state, and your app draws it. Runs in React Native, browsers and Node 18+, with no runtime
6
+ dependencies.
7
+
8
+ ```sh
9
+ npm install @vinaverse/client
10
+ ```
11
+
12
+ ## Use
13
+
14
+ Your backend creates the session with its service key (`@vinaverse/server`) and hands the app the session's `id`,
15
+ `realtime_url`, `turns_url` and `client_secret`. The app never holds a service key: the client refuses one.
16
+
17
+ ```ts
18
+ import { VinaverseSession, isKnownCard } from '@vinaverse/client';
19
+
20
+ const session = new VinaverseSession({
21
+ session, // from your backend
22
+ // On 4401, a fresh secret for the session in use: after resume() that is the newer one.
23
+ refreshSecret: (sessionId) => api.refreshClientSecret(sessionId),
24
+ // React Native and Node 22+: their WebSocket takes headers
25
+ // auth: 'header',
26
+ });
27
+
28
+ session.on('state', (state) => render(state)); // messages, cards, the open question, what is not sent
29
+ session.on('event', (event) => {
30
+ if (event.type === 'message.delta') appendStreaming(event.text);
31
+ });
32
+ session.on('status', (status) => {
33
+ if (status !== 'ended') return;
34
+ // Ended idle, expired or at its limit: on the next action, a new session for the conversation, then resume().
35
+ if (session.state.reconnect === 'new_session_on_next_action') resumeOnNextAction();
36
+ else showContinuedElsewhere(); // replaced or revoked: resume only on the user's action
37
+ });
38
+ session.connect(); // now: a client secret opens its first connection within two minutes of issue
39
+
40
+ session.sendText('Find me a silk press stylist near Midtown');
41
+ session.answer(card.id, card.question.question_id, { values: ['opt_repair'] });
42
+ session.reportOutcome(proposalCard.id, 'confirmed'); // after your backend applied it
43
+
44
+ for (const card of session.state.cards) if (isKnownCard(card.card)) draw(card);
45
+ ```
46
+
47
+ - **Connect early.** The gateway accepts a client secret's first connection within two minutes of issue
48
+ (`client_secret.expires_at`). Call `connect()` when the conversation opens, or pass `refreshSecret`: otherwise a
49
+ person who types after that window gets an `unauthorized` session.
50
+ - **Resume** a conversation on a new session your backend created (by `conversation_key` or `conversation_id`):
51
+ `session.resume(newSession)`. The client resends what the old one never acknowledged.
52
+ - **Idle.** After 10 minutes without input (pings do not count) the gateway ends the session: the status is `ended`
53
+ with `state.reconnect` `new_session_on_next_action`. On the person's next action, have your backend create a new
54
+ session for the same conversation (`conversation_id` or `conversation_key`) and call `session.resume(newSession)`.
55
+ Anything sent in between is held and resent there (`state.heldForNextSession` lists it, to draw as "sends when
56
+ you continue"); `connect()` would only spend a dead secret, so it throws `needs_new_session`.
57
+ - **Turn limit.** When a session has no turns left, the text sent is held, not refused: the status is `ended` with
58
+ `state.reconnect` `new_session_on_next_action`, as after an idle close. Create the next session from your backend and
59
+ `resume()`; the held text is sent there.
60
+ - **The end of the session.** `session.expiring` arrives about a minute before it (WebSocket only), and
61
+ `state.expiring` holds its seconds left: a good moment to have your backend create the next session.
62
+ - **Background**: `session.setForeground(false)` stops the pings.
63
+ - **Drawing the conversation.** Sort `state.messages` and `state.cards` together by `seq`, and key each by its `key`
64
+ (messages) or `id` (cards). `state.streaming` is the reply so far; a reply cut off stays, with `partial`. When a
65
+ person from the business takes over, `state.takeover` names their role: draw `role: 'staff'` messages as that
66
+ person, never as the agent. Always show `state.disclosure`.
67
+ - **Without a WebSocket** (after two failed first connections, or none in the runtime) the session runs turns over
68
+ SSE (`status === 'sse'`) and `connect()` tries the WebSocket again. Answers to question cards need the WebSocket:
69
+ one that cannot be sent is shown as not sent (its question unlocks) and reported on the `error` listener.
70
+ - **`auth: 'header'`** works where the WebSocket takes headers: React Native, and Node 22+ with the default factory.
71
+ A browser's cannot: keep the default (`subprotocol`).
72
+
73
+ ## A UI on top: the conversation store
74
+
75
+ `@vinaverse/client/conversation` holds a whole conversation for a UI, with no framework: an immutable, structurally
76
+ shared view (`getSnapshot`, `subscribe`), the streaming reply once per frame, resume on the person's next action,
77
+ pending and held texts, proposals decided through your handler. The UI kits bind to it.
78
+
79
+ ```ts
80
+ import { createConversation } from '@vinaverse/client/conversation';
81
+
82
+ const conversation = createConversation({ getSession: (req) => api.vinaverseSession(req) });
83
+ conversation.start();
84
+ conversation.subscribe(() => draw(conversation.getSnapshot()));
85
+ ```
86
+
87
+ [docs/architecture/conversation.md](../../docs/architecture/conversation.md).
88
+
89
+ How it works, and what it guarantees: [docs/architecture/client.md](../../docs/architecture/client.md).
90
+
91
+ ## License
92
+
93
+ Apache-2.0