@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.
- package/dist/{chunk-AWHPWRGT.js → chunk-MKUNSAMT.js} +73 -11
- package/dist/chunk-MKUNSAMT.js.map +1 -0
- package/dist/conversation/index.cjs +241 -28
- package/dist/conversation/index.cjs.map +1 -1
- package/dist/conversation/index.d.cts +71 -6
- package/dist/conversation/index.d.ts +71 -6
- package/dist/conversation/index.js +173 -23
- 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 +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{session--W6JXfwo.d.cts → session-C3qZwGPG.d.cts} +130 -7
- package/dist/{session--W6JXfwo.d.ts → session-C3qZwGPG.d.ts} +130 -7
- package/package.json +4 -4
- package/dist/chunk-AWHPWRGT.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ConversationEngine, randomId, VinaverseSession, VinaverseClientError, isKnownCard } from '../chunk-
|
|
2
|
-
export { CONTRACT, VinaverseClientError, isKnownCard } from '../chunk-
|
|
1
|
+
import { ConversationEngine, randomId, VinaverseSession, VinaverseClientError, transcriptHasIds, isKnownCard } from '../chunk-MKUNSAMT.js';
|
|
2
|
+
export { CONTRACT, VinaverseClientError, isKnownCard } from '../chunk-MKUNSAMT.js';
|
|
3
3
|
|
|
4
4
|
// src/conversation/answers.ts
|
|
5
5
|
var CHOICE = /* @__PURE__ */ new Set([
|
|
@@ -153,7 +153,8 @@ function buildTimeline(input, previous) {
|
|
|
153
153
|
type: "card",
|
|
154
154
|
key: card.id,
|
|
155
155
|
card,
|
|
156
|
-
busy: input.busy.has(card.id),
|
|
156
|
+
busy: !input.inert?.has(card.id) && input.busy.has(card.id),
|
|
157
|
+
...input.inert?.has(card.id) ? { restored: true } : {},
|
|
157
158
|
...input.decisionErrors?.has(card.id) ? { decisionError: true } : {}
|
|
158
159
|
},
|
|
159
160
|
order: input.cardOrder?.get(card.id) ?? card.seq
|
|
@@ -185,17 +186,10 @@ function buildTimeline(input, previous) {
|
|
|
185
186
|
});
|
|
186
187
|
}
|
|
187
188
|
live.sort((a, b) => a.order - b.order);
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!transcript)
|
|
193
|
-
for (const item of input.history) {
|
|
194
|
-
const current = liveByKey.get(item.key);
|
|
195
|
-
items.push(current ?? item);
|
|
196
|
-
if (current) placed.add(item.key);
|
|
197
|
-
}
|
|
198
|
-
for (const { item } of live) if (!placed.has(item.key)) items.push(item);
|
|
189
|
+
const items = mergeByKey(
|
|
190
|
+
input.transcriptSeen === true ? [] : input.history,
|
|
191
|
+
live.map((l) => l.item)
|
|
192
|
+
);
|
|
199
193
|
let same = items.length === previous.length;
|
|
200
194
|
const shared = items.map((item, i) => {
|
|
201
195
|
const prev = before.get(item.key);
|
|
@@ -205,6 +199,24 @@ function buildTimeline(input, previous) {
|
|
|
205
199
|
});
|
|
206
200
|
return same ? previous : shared;
|
|
207
201
|
}
|
|
202
|
+
function mergeByKey(history, live) {
|
|
203
|
+
if (!history.length) return [...live];
|
|
204
|
+
const items = [...history];
|
|
205
|
+
const at = (key) => items.findIndex((item) => item.key === key);
|
|
206
|
+
let last = -1;
|
|
207
|
+
live.forEach((item, i) => {
|
|
208
|
+
const found = at(item.key);
|
|
209
|
+
if (found >= 0) {
|
|
210
|
+
items[found] = item;
|
|
211
|
+
last = found;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const next = live.slice(i + 1).map((l) => at(l.key)).find((p) => p >= 0);
|
|
215
|
+
last = next === void 0 ? items.length : last >= 0 ? last + 1 : next;
|
|
216
|
+
items.splice(last, 0, item);
|
|
217
|
+
});
|
|
218
|
+
return items;
|
|
219
|
+
}
|
|
208
220
|
function messageItem(m) {
|
|
209
221
|
const role = m.role === "assistant" ? "agent" : m.role;
|
|
210
222
|
return {
|
|
@@ -236,6 +248,7 @@ function persistable(item) {
|
|
|
236
248
|
|
|
237
249
|
// src/conversation/store.ts
|
|
238
250
|
var MAX_TEXT = 2e3;
|
|
251
|
+
var TURN_INPUTS = 50;
|
|
239
252
|
var PERSIST_DELAY = 500;
|
|
240
253
|
var PERSIST_VERSION = 1;
|
|
241
254
|
var NO_ITEMS = [];
|
|
@@ -266,6 +279,8 @@ var ConversationStore = class {
|
|
|
266
279
|
/** Proposals whose tenant decision threw: shown as recoverable, the person may decide again. */
|
|
267
280
|
decisionErrors = /* @__PURE__ */ new Set();
|
|
268
281
|
system = [];
|
|
282
|
+
/** The person's text that started each recent turn, by `turn_id` (`turn.started` echoes its `client_event_id`). */
|
|
283
|
+
turnInputs = /* @__PURE__ */ new Map();
|
|
269
284
|
/** Numbers system rows that carry no id of their own. */
|
|
270
285
|
systemSeq = 0;
|
|
271
286
|
history = NO_ITEMS;
|
|
@@ -276,6 +291,8 @@ var ConversationStore = class {
|
|
|
276
291
|
readySession = null;
|
|
277
292
|
cardOrder = /* @__PURE__ */ new Map();
|
|
278
293
|
kept = [];
|
|
294
|
+
/** Cards an earlier session drew, kept by a merged transcript: drawn as restored, taking no tap. */
|
|
295
|
+
inert = /* @__PURE__ */ new Set();
|
|
279
296
|
locale;
|
|
280
297
|
/** The request for a session in flight, to abort when it is no longer wanted. */
|
|
281
298
|
fetching = null;
|
|
@@ -317,6 +334,8 @@ var ConversationStore = class {
|
|
|
317
334
|
this.live();
|
|
318
335
|
if (this.session) {
|
|
319
336
|
if (this.session.status === "idle") this.session.connect();
|
|
337
|
+
else if (this.session.status === "unauthorized")
|
|
338
|
+
void this.resumeWith("resume");
|
|
320
339
|
return;
|
|
321
340
|
}
|
|
322
341
|
void this.ensureSession("open");
|
|
@@ -381,9 +400,19 @@ var ConversationStore = class {
|
|
|
381
400
|
session.close();
|
|
382
401
|
this.update();
|
|
383
402
|
}
|
|
384
|
-
/**
|
|
385
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Back in the foreground, or the person acted while away: a new connection to the same session, with a fresh secret
|
|
405
|
+
* when the backend gives one. A session that passed its end meanwhile is never reopened (a fresh secret cannot bring
|
|
406
|
+
* back an expired tool token): the person's action continues the conversation in a new session from `getSession`,
|
|
407
|
+
* and until then nothing connects.
|
|
408
|
+
*/
|
|
409
|
+
async wake(byPerson = false) {
|
|
386
410
|
const info = this.info;
|
|
411
|
+
if (info && this.sessionOver()) {
|
|
412
|
+
if (byPerson) await this.resumeWith("resume");
|
|
413
|
+
else this.update();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
387
416
|
this.suspended = false;
|
|
388
417
|
if (!info) return;
|
|
389
418
|
let next = info;
|
|
@@ -423,7 +452,7 @@ var ConversationStore = class {
|
|
|
423
452
|
}
|
|
424
453
|
if (this.suspended) {
|
|
425
454
|
this.early.set(id, text);
|
|
426
|
-
void this.wake();
|
|
455
|
+
void this.wake(true);
|
|
427
456
|
this.update();
|
|
428
457
|
return id;
|
|
429
458
|
}
|
|
@@ -433,6 +462,12 @@ var ConversationStore = class {
|
|
|
433
462
|
this.update();
|
|
434
463
|
return id;
|
|
435
464
|
}
|
|
465
|
+
if (this.sessionOver()) {
|
|
466
|
+
this.early.set(id, text);
|
|
467
|
+
void this.resumeWith("resume");
|
|
468
|
+
this.update();
|
|
469
|
+
return id;
|
|
470
|
+
}
|
|
436
471
|
this.nextId = id;
|
|
437
472
|
this.session.sendText(text);
|
|
438
473
|
this.nextId = null;
|
|
@@ -495,6 +530,24 @@ var ConversationStore = class {
|
|
|
495
530
|
this.sent.delete(key);
|
|
496
531
|
return this.send(sent.text);
|
|
497
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Send again the person's text whose turn failed before any reply (a `turn_failed` row with `data.retry`), as a new
|
|
535
|
+
* message (the person's action). Once per row: its `retry` turns false. Returns the new id, or null when the row
|
|
536
|
+
* allows no retry.
|
|
537
|
+
*/
|
|
538
|
+
retryTurn(key) {
|
|
539
|
+
const at = this.system.findIndex(
|
|
540
|
+
(s) => s.item.key === key && s.item.event === "turn_failed"
|
|
541
|
+
);
|
|
542
|
+
const entry = this.system[at];
|
|
543
|
+
if (entry?.item.data.retry !== true) return null;
|
|
544
|
+
const text = this.turnText(String(entry.item.data.turn ?? ""));
|
|
545
|
+
if (text === void 0) return null;
|
|
546
|
+
this.system = this.system.map(
|
|
547
|
+
(s, i) => i === at ? { ...s, item: { ...s.item, data: { ...s.item.data, retry: false } } } : s
|
|
548
|
+
);
|
|
549
|
+
return this.send(text);
|
|
550
|
+
}
|
|
498
551
|
/** After the conversation continued on another device: continue it here (the person's choice). */
|
|
499
552
|
continueHere() {
|
|
500
553
|
return this.resumeWith("continue_here");
|
|
@@ -551,11 +604,21 @@ var ConversationStore = class {
|
|
|
551
604
|
}
|
|
552
605
|
open(info) {
|
|
553
606
|
this.info = info;
|
|
607
|
+
this.suspended = false;
|
|
608
|
+
const refreshSecret = this.options.refreshSecret;
|
|
554
609
|
const session = new VinaverseSession({
|
|
555
610
|
...this.options.transport,
|
|
556
611
|
session: info,
|
|
557
612
|
engine: this.engine,
|
|
558
|
-
...
|
|
613
|
+
...refreshSecret ? {
|
|
614
|
+
// Past the session's end a fresh secret cannot help: refused here, and the store rolls over instead.
|
|
615
|
+
refreshSecret: (id) => this.sessionOver() ? Promise.reject(
|
|
616
|
+
new VinaverseClientError(
|
|
617
|
+
"needs_new_session",
|
|
618
|
+
"This session has ended."
|
|
619
|
+
)
|
|
620
|
+
) : refreshSecret(id)
|
|
621
|
+
} : {},
|
|
559
622
|
createId: () => this.nextId ?? (this.options.createId ?? randomId)()
|
|
560
623
|
});
|
|
561
624
|
this.session = session;
|
|
@@ -563,7 +626,9 @@ var ConversationStore = class {
|
|
|
563
626
|
session.on("state", () => this.update()),
|
|
564
627
|
session.on("status", (status) => this.statusChanged(status)),
|
|
565
628
|
session.on("event", (event) => this.event(event)),
|
|
566
|
-
session.on("error", (error) =>
|
|
629
|
+
session.on("error", (error) => {
|
|
630
|
+
if (!this.sessionOver()) this.options.onError?.(error);
|
|
631
|
+
})
|
|
567
632
|
);
|
|
568
633
|
session.setForeground(this.foreground);
|
|
569
634
|
session.connect();
|
|
@@ -590,6 +655,10 @@ var ConversationStore = class {
|
|
|
590
655
|
this.resuming = this.fetchSession(reason).then((info) => {
|
|
591
656
|
this.resuming = null;
|
|
592
657
|
if (!info || this.disposed || !this.session) return;
|
|
658
|
+
if (this.session.status === "closed") {
|
|
659
|
+
this.open(info);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
593
662
|
this.info = info;
|
|
594
663
|
this.session.resume(info);
|
|
595
664
|
this.flushEarly();
|
|
@@ -604,10 +673,18 @@ var ConversationStore = class {
|
|
|
604
673
|
}
|
|
605
674
|
reconnectIfDown() {
|
|
606
675
|
const session = this.session;
|
|
607
|
-
if (session && session.status === "idle" && this.
|
|
676
|
+
if (session && session.status === "idle" && !this.sessionOver() && // past its end: the person's next action gets a new session
|
|
677
|
+
this.nextFrom(this.engine.state()) === null)
|
|
608
678
|
session.connect();
|
|
609
679
|
}
|
|
680
|
+
/** A refusal already led to a new session, and none has been ready since: another refusal is no cue. */
|
|
681
|
+
rolled = false;
|
|
610
682
|
statusChanged(status) {
|
|
683
|
+
if (status === "ready") this.rolled = false;
|
|
684
|
+
if (status === "unauthorized" && !this.rolled) {
|
|
685
|
+
this.rolled = true;
|
|
686
|
+
void this.resumeWith("resume");
|
|
687
|
+
}
|
|
611
688
|
if (status === "idle")
|
|
612
689
|
this.offlineAfterRetries = this.lastStatus === "reconnecting";
|
|
613
690
|
else if (status !== "reconnecting") this.offlineAfterRetries = false;
|
|
@@ -632,9 +709,18 @@ var ConversationStore = class {
|
|
|
632
709
|
const session = frame.session?.id;
|
|
633
710
|
const moved = typeof session === "string" && session !== this.readySession;
|
|
634
711
|
if (typeof session === "string") this.readySession = session;
|
|
635
|
-
if (frame.resumed === true && Array.isArray(frame.transcript)) {
|
|
712
|
+
if (frame.resumed === true && Array.isArray(frame.transcript) && !transcriptHasIds(frame.transcript)) {
|
|
636
713
|
this.transcriptSeen = true;
|
|
637
714
|
this.anchorCards(moved);
|
|
715
|
+
} else if (moved && frame.resumed === true) {
|
|
716
|
+
const pending = new Set(
|
|
717
|
+
(Array.isArray(frame.pending_cards) ? frame.pending_cards : []).map(
|
|
718
|
+
(c) => str(c.id)
|
|
719
|
+
)
|
|
720
|
+
);
|
|
721
|
+
for (const card of this.engine.state().cards)
|
|
722
|
+
if (pending.has(card.id)) this.inert.delete(card.id);
|
|
723
|
+
else this.inert.add(card.id);
|
|
638
724
|
}
|
|
639
725
|
return;
|
|
640
726
|
}
|
|
@@ -667,6 +753,17 @@ var ConversationStore = class {
|
|
|
667
753
|
code: str(frame.code)
|
|
668
754
|
});
|
|
669
755
|
return;
|
|
756
|
+
case "turn.started": {
|
|
757
|
+
const turn = str(frame.turn_id);
|
|
758
|
+
const input = str(frame.client_event_id);
|
|
759
|
+
if (turn === null || input === null) return;
|
|
760
|
+
this.turnInputs.set(turn, input);
|
|
761
|
+
if (this.turnInputs.size > TURN_INPUTS) {
|
|
762
|
+
const oldest = this.turnInputs.keys().next().value;
|
|
763
|
+
if (oldest !== void 0) this.turnInputs.delete(oldest);
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
670
767
|
case "turn.failed": {
|
|
671
768
|
const turn = str(frame.turn_id) ?? "";
|
|
672
769
|
if (this.engine.state().messages.some((m) => m.key === `a:${turn}`))
|
|
@@ -675,7 +772,9 @@ var ConversationStore = class {
|
|
|
675
772
|
add(`failed:${turn}`, "turn_failed", {
|
|
676
773
|
code: str(frame.code),
|
|
677
774
|
message: str(frame.message),
|
|
678
|
-
retryable: frame.retryable === true
|
|
775
|
+
retryable: frame.retryable === true,
|
|
776
|
+
retry: frame.retryable === true && this.turnText(turn) !== void 0,
|
|
777
|
+
turn
|
|
679
778
|
});
|
|
680
779
|
return;
|
|
681
780
|
}
|
|
@@ -683,6 +782,13 @@ var ConversationStore = class {
|
|
|
683
782
|
return;
|
|
684
783
|
}
|
|
685
784
|
}
|
|
785
|
+
/** The person's text that started a turn, while its message is on screen. */
|
|
786
|
+
turnText(turn) {
|
|
787
|
+
const input = this.turnInputs.get(turn);
|
|
788
|
+
if (input === void 0) return void 0;
|
|
789
|
+
const message = this.engine.state().messages.find((m) => m.key === input && m.role === "person");
|
|
790
|
+
return message?.text || void 0;
|
|
791
|
+
}
|
|
686
792
|
/**
|
|
687
793
|
* A resumed session's transcript replaced the messages, and a chat transcript carries text only: the engine drops
|
|
688
794
|
* the cards it drew, and draws the pending ones again at the end. Each card on screen is anchored just after the
|
|
@@ -773,6 +879,7 @@ var ConversationStore = class {
|
|
|
773
879
|
transcriptSeen: this.transcriptSeen,
|
|
774
880
|
cardOrder: this.cardOrder,
|
|
775
881
|
kept: this.kept,
|
|
882
|
+
inert: this.inert,
|
|
776
883
|
system: this.system
|
|
777
884
|
},
|
|
778
885
|
prev?.items ?? NO_ITEMS
|
|
@@ -784,10 +891,12 @@ var ConversationStore = class {
|
|
|
784
891
|
const avatarUrl = this.info?.agent?.avatar_url ?? null;
|
|
785
892
|
const view = {
|
|
786
893
|
status,
|
|
894
|
+
conversationId: s.conversationId,
|
|
787
895
|
banner: banner(status, next),
|
|
788
896
|
next,
|
|
789
897
|
agent: sameAgent(prev?.agent, name, avatarUrl) ? prev.agent : { name, avatarUrl },
|
|
790
898
|
disclosure: s.disclosure ?? this.info?.disclosure ?? null,
|
|
899
|
+
welcome: this.info?.welcome ?? null,
|
|
791
900
|
takeover: s.takeover,
|
|
792
901
|
items,
|
|
793
902
|
activity: prev && sameTools(prev.activity, activity) ? prev.activity : activity,
|
|
@@ -894,6 +1003,11 @@ var ConversationStore = class {
|
|
|
894
1003
|
}, PERSIST_DELAY);
|
|
895
1004
|
}
|
|
896
1005
|
// ------------------------------------------------------------ guards
|
|
1006
|
+
/** Past the session's end (`expires_at`), when the backend passed it on. */
|
|
1007
|
+
sessionOver() {
|
|
1008
|
+
const end = this.info?.expires_at;
|
|
1009
|
+
return end !== void 0 && Date.now() >= Date.parse(end);
|
|
1010
|
+
}
|
|
897
1011
|
live() {
|
|
898
1012
|
if (this.disposed)
|
|
899
1013
|
throw new VinaverseClientError(
|
|
@@ -947,6 +1061,42 @@ function shallowEqual(a, b) {
|
|
|
947
1061
|
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;
|
|
948
1062
|
}
|
|
949
1063
|
|
|
950
|
-
|
|
1064
|
+
// src/conversation/modes.ts
|
|
1065
|
+
function createConversationsByMode(options) {
|
|
1066
|
+
const { getSession, modes = [], ...shared } = options;
|
|
1067
|
+
const base = shared.persistenceKey ?? "vinaverse.conversation";
|
|
1068
|
+
const stores = /* @__PURE__ */ new Map();
|
|
1069
|
+
const get = (mode) => {
|
|
1070
|
+
let store = stores.get(mode);
|
|
1071
|
+
if (!store) {
|
|
1072
|
+
store = createConversation({
|
|
1073
|
+
...shared,
|
|
1074
|
+
persistenceKey: `${base}.${mode}`,
|
|
1075
|
+
getSession: (request) => getSession({ ...request, mode })
|
|
1076
|
+
});
|
|
1077
|
+
stores.set(mode, store);
|
|
1078
|
+
}
|
|
1079
|
+
return store;
|
|
1080
|
+
};
|
|
1081
|
+
return {
|
|
1082
|
+
get,
|
|
1083
|
+
select(mode) {
|
|
1084
|
+
for (const [other, store] of stores)
|
|
1085
|
+
if (other !== mode) store.setForeground(false);
|
|
1086
|
+
return get(mode);
|
|
1087
|
+
},
|
|
1088
|
+
dispose() {
|
|
1089
|
+
for (const store of stores.values()) store.dispose();
|
|
1090
|
+
stores.clear();
|
|
1091
|
+
},
|
|
1092
|
+
clear() {
|
|
1093
|
+
for (const mode of /* @__PURE__ */ new Set([...stores.keys(), ...modes]))
|
|
1094
|
+
get(mode).clear();
|
|
1095
|
+
stores.clear();
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export { ConversationStore, MAX_TEXT, answerProblem, codePoints, createConversation, createConversationsByMode, isAllowedLink, isValidLinkPrefix };
|
|
951
1101
|
//# sourceMappingURL=index.js.map
|
|
952
1102
|
//# sourceMappingURL=index.js.map
|