@artooi/ag-ui-web-component 0.41.3 → 0.43.0
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/CHANGELOG.md +304 -1
- package/README.md +113 -24
- package/dist/ag-ui-web-component.bundle.js +33 -33
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/constants.d.ts +3 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +49 -9
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +20 -7
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/storage_scope.d.ts +14 -4
- package/dist/core/storage_scope.d.ts.map +1 -1
- package/dist/index.js +729 -115
- package/dist/index.js.map +3 -3
- package/dist/tools/tool_dispatch.d.ts +23 -0
- package/dist/tools/tool_dispatch.d.ts.map +1 -1
- package/dist/ui/composer/voice_input.d.ts +10 -4
- package/dist/ui/composer/voice_input.d.ts.map +1 -1
- package/dist/ui/history/conversation_history.d.ts +84 -1
- package/dist/ui/history/conversation_history.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +5 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +3 -1
- package/src/core/ag_ui_chat.ts +498 -70
- package/src/core/agui_client.ts +55 -12
- package/src/core/storage_scope.ts +21 -8
- package/src/tools/tool_dispatch.ts +153 -6
- package/src/ui/composer/voice_input.ts +79 -14
- package/src/ui/history/conversation_history.ts +392 -56
- package/src/ui/ui_strings.ts +6 -0
- package/src/version.ts +1 -1
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -556,6 +556,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
556
556
|
readonly #queuedRow: HTMLDivElement = document.createElement("div");
|
|
557
557
|
/** How far back the composer has been walked; null while the user is typing. */
|
|
558
558
|
#recallIndex: number | null = null;
|
|
559
|
+
/**
|
|
560
|
+
* What the walk last put in the composer, read back from it. A walk is live
|
|
561
|
+
* only while the composer still holds exactly this, so a write from anywhere
|
|
562
|
+
* else ends it. Stale while no walk is in progress, which is harmless: the
|
|
563
|
+
* comparison can only ever end a walk, never start one.
|
|
564
|
+
*/
|
|
565
|
+
#recalled = "";
|
|
559
566
|
|
|
560
567
|
/**
|
|
561
568
|
* Re-clamp the dragged launcher when the window changes size. Bound once as
|
|
@@ -605,6 +612,59 @@ export class AgUiChat extends HTMLElement {
|
|
|
605
612
|
* history replay would draw the conversation a second time beneath it.
|
|
606
613
|
*/
|
|
607
614
|
#connectedBefore = false;
|
|
615
|
+
/**
|
|
616
|
+
* The `user-key` whose state the element holds -- the transcript, the recall
|
|
617
|
+
* history, the Always allow waivers and the shared state -- or `null` before
|
|
618
|
+
* the first connect, when it holds nobody's.
|
|
619
|
+
*
|
|
620
|
+
* Not the same thing as the attribute. The attribute names the principal the
|
|
621
|
+
* host says is here now, and it can change while the element is out of the
|
|
622
|
+
* document, where nothing acts on it; this is who the state still belongs
|
|
623
|
+
* to. Connecting compares the two, which is how a key changed while detached
|
|
624
|
+
* still gets the handover a live change gets.
|
|
625
|
+
*/
|
|
626
|
+
#principalKey: string | null = null;
|
|
627
|
+
/**
|
|
628
|
+
* The current principal's tenure: replaced on every handover from one
|
|
629
|
+
* principal to another, and captured by each client as it is built, so that
|
|
630
|
+
* a client can tell whether the principal it was built for is still the one
|
|
631
|
+
* this element serves.
|
|
632
|
+
*
|
|
633
|
+
* A client outlives the handover that stops it. A stopped run saves what it
|
|
634
|
+
* had once its request closes, a tool handler cannot be aborted and its
|
|
635
|
+
* result is kept, and an event already read off the wire is still applied --
|
|
636
|
+
* all of it after the store was purged and scoped to whoever arrived, so
|
|
637
|
+
* each one wrote the previous principal's words into the next one's storage
|
|
638
|
+
* or state. Writing into the previous principal's store instead would only
|
|
639
|
+
* re-create what the purge just removed, so such a client writes nothing.
|
|
640
|
+
*
|
|
641
|
+
* An identity rather than the key itself, for two reasons. The key's first
|
|
642
|
+
* arrival is an adoption, not a handover, and a run in flight across it is
|
|
643
|
+
* the same person's and keeps saving. And a key that leaves and comes back
|
|
644
|
+
* is a second tenure: the first one's stopped run must not re-create a
|
|
645
|
+
* conversation the handover purged.
|
|
646
|
+
*/
|
|
647
|
+
#tenure: object = {};
|
|
648
|
+
/**
|
|
649
|
+
* The conversation on screen: replaced every time the transcript is cleared
|
|
650
|
+
* for another one -- New chat, a drawer switch, deleting the active thread,
|
|
651
|
+
* `reload()`, re-insertion and a handover all pass through
|
|
652
|
+
* {@link #resetConversation} -- and captured by each frontend tool call as it
|
|
653
|
+
* is dispatched, so the call can tell whether the conversation it was drawn
|
|
654
|
+
* in is still the one showing.
|
|
655
|
+
*
|
|
656
|
+
* A call outlives the reset that stops its run. A handler cannot be aborted,
|
|
657
|
+
* and a card open at the time is declined, so the call resumes once the new
|
|
658
|
+
* conversation is showing; drawing the run's pending indicator then put it
|
|
659
|
+
* in a conversation with no run to take it down.
|
|
660
|
+
*
|
|
661
|
+
* Not {@link #tenure}, which only a handover replaces. A reset is not a
|
|
662
|
+
* change of principal: the call still belongs to someone this element
|
|
663
|
+
* serves, so it still settles its card, clears its checkpoint and returns
|
|
664
|
+
* its result, and only the drawing is skipped. The tenure checks end the
|
|
665
|
+
* call before any of that.
|
|
666
|
+
*/
|
|
667
|
+
#conversation: object = {};
|
|
608
668
|
/**
|
|
609
669
|
* The remote store `data-threads-url` wrapped around the conversation store,
|
|
610
670
|
* and the store inside it, so connecting again can wrap that store rather
|
|
@@ -624,6 +684,42 @@ export class AgUiChat extends HTMLElement {
|
|
|
624
684
|
// the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
|
|
625
685
|
// rounds, but the user must still be able to stop there.
|
|
626
686
|
#running = false;
|
|
687
|
+
/**
|
|
688
|
+
* The send {@link sendMessage} has taken, or the retry {@link retryLastTurn}
|
|
689
|
+
* has, whose run has not started yet; null otherwise.
|
|
690
|
+
*
|
|
691
|
+
* `running` turns on when the client starts the run, a microtask after it is
|
|
692
|
+
* asked for one, and a send does two things before it asks: it draws the
|
|
693
|
+
* turn, and it dispatches the submit event, whose listeners run in the middle
|
|
694
|
+
* of the send. A send from one of them, or a second call in the same task,
|
|
695
|
+
* passed every guard and started a second run on the same client -- two
|
|
696
|
+
* requests, and the listener's turn drawn between the user's turn and its
|
|
697
|
+
* answer. It is the gap a picked checkpoint closes by counting from the pick,
|
|
698
|
+
* closed the same way for a send. A retry asks for its run the same way, so
|
|
699
|
+
* it takes the same hold, and so does every restore, from before it asks the
|
|
700
|
+
* store for the conversation, through the history host's `holdRun`: until
|
|
701
|
+
* the store answers, the client can only be built from nothing.
|
|
702
|
+
*
|
|
703
|
+
* Let go of once the run starts, because `running` holds from then until the
|
|
704
|
+
* run settles, and a send held past that would refuse a host's follow-up from
|
|
705
|
+
* the run-finished event, which fires as the run settles. Let go of by the
|
|
706
|
+
* send's, the retry's or the restore's end when its run never starts.
|
|
707
|
+
*
|
|
708
|
+
* An object rather than a flag, so a send or a retry ending releases only
|
|
709
|
+
* what it took. By the time either ends, the turn queued behind it has
|
|
710
|
+
* usually taken the field for itself, and a Stop lets go of it for the next
|
|
711
|
+
* send to take; the earlier one ending must release neither. It is also how
|
|
712
|
+
* {@link sendMessage} tells the one hold it waits on from all the others it
|
|
713
|
+
* refuses behind: see {@link #restoreHold}.
|
|
714
|
+
*/
|
|
715
|
+
#sending: object | null = null;
|
|
716
|
+
/**
|
|
717
|
+
* The hold the latest restore with no run to resume took, and what settles
|
|
718
|
+
* once that restore has ended or been stopped; `null` when the latest
|
|
719
|
+
* restore will resume a run. Only meaningful while `#sending` is that hold:
|
|
720
|
+
* then a host's {@link sendMessage} waits for it rather than refusing.
|
|
721
|
+
*/
|
|
722
|
+
#restoreHold: { readonly hold: object; readonly restored: Promise<void> } | null = null;
|
|
627
723
|
/** The decision a run is suspended on, which a Stop abandons. */
|
|
628
724
|
readonly #decision = new PendingDecision();
|
|
629
725
|
/**
|
|
@@ -715,6 +811,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
715
811
|
getContext: () => this.getContext(),
|
|
716
812
|
conversationStore: () => this.conversationStore,
|
|
717
813
|
threadId: () => this.#history.threadId,
|
|
814
|
+
// Read live and compared, the way each client compares the one it was
|
|
815
|
+
// built with: see #tenure.
|
|
816
|
+
tenure: () => this.#tenure,
|
|
817
|
+
// The same, for the conversation on screen: see #conversation.
|
|
818
|
+
conversation: () => this.#conversation,
|
|
718
819
|
});
|
|
719
820
|
this.#runHandlers = new RunHandlers({
|
|
720
821
|
element: this,
|
|
@@ -863,6 +964,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
863
964
|
// A continuation sends only what the composer holds. The tray is not
|
|
864
965
|
// read, so nothing rides along to be drawn or announced.
|
|
865
966
|
announceTurn: (content) => this.#announceTurn(content, []),
|
|
967
|
+
recordTurn: (content) => this.#recordTurn(content),
|
|
866
968
|
autoGrow: () => autoGrow(this.#input),
|
|
867
969
|
continuationEnded: () => this.#flushQueued(),
|
|
868
970
|
client: () => this.#client,
|
|
@@ -871,7 +973,41 @@ export class AgUiChat extends HTMLElement {
|
|
|
871
973
|
releaseClient: () => {
|
|
872
974
|
this.#client = null;
|
|
873
975
|
},
|
|
874
|
-
|
|
976
|
+
// A held send or retry is as much in flight to a pick as a run is: it has
|
|
977
|
+
// asked this element's client for a run that has not started, and a
|
|
978
|
+
// continuation started beside it was a second run in one conversation.
|
|
979
|
+
// So is a restore, which holds through the same field from before the
|
|
980
|
+
// store answers.
|
|
981
|
+
running: () => this.#running || this.#sending !== null,
|
|
982
|
+
// The hold a send takes, taken for every restore, so what refuses a send
|
|
983
|
+
// held short of its run holds one made while the store is still
|
|
984
|
+
// answering: Send parks the turn, a Retry and a pick refuse, and
|
|
985
|
+
// `sendMessage` refuses behind a restore that will resume a run and waits
|
|
986
|
+
// for one that will not -- `restored` is what it waits on, given only for
|
|
987
|
+
// that kind. The same field rather than a flag beside it, because what
|
|
988
|
+
// lets go of a send's hold is right for this one too. The resumed run
|
|
989
|
+
// starting lets go, in `#setRunning`, so there is no gap after the resume
|
|
990
|
+
// is asked for, and a Stop does, in `#cancelRun`. A flag the restore
|
|
991
|
+
// cleared itself would still be set when the resumed run settles and
|
|
992
|
+
// sends the parked turn, so that send would be refused after the turn had
|
|
993
|
+
// left the queue, and lost.
|
|
994
|
+
//
|
|
995
|
+
// Whether the restore was stopped is not read off this hold, because a
|
|
996
|
+
// host's send that waited takes it over; the restore asks its own
|
|
997
|
+
// generation.
|
|
998
|
+
holdRun: (restored) => {
|
|
999
|
+
const hold = {};
|
|
1000
|
+
this.#sending = hold;
|
|
1001
|
+
this.#restoreHold = restored === null ? null : { hold, restored };
|
|
1002
|
+
return {
|
|
1003
|
+
release: () => {
|
|
1004
|
+
if (this.#sending === hold) {
|
|
1005
|
+
this.#sending = null;
|
|
1006
|
+
this.#flushQueued();
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
};
|
|
1010
|
+
},
|
|
875
1011
|
cancelRun: () => this.#cancelRun(),
|
|
876
1012
|
resetState: () => this.#resetState(),
|
|
877
1013
|
setRunning: (running) => this.#setRunning(running),
|
|
@@ -941,8 +1077,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
941
1077
|
return;
|
|
942
1078
|
}
|
|
943
1079
|
if (name === "user-key") {
|
|
944
|
-
//
|
|
945
|
-
//
|
|
1080
|
+
// Acted on here only while connected. Before the first connect there is
|
|
1081
|
+
// nothing to move: connectedCallback resolves the namespace from the
|
|
1082
|
+
// attribute as it stands by then. Between a disconnect and the next
|
|
1083
|
+
// connect there is, and connecting hands it over -- see #principalKey
|
|
1084
|
+
// and connectedCallback for why it waits until then. An absent attribute
|
|
946
1085
|
// and an empty one name the same (unnamed) principal, so neither is a
|
|
947
1086
|
// change worth acting on.
|
|
948
1087
|
if (this.#connected && (previous ?? "") !== (value ?? "")) {
|
|
@@ -1238,7 +1377,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
1238
1377
|
// run was cancelled on the way out, and the transcript is rebuilt below
|
|
1239
1378
|
// from persisted history -- which could only be appended to what is still
|
|
1240
1379
|
// showing. The composer's recall history is kept, because a move is not a
|
|
1241
|
-
// farewell and
|
|
1380
|
+
// farewell, and the Stop on the way out dropped the queued messages because
|
|
1381
|
+
// recall already held them.
|
|
1242
1382
|
if (this.#connectedBefore) {
|
|
1243
1383
|
this.#resetConversation();
|
|
1244
1384
|
}
|
|
@@ -1283,6 +1423,33 @@ export class AgUiChat extends HTMLElement {
|
|
|
1283
1423
|
}
|
|
1284
1424
|
this.#syncLauncher();
|
|
1285
1425
|
this.#skills.init();
|
|
1426
|
+
// A `user-key` that changed while the element was out of the document gets
|
|
1427
|
+
// the handover a live change gets, here, because this is the first moment
|
|
1428
|
+
// the element can make it. A plain move with the same key skips this and
|
|
1429
|
+
// keeps the recall history, as above; a first connect skips it too, since
|
|
1430
|
+
// an element that has never connected holds nobody's state -- and treating
|
|
1431
|
+
// its first key as an arrival would adopt whatever key-less conversation
|
|
1432
|
+
// an earlier visitor left in the tab.
|
|
1433
|
+
//
|
|
1434
|
+
// Deferred to here rather than made the moment the attribute changes, for
|
|
1435
|
+
// three reasons. Out of the document, the arriving half -- replaying the
|
|
1436
|
+
// next principal's history, listing their threads -- is requests, and
|
|
1437
|
+
// nothing should go out for a node that has left (see #startup). Only where
|
|
1438
|
+
// the key ends up counts: one that goes away and comes back while detached
|
|
1439
|
+
// hands nothing over, because nobody else saw this panel in between. And
|
|
1440
|
+
// the namespace was claimed again just above, so the previous principal's
|
|
1441
|
+
// is resolved against the one this element holds now: a namespace another
|
|
1442
|
+
// element claimed while this one was out is never purged from under it.
|
|
1443
|
+
//
|
|
1444
|
+
// Before the store is scoped, which is the arriving half connecting does
|
|
1445
|
+
// anyway: an adoption has to move the conversation before the store is
|
|
1446
|
+
// asked which thread is active, or it mints a fresh one there first.
|
|
1447
|
+
const held = this.#principalKey;
|
|
1448
|
+
const changed = held !== null && held !== this.userKey;
|
|
1449
|
+
if (changed) {
|
|
1450
|
+
this.#changePrincipal(held, this.userKey);
|
|
1451
|
+
}
|
|
1452
|
+
this.#principalKey = this.userKey;
|
|
1286
1453
|
// Namespace the built-in default store too (a host-injected store is used
|
|
1287
1454
|
// verbatim). Must precede #wireThreadStore, which wraps the current store.
|
|
1288
1455
|
this.conversationStore = this.#storage.scopeStore(this.#unwrappedStore(), this.userKey);
|
|
@@ -1301,6 +1468,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
1301
1468
|
// through a framework ref still has a chance to be heard — see #startup.
|
|
1302
1469
|
queueMicrotask(() => this.#startup());
|
|
1303
1470
|
void this.#history.rehydrate();
|
|
1471
|
+
if (changed) {
|
|
1472
|
+
// The last of what a live change does: a drawer left open across the
|
|
1473
|
+
// move still lists the threads of the principal who left, titles and
|
|
1474
|
+
// all, until something reloads it. A plain move leaves it alone, since
|
|
1475
|
+
// the list it shows is still this principal's.
|
|
1476
|
+
void this.#history.refreshDrawer();
|
|
1477
|
+
}
|
|
1304
1478
|
// Last: everything above reads (and some of it sets) attributes, and none
|
|
1305
1479
|
// of that should trip the connect-time warning.
|
|
1306
1480
|
this.#connected = true;
|
|
@@ -1681,8 +1855,22 @@ export class AgUiChat extends HTMLElement {
|
|
|
1681
1855
|
* logout, because a logout is a navigation (or, in a single-page app, not
|
|
1682
1856
|
* even that) rather than a tab close. Nothing remounts, so the host naming
|
|
1683
1857
|
* the new principal is the only signal the element will ever get.
|
|
1858
|
+
*
|
|
1859
|
+
* Two callers. A live change runs all of it. connectedCallback runs it for a
|
|
1860
|
+
* key that changed while the element was out of the document, and there it
|
|
1861
|
+
* runs before the store is scoped and before `#connected` is set -- which is
|
|
1862
|
+
* what `live` reads, since `isConnected` is already true by then. That call
|
|
1863
|
+
* does the half about what storage and the element still hold. The half
|
|
1864
|
+
* about the principal arriving -- the store scoped under the new key, its
|
|
1865
|
+
* active thread, the replay -- is what connecting goes on to do anyway, and
|
|
1866
|
+
* run here too it would read the old store before the new one exists,
|
|
1867
|
+
* minting a thread pointer in the namespace just purged and replaying the
|
|
1868
|
+
* history twice. Nor would it leave alone a store the host assigned while
|
|
1869
|
+
* the element was out.
|
|
1684
1870
|
*/
|
|
1685
1871
|
#changePrincipal(previousKey: string, nextKey: string): void {
|
|
1872
|
+
this.#principalKey = nextKey;
|
|
1873
|
+
const live = this.#connected;
|
|
1686
1874
|
const previous = this.#storage.conversationNamespace(previousKey);
|
|
1687
1875
|
const next = this.#storage.conversationNamespace(nextKey);
|
|
1688
1876
|
if (previousKey === "") {
|
|
@@ -1693,11 +1881,18 @@ export class AgUiChat extends HTMLElement {
|
|
|
1693
1881
|
// copying also matters: a copy left behind under the unscoped namespace
|
|
1694
1882
|
// is a transcript the next key-less mount would happily adopt.
|
|
1695
1883
|
SessionStorageStore.adopt(previous, next);
|
|
1696
|
-
|
|
1884
|
+
if (live) {
|
|
1885
|
+
this.#rescopeStore(next);
|
|
1886
|
+
}
|
|
1697
1887
|
return;
|
|
1698
1888
|
}
|
|
1889
|
+
// First, ahead of the purge: from here on, nothing a client built for the
|
|
1890
|
+
// previous principal does is written anywhere. See #tenure.
|
|
1891
|
+
this.#tenure = {};
|
|
1699
1892
|
SessionStorageStore.purge(previous);
|
|
1700
|
-
|
|
1893
|
+
if (live) {
|
|
1894
|
+
this.#rescopeStore(next);
|
|
1895
|
+
}
|
|
1701
1896
|
// An Always allow is the previous principal's decision too, and it lives in
|
|
1702
1897
|
// memory rather than in the store just purged, so it has to be forgotten
|
|
1703
1898
|
// here or it outlives them. The adoption above keeps it, for the same
|
|
@@ -1715,8 +1910,29 @@ export class AgUiChat extends HTMLElement {
|
|
|
1715
1910
|
// the new principal's first run. A host that seeds state assigns it again
|
|
1716
1911
|
// for the one who arrived.
|
|
1717
1912
|
this.#sharedState = {};
|
|
1913
|
+
// And the turn being written, also only here. A turn typed and not sent is
|
|
1914
|
+
// the previous principal's words as much as the transcript is, and so is
|
|
1915
|
+
// everything else that reaches the box: a quotation and a skill's template
|
|
1916
|
+
// are both text in it. The tray went with the transcript above. New chat
|
|
1917
|
+
// keeps the text, because the same person is still typing, and an adoption
|
|
1918
|
+
// returned before any of this for the same reason. Through the input
|
|
1919
|
+
// handler rather than beside it, so the hint and the slash palette that
|
|
1920
|
+
// were answering that text come down with it.
|
|
1921
|
+
this.#input.value = "";
|
|
1922
|
+
this.#onInput();
|
|
1718
1923
|
this.#setRunning(false);
|
|
1719
1924
|
this.#setUnread(0);
|
|
1925
|
+
if (!live) {
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
// A recording in progress is theirs too, and once stopped it would be
|
|
1929
|
+
// transcribed into the next principal's composer. Taking the mic down ends
|
|
1930
|
+
// it untranscribed, and putting a fresh one up leaves the next principal
|
|
1931
|
+
// a working control -- the way leaving and connecting do it, which is also
|
|
1932
|
+
// why only a live change does it here: a detached one has been through
|
|
1933
|
+
// both already.
|
|
1934
|
+
this.#voice.dispose();
|
|
1935
|
+
this.#voice.wire();
|
|
1720
1936
|
this.#history.adoptActiveThread();
|
|
1721
1937
|
void this.#history.rehydrate();
|
|
1722
1938
|
void this.#history.refreshDrawer();
|
|
@@ -1726,13 +1942,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
1726
1942
|
* Rebuild the `sessionStorage` store under `namespace`, re-wrapping it for
|
|
1727
1943
|
* `data-threads-url` exactly as connecting did.
|
|
1728
1944
|
*
|
|
1729
|
-
* A store
|
|
1730
|
-
* somewhere the element cannot see has to
|
|
1945
|
+
* A store the host assigned is left alone, whether before connecting or
|
|
1946
|
+
* since: a store that holds its data somewhere the element cannot see has to
|
|
1947
|
+
* scope itself. The exception is a plain `SessionStorageStore` assigned
|
|
1948
|
+
* before connecting, which connecting cannot tell from the default and so
|
|
1949
|
+
* took for the element's own. Hence the unwrapped store in use is what the scope is asked
|
|
1950
|
+
* about, rather than what it remembered when connecting. The transcript on
|
|
1731
1951
|
* screen is still cleared either way — the host swapped principals, and that
|
|
1732
1952
|
* much is the element's to act on.
|
|
1733
1953
|
*/
|
|
1734
1954
|
#rescopeStore(namespace: string): void {
|
|
1735
|
-
const store = this.#storage.rescopeStore(namespace);
|
|
1955
|
+
const store = this.#storage.rescopeStore(this.#unwrappedStore(), namespace);
|
|
1736
1956
|
if (store === null) {
|
|
1737
1957
|
return;
|
|
1738
1958
|
}
|
|
@@ -1862,6 +2082,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
1862
2082
|
// not draw into this one, put a new run's Stop back to Send, or report the
|
|
1863
2083
|
// new conversation's tools as its own.
|
|
1864
2084
|
this.#runHandlers.detach();
|
|
2085
|
+
// A frontend tool call of that run can resume later still, for the same
|
|
2086
|
+
// reason, and must not draw into this one either: see #conversation.
|
|
2087
|
+
this.#conversation = {};
|
|
1865
2088
|
this.#clearTranscript();
|
|
1866
2089
|
this.#history.forgetRestored();
|
|
1867
2090
|
}
|
|
@@ -1900,8 +2123,20 @@ export class AgUiChat extends HTMLElement {
|
|
|
1900
2123
|
*
|
|
1901
2124
|
* History is truncated to the most recent user message inclusive and the run
|
|
1902
2125
|
* repeats, so the agent answers what it was asked rather than being told its
|
|
1903
|
-
* last answer was wrong. Returns `false` when there is nothing to retry or
|
|
1904
|
-
*
|
|
2126
|
+
* last answer was wrong. Returns `false` when there is nothing to retry, or
|
|
2127
|
+
* while anything that holds a send is in flight: a run, a send or another
|
|
2128
|
+
* retry whose run has not started yet, a checkpoint continuation, or a
|
|
2129
|
+
* restore of the stored conversation that is still loading. It does not wait
|
|
2130
|
+
* for a restore as a send does: nothing is on screen to retry yet, and after
|
|
2131
|
+
* the load it would re-ask the previous visit's last question. Also `false`
|
|
2132
|
+
* when host code it
|
|
2133
|
+
* runs before its run starts -- the store's save, or an activity or tool
|
|
2134
|
+
* renderer the replay draws through -- has stopped it by starting a new chat
|
|
2135
|
+
* or changing `user-key`. A tool or context provider that stops it does so
|
|
2136
|
+
* inside the client's resume, past the last point this can see: nothing is
|
|
2137
|
+
* sent, and this resolves `true`, because both a run starting and a Stop let
|
|
2138
|
+
* go of the hold it checks. Rejects with the store's error when the store
|
|
2139
|
+
* refuses the save, and then the conversation is as it was.
|
|
1905
2140
|
*
|
|
1906
2141
|
* Public because a host with its own message UI wants the same button, and
|
|
1907
2142
|
* because the failed-run notice reaches it from outside the action row.
|
|
@@ -1912,24 +2147,63 @@ export class AgUiChat extends HTMLElement {
|
|
|
1912
2147
|
* again -- unless the user waived it for this session.
|
|
1913
2148
|
*/
|
|
1914
2149
|
async retryLastTurn(): Promise<boolean> {
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
if (
|
|
2150
|
+
// Refused for whatever refuses a send, because it starts a run on the same
|
|
2151
|
+
// client. That includes a send held short of its run, which `running` does
|
|
2152
|
+
// not see yet, and a picked checkpoint, which runs on a client of its own
|
|
2153
|
+
// and leaves this one idle -- so a Retry beside it was a second run in the
|
|
2154
|
+
// one conversation, each saving over the other.
|
|
2155
|
+
if (this.#running || this.#sending !== null || this.#history.continuation !== null) {
|
|
1921
2156
|
return false;
|
|
1922
2157
|
}
|
|
1923
|
-
//
|
|
1924
|
-
//
|
|
1925
|
-
//
|
|
1926
|
-
//
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2158
|
+
// Held from here as a send is, and for the same reason: the client starts
|
|
2159
|
+
// the run a microtask after it is asked, and nothing above sees it until
|
|
2160
|
+
// then. A send in that time went out as a second run on the same client,
|
|
2161
|
+
// the built-in Send sent rather than queued, and a pick started a
|
|
2162
|
+
// continuation beside it. Released once the run starts, by `#setRunning`,
|
|
2163
|
+
// and below when it never does.
|
|
2164
|
+
const hold = {};
|
|
2165
|
+
this.#sending = hold;
|
|
2166
|
+
try {
|
|
2167
|
+
const client = this.#ensureClient();
|
|
2168
|
+
const kept = client.truncateToLastUser();
|
|
2169
|
+
if (kept === null) {
|
|
2170
|
+
return false;
|
|
2171
|
+
}
|
|
2172
|
+
// The truncation saved through the host's store, so host code has run
|
|
2173
|
+
// with the Retry under way, and a store that started a new chat or
|
|
2174
|
+
// changed `user-key` from there has stopped it. Going on drew the
|
|
2175
|
+
// abandoned conversation into the one that replaced it and asked its
|
|
2176
|
+
// answer again on a client nothing holds.
|
|
2177
|
+
if (this.#sending !== hold) {
|
|
2178
|
+
return false;
|
|
2179
|
+
}
|
|
2180
|
+
// Re-render between the truncation and the run: the kept turns replay as
|
|
2181
|
+
// restored history (static, no entrance animation), and only the new
|
|
2182
|
+
// answer arrives live. Streaming into the old transcript would put the
|
|
2183
|
+
// new answer underneath the one it replaces.
|
|
2184
|
+
this.#clearTranscript();
|
|
2185
|
+
for (const message of kept) {
|
|
2186
|
+
this.#history.replay(message);
|
|
2187
|
+
// A replay draws through the host's activity and tool renderers, which
|
|
2188
|
+
// can stop the Retry as the store's save can. Checked after each one,
|
|
2189
|
+
// so the rest of the abandoned conversation is not drawn into the chat
|
|
2190
|
+
// that replaced it, and after the last, so its answer is not asked
|
|
2191
|
+
// again.
|
|
2192
|
+
if (this.#sending !== hold) {
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
await client.resume();
|
|
2197
|
+
return true;
|
|
2198
|
+
} finally {
|
|
2199
|
+
// Only its own hold, as a send's end releases only its own. A turn the
|
|
2200
|
+
// composer queued behind it is sent from here when its run never started,
|
|
2201
|
+
// because no run settles to send it.
|
|
2202
|
+
if (this.#sending === hold) {
|
|
2203
|
+
this.#sending = null;
|
|
2204
|
+
this.#flushQueued();
|
|
2205
|
+
}
|
|
1930
2206
|
}
|
|
1931
|
-
await client.resume();
|
|
1932
|
-
return true;
|
|
1933
2207
|
}
|
|
1934
2208
|
|
|
1935
2209
|
/**
|
|
@@ -2332,7 +2606,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
2332
2606
|
autoGrow(this.#input);
|
|
2333
2607
|
// Typing puts the composer back in the user's hands: the next ArrowUp
|
|
2334
2608
|
// starts from the newest turn again rather than continuing a walk through
|
|
2335
|
-
// history the user has since edited.
|
|
2609
|
+
// history the user has since edited. Needed beside the walk's own check
|
|
2610
|
+
// below, which compares text: an edit typed and then deleted leaves the
|
|
2611
|
+
// turn there unchanged, and the user has still taken the box back.
|
|
2336
2612
|
this.#recallIndex = null;
|
|
2337
2613
|
}
|
|
2338
2614
|
|
|
@@ -2369,6 +2645,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
2369
2645
|
* which is what every shell and every coding agent means by this. Arrowing
|
|
2370
2646
|
* forward past the newest empties the composer again rather than sticking on
|
|
2371
2647
|
* it, so the way out is the same key that got you in.
|
|
2648
|
+
*
|
|
2649
|
+
* A walk ends with any write into the composer that is not its own, so that
|
|
2650
|
+
* whatever was written is never replaced by the next step.
|
|
2372
2651
|
*/
|
|
2373
2652
|
#recallHistory(event: KeyboardEvent): void {
|
|
2374
2653
|
const back = event.key === "ArrowUp";
|
|
@@ -2379,6 +2658,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
2379
2658
|
if (drafts.length === 0) {
|
|
2380
2659
|
return;
|
|
2381
2660
|
}
|
|
2661
|
+
// The walk checks the composer rather than trusting every writer to end
|
|
2662
|
+
// it. Writers are spread across the element -- a quotation, a skill
|
|
2663
|
+
// prefill, dictation, the clears after a send -- and a walk used to end
|
|
2664
|
+
// only for those that also went through the input handler or
|
|
2665
|
+
// `#recordTurn`. A quotation and a prefill did neither, and the next
|
|
2666
|
+
// ArrowUp replaced them with a past turn.
|
|
2667
|
+
// A host page can write the composer too, which no hook of ours can see.
|
|
2668
|
+
// Comparing here is the one rule a writer added later cannot forget.
|
|
2669
|
+
if (this.#input.value !== this.#recalled) {
|
|
2670
|
+
this.#recallIndex = null;
|
|
2671
|
+
}
|
|
2382
2672
|
// An empty composer is the only safe entry: anything typed is the user's,
|
|
2383
2673
|
// and replacing it with a past turn would lose it without asking.
|
|
2384
2674
|
if (this.#recallIndex === null && (!back || this.#input.value !== "")) {
|
|
@@ -2395,6 +2685,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
2395
2685
|
// honestly -- and an unreachable default is worse than an assertion,
|
|
2396
2686
|
// because it looks like a case somebody thought about.
|
|
2397
2687
|
this.#input.value = next < 0 ? "" : (drafts[next] as string);
|
|
2688
|
+
// Read back rather than taken from the draft, so a value the textarea
|
|
2689
|
+
// normalises on the way in -- its line endings -- still reads as the
|
|
2690
|
+
// walk's own on the next step instead of ending it.
|
|
2691
|
+
this.#recalled = this.#input.value;
|
|
2398
2692
|
this.#input.setSelectionRange(this.#input.value.length, this.#input.value.length);
|
|
2399
2693
|
autoGrow(this.#input);
|
|
2400
2694
|
}
|
|
@@ -2410,24 +2704,24 @@ export class AgUiChat extends HTMLElement {
|
|
|
2410
2704
|
// the user has just stopped is the opposite of what stopping meant, and it
|
|
2411
2705
|
// would arrive after they had already turned away.
|
|
2412
2706
|
//
|
|
2413
|
-
// Not sending it is not the same as destroying it, though
|
|
2414
|
-
//
|
|
2415
|
-
//
|
|
2416
|
-
//
|
|
2417
|
-
//
|
|
2418
|
-
// puts the one typed last first.
|
|
2707
|
+
// Not sending it is not the same as destroying it, though, and nothing is
|
|
2708
|
+
// destroyed here: a queued turn entered the recall history the moment it
|
|
2709
|
+
// left the composer, so ArrowUp gets it back. This used to be where it
|
|
2710
|
+
// entered, which left it out of reach for as long as it waited, and once
|
|
2711
|
+
// it was recorded on the way in, moving it again put each one in twice.
|
|
2419
2712
|
//
|
|
2420
2713
|
// This path is also reached from `disconnectedCallback`, where a DOM move
|
|
2421
2714
|
// and a framework re-render both look like a farewell and neither is one.
|
|
2422
|
-
for (const text of this.#queued) {
|
|
2423
|
-
if (this.#sentDrafts[0] !== text) {
|
|
2424
|
-
this.#sentDrafts.unshift(text);
|
|
2425
|
-
}
|
|
2426
|
-
}
|
|
2427
2715
|
this.#queued.length = 0;
|
|
2428
2716
|
this.#renderQueued();
|
|
2429
2717
|
this.#decision.abort();
|
|
2430
2718
|
this.#client?.cancel();
|
|
2719
|
+
// A send whose run has not started is ended here too, because nothing else
|
|
2720
|
+
// can reach it: the cancel above lands on a client that has not been asked
|
|
2721
|
+
// for the run yet, and a client forgets a Stop when it is next asked for one.
|
|
2722
|
+
// This is how a submit listener that starts a new chat, switches thread or
|
|
2723
|
+
// moves the element stops the send it is hearing about.
|
|
2724
|
+
this.#sending = null;
|
|
2431
2725
|
// A checkpoint continuation is as much the run in flight -- the composer
|
|
2432
2726
|
// offers Stop for it -- but it runs on a client the element does not hold.
|
|
2433
2727
|
this.#history.stopContinuation();
|
|
@@ -2443,6 +2737,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
2443
2737
|
#setRunning(running: boolean): void {
|
|
2444
2738
|
const settled = this.#running && !running;
|
|
2445
2739
|
this.#running = running;
|
|
2740
|
+
// The send that asked for this run is covered from here until it settles,
|
|
2741
|
+
// so its hold ends now. Held to the send's end instead, it was still held
|
|
2742
|
+
// when the run-finished event fired, and a host's follow-up from it was
|
|
2743
|
+
// refused.
|
|
2744
|
+
if (running) {
|
|
2745
|
+
this.#sending = null;
|
|
2746
|
+
}
|
|
2446
2747
|
const label = running ? this.#strings.stop : this.#strings.send;
|
|
2447
2748
|
this.#send.title = label;
|
|
2448
2749
|
this.#send.setAttribute("aria-label", label);
|
|
@@ -2502,6 +2803,28 @@ export class AgUiChat extends HTMLElement {
|
|
|
2502
2803
|
}
|
|
2503
2804
|
}
|
|
2504
2805
|
|
|
2806
|
+
/**
|
|
2807
|
+
* Put a turn that has left the composer at the front of the recall history,
|
|
2808
|
+
* and start the next walk from it.
|
|
2809
|
+
*
|
|
2810
|
+
* Every turn the user typed and pressed a key to send comes through here:
|
|
2811
|
+
* sent at once, queued behind a run, or sent to continue a checkpoint. A
|
|
2812
|
+
* host's own `sendMessage` does not, because its text was never in the
|
|
2813
|
+
* composer, and the arrow keys walk back through what the user wrote there.
|
|
2814
|
+
*/
|
|
2815
|
+
#recordTurn(content: string): void {
|
|
2816
|
+
// A repeat of the last one is not a second entry: the point is to reach
|
|
2817
|
+
// what was said, not how often. Nothing is recorded for an attachment sent
|
|
2818
|
+
// with no text, which has nothing for the composer to hold.
|
|
2819
|
+
if (content !== "" && this.#sentDrafts[0] !== content) {
|
|
2820
|
+
this.#sentDrafts.unshift(content);
|
|
2821
|
+
}
|
|
2822
|
+
// A press sends without an input event, which is what ends a walk
|
|
2823
|
+
// otherwise. Left where it was, the next ArrowUp stepped past the turn just
|
|
2824
|
+
// taken, or past whichever one now sits where it had walked to.
|
|
2825
|
+
this.#recallIndex = null;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2505
2828
|
async #submit(): Promise<void> {
|
|
2506
2829
|
// Ignore a submit while a run is in flight — the single choke point for
|
|
2507
2830
|
// both Enter and the Send button. The button already turns into Stop, but
|
|
@@ -2528,7 +2851,20 @@ export class AgUiChat extends HTMLElement {
|
|
|
2528
2851
|
// an attachment is settled state the tray is holding and the composer has
|
|
2529
2852
|
// no second copy of, so parking it here would mean deciding what happens
|
|
2530
2853
|
// when the user then removes the chip.
|
|
2531
|
-
|
|
2854
|
+
//
|
|
2855
|
+
// A send counts from the moment it is taken, for the same reason a pick
|
|
2856
|
+
// does. A second Send in the same task as the first -- a host clicking it
|
|
2857
|
+
// from code -- reaches here before the first's run has started, and
|
|
2858
|
+
// `sendMessage` refuses it then, so without this the box would be cleared
|
|
2859
|
+
// of a turn that went nowhere.
|
|
2860
|
+
//
|
|
2861
|
+
// Recorded for recall before it is queued or sent, because it leaves the
|
|
2862
|
+
// box either way. A turn queued here is the newest thing the user typed,
|
|
2863
|
+
// and it is still the newest while it waits; the queue later sends it
|
|
2864
|
+
// through `sendMessage`, which records nothing, so this is the only point
|
|
2865
|
+
// that sees it as the composer's.
|
|
2866
|
+
this.#recordTurn(content);
|
|
2867
|
+
if (this.#running || this.#sending !== null || this.#history.continuation !== null) {
|
|
2532
2868
|
if (content !== "") {
|
|
2533
2869
|
this.#queued.push(content);
|
|
2534
2870
|
this.#renderQueued();
|
|
@@ -2537,13 +2873,6 @@ export class AgUiChat extends HTMLElement {
|
|
|
2537
2873
|
}
|
|
2538
2874
|
return;
|
|
2539
2875
|
}
|
|
2540
|
-
// Recorded before the box is cleared, newest first, so the arrow keys walk
|
|
2541
|
-
// back through it. A repeat of the last one is not a second entry: the
|
|
2542
|
-
// point is to reach what was said, not how often.
|
|
2543
|
-
if (content !== "" && this.#sentDrafts[0] !== content) {
|
|
2544
|
-
this.#sentDrafts.unshift(content);
|
|
2545
|
-
}
|
|
2546
|
-
this.#recallIndex = null;
|
|
2547
2876
|
this.#input.value = "";
|
|
2548
2877
|
autoGrow(this.#input);
|
|
2549
2878
|
// A file still uploading does not ride along — `readyRefs()` returns only
|
|
@@ -2578,31 +2907,108 @@ export class AgUiChat extends HTMLElement {
|
|
|
2578
2907
|
* `attachments` are durable {@link AttachmentRef}s — what {@link attachFile}
|
|
2579
2908
|
* resolves to and what {@link ATTACHMENT_EVENT} reports.
|
|
2580
2909
|
*
|
|
2581
|
-
*
|
|
2582
|
-
*
|
|
2583
|
-
*
|
|
2584
|
-
*
|
|
2585
|
-
*
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2910
|
+
* Resolves `true` once the turn has been handed to the client for its run,
|
|
2911
|
+
* which is when the interaction settles, frontend-tool rounds included; a
|
|
2912
|
+
* run a tool or context provider then stops still counts, as it does for
|
|
2913
|
+
* {@link retryLastTurn}. Resolves `false` when nothing was sent.
|
|
2914
|
+
*
|
|
2915
|
+
* Nothing is sent for an empty message, nor while a run or a picked
|
|
2916
|
+
* checkpoint's continuation is in flight, since a second concurrent run
|
|
2917
|
+
* would orphan the first. A send is in flight from the moment it is taken
|
|
2918
|
+
* rather than from its run's start, so nothing is sent from a
|
|
2919
|
+
* {@link SUBMIT_EVENT} listener or for a second call in the same task
|
|
2920
|
+
* either. Nor with no `endpoint`, which says so in the transcript. Nor
|
|
2921
|
+
* behind a reload's restore that will resume a run a navigating tool
|
|
2922
|
+
* interrupted, since that restore ends by starting the run.
|
|
2923
|
+
*
|
|
2924
|
+
* **While the element restores a stored conversation with no run to resume
|
|
2925
|
+
* -- on connect, {@link reload}, a thread picked from the drawer, a change
|
|
2926
|
+
* from one `user-key` to another -- it waits, then sends.** The turn has to follow the
|
|
2927
|
+
* conversation it is part of: sent at once, it went out without it, the
|
|
2928
|
+
* conversation was then drawn under it, and its save replaced the stored
|
|
2929
|
+
* one. So the bubble, the {@link SUBMIT_EVENT} and the request all come after
|
|
2930
|
+
* the replay, by the length of the store's load. A New chat, a thread
|
|
2931
|
+
* switch, a change from one `user-key` to another, a reload or the element
|
|
2932
|
+
* leaving the page while it waits drops it, resolving `false`: the
|
|
2933
|
+
* conversation it was addressed to is no longer on screen. Leaving the page
|
|
2934
|
+
* resolves it only once the load answers or the element is back, since
|
|
2935
|
+
* that is when the restore learns it was stopped. A first `user-key` on an
|
|
2936
|
+
* anonymous conversation adopts it rather than replacing it, and stops
|
|
2937
|
+
* nothing. A second call while one waits is
|
|
2938
|
+
* refused, as a second call in the same task is.
|
|
2939
|
+
*
|
|
2940
|
+
* Unlike the built-in Send it does not queue: it returns, and the caller
|
|
2941
|
+
* keeps what it tried to send. Nor does it consult the tray -- what you pass
|
|
2942
|
+
* is what is sent.
|
|
2943
|
+
*/
|
|
2944
|
+
async sendMessage(content: string, attachments: readonly AttachmentRef[] = []): Promise<boolean> {
|
|
2945
|
+
if (content === "" && attachments.length === 0) {
|
|
2946
|
+
return false;
|
|
2594
2947
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
this
|
|
2948
|
+
const restore = this.#restoreHold;
|
|
2949
|
+
if (restore !== null && this.#sending === restore.hold) {
|
|
2950
|
+
// Taken over from the restore, so a second host call is refused, the
|
|
2951
|
+
// built-in Send parks behind this one rather than going first, and the
|
|
2952
|
+
// restore's own release when it ends lets go of nothing -- this send's
|
|
2953
|
+
// run settling, or its end, is what sends the parked turn.
|
|
2954
|
+
const waiting = {};
|
|
2955
|
+
this.#sending = waiting;
|
|
2956
|
+
await restore.restored;
|
|
2957
|
+
// Anything that stopped the restore let go of this too, by the Stop
|
|
2958
|
+
// that New chat, a thread switch, a principal handover, a reload and the
|
|
2959
|
+
// element leaving all reach. Otherwise let go of here and the guards
|
|
2960
|
+
// asked again from the top, as for a call made now.
|
|
2961
|
+
//
|
|
2962
|
+
// Removing this check fails "drops the waiting send and the queued turn
|
|
2963
|
+
// when New chat is pressed", among the other drop tests.
|
|
2964
|
+
if (this.#sending !== waiting) {
|
|
2965
|
+
return false;
|
|
2966
|
+
}
|
|
2967
|
+
this.#sending = null;
|
|
2968
|
+
return this.sendMessage(content, attachments);
|
|
2969
|
+
}
|
|
2970
|
+
if (this.#running || this.#sending !== null || this.#history.continuation !== null) {
|
|
2971
|
+
return false;
|
|
2972
|
+
}
|
|
2973
|
+
const send = {};
|
|
2974
|
+
this.#sending = send;
|
|
2975
|
+
try {
|
|
2976
|
+
// Only a send travels. Every other way out of the empty state -- a
|
|
2977
|
+
// restored transcript, a thread picked from the drawer, a resumed
|
|
2978
|
+
// checkpoint -- is a change of context rather than a continuation of what
|
|
2979
|
+
// the user was doing, and snaps. Armed before the bubble lands so both
|
|
2980
|
+
// writes reach the same style recalculation, which is what makes the
|
|
2981
|
+
// change a transition rather than a jump.
|
|
2982
|
+
if (this.#transcript.isEmpty()) {
|
|
2983
|
+
this.setAttribute("data-composer-settling", "");
|
|
2984
|
+
}
|
|
2985
|
+
// Taken before the announcement, because its listeners run here: a host's
|
|
2986
|
+
// own send from one is refused as overlapping this one, as it would be
|
|
2987
|
+
// from any later moment of it.
|
|
2988
|
+
this.#announceTurn(content, attachments);
|
|
2989
|
+
// A listener can also have stopped it -- New chat, a thread switch, a
|
|
2990
|
+
// change of principal, moving the element. All of them reach the Stop,
|
|
2991
|
+
// which let go of this send, and the turn would otherwise go out into the
|
|
2992
|
+
// conversation that replaced it, or from an element nothing can stop.
|
|
2993
|
+
if (this.#sending !== send) {
|
|
2994
|
+
return false;
|
|
2995
|
+
}
|
|
2996
|
+
// Read where the request is made, which says why nothing was sent when
|
|
2997
|
+
// there is no endpoint to send to.
|
|
2998
|
+
const connected = this.endpoint !== "";
|
|
2999
|
+
await this.#client_send(content, attachments);
|
|
3000
|
+
return connected;
|
|
3001
|
+
} finally {
|
|
3002
|
+
// Still held here only if its run never started: no endpoint, or a
|
|
3003
|
+
// client or store that threw before asking for one. No run settles
|
|
3004
|
+
// then, so this is what sends a turn queued behind it. Not released if
|
|
3005
|
+
// it is no longer this send's: the run starting let go of it, or a Stop
|
|
3006
|
+
// did, and the field may hold the next send by now.
|
|
3007
|
+
if (this.#sending === send) {
|
|
3008
|
+
this.#sending = null;
|
|
3009
|
+
this.#flushQueued();
|
|
3010
|
+
}
|
|
2603
3011
|
}
|
|
2604
|
-
this.#announceTurn(content, attachments);
|
|
2605
|
-
await this.#client_send(content, attachments);
|
|
2606
3012
|
}
|
|
2607
3013
|
|
|
2608
3014
|
/**
|
|
@@ -2714,6 +3120,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
2714
3120
|
// closes -- and read live, that save landed after New chat had moved the
|
|
2715
3121
|
// active thread on, filing the conversation being left under the new one.
|
|
2716
3122
|
const threadId = this.#history.threadId;
|
|
3123
|
+
// Fixed for the same reason, and checked by every write below: see #tenure.
|
|
3124
|
+
// The thread id alone cannot do it, because the store is still read live --
|
|
3125
|
+
// it has to be, since an adoption rescopes it under a run that keeps going.
|
|
3126
|
+
const tenure = this.#tenure;
|
|
2717
3127
|
const agent = this.agentFactory({
|
|
2718
3128
|
endpoint: seed.endpoint,
|
|
2719
3129
|
headers: this.#requestHeaders(),
|
|
@@ -2738,12 +3148,30 @@ export class AgUiChat extends HTMLElement {
|
|
|
2738
3148
|
// list per thread. For the conversation's own client that is its
|
|
2739
3149
|
// history; a continuation holds only what it adds, and writes the
|
|
2740
3150
|
// conversation it continues ahead of that.
|
|
3151
|
+
//
|
|
3152
|
+
// Every message list a client saves reaches the store through here -- a
|
|
3153
|
+
// send, a round's end, a tool's result, the answers it writes for calls
|
|
3154
|
+
// left open or declined by a Stop, a Stop's last save -- and so does a
|
|
3155
|
+
// `data-threads-url` store's local copy, which is the only thing that
|
|
3156
|
+
// wrapper does on a save. So this is the one gate for them. `onSaved` is
|
|
3157
|
+
// behind it as well: it hands the saved conversation on as the seed of
|
|
3158
|
+
// the next client, which would be the next principal's.
|
|
2741
3159
|
onPersist: (messages) => {
|
|
3160
|
+
if (tenure !== this.#tenure) {
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
2742
3163
|
const conversation = [...seed.follows, ...messages];
|
|
2743
3164
|
this.conversationStore.saveMessages(threadId, conversation);
|
|
2744
3165
|
seed.onSaved?.(conversation);
|
|
2745
3166
|
},
|
|
2746
|
-
|
|
3167
|
+
// Shared state is the next principal's first `RunAgentInput.state`, and
|
|
3168
|
+
// the handover has just emptied it. A snapshot already read off the wire
|
|
3169
|
+
// is applied after the Stop, and would put it back.
|
|
3170
|
+
onStateChanged: (state) => {
|
|
3171
|
+
if (tenure === this.#tenure) {
|
|
3172
|
+
this.#onSharedStateChanged(state);
|
|
3173
|
+
}
|
|
3174
|
+
},
|
|
2747
3175
|
connectionLostMessage: this.#strings.connectionLost,
|
|
2748
3176
|
unfinishedMessage: this.#strings.callNotFinished,
|
|
2749
3177
|
declinedMessage: this.#strings.declinedAction,
|