@vinaverse/client 0.1.0-alpha.3 → 0.1.0-alpha.4

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.
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/contract.ts
4
4
  var CONTRACT = {
5
- api: "v1-draft.17",
6
- commit: "fe4fb7d2945e1ced2bce7d44e85a4b28005e46df"
5
+ api: "v1-draft.20",
6
+ commit: "5f6f41bdbfbdd6eadf21180eed1ffddfa5310355"
7
7
  };
8
8
 
9
9
  // src/errors.ts
@@ -36,7 +36,8 @@ var CARD_KINDS = {
36
36
  document: true,
37
37
  question: true,
38
38
  proposal: true,
39
- next_step: true
39
+ next_step: true,
40
+ links: true
40
41
  };
41
42
  function isKnownCard(card) {
42
43
  return Object.hasOwn(CARD_KINDS, card.kind);
@@ -416,7 +417,9 @@ var ConversationEngine = class {
416
417
  this.takeover = null;
417
418
  this.cutOff("interrupted");
418
419
  if (event.resumed !== true) return;
419
- if (event.transcript !== void 0) {
420
+ if (event.transcript !== void 0 && transcriptHasIds(event.transcript)) {
421
+ this.mergeTranscript(asArray(event.transcript));
422
+ } else if (event.transcript !== void 0) {
420
423
  this.messages = [];
421
424
  this.replaced = this.cards;
422
425
  this.cards = /* @__PURE__ */ new Map();
@@ -438,7 +441,61 @@ var ConversationEngine = class {
438
441
  this.hold(card, str(card.turn_id));
439
442
  this.replaced = null;
440
443
  }
441
- hold(raw, turnId) {
444
+ /**
445
+ * A transcript merged by key: an item the app holds keeps its place, with the text the server recorded; one it does
446
+ * not hold goes just after the nearest earlier item it does (just before the first later one when none came before,
447
+ * at the end when it holds none of them). Nothing the app holds is dropped.
448
+ */
449
+ mergeTranscript(items) {
450
+ const seqOf = (key) => key === void 0 ? void 0 : (this.messages.find((m) => m.key === key) ?? this.cards.get(key))?.seq;
451
+ const keys = items.map(
452
+ (item) => item.role === "card" ? str(item.card?.id) : transcriptKey(str(item.role) ?? "", str(item.id), str(item.at))
453
+ );
454
+ let prev;
455
+ items.forEach((item, i) => {
456
+ const key = keys[i];
457
+ if (key === void 0) return;
458
+ const known = seqOf(key);
459
+ let at;
460
+ if (known === void 0) {
461
+ const near = prev ?? keys.slice(i + 1).map(seqOf).find((q) => q !== void 0);
462
+ if (near !== void 0) {
463
+ const seqs = [...this.messages, ...this.cards.values()].map(
464
+ (h) => h.seq
465
+ );
466
+ const bound = prev === void 0 ? Math.max(near - 1, ...seqs.filter((q) => q < near)) : Math.min(...seqs.filter((q) => q > near));
467
+ if (Number.isFinite(bound)) at = (near + bound) / 2;
468
+ }
469
+ }
470
+ if (item.role === "card") this.hold(item.card, void 0, at);
471
+ else {
472
+ const text = str(item.text) ?? "";
473
+ const partial = item.interrupted === true ? "interrupted" : void 0;
474
+ if (known === void 0)
475
+ this.say(
476
+ str(item.role) ?? "",
477
+ text,
478
+ key,
479
+ partial ? { partial } : {},
480
+ at
481
+ );
482
+ else
483
+ this.messages = this.messages.map((m) => {
484
+ if (m.key !== key) return m;
485
+ const { partial: local, ...rest } = m;
486
+ const kept = partial ?? (local === "interrupted" ? void 0 : local);
487
+ return {
488
+ ...rest,
489
+ text: text || m.text,
490
+ ...kept ? { partial: kept } : {}
491
+ };
492
+ });
493
+ }
494
+ prev = seqOf(key);
495
+ });
496
+ this.messages.sort((a, b) => a.seq - b.seq);
497
+ }
498
+ hold(raw, turnId, at) {
442
499
  if (!raw) return;
443
500
  const id = str(raw.id);
444
501
  if (id === void 0) return;
@@ -452,7 +509,7 @@ var ConversationEngine = class {
452
509
  locked: lockedFrom(question),
453
510
  ...turnId !== void 0 ? { turnId } : {},
454
511
  // A card still held keeps its place when it is drawn again (a reconnect's pending cards, a repeated ui.cards).
455
- seq: this.cards.get(id)?.seq ?? this.seq++
512
+ seq: this.cards.get(id)?.seq ?? at ?? this.seq++
456
513
  };
457
514
  for (const held of this.held) {
458
515
  const sent = this.inputs.get(held);
@@ -548,9 +605,9 @@ var ConversationEngine = class {
548
605
  this.unqueue(id);
549
606
  }
550
607
  /** Adds a message once: a key already held (a frame seen twice) changes nothing. */
551
- say(role, text, key, extra = {}, seq = this.seq++) {
608
+ say(role, text, key, extra = {}, seq) {
552
609
  if (this.messages.some((m) => m.key === key)) return;
553
- this.messages.push({ role, text, key, seq, ...extra });
610
+ this.messages.push({ role, text, key, seq: seq ?? this.seq++, ...extra });
554
611
  }
555
612
  /** The turn's streamed text, kept as the agent's message when the turn ends without its `message.completed`. */
556
613
  cutOff(partial) {
@@ -645,6 +702,11 @@ function transcriptKey(role, id, at) {
645
702
  if (role === "staff") return `s:${id}`;
646
703
  return id;
647
704
  }
705
+ function transcriptHasIds(transcript) {
706
+ return asArray(transcript).every(
707
+ (item) => item.role === "card" ? str(item.card?.id) !== void 0 : str(item.id) !== void 0
708
+ );
709
+ }
648
710
  function lockedFrom(question) {
649
711
  if (!question) return [];
650
712
  if (question.state !== "open") {
@@ -1467,7 +1529,8 @@ function buildTimeline(input, previous) {
1467
1529
  type: "card",
1468
1530
  key: card.id,
1469
1531
  card,
1470
- busy: input.busy.has(card.id),
1532
+ busy: !input.inert?.has(card.id) && input.busy.has(card.id),
1533
+ ...input.inert?.has(card.id) ? { restored: true } : {},
1471
1534
  ...input.decisionErrors?.has(card.id) ? { decisionError: true } : {}
1472
1535
  },
1473
1536
  order: input.cardOrder?.get(card.id) ?? card.seq
@@ -1499,17 +1562,10 @@ function buildTimeline(input, previous) {
1499
1562
  });
1500
1563
  }
1501
1564
  live.sort((a, b) => a.order - b.order);
1502
- const liveByKey = new Map(live.map((l) => [l.item.key, l.item]));
1503
- const transcript = input.transcriptSeen === true;
1504
- const items = [];
1505
- const placed = /* @__PURE__ */ new Set();
1506
- if (!transcript)
1507
- for (const item of input.history) {
1508
- const current = liveByKey.get(item.key);
1509
- items.push(current ?? item);
1510
- if (current) placed.add(item.key);
1511
- }
1512
- for (const { item } of live) if (!placed.has(item.key)) items.push(item);
1565
+ const items = mergeByKey(
1566
+ input.transcriptSeen === true ? [] : input.history,
1567
+ live.map((l) => l.item)
1568
+ );
1513
1569
  let same = items.length === previous.length;
1514
1570
  const shared = items.map((item, i) => {
1515
1571
  const prev = before.get(item.key);
@@ -1519,6 +1575,24 @@ function buildTimeline(input, previous) {
1519
1575
  });
1520
1576
  return same ? previous : shared;
1521
1577
  }
1578
+ function mergeByKey(history, live) {
1579
+ if (!history.length) return [...live];
1580
+ const items = [...history];
1581
+ const at = (key) => items.findIndex((item) => item.key === key);
1582
+ let last = -1;
1583
+ live.forEach((item, i) => {
1584
+ const found = at(item.key);
1585
+ if (found >= 0) {
1586
+ items[found] = item;
1587
+ last = found;
1588
+ return;
1589
+ }
1590
+ const next = live.slice(i + 1).map((l) => at(l.key)).find((p) => p >= 0);
1591
+ last = next === void 0 ? items.length : last >= 0 ? last + 1 : next;
1592
+ items.splice(last, 0, item);
1593
+ });
1594
+ return items;
1595
+ }
1522
1596
  function messageItem(m) {
1523
1597
  const role = m.role === "assistant" ? "agent" : m.role;
1524
1598
  return {
@@ -1593,6 +1667,8 @@ var ConversationStore = class {
1593
1667
  readySession = null;
1594
1668
  cardOrder = /* @__PURE__ */ new Map();
1595
1669
  kept = [];
1670
+ /** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
1671
+ inert = /* @__PURE__ */ new Set();
1596
1672
  locale;
1597
1673
  /** The request for a session in flight, to abort when it is no longer wanted. */
1598
1674
  fetching = null;
@@ -1634,6 +1710,8 @@ var ConversationStore = class {
1634
1710
  this.live();
1635
1711
  if (this.session) {
1636
1712
  if (this.session.status === "idle") this.session.connect();
1713
+ else if (this.session.status === "unauthorized")
1714
+ void this.resumeWith("resume");
1637
1715
  return;
1638
1716
  }
1639
1717
  void this.ensureSession("open");
@@ -1698,9 +1776,19 @@ var ConversationStore = class {
1698
1776
  session.close();
1699
1777
  this.update();
1700
1778
  }
1701
- /** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
1702
- async wake() {
1779
+ /**
1780
+ * Back in the foreground, or the person acted while away: a new connection to the same session, with a fresh secret
1781
+ * when the backend gives one. A session that passed its end meanwhile is never reopened (a fresh secret cannot bring
1782
+ * back an expired tool token): the person's action continues the conversation in a new session from `getSession`,
1783
+ * and until then nothing connects.
1784
+ */
1785
+ async wake(byPerson = false) {
1703
1786
  const info = this.info;
1787
+ if (info && this.sessionOver()) {
1788
+ if (byPerson) await this.resumeWith("resume");
1789
+ else this.update();
1790
+ return;
1791
+ }
1704
1792
  this.suspended = false;
1705
1793
  if (!info) return;
1706
1794
  let next = info;
@@ -1740,7 +1828,7 @@ var ConversationStore = class {
1740
1828
  }
1741
1829
  if (this.suspended) {
1742
1830
  this.early.set(id, text);
1743
- void this.wake();
1831
+ void this.wake(true);
1744
1832
  this.update();
1745
1833
  return id;
1746
1834
  }
@@ -1750,6 +1838,12 @@ var ConversationStore = class {
1750
1838
  this.update();
1751
1839
  return id;
1752
1840
  }
1841
+ if (this.sessionOver()) {
1842
+ this.early.set(id, text);
1843
+ void this.resumeWith("resume");
1844
+ this.update();
1845
+ return id;
1846
+ }
1753
1847
  this.nextId = id;
1754
1848
  this.session.sendText(text);
1755
1849
  this.nextId = null;
@@ -1886,11 +1980,21 @@ var ConversationStore = class {
1886
1980
  }
1887
1981
  open(info) {
1888
1982
  this.info = info;
1983
+ this.suspended = false;
1984
+ const refreshSecret = this.options.refreshSecret;
1889
1985
  const session = new VinaverseSession({
1890
1986
  ...this.options.transport,
1891
1987
  session: info,
1892
1988
  engine: this.engine,
1893
- ...this.options.refreshSecret ? { refreshSecret: this.options.refreshSecret } : {},
1989
+ ...refreshSecret ? {
1990
+ // Past the session's end a fresh secret cannot help: refused here, and the store rolls over instead.
1991
+ refreshSecret: (id) => this.sessionOver() ? Promise.reject(
1992
+ new VinaverseClientError(
1993
+ "needs_new_session",
1994
+ "This session has ended."
1995
+ )
1996
+ ) : refreshSecret(id)
1997
+ } : {},
1894
1998
  createId: () => this.nextId ?? (this.options.createId ?? randomId)()
1895
1999
  });
1896
2000
  this.session = session;
@@ -1898,7 +2002,9 @@ var ConversationStore = class {
1898
2002
  session.on("state", () => this.update()),
1899
2003
  session.on("status", (status) => this.statusChanged(status)),
1900
2004
  session.on("event", (event) => this.event(event)),
1901
- session.on("error", (error) => this.options.onError?.(error))
2005
+ session.on("error", (error) => {
2006
+ if (!this.sessionOver()) this.options.onError?.(error);
2007
+ })
1902
2008
  );
1903
2009
  session.setForeground(this.foreground);
1904
2010
  session.connect();
@@ -1925,6 +2031,10 @@ var ConversationStore = class {
1925
2031
  this.resuming = this.fetchSession(reason).then((info) => {
1926
2032
  this.resuming = null;
1927
2033
  if (!info || this.disposed || !this.session) return;
2034
+ if (this.session.status === "closed") {
2035
+ this.open(info);
2036
+ return;
2037
+ }
1928
2038
  this.info = info;
1929
2039
  this.session.resume(info);
1930
2040
  this.flushEarly();
@@ -1939,10 +2049,18 @@ var ConversationStore = class {
1939
2049
  }
1940
2050
  reconnectIfDown() {
1941
2051
  const session = this.session;
1942
- if (session && session.status === "idle" && this.nextFrom(this.engine.state()) === null)
2052
+ if (session && session.status === "idle" && !this.sessionOver() && // past its end: the person's next action gets a new session
2053
+ this.nextFrom(this.engine.state()) === null)
1943
2054
  session.connect();
1944
2055
  }
2056
+ /** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
2057
+ rolled = false;
1945
2058
  statusChanged(status) {
2059
+ if (status === "ready") this.rolled = false;
2060
+ if (status === "unauthorized" && !this.rolled) {
2061
+ this.rolled = true;
2062
+ void this.resumeWith("resume");
2063
+ }
1946
2064
  if (status === "idle")
1947
2065
  this.offlineAfterRetries = this.lastStatus === "reconnecting";
1948
2066
  else if (status !== "reconnecting") this.offlineAfterRetries = false;
@@ -1967,9 +2085,18 @@ var ConversationStore = class {
1967
2085
  const session = frame.session?.id;
1968
2086
  const moved = typeof session === "string" && session !== this.readySession;
1969
2087
  if (typeof session === "string") this.readySession = session;
1970
- if (frame.resumed === true && Array.isArray(frame.transcript)) {
2088
+ if (frame.resumed === true && Array.isArray(frame.transcript) && !transcriptHasIds(frame.transcript)) {
1971
2089
  this.transcriptSeen = true;
1972
2090
  this.anchorCards(moved);
2091
+ } else if (moved && frame.resumed === true) {
2092
+ const pending = new Set(
2093
+ (Array.isArray(frame.pending_cards) ? frame.pending_cards : []).map(
2094
+ (c) => str2(c.id)
2095
+ )
2096
+ );
2097
+ for (const card of this.engine.state().cards)
2098
+ if (pending.has(card.id)) this.inert.delete(card.id);
2099
+ else this.inert.add(card.id);
1973
2100
  }
1974
2101
  return;
1975
2102
  }
@@ -2128,6 +2255,7 @@ var ConversationStore = class {
2128
2255
  transcriptSeen: this.transcriptSeen,
2129
2256
  cardOrder: this.cardOrder,
2130
2257
  kept: this.kept,
2258
+ inert: this.inert,
2131
2259
  system: this.system
2132
2260
  },
2133
2261
  prev?.items ?? NO_ITEMS
@@ -2144,6 +2272,7 @@ var ConversationStore = class {
2144
2272
  next,
2145
2273
  agent: sameAgent(prev?.agent, name, avatarUrl) ? prev.agent : { name, avatarUrl },
2146
2274
  disclosure: s.disclosure ?? this.info?.disclosure ?? null,
2275
+ welcome: this.info?.welcome ?? null,
2147
2276
  takeover: s.takeover,
2148
2277
  items,
2149
2278
  activity: prev && sameTools(prev.activity, activity) ? prev.activity : activity,
@@ -2250,6 +2379,11 @@ var ConversationStore = class {
2250
2379
  }, PERSIST_DELAY);
2251
2380
  }
2252
2381
  // ------------------------------------------------------------ guards
2382
+ /** Past the session's end (`expires_at`), when the backend passed it on. */
2383
+ sessionOver() {
2384
+ const end = this.info?.expires_at;
2385
+ return end !== void 0 && Date.now() >= Date.parse(end);
2386
+ }
2253
2387
  live() {
2254
2388
  if (this.disposed)
2255
2389
  throw new VinaverseClientError(
@@ -2303,6 +2437,42 @@ function shallowEqual(a, b) {
2303
2437
  return a.status === b.status && a.banner === b.banner && a.next === b.next && a.agent.name === b.agent.name && a.agent.avatarUrl === b.agent.avatarUrl && a.disclosure === b.disclosure && a.takeover?.by === b.takeover?.by && a.takeover?.at === b.takeover?.at && a.takeover === null === (b.takeover === null) && a.items === b.items && a.activity === b.activity && a.responding === b.responding && a.openQuestion === b.openQuestion && a.answersNeedWebSocket === b.answersNeedWebSocket && a.turnsLeft === b.turnsLeft && a.history === b.history;
2304
2438
  }
2305
2439
 
2440
+ // src/conversation/modes.ts
2441
+ function createConversationsByMode(options) {
2442
+ const { getSession, modes = [], ...shared } = options;
2443
+ const base = shared.persistenceKey ?? "vinaverse.conversation";
2444
+ const stores = /* @__PURE__ */ new Map();
2445
+ const get = (mode) => {
2446
+ let store = stores.get(mode);
2447
+ if (!store) {
2448
+ store = createConversation({
2449
+ ...shared,
2450
+ persistenceKey: `${base}.${mode}`,
2451
+ getSession: (request) => getSession({ ...request, mode })
2452
+ });
2453
+ stores.set(mode, store);
2454
+ }
2455
+ return store;
2456
+ };
2457
+ return {
2458
+ get,
2459
+ select(mode) {
2460
+ for (const [other, store] of stores)
2461
+ if (other !== mode) store.setForeground(false);
2462
+ return get(mode);
2463
+ },
2464
+ dispose() {
2465
+ for (const store of stores.values()) store.dispose();
2466
+ stores.clear();
2467
+ },
2468
+ clear() {
2469
+ for (const mode of /* @__PURE__ */ new Set([...stores.keys(), ...modes]))
2470
+ get(mode).clear();
2471
+ stores.clear();
2472
+ }
2473
+ };
2474
+ }
2475
+
2306
2476
  exports.CONTRACT = CONTRACT;
2307
2477
  exports.ConversationStore = ConversationStore;
2308
2478
  exports.MAX_TEXT = MAX_TEXT;
@@ -2310,6 +2480,7 @@ exports.VinaverseClientError = VinaverseClientError;
2310
2480
  exports.answerProblem = answerProblem;
2311
2481
  exports.codePoints = codePoints;
2312
2482
  exports.createConversation = createConversation;
2483
+ exports.createConversationsByMode = createConversationsByMode;
2313
2484
  exports.isAllowedLink = isAllowedLink;
2314
2485
  exports.isKnownCard = isKnownCard;
2315
2486
  exports.isValidLinkPrefix = isValidLinkPrefix;