@vinaverse/client 0.1.0-alpha.1 → 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.
- package/LICENSE +202 -0
- package/README.md +111 -1
- package/dist/agent/index.cjs +3319 -0
- package/dist/agent/index.cjs.map +1 -0
- package/dist/agent/index.d.cts +57 -0
- package/dist/agent/index.d.ts +57 -0
- package/dist/agent/index.js +99 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/backend/index.cjs +268 -0
- package/dist/backend/index.cjs.map +1 -0
- package/dist/backend/index.d.cts +72 -0
- package/dist/backend/index.d.ts +72 -0
- package/dist/backend/index.js +250 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/chunk-46D3FSLU.js +997 -0
- package/dist/chunk-46D3FSLU.js.map +1 -0
- package/dist/{chunk-AWHPWRGT.js → chunk-GWLLUOFY.js} +71 -15
- package/dist/chunk-GWLLUOFY.js.map +1 -0
- package/dist/chunk-O3NLAZGZ.js +859 -0
- package/dist/chunk-O3NLAZGZ.js.map +1 -0
- package/dist/chunk-YNUHXJD6.js +9 -0
- package/dist/chunk-YNUHXJD6.js.map +1 -0
- package/dist/contract-DmQs8amJ.d.cts +6 -0
- package/dist/contract-DmQs8amJ.d.ts +6 -0
- package/dist/conversation/index.cjs +356 -27
- package/dist/conversation/index.cjs.map +1 -1
- package/dist/conversation/index.d.cts +23 -307
- package/dist/conversation/index.d.ts +23 -307
- package/dist/conversation/index.js +36 -760
- package/dist/conversation/index.js.map +1 -1
- package/dist/index.cjs +70 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/runtime-CISPp9nJ.d.cts +371 -0
- package/dist/runtime-Z1RI10lU.d.ts +371 -0
- package/dist/{session--W6JXfwo.d.cts → session-CPz4pcPu.d.cts} +3249 -1626
- package/dist/{session--W6JXfwo.d.ts → session-CPz4pcPu.d.ts} +3249 -1626
- package/dist/store-BxiicOKG.d.cts +414 -0
- package/dist/store-DZka9T2-.d.ts +414 -0
- package/dist/testing/index.cjs +274 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +60 -0
- package/dist/testing/index.d.ts +60 -0
- package/dist/testing/index.js +259 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +82 -11
- package/dist/chunk-AWHPWRGT.js.map +0 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/contract.ts
|
|
4
4
|
var CONTRACT = {
|
|
5
|
-
api: "v1-draft.
|
|
6
|
-
commit: "
|
|
5
|
+
api: "v1-draft.22",
|
|
6
|
+
commit: "4695e3dc042e6e0cf1936aae24fcb093eacd27c8"
|
|
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
|
-
|
|
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
|
|
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
|
|
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,13 +1529,17 @@ 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
|
-
order: card.seq
|
|
1536
|
+
order: input.cardOrder?.get(card.id) ?? card.seq
|
|
1474
1537
|
});
|
|
1475
1538
|
}
|
|
1476
1539
|
for (const { item, order } of input.system) live.push({ item, order });
|
|
1540
|
+
const held = new Set(state.cards.map((c) => c.id));
|
|
1541
|
+
for (const { item, order } of input.kept ?? [])
|
|
1542
|
+
if (!held.has(item.key)) live.push({ item, order });
|
|
1477
1543
|
for (const [id, sent] of input.sent) {
|
|
1478
1544
|
const pending = pendingState(id, input);
|
|
1479
1545
|
if (pending === null) continue;
|
|
@@ -1496,17 +1562,10 @@ function buildTimeline(input, previous) {
|
|
|
1496
1562
|
});
|
|
1497
1563
|
}
|
|
1498
1564
|
live.sort((a, b) => a.order - b.order);
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
if (!transcript)
|
|
1504
|
-
for (const item of input.history) {
|
|
1505
|
-
const current = liveByKey.get(item.key);
|
|
1506
|
-
items.push(current ?? item);
|
|
1507
|
-
if (current) placed.add(item.key);
|
|
1508
|
-
}
|
|
1509
|
-
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
|
+
);
|
|
1510
1569
|
let same = items.length === previous.length;
|
|
1511
1570
|
const shared = items.map((item, i) => {
|
|
1512
1571
|
const prev = before.get(item.key);
|
|
@@ -1516,6 +1575,24 @@ function buildTimeline(input, previous) {
|
|
|
1516
1575
|
});
|
|
1517
1576
|
return same ? previous : shared;
|
|
1518
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
|
+
}
|
|
1519
1596
|
function messageItem(m) {
|
|
1520
1597
|
const role = m.role === "assistant" ? "agent" : m.role;
|
|
1521
1598
|
return {
|
|
@@ -1547,6 +1624,7 @@ function persistable(item) {
|
|
|
1547
1624
|
|
|
1548
1625
|
// src/conversation/store.ts
|
|
1549
1626
|
var MAX_TEXT = 2e3;
|
|
1627
|
+
var TURN_INPUTS = 50;
|
|
1550
1628
|
var PERSIST_DELAY = 500;
|
|
1551
1629
|
var PERSIST_VERSION = 1;
|
|
1552
1630
|
var NO_ITEMS = [];
|
|
@@ -1577,12 +1655,20 @@ var ConversationStore = class {
|
|
|
1577
1655
|
/** Proposals whose tenant decision threw: shown as recoverable, the person may decide again. */
|
|
1578
1656
|
decisionErrors = /* @__PURE__ */ new Set();
|
|
1579
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();
|
|
1580
1660
|
/** Numbers system rows that carry no id of their own. */
|
|
1581
1661
|
systemSeq = 0;
|
|
1582
1662
|
history = NO_ITEMS;
|
|
1583
1663
|
historyState = "none";
|
|
1584
1664
|
/** A resumed session.ready carried a transcript: the server's history replaced what the device kept. */
|
|
1585
1665
|
transcriptSeen = false;
|
|
1666
|
+
/** The session of the last session.ready: a card of another one takes no tap (§6). */
|
|
1667
|
+
readySession = null;
|
|
1668
|
+
cardOrder = /* @__PURE__ */ new Map();
|
|
1669
|
+
kept = [];
|
|
1670
|
+
/** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
|
|
1671
|
+
inert = /* @__PURE__ */ new Set();
|
|
1586
1672
|
locale;
|
|
1587
1673
|
/** The request for a session in flight, to abort when it is no longer wanted. */
|
|
1588
1674
|
fetching = null;
|
|
@@ -1624,6 +1710,8 @@ var ConversationStore = class {
|
|
|
1624
1710
|
this.live();
|
|
1625
1711
|
if (this.session) {
|
|
1626
1712
|
if (this.session.status === "idle") this.session.connect();
|
|
1713
|
+
else if (this.session.status === "unauthorized")
|
|
1714
|
+
void this.resumeWith("resume");
|
|
1627
1715
|
return;
|
|
1628
1716
|
}
|
|
1629
1717
|
void this.ensureSession("open");
|
|
@@ -1664,9 +1752,59 @@ var ConversationStore = class {
|
|
|
1664
1752
|
/** The app came to the foreground or went to the background (AppState, `visibilitychange`). */
|
|
1665
1753
|
setForeground(foreground) {
|
|
1666
1754
|
this.foreground = foreground;
|
|
1755
|
+
if (this.options.closeInBackground && !this.disposed) {
|
|
1756
|
+
if (!foreground) {
|
|
1757
|
+
this.suspend();
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
if (this.suspended) {
|
|
1761
|
+
void this.wake();
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1667
1765
|
this.session?.setForeground(foreground);
|
|
1668
1766
|
if (foreground) this.reconnectIfDown();
|
|
1669
1767
|
}
|
|
1768
|
+
/** In the background (`closeInBackground`): the connection is closed; the engine keeps what was sent, to resend. */
|
|
1769
|
+
suspended = false;
|
|
1770
|
+
suspend() {
|
|
1771
|
+
const session = this.session;
|
|
1772
|
+
if (!session || this.suspended || session.status === "ended") return;
|
|
1773
|
+
this.suspended = true;
|
|
1774
|
+
for (const off of this.unsubscribe) off();
|
|
1775
|
+
this.unsubscribe = [];
|
|
1776
|
+
session.close();
|
|
1777
|
+
this.update();
|
|
1778
|
+
}
|
|
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) {
|
|
1786
|
+
const info = this.info;
|
|
1787
|
+
if (info && this.sessionOver()) {
|
|
1788
|
+
if (byPerson) await this.resumeWith("resume");
|
|
1789
|
+
else this.update();
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
this.suspended = false;
|
|
1793
|
+
if (!info) return;
|
|
1794
|
+
let next = info;
|
|
1795
|
+
if (this.options.refreshSecret) {
|
|
1796
|
+
try {
|
|
1797
|
+
next = {
|
|
1798
|
+
...info,
|
|
1799
|
+
client_secret: await this.options.refreshSecret(info.id)
|
|
1800
|
+
};
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
this.options.onError?.(error);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
if (this.disposed || this.suspended) return;
|
|
1806
|
+
this.open(next);
|
|
1807
|
+
}
|
|
1670
1808
|
/** The network as the app sees it; null when unknown. Back online reconnects a connection that gave up. */
|
|
1671
1809
|
setOnline(online) {
|
|
1672
1810
|
this.online = online;
|
|
@@ -1682,18 +1820,31 @@ var ConversationStore = class {
|
|
|
1682
1820
|
throw new RangeError(`A message is at most ${MAX_TEXT} characters.`);
|
|
1683
1821
|
const id = (this.options.createId ?? randomId)();
|
|
1684
1822
|
this.sent.set(id, { text, order: lastOrder(this.engine.state()) + 0.5 });
|
|
1823
|
+
this.tellActivity({ type: "message_sent", id });
|
|
1685
1824
|
if (!this.session) {
|
|
1686
1825
|
this.early.set(id, text);
|
|
1687
1826
|
void this.ensureSession("open");
|
|
1688
1827
|
this.update();
|
|
1689
1828
|
return id;
|
|
1690
1829
|
}
|
|
1830
|
+
if (this.suspended) {
|
|
1831
|
+
this.early.set(id, text);
|
|
1832
|
+
void this.wake(true);
|
|
1833
|
+
this.update();
|
|
1834
|
+
return id;
|
|
1835
|
+
}
|
|
1691
1836
|
if (this.nextFrom(this.engine.state()) === "continue_here") {
|
|
1692
1837
|
this.early.set(id, text);
|
|
1693
1838
|
void this.resumeWith("continue_here");
|
|
1694
1839
|
this.update();
|
|
1695
1840
|
return id;
|
|
1696
1841
|
}
|
|
1842
|
+
if (this.sessionOver()) {
|
|
1843
|
+
this.early.set(id, text);
|
|
1844
|
+
void this.resumeWith("resume");
|
|
1845
|
+
this.update();
|
|
1846
|
+
return id;
|
|
1847
|
+
}
|
|
1697
1848
|
this.nextId = id;
|
|
1698
1849
|
this.session.sendText(text);
|
|
1699
1850
|
this.nextId = null;
|
|
@@ -1756,6 +1907,24 @@ var ConversationStore = class {
|
|
|
1756
1907
|
this.sent.delete(key);
|
|
1757
1908
|
return this.send(sent.text);
|
|
1758
1909
|
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Send again the person's text whose turn failed before any reply (a `turn_failed` row with `data.retry`), as a new
|
|
1912
|
+
* message (the person's action). Once per row: its `retry` turns false. Returns the new id, or null when the row
|
|
1913
|
+
* allows no retry.
|
|
1914
|
+
*/
|
|
1915
|
+
retryTurn(key) {
|
|
1916
|
+
const at = this.system.findIndex(
|
|
1917
|
+
(s) => s.item.key === key && s.item.event === "turn_failed"
|
|
1918
|
+
);
|
|
1919
|
+
const entry = this.system[at];
|
|
1920
|
+
if (entry?.item.data.retry !== true) return null;
|
|
1921
|
+
const text = this.turnText(String(entry.item.data.turn ?? ""));
|
|
1922
|
+
if (text === void 0) return null;
|
|
1923
|
+
this.system = this.system.map(
|
|
1924
|
+
(s, i) => i === at ? { ...s, item: { ...s.item, data: { ...s.item.data, retry: false } } } : s
|
|
1925
|
+
);
|
|
1926
|
+
return this.send(text);
|
|
1927
|
+
}
|
|
1759
1928
|
/** After the conversation continued on another device: continue it here (the person's choice). */
|
|
1760
1929
|
continueHere() {
|
|
1761
1930
|
return this.resumeWith("continue_here");
|
|
@@ -1812,11 +1981,21 @@ var ConversationStore = class {
|
|
|
1812
1981
|
}
|
|
1813
1982
|
open(info) {
|
|
1814
1983
|
this.info = info;
|
|
1984
|
+
this.suspended = false;
|
|
1985
|
+
const refreshSecret = this.options.refreshSecret;
|
|
1815
1986
|
const session = new VinaverseSession({
|
|
1816
1987
|
...this.options.transport,
|
|
1817
1988
|
session: info,
|
|
1818
1989
|
engine: this.engine,
|
|
1819
|
-
...
|
|
1990
|
+
...refreshSecret ? {
|
|
1991
|
+
// Past the session's end a fresh secret cannot help: refused here, and the store rolls over instead.
|
|
1992
|
+
refreshSecret: (id) => this.sessionOver() ? Promise.reject(
|
|
1993
|
+
new VinaverseClientError(
|
|
1994
|
+
"needs_new_session",
|
|
1995
|
+
"This session has ended."
|
|
1996
|
+
)
|
|
1997
|
+
) : refreshSecret(id)
|
|
1998
|
+
} : {},
|
|
1820
1999
|
createId: () => this.nextId ?? (this.options.createId ?? randomId)()
|
|
1821
2000
|
});
|
|
1822
2001
|
this.session = session;
|
|
@@ -1824,7 +2003,9 @@ var ConversationStore = class {
|
|
|
1824
2003
|
session.on("state", () => this.update()),
|
|
1825
2004
|
session.on("status", (status) => this.statusChanged(status)),
|
|
1826
2005
|
session.on("event", (event) => this.event(event)),
|
|
1827
|
-
session.on("error", (error) =>
|
|
2006
|
+
session.on("error", (error) => {
|
|
2007
|
+
if (!this.sessionOver()) this.options.onError?.(error);
|
|
2008
|
+
})
|
|
1828
2009
|
);
|
|
1829
2010
|
session.setForeground(this.foreground);
|
|
1830
2011
|
session.connect();
|
|
@@ -1851,6 +2032,10 @@ var ConversationStore = class {
|
|
|
1851
2032
|
this.resuming = this.fetchSession(reason).then((info) => {
|
|
1852
2033
|
this.resuming = null;
|
|
1853
2034
|
if (!info || this.disposed || !this.session) return;
|
|
2035
|
+
if (this.session.status === "closed") {
|
|
2036
|
+
this.open(info);
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
1854
2039
|
this.info = info;
|
|
1855
2040
|
this.session.resume(info);
|
|
1856
2041
|
this.flushEarly();
|
|
@@ -1865,19 +2050,48 @@ var ConversationStore = class {
|
|
|
1865
2050
|
}
|
|
1866
2051
|
reconnectIfDown() {
|
|
1867
2052
|
const session = this.session;
|
|
1868
|
-
if (session && session.status === "idle" && this.
|
|
2053
|
+
if (session && session.status === "idle" && !this.sessionOver() && // past its end: the person's next action gets a new session
|
|
2054
|
+
this.nextFrom(this.engine.state()) === null)
|
|
1869
2055
|
session.connect();
|
|
1870
2056
|
}
|
|
2057
|
+
/** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
|
|
2058
|
+
rolled = false;
|
|
1871
2059
|
statusChanged(status) {
|
|
2060
|
+
if (status === "ready") this.rolled = false;
|
|
2061
|
+
if (status === "unauthorized" && !this.rolled) {
|
|
2062
|
+
this.rolled = true;
|
|
2063
|
+
void this.resumeWith("resume");
|
|
2064
|
+
}
|
|
1872
2065
|
if (status === "idle")
|
|
1873
2066
|
this.offlineAfterRetries = this.lastStatus === "reconnecting";
|
|
1874
2067
|
else if (status !== "reconnecting") this.offlineAfterRetries = false;
|
|
1875
2068
|
this.lastStatus = status;
|
|
1876
2069
|
this.update();
|
|
1877
2070
|
}
|
|
2071
|
+
/** The app's analytics hears of a step; what it throws is reported (`onError`), never in the way of a send or a frame. */
|
|
2072
|
+
tellActivity(activity) {
|
|
2073
|
+
try {
|
|
2074
|
+
this.options.onActivity?.(activity);
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
this.options.onError?.(error);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
/** The live frames analytics hears of: a reply finished, cards drawn. */
|
|
2080
|
+
activity(frame) {
|
|
2081
|
+
if (!this.options.onActivity) return;
|
|
2082
|
+
const on = (a) => this.tellActivity(a);
|
|
2083
|
+
if (frame.type === "message.completed" && typeof frame.turn_id === "string")
|
|
2084
|
+
on({ type: "reply_completed", turnId: frame.turn_id });
|
|
2085
|
+
if (frame.type === "ui.cards" && Array.isArray(frame.cards)) {
|
|
2086
|
+
for (const card of frame.cards)
|
|
2087
|
+
if (typeof card.id === "string" && typeof card.kind === "string")
|
|
2088
|
+
on({ type: "card_shown", cardId: card.id, kind: card.kind });
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
1878
2091
|
event(event) {
|
|
1879
2092
|
const frame = event;
|
|
1880
2093
|
const str2 = (v) => typeof v === "string" ? v : null;
|
|
2094
|
+
this.activity(frame);
|
|
1881
2095
|
const add = (key, kind, data) => {
|
|
1882
2096
|
if (this.system.some((s) => s.item.key === key)) return;
|
|
1883
2097
|
this.system = [
|
|
@@ -1889,10 +2103,25 @@ var ConversationStore = class {
|
|
|
1889
2103
|
];
|
|
1890
2104
|
};
|
|
1891
2105
|
switch (frame.type) {
|
|
1892
|
-
case "session.ready":
|
|
1893
|
-
|
|
2106
|
+
case "session.ready": {
|
|
2107
|
+
const session = frame.session?.id;
|
|
2108
|
+
const moved = typeof session === "string" && session !== this.readySession;
|
|
2109
|
+
if (typeof session === "string") this.readySession = session;
|
|
2110
|
+
if (frame.resumed === true && Array.isArray(frame.transcript) && !transcriptHasIds(frame.transcript)) {
|
|
1894
2111
|
this.transcriptSeen = true;
|
|
2112
|
+
this.anchorCards(moved);
|
|
2113
|
+
} else if (moved && frame.resumed === true) {
|
|
2114
|
+
const pending = new Set(
|
|
2115
|
+
(Array.isArray(frame.pending_cards) ? frame.pending_cards : []).map(
|
|
2116
|
+
(c) => str2(c.id)
|
|
2117
|
+
)
|
|
2118
|
+
);
|
|
2119
|
+
for (const card of this.engine.state().cards)
|
|
2120
|
+
if (pending.has(card.id)) this.inert.delete(card.id);
|
|
2121
|
+
else this.inert.add(card.id);
|
|
2122
|
+
}
|
|
1895
2123
|
return;
|
|
2124
|
+
}
|
|
1896
2125
|
// `at` is required (v1-draft.15); the same takeover announced again after a reconnect has the same `at`, so it
|
|
1897
2126
|
// draws one row. A frame without one still gets a row of its own.
|
|
1898
2127
|
case "takeover.started":
|
|
@@ -1922,6 +2151,17 @@ var ConversationStore = class {
|
|
|
1922
2151
|
code: str2(frame.code)
|
|
1923
2152
|
});
|
|
1924
2153
|
return;
|
|
2154
|
+
case "turn.started": {
|
|
2155
|
+
const turn = str2(frame.turn_id);
|
|
2156
|
+
const input = str2(frame.client_event_id);
|
|
2157
|
+
if (turn === null || input === null) return;
|
|
2158
|
+
this.turnInputs.set(turn, input);
|
|
2159
|
+
if (this.turnInputs.size > TURN_INPUTS) {
|
|
2160
|
+
const oldest = this.turnInputs.keys().next().value;
|
|
2161
|
+
if (oldest !== void 0) this.turnInputs.delete(oldest);
|
|
2162
|
+
}
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
1925
2165
|
case "turn.failed": {
|
|
1926
2166
|
const turn = str2(frame.turn_id) ?? "";
|
|
1927
2167
|
if (this.engine.state().messages.some((m) => m.key === `a:${turn}`))
|
|
@@ -1930,7 +2170,9 @@ var ConversationStore = class {
|
|
|
1930
2170
|
add(`failed:${turn}`, "turn_failed", {
|
|
1931
2171
|
code: str2(frame.code),
|
|
1932
2172
|
message: str2(frame.message),
|
|
1933
|
-
retryable: frame.retryable === true
|
|
2173
|
+
retryable: frame.retryable === true,
|
|
2174
|
+
retry: frame.retryable === true && this.turnText(turn) !== void 0,
|
|
2175
|
+
turn
|
|
1934
2176
|
});
|
|
1935
2177
|
return;
|
|
1936
2178
|
}
|
|
@@ -1938,6 +2180,45 @@ var ConversationStore = class {
|
|
|
1938
2180
|
return;
|
|
1939
2181
|
}
|
|
1940
2182
|
}
|
|
2183
|
+
/** The person's text that started a turn, while its message is on screen. */
|
|
2184
|
+
turnText(turn) {
|
|
2185
|
+
const input = this.turnInputs.get(turn);
|
|
2186
|
+
if (input === void 0) return void 0;
|
|
2187
|
+
const message = this.engine.state().messages.find((m) => m.key === input && m.role === "person");
|
|
2188
|
+
return message?.text || void 0;
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
|
|
2192
|
+
* the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
|
|
2193
|
+
* message it followed (by key; the transcript keeps the live keys), so a pending proposal stays where it was, and a
|
|
2194
|
+
* card the transcript dropped stays drawn there; taking no tap when it belongs to an earlier session.
|
|
2195
|
+
*/
|
|
2196
|
+
anchorCards(sessionChanged) {
|
|
2197
|
+
const state = this.engine.state();
|
|
2198
|
+
const seqOf = new Map(state.messages.map((m) => [m.key, m.seq]));
|
|
2199
|
+
const first = Math.min(...state.messages.map((m) => m.seq), 0);
|
|
2200
|
+
const held = new Set(state.cards.map((c) => c.id));
|
|
2201
|
+
const order = /* @__PURE__ */ new Map();
|
|
2202
|
+
const kept = [];
|
|
2203
|
+
let anchor = first - 1;
|
|
2204
|
+
let step = 0;
|
|
2205
|
+
for (const item of this.view.items) {
|
|
2206
|
+
if (item.type === "message" && seqOf.has(item.key)) {
|
|
2207
|
+
anchor = seqOf.get(item.key);
|
|
2208
|
+
step = 0;
|
|
2209
|
+
} else if (item.type === "card") {
|
|
2210
|
+
const at = anchor + 0.5 + ++step / 1e3;
|
|
2211
|
+
order.set(item.key, at);
|
|
2212
|
+
if (!held.has(item.key))
|
|
2213
|
+
kept.push({
|
|
2214
|
+
item: sessionChanged ? { ...item, busy: false, restored: true } : item,
|
|
2215
|
+
order: at
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
this.cardOrder = order;
|
|
2220
|
+
this.kept = kept;
|
|
2221
|
+
}
|
|
1941
2222
|
// ------------------------------------------------------------ the view
|
|
1942
2223
|
/**
|
|
1943
2224
|
* Recomputes now (getSnapshot and getStreaming are always current) and notifies from the scheduler, at most once
|
|
@@ -1994,6 +2275,9 @@ var ConversationStore = class {
|
|
|
1994
2275
|
busy: this.busy,
|
|
1995
2276
|
decisionErrors: this.decisionErrors,
|
|
1996
2277
|
transcriptSeen: this.transcriptSeen,
|
|
2278
|
+
cardOrder: this.cardOrder,
|
|
2279
|
+
kept: this.kept,
|
|
2280
|
+
inert: this.inert,
|
|
1997
2281
|
system: this.system
|
|
1998
2282
|
},
|
|
1999
2283
|
prev?.items ?? NO_ITEMS
|
|
@@ -2005,10 +2289,12 @@ var ConversationStore = class {
|
|
|
2005
2289
|
const avatarUrl = this.info?.agent?.avatar_url ?? null;
|
|
2006
2290
|
const view = {
|
|
2007
2291
|
status,
|
|
2292
|
+
conversationId: s.conversationId,
|
|
2008
2293
|
banner: banner(status, next),
|
|
2009
2294
|
next,
|
|
2010
2295
|
agent: sameAgent(prev?.agent, name, avatarUrl) ? prev.agent : { name, avatarUrl },
|
|
2011
2296
|
disclosure: s.disclosure ?? this.info?.disclosure ?? null,
|
|
2297
|
+
welcome: this.info?.welcome ?? null,
|
|
2012
2298
|
takeover: s.takeover,
|
|
2013
2299
|
items,
|
|
2014
2300
|
activity: prev && sameTools(prev.activity, activity) ? prev.activity : activity,
|
|
@@ -2023,6 +2309,7 @@ var ConversationStore = class {
|
|
|
2023
2309
|
}
|
|
2024
2310
|
status() {
|
|
2025
2311
|
if (this.disposed) return "closed";
|
|
2312
|
+
if (this.suspended) return "idle";
|
|
2026
2313
|
const session = this.session;
|
|
2027
2314
|
if (!session) {
|
|
2028
2315
|
if (this.failed) return "unavailable";
|
|
@@ -2114,6 +2401,11 @@ var ConversationStore = class {
|
|
|
2114
2401
|
}, PERSIST_DELAY);
|
|
2115
2402
|
}
|
|
2116
2403
|
// ------------------------------------------------------------ guards
|
|
2404
|
+
/** Past the session's end (`expires_at`), when the backend passed it on. */
|
|
2405
|
+
sessionOver() {
|
|
2406
|
+
const end = this.info?.expires_at;
|
|
2407
|
+
return end !== void 0 && (this.options.clock?.now() ?? Date.now()) >= Date.parse(end);
|
|
2408
|
+
}
|
|
2117
2409
|
live() {
|
|
2118
2410
|
if (this.disposed)
|
|
2119
2411
|
throw new VinaverseClientError(
|
|
@@ -2167,6 +2459,42 @@ function shallowEqual(a, b) {
|
|
|
2167
2459
|
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;
|
|
2168
2460
|
}
|
|
2169
2461
|
|
|
2462
|
+
// src/conversation/modes.ts
|
|
2463
|
+
function createConversationsByMode(options) {
|
|
2464
|
+
const { getSession, modes = [], ...shared } = options;
|
|
2465
|
+
const base = shared.persistenceKey ?? "vinaverse.conversation";
|
|
2466
|
+
const stores = /* @__PURE__ */ new Map();
|
|
2467
|
+
const get = (mode) => {
|
|
2468
|
+
let store = stores.get(mode);
|
|
2469
|
+
if (!store) {
|
|
2470
|
+
store = createConversation({
|
|
2471
|
+
...shared,
|
|
2472
|
+
persistenceKey: `${base}.${mode}`,
|
|
2473
|
+
getSession: (request) => getSession({ ...request, mode })
|
|
2474
|
+
});
|
|
2475
|
+
stores.set(mode, store);
|
|
2476
|
+
}
|
|
2477
|
+
return store;
|
|
2478
|
+
};
|
|
2479
|
+
return {
|
|
2480
|
+
get,
|
|
2481
|
+
select(mode) {
|
|
2482
|
+
for (const [other, store] of stores)
|
|
2483
|
+
if (other !== mode) store.setForeground(false);
|
|
2484
|
+
return get(mode);
|
|
2485
|
+
},
|
|
2486
|
+
dispose() {
|
|
2487
|
+
for (const store of stores.values()) store.dispose();
|
|
2488
|
+
stores.clear();
|
|
2489
|
+
},
|
|
2490
|
+
clear() {
|
|
2491
|
+
for (const mode of /* @__PURE__ */ new Set([...stores.keys(), ...modes]))
|
|
2492
|
+
get(mode).clear();
|
|
2493
|
+
stores.clear();
|
|
2494
|
+
}
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2170
2498
|
exports.CONTRACT = CONTRACT;
|
|
2171
2499
|
exports.ConversationStore = ConversationStore;
|
|
2172
2500
|
exports.MAX_TEXT = MAX_TEXT;
|
|
@@ -2174,6 +2502,7 @@ exports.VinaverseClientError = VinaverseClientError;
|
|
|
2174
2502
|
exports.answerProblem = answerProblem;
|
|
2175
2503
|
exports.codePoints = codePoints;
|
|
2176
2504
|
exports.createConversation = createConversation;
|
|
2505
|
+
exports.createConversationsByMode = createConversationsByMode;
|
|
2177
2506
|
exports.isAllowedLink = isAllowedLink;
|
|
2178
2507
|
exports.isKnownCard = isKnownCard;
|
|
2179
2508
|
exports.isValidLinkPrefix = isValidLinkPrefix;
|