@vinaverse/client 0.0.0-stage → 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.
Files changed (49) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +202 -2
  3. package/dist/agent/index.cjs +3319 -0
  4. package/dist/agent/index.cjs.map +1 -0
  5. package/dist/agent/index.d.cts +57 -0
  6. package/dist/agent/index.d.ts +57 -0
  7. package/dist/agent/index.js +99 -0
  8. package/dist/agent/index.js.map +1 -0
  9. package/dist/backend/index.cjs +268 -0
  10. package/dist/backend/index.cjs.map +1 -0
  11. package/dist/backend/index.d.cts +72 -0
  12. package/dist/backend/index.d.ts +72 -0
  13. package/dist/backend/index.js +250 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/chunk-46D3FSLU.js +997 -0
  16. package/dist/chunk-46D3FSLU.js.map +1 -0
  17. package/dist/chunk-GWLLUOFY.js +1374 -0
  18. package/dist/chunk-GWLLUOFY.js.map +1 -0
  19. package/dist/chunk-O3NLAZGZ.js +859 -0
  20. package/dist/chunk-O3NLAZGZ.js.map +1 -0
  21. package/dist/chunk-YNUHXJD6.js +9 -0
  22. package/dist/chunk-YNUHXJD6.js.map +1 -0
  23. package/dist/contract-DmQs8amJ.d.cts +6 -0
  24. package/dist/contract-DmQs8amJ.d.ts +6 -0
  25. package/dist/conversation/index.cjs +2510 -0
  26. package/dist/conversation/index.cjs.map +1 -0
  27. package/dist/conversation/index.d.cts +65 -0
  28. package/dist/conversation/index.d.ts +65 -0
  29. package/dist/conversation/index.js +134 -0
  30. package/dist/conversation/index.js.map +1 -0
  31. package/dist/index.cjs +1390 -0
  32. package/dist/index.cjs.map +1 -0
  33. package/dist/index.d.cts +11 -0
  34. package/dist/index.d.ts +11 -0
  35. package/dist/index.js +4 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/runtime-CISPp9nJ.d.cts +371 -0
  38. package/dist/runtime-Z1RI10lU.d.ts +371 -0
  39. package/dist/session-CPz4pcPu.d.cts +3651 -0
  40. package/dist/session-CPz4pcPu.d.ts +3651 -0
  41. package/dist/store-BxiicOKG.d.cts +414 -0
  42. package/dist/store-DZka9T2-.d.ts +414 -0
  43. package/dist/testing/index.cjs +274 -0
  44. package/dist/testing/index.cjs.map +1 -0
  45. package/dist/testing/index.d.cts +60 -0
  46. package/dist/testing/index.d.ts +60 -0
  47. package/dist/testing/index.js +259 -0
  48. package/dist/testing/index.js.map +1 -0
  49. package/package.json +151 -3
