@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 +92 -2
- package/dist/chunk-AWHPWRGT.js +1318 -0
- package/dist/chunk-AWHPWRGT.js.map +1 -0
- package/dist/conversation/index.cjs +2181 -0
- package/dist/conversation/index.cjs.map +1 -0
- package/dist/conversation/index.d.cts +349 -0
- package/dist/conversation/index.d.ts +349 -0
- package/dist/conversation/index.js +858 -0
- package/dist/conversation/index.js.map +1 -0
- package/dist/index.cjs +1328 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/session--W6JXfwo.d.cts +2028 -0
- package/dist/session--W6JXfwo.d.ts +2028 -0
- package/package.json +81 -4
package/README.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @vinaverse/client
|
|
2
2
|
|
|
3
|
-
|
|
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
|