@vinaverse/client 0.1.0-alpha.2 → 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 {
@@ -1550,6 +1624,7 @@ function persistable(item) {
1550
1624
 
1551
1625
  // src/conversation/store.ts
1552
1626
  var MAX_TEXT = 2e3;
1627
+ var TURN_INPUTS = 50;
1553
1628
  var PERSIST_DELAY = 500;
1554
1629
  var PERSIST_VERSION = 1;
1555
1630
  var NO_ITEMS = [];
@@ -1580,6 +1655,8 @@ var ConversationStore = class {
1580
1655
  /** Proposals whose tenant decision threw: shown as recoverable, the person may decide again. */
1581
1656
  decisionErrors = /* @__PURE__ */ new Set();
1582
1657
  system = [];
1658
+ /** The person's text that started each recent turn, by `turn_id` (`turn.started` echoes its `client_event_id`). */
1659
+ turnInputs = /* @__PURE__ */ new Map();
1583
1660
  /** Numbers system rows that carry no id of their own. */
1584
1661
  systemSeq = 0;
1585
1662
  history = NO_ITEMS;
@@ -1590,6 +1667,8 @@ var ConversationStore = class {
1590
1667
  readySession = null;
1591
1668
  cardOrder = /* @__PURE__ */ new Map();
1592
1669
  kept = [];
1670
+ /** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
1671
+ inert = /* @__PURE__ */ new Set();
1593
1672
  locale;
1594
1673
  /** The request for a session in flight, to abort when it is no longer wanted. */
1595
1674
  fetching = null;
@@ -1631,6 +1710,8 @@ var ConversationStore = class {
1631
1710
  this.live();
1632
1711
  if (this.session) {
1633
1712
  if (this.session.status === "idle") this.session.connect();
1713
+ else if (this.session.status === "unauthorized")
1714
+ void this.resumeWith("resume");
1634
1715
  return;
1635
1716
  }
1636
1717
  void this.ensureSession("open");
@@ -1695,9 +1776,19 @@ var ConversationStore = class {
1695
1776
  session.close();
1696
1777
  this.update();
1697
1778
  }
1698
- /** Back in the foreground: a new connection to the same session, with a fresh secret when the backend gives one. */
1699
- 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) {
1700
1786
  const info = this.info;
1787
+ if (info && this.sessionOver()) {
1788
+ if (byPerson) await this.resumeWith("resume");
1789
+ else this.update();
1790
+ return;
1791
+ }
1701
1792
  this.suspended = false;
1702
1793
  if (!info) return;
1703
1794
  let next = info;
@@ -1737,7 +1828,7 @@ var ConversationStore = class {
1737
1828
  }
1738
1829
  if (this.suspended) {
1739
1830
  this.early.set(id, text);
1740
- void this.wake();
1831
+ void this.wake(true);
1741
1832
  this.update();
1742
1833
  return id;
1743
1834
  }
@@ -1747,6 +1838,12 @@ var ConversationStore = class {
1747
1838
  this.update();
1748
1839
  return id;
1749
1840
  }
1841
+ if (this.sessionOver()) {
1842
+ this.early.set(id, text);
1843
+ void this.resumeWith("resume");
1844
+ this.update();
1845
+ return id;
1846
+ }
1750
1847
  this.nextId = id;
1751
1848
  this.session.sendText(text);
1752
1849
  this.nextId = null;
@@ -1809,6 +1906,24 @@ var ConversationStore = class {
1809
1906
  this.sent.delete(key);
1810
1907
  return this.send(sent.text);
1811
1908
  }
1909
+ /**
1910
+ * Send again the person's text whose turn failed before any reply (a `turn_failed` row with `data.retry`), as a new
1911
+ * message (the person's action). Once per row: its `retry` turns false. Returns the new id, or null when the row
1912
+ * allows no retry.
1913
+ */
1914
+ retryTurn(key) {
1915
+ const at = this.system.findIndex(
1916
+ (s) => s.item.key === key && s.item.event === "turn_failed"
1917
+ );
1918
+ const entry = this.system[at];
1919
+ if (entry?.item.data.retry !== true) return null;
1920
+ const text = this.turnText(String(entry.item.data.turn ?? ""));
1921
+ if (text === void 0) return null;
1922
+ this.system = this.system.map(
1923
+ (s, i) => i === at ? { ...s, item: { ...s.item, data: { ...s.item.data, retry: false } } } : s
1924
+ );
1925
+ return this.send(text);
1926
+ }
1812
1927
  /** After the conversation continued on another device: continue it here (the person's choice). */
1813
1928
  continueHere() {
1814
1929
  return this.resumeWith("continue_here");
@@ -1865,11 +1980,21 @@ var ConversationStore = class {
1865
1980
  }
1866
1981
  open(info) {
1867
1982
  this.info = info;
1983
+ this.suspended = false;
1984
+ const refreshSecret = this.options.refreshSecret;
1868
1985
  const session = new VinaverseSession({
1869
1986
  ...this.options.transport,
1870
1987
  session: info,
1871
1988
  engine: this.engine,
1872
- ...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
+ } : {},
1873
1998
  createId: () => this.nextId ?? (this.options.createId ?? randomId)()
1874
1999
  });
1875
2000
  this.session = session;
@@ -1877,7 +2002,9 @@ var ConversationStore = class {
1877
2002
  session.on("state", () => this.update()),
1878
2003
  session.on("status", (status) => this.statusChanged(status)),
1879
2004
  session.on("event", (event) => this.event(event)),
1880
- session.on("error", (error) => this.options.onError?.(error))
2005
+ session.on("error", (error) => {
2006
+ if (!this.sessionOver()) this.options.onError?.(error);
2007
+ })
1881
2008
  );
1882
2009
  session.setForeground(this.foreground);
1883
2010
  session.connect();
@@ -1904,6 +2031,10 @@ var ConversationStore = class {
1904
2031
  this.resuming = this.fetchSession(reason).then((info) => {
1905
2032
  this.resuming = null;
1906
2033
  if (!info || this.disposed || !this.session) return;
2034
+ if (this.session.status === "closed") {
2035
+ this.open(info);
2036
+ return;
2037
+ }
1907
2038
  this.info = info;
1908
2039
  this.session.resume(info);
1909
2040
  this.flushEarly();
@@ -1918,10 +2049,18 @@ var ConversationStore = class {
1918
2049
  }
1919
2050
  reconnectIfDown() {
1920
2051
  const session = this.session;
1921
- 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)
1922
2054
  session.connect();
1923
2055
  }
2056
+ /** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
2057
+ rolled = false;
1924
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
+ }
1925
2064
  if (status === "idle")
1926
2065
  this.offlineAfterRetries = this.lastStatus === "reconnecting";
1927
2066
  else if (status !== "reconnecting") this.offlineAfterRetries = false;
@@ -1946,9 +2085,18 @@ var ConversationStore = class {
1946
2085
  const session = frame.session?.id;
1947
2086
  const moved = typeof session === "string" && session !== this.readySession;
1948
2087
  if (typeof session === "string") this.readySession = session;
1949
- if (frame.resumed === true && Array.isArray(frame.transcript)) {
2088
+ if (frame.resumed === true && Array.isArray(frame.transcript) && !transcriptHasIds(frame.transcript)) {
1950
2089
  this.transcriptSeen = true;
1951
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);
1952
2100
  }
1953
2101
  return;
1954
2102
  }
@@ -1981,6 +2129,17 @@ var ConversationStore = class {
1981
2129
  code: str2(frame.code)
1982
2130
  });
1983
2131
  return;
2132
+ case "turn.started": {
2133
+ const turn = str2(frame.turn_id);
2134
+ const input = str2(frame.client_event_id);
2135
+ if (turn === null || input === null) return;
2136
+ this.turnInputs.set(turn, input);
2137
+ if (this.turnInputs.size > TURN_INPUTS) {
2138
+ const oldest = this.turnInputs.keys().next().value;
2139
+ if (oldest !== void 0) this.turnInputs.delete(oldest);
2140
+ }
2141
+ return;
2142
+ }
1984
2143
  case "turn.failed": {
1985
2144
  const turn = str2(frame.turn_id) ?? "";
1986
2145
  if (this.engine.state().messages.some((m) => m.key === `a:${turn}`))
@@ -1989,7 +2148,9 @@ var ConversationStore = class {
1989
2148
  add(`failed:${turn}`, "turn_failed", {
1990
2149
  code: str2(frame.code),
1991
2150
  message: str2(frame.message),
1992
- retryable: frame.retryable === true
2151
+ retryable: frame.retryable === true,
2152
+ retry: frame.retryable === true && this.turnText(turn) !== void 0,
2153
+ turn
1993
2154
  });
1994
2155
  return;
1995
2156
  }
@@ -1997,6 +2158,13 @@ var ConversationStore = class {
1997
2158
  return;
1998
2159
  }
1999
2160
  }
2161
+ /** The person's text that started a turn, while its message is on screen. */
2162
+ turnText(turn) {
2163
+ const input = this.turnInputs.get(turn);
2164
+ if (input === void 0) return void 0;
2165
+ const message = this.engine.state().messages.find((m) => m.key === input && m.role === "person");
2166
+ return message?.text || void 0;
2167
+ }
2000
2168
  /**
2001
2169
  * A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
2002
2170
  * the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
@@ -2087,6 +2255,7 @@ var ConversationStore = class {
2087
2255
  transcriptSeen: this.transcriptSeen,
2088
2256
  cardOrder: this.cardOrder,
2089
2257
  kept: this.kept,
2258
+ inert: this.inert,
2090
2259
  system: this.system
2091
2260
  },
2092
2261
  prev?.items ?? NO_ITEMS
@@ -2098,10 +2267,12 @@ var ConversationStore = class {
2098
2267
  const avatarUrl = this.info?.agent?.avatar_url ?? null;
2099
2268
  const view = {
2100
2269
  status,
2270
+ conversationId: s.conversationId,
2101
2271
  banner: banner(status, next),
2102
2272
  next,
2103
2273
  agent: sameAgent(prev?.agent, name, avatarUrl) ? prev.agent : { name, avatarUrl },
2104
2274
  disclosure: s.disclosure ?? this.info?.disclosure ?? null,
2275
+ welcome: this.info?.welcome ?? null,
2105
2276
  takeover: s.takeover,
2106
2277
  items,
2107
2278
  activity: prev && sameTools(prev.activity, activity) ? prev.activity : activity,
@@ -2208,6 +2379,11 @@ var ConversationStore = class {
2208
2379
  }, PERSIST_DELAY);
2209
2380
  }
2210
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
+ }
2211
2387
  live() {
2212
2388
  if (this.disposed)
2213
2389
  throw new VinaverseClientError(
@@ -2261,6 +2437,42 @@ function shallowEqual(a, b) {
2261
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;
2262
2438
  }
2263
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
+
2264
2476
  exports.CONTRACT = CONTRACT;
2265
2477
  exports.ConversationStore = ConversationStore;
2266
2478
  exports.MAX_TEXT = MAX_TEXT;
@@ -2268,6 +2480,7 @@ exports.VinaverseClientError = VinaverseClientError;
2268
2480
  exports.answerProblem = answerProblem;
2269
2481
  exports.codePoints = codePoints;
2270
2482
  exports.createConversation = createConversation;
2483
+ exports.createConversationsByMode = createConversationsByMode;
2271
2484
  exports.isAllowedLink = isAllowedLink;
2272
2485
  exports.isKnownCard = isKnownCard;
2273
2486
  exports.isValidLinkPrefix = isValidLinkPrefix;