@@ -0,0 +1,859 @@
1
+ import { createConversation } from './chunk-46D3FSLU.js';
2
+ import { randomId, VinaverseClientError } from './chunk-GWLLUOFY.js';
3
+
4
+ // src/agent/runtime.ts
5
+ var AgentRefusal = class extends Error {
6
+ constructor(kind, options = {}) {
7
+ super(
8
+ options.message ?? (kind === "ended" ? "The conversation is over." : "No agent now.")
9
+ );
10
+ this.kind = kind;
11
+ this.name = "AgentRefusal";
12
+ this.reason = options.reason ?? (kind === "ended" ? "ended" : "unknown");
13
+ this.code = options.code ?? null;
14
+ }
15
+ kind;
16
+ reason;
17
+ code;
18
+ };
19
+ var GREETED = "vinaverse.greeted";
20
+ var GREETED_BEFORE = "vinaverse.agent.greeted";
21
+ function greetedKey(subject, mode) {
22
+ return `${GREETED}.${JSON.stringify([subject, mode])}`;
23
+ }
24
+ function tupleOf(key, prefix) {
25
+ if (!key.startsWith(`${prefix}.[`)) return null;
26
+ try {
27
+ const t = JSON.parse(key.slice(prefix.length + 1));
28
+ return Array.isArray(t) ? t : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function legacyTranscripts(keys, prefix) {
34
+ const out = [];
35
+ for (const key of keys) {
36
+ const t = tupleOf(key, prefix);
37
+ if (t)
38
+ out.push(
39
+ `${prefix}.${t[1] ?? "guest"}|${t[2]}|${t[3] ?? "-"}|${t[4] ?? "chat"}`
40
+ );
41
+ }
42
+ return out;
43
+ }
44
+ function legacyGreetings(pairs) {
45
+ const out = [];
46
+ for (const [subject, mode] of pairs)
47
+ out.push(`${GREETED_BEFORE}.${subject ?? "guest"}:${mode}`);
48
+ return out;
49
+ }
50
+ var NOT_KEPT = /* @__PURE__ */ new Set(["starting", "unavailable", "unauthorized", "closed"]);
51
+ var CLEARED = { screen: "other" };
52
+ function hashKey(key, salt) {
53
+ return `k${sha256(`${salt}\0${key}`).slice(0, 32)}`;
54
+ }
55
+ var K;
56
+ var H;
57
+ function sha256(text) {
58
+ if (!K || !H) {
59
+ K = [];
60
+ H = [];
61
+ for (let n = 2; K.length < 64; n++) {
62
+ let prime = true;
63
+ for (let d = 2; d * d <= n; d++) if (n % d === 0) prime = false;
64
+ if (!prime) continue;
65
+ if (H.length < 8) H.push(Math.sqrt(n) % 1 * 2 ** 32 | 0);
66
+ K.push(Math.cbrt(n) % 1 * 2 ** 32 | 0);
67
+ }
68
+ }
69
+ const bytes = [];
70
+ const s = encodeURIComponent(text);
71
+ for (let i = 0; i < s.length; i++)
72
+ if (s[i] === "%") {
73
+ bytes.push(Number.parseInt(s.slice(i + 1, i + 3), 16));
74
+ i += 2;
75
+ } else bytes.push(s.charCodeAt(i));
76
+ const bits = bytes.length * 8;
77
+ bytes.push(128);
78
+ while (bytes.length % 64 !== 56) bytes.push(0);
79
+ for (const word of [Math.floor(bits / 2 ** 32), bits >>> 0])
80
+ bytes.push(
81
+ word >>> 24,
82
+ word >>> 16 & 255,
83
+ word >>> 8 & 255,
84
+ word & 255
85
+ );
86
+ const h = [...H];
87
+ const w = new Array(64);
88
+ const ror = (x, n) => x >>> n | x << 32 - n;
89
+ for (let at = 0; at < bytes.length; at += 64) {
90
+ for (let i = 0; i < 64; i++) {
91
+ if (i < 16) {
92
+ const b2 = at + i * 4;
93
+ w[i] = bytes[b2] << 24 | bytes[b2 + 1] << 16 | bytes[b2 + 2] << 8 | bytes[b2 + 3];
94
+ } else {
95
+ const x = w[i - 15];
96
+ const y = w[i - 2];
97
+ w[i] = w[i - 16] + (ror(x, 7) ^ ror(x, 18) ^ x >>> 3) + w[i - 7] + (ror(y, 17) ^ ror(y, 19) ^ y >>> 10) | 0;
98
+ }
99
+ }
100
+ let [a, b, c, d, e, f, g, k] = h;
101
+ for (let i = 0; i < 64; i++) {
102
+ const t1 = k + (ror(e, 6) ^ ror(e, 11) ^ ror(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] | 0;
103
+ const t2 = (ror(a, 2) ^ ror(a, 13) ^ ror(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
104
+ k = g;
105
+ g = f;
106
+ f = e;
107
+ e = d + t1 | 0;
108
+ d = c;
109
+ c = b;
110
+ b = a;
111
+ a = t1 + t2 | 0;
112
+ }
113
+ [a, b, c, d, e, f, g, k].forEach((v, i) => {
114
+ h[i] = h[i] + v | 0;
115
+ });
116
+ }
117
+ return h.map((v) => (v >>> 0).toString(16).padStart(8, "0")).join("");
118
+ }
119
+ var NO_FEATURES = {
120
+ history: false,
121
+ memory: false,
122
+ businessAgent: false,
123
+ businessSetup: false
124
+ };
125
+ var SYSTEM_CLOCK = { now: () => Date.now() };
126
+ function agentKey(key, agent = null) {
127
+ return JSON.stringify([
128
+ agent,
129
+ key.subject,
130
+ key.mode,
131
+ key.scope ?? null,
132
+ key.thread ?? "chat"
133
+ ]);
134
+ }
135
+ var samePlace = (a, b) => a?.screen === b?.screen && (a?.entity?.type ?? null) === (b?.entity?.type ?? null) && (a?.entity?.id ?? null) === (b?.entity?.id ?? null) && JSON.stringify(a?.filters ?? null) === JSON.stringify(b?.filters ?? null);
136
+ function usable(session, insecure) {
137
+ return typeof session.id === "string" && session.id !== "" && (insecure.realtime === true || /^wss:\/\//i.test(session.realtime_url ?? "")) && (insecure.turns === true || /^https:\/\//i.test(session.turns_url ?? "")) && Boolean(session.client_secret);
138
+ }
139
+ function warnInsecure(insecure, warn) {
140
+ if (!(insecure.realtime || insecure.turns)) return;
141
+ if (globalThis.__DEV__ === true) return;
142
+ warn(
143
+ "@vinaverse/client/agent: insecureAddresses lets sessions use plaintext ws:// or http:// addresses. For mocks and local development only.",
144
+ { code: "insecure_addresses" }
145
+ );
146
+ }
147
+ var OPEN_KEPT_MS = 24 * 36e5;
148
+ var MAX_WAITING = 100;
149
+ function createAgentRuntime(options) {
150
+ const { backend } = options;
151
+ const policy = {
152
+ delays: options.retry?.delays ?? [500, 1500, 3500],
153
+ isRetryable: options.retry?.isRetryable ?? retryable
154
+ };
155
+ const clock = options.clock ?? SYSTEM_CLOCK;
156
+ const keepAlive = options.keepAlive ?? "session";
157
+ const staleMs = options.statusStaleMs ?? 5 * 6e4;
158
+ const insecure = options.insecureAddresses ?? {};
159
+ warnInsecure(insecure, options.onWarning ?? ((m) => console.warn(m)));
160
+ const live = /* @__PURE__ */ new Map();
161
+ const current = /* @__PURE__ */ new Map();
162
+ const listeners = /* @__PURE__ */ new Set();
163
+ let status = null;
164
+ let reading = null;
165
+ const NO_STATUS = {
166
+ status: null,
167
+ reading: false,
168
+ failed: false
169
+ };
170
+ let statusState = NO_STATUS;
171
+ const setStatusState = (next) => {
172
+ statusState = { status: status?.value ?? null, ...next };
173
+ changed();
174
+ };
175
+ let sessionName = null;
176
+ let generation = 0;
177
+ const keyOf = (key) => agentKey(key, options.agent ?? null);
178
+ const prefix = options.conversation?.persistenceKey ?? "vinaverse.agent";
179
+ const storage = guardedStorage(
180
+ options.conversation?.persistence,
181
+ prefix,
182
+ () => generation
183
+ );
184
+ const greetings = guardedStorage(
185
+ options.greetingStorage,
186
+ GREETED,
187
+ () => generation
188
+ );
189
+ const warn = options.onWarning ?? ((m) => console.warn(m));
190
+ const isolated = (what, call) => {
191
+ try {
192
+ call();
193
+ } catch (error) {
194
+ warn(
195
+ `@vinaverse/client/agent: the app's ${what} threw (${error instanceof Error ? error.message : String(error)}); the conversation went on.`,
196
+ { code: "callback_threw", callback: what }
197
+ );
198
+ }
199
+ };
200
+ const saltStore = options.conversation?.persistence ?? options.greetingStorage;
201
+ const saltKey = `${prefix}:salt`;
202
+ let salt = null;
203
+ const waiting = [];
204
+ const loadSalt = () => {
205
+ if (!options.onEvent) return;
206
+ const settle = (kept2) => {
207
+ if (salt !== null) return;
208
+ if (kept2) salt = kept2;
209
+ else {
210
+ salt = randomId().slice(3);
211
+ void Promise.resolve(saltStore?.setItem(saltKey, salt)).catch(
212
+ () => void 0
213
+ );
214
+ }
215
+ for (const event of waiting.splice(0)) deliver(event);
216
+ };
217
+ try {
218
+ const got = saltStore?.getItem(saltKey);
219
+ if (typeof got === "string" || got == null) settle(got);
220
+ else got.then(settle, () => settle(null));
221
+ } catch {
222
+ settle(null);
223
+ }
224
+ };
225
+ const deliver = (event) => isolated(
226
+ "onEvent",
227
+ () => options.onEvent?.(
228
+ event.key ? { ...event, key: hashKey(event.key, salt ?? "") } : event
229
+ )
230
+ );
231
+ let dropping = false;
232
+ const emit = (event) => {
233
+ if (!options.onEvent) return;
234
+ if (salt !== null) return deliver(event);
235
+ if (waiting.push(event) > MAX_WAITING) {
236
+ waiting.shift();
237
+ if (!dropping)
238
+ warn(
239
+ "@vinaverse/client/agent: the analytics salt is still being read from storage; the oldest events are dropped.",
240
+ { code: "events_dropped" }
241
+ );
242
+ dropping = true;
243
+ }
244
+ };
245
+ loadSalt();
246
+ const changed = () => {
247
+ for (const l of [...listeners]) l();
248
+ };
249
+ const update = (entry, next) => {
250
+ entry.state = { ...entry.state, ...next };
251
+ for (const l of [...entry.listeners]) l();
252
+ };
253
+ const end = (k) => {
254
+ const entry = live.get(k);
255
+ if (!entry) return;
256
+ live.delete(k);
257
+ clearTimeout(entry.timer);
258
+ entry.store.dispose();
259
+ emit({ type: "conversation_ended", key: k });
260
+ changed();
261
+ };
262
+ const patiently = (run, signal, key) => retrying(
263
+ run,
264
+ policy,
265
+ signal,
266
+ (attempt) => emit({ type: "session_retried", key, attempt })
267
+ );
268
+ async function open(entry, key, request) {
269
+ if (live.get(entry.key) !== entry) throw new AgentRefusal("unavailable");
270
+ const held = entry.failedOpen && clock.now() - entry.failedOpen.at < OPEN_KEPT_MS ? entry.failedOpen : void 0;
271
+ entry.failedOpen = void 0;
272
+ const asked = held ? {
273
+ ...held.request,
274
+ ...request.signal ? { signal: request.signal } : {}
275
+ } : {
276
+ ...request,
277
+ conversationId: request.conversationId ?? current.get(entry.key) ?? null,
278
+ key,
279
+ openId: `op_${randomId().slice(3)}`,
280
+ agent: options.agent ?? null,
281
+ place: entry.place,
282
+ placeRevision: ++entry.revision,
283
+ entryPoint: entry.entryPoint
284
+ };
285
+ const { conversationId, openId, place: told } = asked;
286
+ let session;
287
+ try {
288
+ session = await patiently(
289
+ () => backend.openSession(asked),
290
+ request.signal,
291
+ entry.key
292
+ );
293
+ if (!usable(session, insecure))
294
+ throw new AgentRefusal("unavailable", {
295
+ reason: "invalid_session",
296
+ message: "Unusable session: an id, a secret, wss:// and https:// addresses."
297
+ });
298
+ } catch (error) {
299
+ if (live.get(entry.key) === entry) {
300
+ const failure = isRefusal(error) ? error.kind === "ended" && conversationId !== null ? "ended" : "unavailable" : policy.isRetryable(error) ? "unreachable" : "unavailable";
301
+ const reason = reasonOf(error, failure);
302
+ const code = isRefusal(error) ? error.code : codeOf(error);
303
+ if (failure === "unreachable") {
304
+ const { signal, ...again } = asked;
305
+ entry.failedOpen = { request: again, at: held?.at ?? clock.now() };
306
+ }
307
+ const unsettled = reason === "unknown_open" ? String(field(error, "openId") ?? openId) : null;
308
+ update(entry, { failure, reason, code, openId: unsettled });
309
+ emit({ type: "session_failed", key: entry.key, failure, reason });
310
+ }
311
+ throw error;
312
+ }
313
+ if (live.get(entry.key) !== entry) throw new AgentRefusal("unavailable");
314
+ entry.told = told;
315
+ entry.expiresAt = session.expires_at ? Date.parse(session.expires_at) : null;
316
+ if (session.conversation_id)
317
+ current.set(entry.key, session.conversation_id);
318
+ const name = session.agent?.display_name?.trim();
319
+ if (name && name !== sessionName) {
320
+ sessionName = name;
321
+ changed();
322
+ }
323
+ update(entry, {
324
+ failure: null,
325
+ reason: null,
326
+ code: null,
327
+ openId: null,
328
+ sessionId: session.id,
329
+ conversationId: session.conversation_id ?? conversationId,
330
+ moved: !samePlace(told, entry.place)
331
+ });
332
+ emit({
333
+ type: conversationId === null ? "session_opened" : "session_resumed",
334
+ key: entry.key,
335
+ sessionId: session.id,
336
+ ...entry.entryPoint ? { from: entry.entryPoint } : {}
337
+ });
338
+ return session;
339
+ }
340
+ const kept = (k) => `${prefix}.${k}`;
341
+ const signOut = async (erasing) => {
342
+ generation++;
343
+ if (erasing) salt = null;
344
+ for (const entry of live.values()) {
345
+ clearTimeout(entry.timer);
346
+ entry.store.clear();
347
+ }
348
+ const known = /* @__PURE__ */ new Set([...live.keys(), ...current.keys()]);
349
+ live.clear();
350
+ current.clear();
351
+ sessionName = null;
352
+ status = null;
353
+ reading = null;
354
+ statusState = NO_STATUS;
355
+ changed();
356
+ const pairs = [];
357
+ await Promise.all([
358
+ backend.endSessions?.().catch(() => void 0),
359
+ storage?.purge(
360
+ [`${prefix}.`],
361
+ (keys) => {
362
+ const all = /* @__PURE__ */ new Set([...keys, ...[...known].map(kept)]);
363
+ for (const key of all) {
364
+ const t = tupleOf(key, prefix);
365
+ if (t) pairs.push([t[1], t[2]]);
366
+ }
367
+ return [
368
+ ...all,
369
+ ...legacyTranscripts(all, prefix),
370
+ ...options.legacyKeys ?? []
371
+ ];
372
+ },
373
+ `${GREETED}.`
374
+ )
375
+ ]);
376
+ if (!erasing) return;
377
+ await Promise.all([
378
+ Promise.resolve(saltStore?.removeItem(saltKey)).catch(() => void 0),
379
+ greetings?.purge([`${GREETED}.`, `${GREETED_BEFORE}.`], (keys) => {
380
+ for (const key of keys) {
381
+ const t = tupleOf(key, GREETED);
382
+ if (t) pairs.push([t[0], t[1]]);
383
+ }
384
+ return [...legacyGreetings(pairs), ...options.legacyKeys ?? []];
385
+ })
386
+ ]);
387
+ loadSalt();
388
+ };
389
+ const forget = (k) => {
390
+ current.delete(k);
391
+ const entry = live.get(k);
392
+ if (entry) {
393
+ entry.store.clear();
394
+ end(k);
395
+ } else void storage?.removeItem(kept(k));
396
+ };
397
+ const activity = (entry, a) => {
398
+ const sessionId = entry.state.sessionId;
399
+ if (a.type === "reply_completed" && sessionId) {
400
+ entry.turns.set(a.turnId, sessionId);
401
+ update(entry, {});
402
+ }
403
+ emit({
404
+ type: a.type,
405
+ key: entry.key,
406
+ ...sessionId ? { sessionId } : {},
407
+ ...a.type === "reply_completed" ? { turnId: a.turnId } : {},
408
+ ...a.type === "card_shown" ? { cardId: a.cardId, kind: a.kind } : {}
409
+ });
410
+ };
411
+ function make(key) {
412
+ const k = keyOf(key);
413
+ const own = options.conversation ?? {};
414
+ const refresh = backend.refreshSecret?.bind(backend);
415
+ const entry = {
416
+ key: k,
417
+ store: createConversation({
418
+ ...own,
419
+ ...storage ? {
420
+ persistence: storage,
421
+ persistenceKey: kept(k)
422
+ } : {},
423
+ onActivity: (a) => {
424
+ activity(entry, a);
425
+ isolated("onActivity", () => own.onActivity?.(a));
426
+ },
427
+ getSession: (request) => open(entry, key, request),
428
+ ...refresh ? {
429
+ // A refusal (`session_closed`) is the store's cue to go on in a new session, never a fault.
430
+ refreshSecret: (id) => patiently(() => refresh(id), void 0, k).catch((error) => {
431
+ throw isRefusal(error) ? new VinaverseClientError(
432
+ "needs_new_session",
433
+ error.message
434
+ ) : error;
435
+ })
436
+ } : {},
437
+ clock,
438
+ onError: (error) => {
439
+ if (!isRefusal(error) && codeOf(error) !== "needs_new_session")
440
+ own.onError?.(error);
441
+ }
442
+ }),
443
+ listeners: /* @__PURE__ */ new Set(),
444
+ state: {
445
+ failure: null,
446
+ reason: null,
447
+ code: null,
448
+ moved: false,
449
+ ratings: /* @__PURE__ */ new Map(),
450
+ sessionId: null,
451
+ conversationId: current.get(k) ?? null,
452
+ openId: null
453
+ },
454
+ place: null,
455
+ told: null,
456
+ revision: 0,
457
+ entryPoint: null,
458
+ expiresAt: null,
459
+ shown: 0,
460
+ started: false,
461
+ suspended: false,
462
+ sentOnce: /* @__PURE__ */ new Set(),
463
+ timer: void 0,
464
+ turns: /* @__PURE__ */ new Map(),
465
+ failedOpen: void 0
466
+ };
467
+ return entry;
468
+ }
469
+ const over = (entry) => entry.expiresAt !== null && entry.expiresAt <= clock.now();
470
+ const expireLater = (entry) => {
471
+ clearTimeout(entry.timer);
472
+ if (entry.expiresAt === null) return;
473
+ entry.timer = setTimeout(
474
+ () => {
475
+ entry.timer = void 0;
476
+ if (live.get(entry.key) !== entry || entry.shown > 0) return;
477
+ if (over(entry)) end(entry.key);
478
+ else expireLater(entry);
479
+ },
480
+ Math.max(0, entry.expiresAt - clock.now())
481
+ );
482
+ };
483
+ const tell = (entry, at) => {
484
+ if (at.place !== void 0) entry.place = at.place;
485
+ if (at.entryPoint !== void 0) entry.entryPoint = at.entryPoint;
486
+ const sessionId = entry.state.sessionId;
487
+ const moved = sessionId !== null && !samePlace(entry.told, entry.place);
488
+ update(entry, { moved });
489
+ const updateContext = backend.updateContext?.bind(backend);
490
+ if (!moved || !updateContext || sessionId === null) return;
491
+ const asked = generation;
492
+ const current2 = () => asked === generation && live.get(entry.key) === entry && entry.state.sessionId === sessionId;
493
+ const send = (again) => {
494
+ const place = entry.place;
495
+ const revision = ++entry.revision;
496
+ updateContext(sessionId, place ?? CLEARED, revision).then(
497
+ () => {
498
+ if (!current2() || entry.revision !== revision) return;
499
+ entry.told = place;
500
+ update(entry, { moved: !samePlace(entry.told, entry.place) });
501
+ emit({ type: "context_updated", key: entry.key, sessionId });
502
+ },
503
+ (error) => {
504
+ const theirs = field(error, "currentRevision");
505
+ const conflict = field(error, "outcome") === "context_conflict" || field(error, "reason") === "conflict";
506
+ if (!conflict || again || !current2() || entry.revision !== revision)
507
+ return;
508
+ if (typeof theirs === "number" && theirs >= entry.revision)
509
+ entry.revision = theirs;
510
+ send(true);
511
+ }
512
+ );
513
+ };
514
+ send(false);
515
+ };
516
+ function handle(entry) {
517
+ return {
518
+ key: entry.key,
519
+ store: entry.store,
520
+ show(at = {}) {
521
+ if (live.get(entry.key) !== entry) return () => void 0;
522
+ if (entry.shown === 0 && over(entry)) {
523
+ end(entry.key);
524
+ return () => void 0;
525
+ }
526
+ entry.shown++;
527
+ clearTimeout(entry.timer);
528
+ entry.timer = void 0;
529
+ if (entry.suspended) {
530
+ entry.suspended = false;
531
+ entry.store.setForeground(true);
532
+ }
533
+ tell(entry, at);
534
+ if (!entry.started) {
535
+ entry.started = true;
536
+ entry.store.start();
537
+ }
538
+ let released = false;
539
+ return () => {
540
+ if (released) return;
541
+ released = true;
542
+ entry.shown--;
543
+ if (entry.shown > 0 || live.get(entry.key) !== entry) return;
544
+ if (keepAlive === "view" || NOT_KEPT.has(entry.store.getSnapshot().status) || entry.expiresAt === null || over(entry)) {
545
+ end(entry.key);
546
+ return;
547
+ }
548
+ entry.suspended = true;
549
+ entry.store.setForeground(false);
550
+ expireLater(entry);
551
+ };
552
+ },
553
+ tell: (at) => tell(entry, at),
554
+ getState: () => entry.state,
555
+ subscribe(listener) {
556
+ entry.listeners.add(listener);
557
+ return () => entry.listeners.delete(listener);
558
+ },
559
+ retry() {
560
+ update(entry, {
561
+ failure: null,
562
+ reason: null,
563
+ code: null,
564
+ openId: null
565
+ });
566
+ entry.store.start();
567
+ },
568
+ canRate: (turnId) => entry.turns.has(turnId),
569
+ rate(turnId, rating) {
570
+ const sessionId = entry.turns.get(turnId);
571
+ if (!sessionId || entry.state.ratings.get(turnId) === rating) return;
572
+ update(entry, {
573
+ ratings: new Map(entry.state.ratings).set(turnId, rating)
574
+ });
575
+ emit({ type: "rated", key: entry.key, sessionId, turnId, rating });
576
+ void backend.sendFeedback?.({ sessionId, turnId, rating }).catch(() => void 0);
577
+ },
578
+ sendOnce(text) {
579
+ if (entry.sentOnce.has(text)) return;
580
+ entry.sentOnce.add(text);
581
+ entry.store.send(text);
582
+ }
583
+ };
584
+ }
585
+ const handles = /* @__PURE__ */ new WeakMap();
586
+ const lookup = (key) => {
587
+ const k = keyOf(key);
588
+ let entry = live.get(k);
589
+ if (!entry) {
590
+ entry = make(key);
591
+ live.set(k, entry);
592
+ }
593
+ let h = handles.get(entry);
594
+ if (!h) {
595
+ h = handle(entry);
596
+ handles.set(entry, h);
597
+ }
598
+ return h;
599
+ };
600
+ return {
601
+ conversation(key) {
602
+ const k = keyOf(key);
603
+ const entry = live.get(k);
604
+ if (entry && entry.shown === 0 && over(entry)) end(k);
605
+ return lookup(key);
606
+ },
607
+ peek: lookup,
608
+ startNew(key) {
609
+ const k = keyOf(key);
610
+ current.delete(k);
611
+ end(k);
612
+ },
613
+ dispose(key) {
614
+ end(keyOf(key));
615
+ },
616
+ forget(key) {
617
+ forget(keyOf(key));
618
+ },
619
+ sweep() {
620
+ for (const entry of [...live.values()])
621
+ if (entry.shown === 0 && over(entry)) end(entry.key);
622
+ },
623
+ continue(key, conversationId) {
624
+ const k = keyOf(key);
625
+ if (current.get(k) !== conversationId) {
626
+ forget(k);
627
+ current.set(k, conversationId);
628
+ }
629
+ return lookup(key);
630
+ },
631
+ async signOut() {
632
+ await signOut(false);
633
+ },
634
+ async erase() {
635
+ await signOut(true);
636
+ },
637
+ async deleteConversation(key, conversationId) {
638
+ if (!backend.deleteConversation)
639
+ throw new Error(
640
+ "@vinaverse/client/agent: deleteConversation needs AgentBackend.deleteConversation."
641
+ );
642
+ await backend.deleteConversation(conversationId);
643
+ const k = keyOf(key);
644
+ const continued = current.get(k) ?? live.get(k)?.store.getSnapshot().conversationId ?? null;
645
+ if (continued === conversationId) forget(k);
646
+ },
647
+ greetings: greetings ?? null,
648
+ status() {
649
+ if (status && clock.now() - status.at < staleMs)
650
+ return Promise.resolve(status.value);
651
+ if (reading) return reading;
652
+ const asked = generation;
653
+ const read = readStatus(backend).then(
654
+ (value) => {
655
+ if (asked !== generation) return value;
656
+ reading = null;
657
+ status = { value, at: clock.now() };
658
+ setStatusState({ reading: false, failed: false });
659
+ return value;
660
+ },
661
+ (error) => {
662
+ if (asked === generation) {
663
+ reading = null;
664
+ setStatusState({ reading: false, failed: true });
665
+ }
666
+ throw error;
667
+ }
668
+ );
669
+ reading = read;
670
+ setStatusState({ reading: true, failed: statusState.failed });
671
+ return read;
672
+ },
673
+ getStatus: () => status?.value ?? null,
674
+ getStatusState: () => statusState,
675
+ getName: () => sessionName ?? status?.value.displayName ?? null,
676
+ subscribe(listener) {
677
+ listeners.add(listener);
678
+ return () => listeners.delete(listener);
679
+ },
680
+ emit
681
+ };
682
+ }
683
+ function guardedStorage(raw, prefix, generation) {
684
+ if (!raw) return void 0;
685
+ const indexKey = `${prefix}:index`;
686
+ const chains = /* @__PURE__ */ new Map();
687
+ const pending = /* @__PURE__ */ new Map();
688
+ const queued = (key, op) => {
689
+ pending.set(key, (pending.get(key) ?? 0) + 1);
690
+ const run = (chains.get(key) ?? Promise.resolve()).then(op);
691
+ const settled = run.then(
692
+ () => void 0,
693
+ () => void 0
694
+ );
695
+ chains.set(key, settled);
696
+ void settled.then(() => {
697
+ const left = (pending.get(key) ?? 1) - 1;
698
+ if (left > 0) pending.set(key, left);
699
+ else {
700
+ pending.delete(key);
701
+ if (chains.get(key) === settled) chains.delete(key);
702
+ }
703
+ });
704
+ return run;
705
+ };
706
+ const safe = (p) => p.catch(() => void 0);
707
+ const readIndex = async () => {
708
+ try {
709
+ const value = await raw.getItem(indexKey);
710
+ const list2 = value ? JSON.parse(value) : [];
711
+ return new Set(
712
+ Array.isArray(list2) ? list2.filter((k) => typeof k === "string") : []
713
+ );
714
+ } catch {
715
+ return /* @__PURE__ */ new Set();
716
+ }
717
+ };
718
+ const listed = /* @__PURE__ */ new Set();
719
+ const list = (key) => {
720
+ if (listed.has(key)) return;
721
+ listed.add(key);
722
+ void safe(
723
+ queued(indexKey, async () => {
724
+ const all = await readIndex();
725
+ if (all.has(key)) return;
726
+ all.add(key);
727
+ await raw.setItem(indexKey, JSON.stringify([...all]));
728
+ })
729
+ );
730
+ };
731
+ return {
732
+ getItem(key) {
733
+ const asked = generation();
734
+ const read = pending.has(key) ? queued(key, () => raw.getItem(key)) : raw.getItem(key);
735
+ if (typeof read === "string" || read === null) return read;
736
+ return Promise.resolve(read).then(
737
+ (v) => asked === generation() ? v ?? null : null,
738
+ () => null
739
+ );
740
+ },
741
+ setItem(key, value) {
742
+ const asked = generation();
743
+ return safe(
744
+ queued(key, () => {
745
+ if (asked !== generation()) return;
746
+ list(key);
747
+ return raw.setItem(key, value);
748
+ })
749
+ );
750
+ },
751
+ removeItem: (key) => safe(queued(key, () => raw.removeItem(key))),
752
+ /**
753
+ * Everything kept: the index, what the app's storage finds under `under` (`removePrefix`, else `keys`), and `extra`
754
+ * (older key shapes). Nothing under `keep` goes, so `removePrefix` is used only for a prefix that cannot reach it.
755
+ */
756
+ async purge(under, extra = () => [], keep) {
757
+ const all = /* @__PURE__ */ new Set([...listed, ...await readIndex()]);
758
+ listed.clear();
759
+ for (const p of under)
760
+ try {
761
+ if (raw.removePrefix && !keep?.startsWith(p))
762
+ await raw.removePrefix(p);
763
+ else if (raw.keys) {
764
+ for (const key of await raw.keys(p))
765
+ if (!keep || !key.startsWith(keep)) all.add(key);
766
+ }
767
+ } catch {
768
+ }
769
+ for (const key of extra(all)) all.add(key);
770
+ await Promise.all(
771
+ [...all].map((key) => safe(queued(key, () => raw.removeItem(key))))
772
+ );
773
+ await safe(queued(indexKey, () => raw.removeItem(indexKey)));
774
+ }
775
+ };
776
+ }
777
+ async function readStatus(backend) {
778
+ const read = backend.status ? await backend.status() : { available: true };
779
+ const available = read.available === true;
780
+ const features = read.features ?? NO_FEATURES;
781
+ return {
782
+ available,
783
+ launcher: available && read.launcher === true,
784
+ displayName: read.displayName?.trim() || null,
785
+ avatarUrl: read.avatarUrl?.startsWith("https://") ? read.avatarUrl : null,
786
+ features: {
787
+ history: available && features.history === true,
788
+ memory: available && features.memory === true,
789
+ businessAgent: available && features.businessAgent === true,
790
+ businessSetup: available && features.businessSetup === true
791
+ }
792
+ };
793
+ }
794
+ function isRefusal(error) {
795
+ return error instanceof AgentRefusal || field(error, "name") === "AgentRefusal" && (field(error, "kind") === "ended" || field(error, "kind") === "unavailable");
796
+ }
797
+ var field = (error, name) => typeof error === "object" && error !== null ? error[name] : void 0;
798
+ var retryable = (error) => field(error, "retryable") === true;
799
+ var REASONS = /* @__PURE__ */ new Set([
800
+ "ended",
801
+ "not_found",
802
+ "expired",
803
+ "mismatch",
804
+ "completed",
805
+ "forbidden",
806
+ "conflict",
807
+ "unknown_open",
808
+ "auth",
809
+ "off",
810
+ "rate_limited",
811
+ "network",
812
+ "server",
813
+ "invalid_session",
814
+ "unknown"
815
+ ]);
816
+ function reasonOf(error, failure) {
817
+ if (isRefusal(error)) return error.reason;
818
+ const given = field(error, "reason");
819
+ if (typeof given === "string" && REASONS.has(given))
820
+ return given;
821
+ const status = field(error, "status");
822
+ if (status === 401 || status === 403) return "auth";
823
+ if (status === 429) return "rate_limited";
824
+ if (typeof status === "number" && status >= 500) return "server";
825
+ return failure === "unreachable" ? "network" : "unknown";
826
+ }
827
+ function codeOf(error) {
828
+ const code = field(error, "code") ?? field(error, "outcome");
829
+ return typeof code === "string" ? code : null;
830
+ }
831
+ async function retrying(run, policy, signal, onRetry) {
832
+ for (let attempt = 0; ; attempt++) {
833
+ try {
834
+ return await run();
835
+ } catch (error) {
836
+ const delay = policy.delays[attempt];
837
+ if (delay === void 0 || !policy.isRetryable(error) || signal?.aborted)
838
+ throw error;
839
+ const asked = field(error, "retryAfterMs");
840
+ const wait = typeof asked === "number" && asked > delay ? Math.min(asked, 6e4) : delay;
841
+ await new Promise((resolve, reject) => {
842
+ const timer = setTimeout(() => {
843
+ signal?.removeEventListener("abort", stop);
844
+ resolve();
845
+ }, wait);
846
+ function stop() {
847
+ clearTimeout(timer);
848
+ reject(error);
849
+ }
850
+ signal?.addEventListener("abort", stop, { once: true });
851
+ });
852
+ onRetry(attempt + 1);
853
+ }
854
+ }
855
+ }
856
+
857
+ export { AgentRefusal, agentKey, createAgentRuntime, greetedKey, hashKey, isRefusal };
858
+ //# sourceMappingURL=chunk-O3NLAZGZ.js.map
859
+ //# sourceMappingURL=chunk-O3NLAZGZ.js.map