@artooi/ag-ui-web-component 0.41.3 → 0.42.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 +176 -1
- package/README.md +76 -19
- package/dist/ag-ui-web-component.bundle.js +21 -21
- 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 +17 -5
- 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 +573 -99
- package/dist/index.js.map +2 -2
- package/dist/tools/tool_dispatch.d.ts +13 -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 +68 -1
- package/dist/ui/history/conversation_history.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +3 -1
- package/src/core/ag_ui_chat.ts +398 -60
- package/src/core/agui_client.ts +55 -12
- package/src/core/storage_scope.ts +21 -8
- package/src/tools/tool_dispatch.ts +114 -3
- package/src/ui/composer/voice_input.ts +35 -4
- package/src/ui/history/conversation_history.ts +322 -55
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -1731,11 +1731,19 @@ var ToolDispatch = class {
|
|
|
1731
1731
|
* Execute one frontend tool call the round produced, against its own card:
|
|
1732
1732
|
* refused when the page moved under the round, asked about when a rule gates
|
|
1733
1733
|
* it, and settled with what the handler returned or threw.
|
|
1734
|
+
*
|
|
1735
|
+
* Every await below is a place the run can end, so each is followed by a
|
|
1736
|
+
* check that it has not. A Stop while the host's predicate is pending ends
|
|
1737
|
+
* the call there, and one while the card is open declines it. A handover
|
|
1738
|
+
* during any of the three leaves the call with no transcript to draw in and
|
|
1739
|
+
* no store to write to, so it does neither.
|
|
1734
1740
|
*/
|
|
1735
1741
|
async execute(call) {
|
|
1736
1742
|
if (skillNameFrom(call) !== null) {
|
|
1737
1743
|
return null;
|
|
1738
1744
|
}
|
|
1745
|
+
const tenure = this.#host.tenure();
|
|
1746
|
+
const threadId = this.#host.threadId();
|
|
1739
1747
|
const card = this.#host.transcript.cardFor(call);
|
|
1740
1748
|
this.#host.transcript.forgetCard(call.id);
|
|
1741
1749
|
this.#host.transcript.setCardElement(call.id, card.element);
|
|
@@ -1752,7 +1760,12 @@ var ToolDispatch = class {
|
|
|
1752
1760
|
this.#host.transcript.showPending();
|
|
1753
1761
|
return { content: `Error: ${message}`, error: message, outcome: TOOL_OUTCOME.FAILED };
|
|
1754
1762
|
}
|
|
1763
|
+
const asking = this.#host.decision.open();
|
|
1755
1764
|
const rule = await this.#confirmationRule(call, tool);
|
|
1765
|
+
this.#host.decision.close();
|
|
1766
|
+
if (asking.aborted) {
|
|
1767
|
+
return this.#abandoned(card);
|
|
1768
|
+
}
|
|
1756
1769
|
if (rule === "unanswered") {
|
|
1757
1770
|
const message = this.#host.strings().confirmCheckFailed;
|
|
1758
1771
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
@@ -1776,6 +1789,9 @@ var ToolDispatch = class {
|
|
|
1776
1789
|
this.#host.transcript.follow();
|
|
1777
1790
|
const accepted = await decision;
|
|
1778
1791
|
this.#host.decision.close();
|
|
1792
|
+
if (this.#host.tenure() !== tenure) {
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1779
1795
|
card.recordDecision(accepted ? "approved" : "declined");
|
|
1780
1796
|
if (!accepted) {
|
|
1781
1797
|
const message = this.#host.strings().declinedAction;
|
|
@@ -1783,13 +1799,19 @@ var ToolDispatch = class {
|
|
|
1783
1799
|
this.#host.transcript.showPending();
|
|
1784
1800
|
return { content: message, outcome: TOOL_OUTCOME.DENIED };
|
|
1785
1801
|
}
|
|
1802
|
+
if (signal.aborted) {
|
|
1803
|
+
return this.#abandoned(card);
|
|
1804
|
+
}
|
|
1786
1805
|
}
|
|
1787
1806
|
const navigates = isNavigates(tool.parameters) && this.#host.navigate() === null;
|
|
1788
1807
|
if (navigates) {
|
|
1789
|
-
this.#host.conversationStore().saveCheckpoint(
|
|
1808
|
+
this.#host.conversationStore().saveCheckpoint(threadId, { toolCallId: call.id });
|
|
1790
1809
|
}
|
|
1791
1810
|
try {
|
|
1792
1811
|
const result = await tool.handler(call.args, call.id);
|
|
1812
|
+
if (this.#host.tenure() !== tenure) {
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1793
1815
|
if (tool.render !== void 0) {
|
|
1794
1816
|
this.#host.transcript.renderToolOutput(tool.render, call);
|
|
1795
1817
|
}
|
|
@@ -1802,8 +1824,11 @@ var ToolDispatch = class {
|
|
|
1802
1824
|
this.#host.transcript.showPending();
|
|
1803
1825
|
return { content };
|
|
1804
1826
|
} catch (error) {
|
|
1827
|
+
if (this.#host.tenure() !== tenure) {
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
1805
1830
|
if (navigates) {
|
|
1806
|
-
this.#host.conversationStore().saveCheckpoint(
|
|
1831
|
+
this.#host.conversationStore().saveCheckpoint(threadId, null);
|
|
1807
1832
|
}
|
|
1808
1833
|
const message = error instanceof Error ? error.message : String(error);
|
|
1809
1834
|
card.settle(TOOL_CALL_STATUS.ERROR, message);
|
|
@@ -1960,6 +1985,25 @@ var ToolDispatch = class {
|
|
|
1960
1985
|
}
|
|
1961
1986
|
return isDestructive(tool.parameters) ? "destructive" : null;
|
|
1962
1987
|
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Settle a call whose run ended while it waited, and give the loop nothing.
|
|
1990
|
+
*
|
|
1991
|
+
* The card is settled here because nothing else will: dispatch took it out
|
|
1992
|
+
* of the end-of-run sweep before it waited, so left alone it read "running…"
|
|
1993
|
+
* for good. It says the call did not finish, which is true -- nothing ran and
|
|
1994
|
+
* nobody refused it.
|
|
1995
|
+
*
|
|
1996
|
+
* `null` rather than a result, because a stopped run has no next round to
|
|
1997
|
+
* send one in. The loop moves on and finds the run stopped, and the call is
|
|
1998
|
+
* left unanswered like every call after it in the round, so the client
|
|
1999
|
+
* answers it as not finished on the next request -- in the same words as
|
|
2000
|
+
* this card. After a handover the card is in a transcript already cleared,
|
|
2001
|
+
* and settling it there is seen by no one.
|
|
2002
|
+
*/
|
|
2003
|
+
#abandoned(card) {
|
|
2004
|
+
card.settle(TOOL_CALL_STATUS.INTERRUPTED, this.#host.strings().callNotFinished);
|
|
2005
|
+
return null;
|
|
2006
|
+
}
|
|
1963
2007
|
/**
|
|
1964
2008
|
* Whether the page moved since the current round's context was built.
|
|
1965
2009
|
*
|
|
@@ -3065,6 +3109,12 @@ var VoiceInput = class {
|
|
|
3065
3109
|
this.#fail(this.#strings.transcriptionFailed);
|
|
3066
3110
|
return;
|
|
3067
3111
|
}
|
|
3112
|
+
if (this.#disposed) {
|
|
3113
|
+
for (const track of stream.getTracks()) {
|
|
3114
|
+
track.stop();
|
|
3115
|
+
}
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3068
3118
|
this.#stream = stream;
|
|
3069
3119
|
this.#chunks = [];
|
|
3070
3120
|
this.#hitCap = false;
|
|
@@ -3095,10 +3145,16 @@ var VoiceInput = class {
|
|
|
3095
3145
|
}
|
|
3096
3146
|
}
|
|
3097
3147
|
/**
|
|
3098
|
-
* Tear the control down, for a host element removed mid-recording
|
|
3099
|
-
* live `MediaRecorder`, releases the mic
|
|
3100
|
-
* indicator clears, and suppresses the
|
|
3101
|
-
* disconnected control must not fire `onText` into
|
|
3148
|
+
* Tear the control down, for a host element removed mid-recording or handed
|
|
3149
|
+
* to the next principal. Stops any live `MediaRecorder`, releases the mic
|
|
3150
|
+
* tracks so the browser's recording indicator clears, and suppresses the
|
|
3151
|
+
* pending transcription — a disconnected control must not fire `onText` into
|
|
3152
|
+
* a detached element.
|
|
3153
|
+
*
|
|
3154
|
+
* Disposal is final: nothing un-disposes an instance, and the host's next
|
|
3155
|
+
* wiring builds a new one. So the disposed flag alone tells a stale
|
|
3156
|
+
* continuation from a live one, and a clip already posted when this runs is
|
|
3157
|
+
* dropped whenever its answer arrives.
|
|
3102
3158
|
*/
|
|
3103
3159
|
dispose() {
|
|
3104
3160
|
this.#disposed = true;
|
|
@@ -3118,6 +3174,9 @@ var VoiceInput = class {
|
|
|
3118
3174
|
const audio = new Blob(this.#chunks, { type: mimeType || "audio/webm" });
|
|
3119
3175
|
try {
|
|
3120
3176
|
const text4 = await this.#transcribe(audio);
|
|
3177
|
+
if (this.#disposed) {
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3121
3180
|
this.#setState("idle");
|
|
3122
3181
|
if (this.#hitCap) {
|
|
3123
3182
|
this.element.title = fillUiString(this.#strings.recordingLimit, {
|
|
@@ -3141,6 +3200,9 @@ var VoiceInput = class {
|
|
|
3141
3200
|
this.#stream = null;
|
|
3142
3201
|
}
|
|
3143
3202
|
#fail(message) {
|
|
3203
|
+
if (this.#disposed) {
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3144
3206
|
this.#releaseStream();
|
|
3145
3207
|
this.#recorder = null;
|
|
3146
3208
|
this.#setState("idle");
|
|
@@ -4104,6 +4166,8 @@ function toolStatusFromOutcome(outcome) {
|
|
|
4104
4166
|
}
|
|
4105
4167
|
|
|
4106
4168
|
// src/ui/history/conversation_history.ts
|
|
4169
|
+
var UNHELD = Object.freeze({ release: () => {
|
|
4170
|
+
} });
|
|
4107
4171
|
var ConversationHistory = class {
|
|
4108
4172
|
#host;
|
|
4109
4173
|
/** The active thread's id. Empty until the element connects. */
|
|
@@ -4119,9 +4183,20 @@ var ConversationHistory = class {
|
|
|
4119
4183
|
* the one it continued is still on screen when it saves.
|
|
4120
4184
|
*/
|
|
4121
4185
|
#cleared = 0;
|
|
4122
|
-
|
|
4123
|
-
|
|
4186
|
+
/**
|
|
4187
|
+
* Which restore may draw: bumped by every rehydrate, and by every clear of
|
|
4188
|
+
* the conversation on screen. A restore whose generation is stale draws
|
|
4189
|
+
* nothing more and resumes nothing: a newer one has started, or New chat, a
|
|
4190
|
+
* thread switch or a `user-key` handover has cleared what it was restoring.
|
|
4191
|
+
*/
|
|
4124
4192
|
#generation = 0;
|
|
4193
|
+
/**
|
|
4194
|
+
* The conversation the latest restore claimed, written by every restore as
|
|
4195
|
+
* it starts; `null` before the first. It speaks for the restore now current
|
|
4196
|
+
* only while its generation is the current one, because clearing the
|
|
4197
|
+
* conversation moves that on. See `#forgetStopped` for the one reader.
|
|
4198
|
+
*/
|
|
4199
|
+
#claim = null;
|
|
4125
4200
|
/** Built lazily from `data-runs-url`; `null` when the host didn't opt in. */
|
|
4126
4201
|
#runIndex = null;
|
|
4127
4202
|
/**
|
|
@@ -4169,6 +4244,7 @@ var ConversationHistory = class {
|
|
|
4169
4244
|
forgetRestored() {
|
|
4170
4245
|
this.#restored = [];
|
|
4171
4246
|
this.#cleared += 1;
|
|
4247
|
+
this.#generation += 1;
|
|
4172
4248
|
}
|
|
4173
4249
|
/** Delete the active thread if nothing was ever sent in it. */
|
|
4174
4250
|
reapUnsent() {
|
|
@@ -4281,6 +4357,7 @@ var ConversationHistory = class {
|
|
|
4281
4357
|
this.#refuse(this.#host.strings().continueNeedsTurn);
|
|
4282
4358
|
return;
|
|
4283
4359
|
}
|
|
4360
|
+
this.#host.recordTurn(content);
|
|
4284
4361
|
this.#host.input.value = "";
|
|
4285
4362
|
this.#host.autoGrow();
|
|
4286
4363
|
const cleared = this.#cleared;
|
|
@@ -4357,48 +4434,197 @@ var ConversationHistory = class {
|
|
|
4357
4434
|
* because the resume path answers it from the page the reload landed on; that
|
|
4358
4435
|
* exclusion is held by "resumes with the landed page's result, and its card says
|
|
4359
4436
|
* so" in `ag_ui_chat_reload_mid_run.test.ts`.
|
|
4437
|
+
*
|
|
4438
|
+
* **A restore that resumes holds the composer from before the load.** The
|
|
4439
|
+
* resume is a run, and it starts only once the store has answered. With a
|
|
4440
|
+
* remote store that is a real request, and a send, a Retry or a pick in it
|
|
4441
|
+
* was refused by nothing: each built the conversation's client before the
|
|
4442
|
+
* conversation had loaded, so a send went out carrying none of it and its
|
|
4443
|
+
* first save replaced the stored one, and the resume then ran on that same
|
|
4444
|
+
* client as a second run, answering a call the request no longer held. So
|
|
4445
|
+
* the composer is held as a send holds it, and the resume gives way to
|
|
4446
|
+
* nothing: what was typed parks behind the resumed run and goes out after its
|
|
4447
|
+
* answer, and a host's send or Retry is refused as it is behind any run.
|
|
4448
|
+
*
|
|
4449
|
+
* The other way round, the resume standing down for the send, was rejected.
|
|
4450
|
+
* The send that got in first had already gone out without the conversation,
|
|
4451
|
+
* so standing down kept that loss, and it also dropped the run the navigation
|
|
4452
|
+
* was part of, which the user asked for before the reload: the agent stopped
|
|
4453
|
+
* halfway through the task, with the navigating call never answered.
|
|
4454
|
+
*
|
|
4455
|
+
* **A restore stops where it was stopped, held or not.** New chat, a thread
|
|
4456
|
+
* switch, a `user-key` handover, a newer restore or the element leaving the
|
|
4457
|
+
* page can each come while the store answers, and from host code the restore
|
|
4458
|
+
* runs itself: the activity and tool renderers each replayed message draws
|
|
4459
|
+
* through, and `navigationResult`. From then on it draws nothing more and
|
|
4460
|
+
* resumes nothing. The hold cannot say so, because a restore with no run to
|
|
4461
|
+
* resume holds nothing. So the restore asks its generation, which clearing
|
|
4462
|
+
* the conversation moves on as a newer restore does, and asks it after the
|
|
4463
|
+
* load, after each replayed message -- as a Retry asks after each one -- and
|
|
4464
|
+
* once more before the resume. Stopped by anything but the element leaving
|
|
4465
|
+
* the page, it also forgets the checkpoint it loaded, since that was a Stop
|
|
4466
|
+
* for the run it would have resumed -- unless what replaced it is a restore
|
|
4467
|
+
* of the same conversation, still current, which answers for that run
|
|
4468
|
+
* instead; see `#forgetStopped`.
|
|
4469
|
+
*
|
|
4470
|
+
* What a restore with no run to resume still does not do is refuse a send
|
|
4471
|
+
* made while it loads. That send goes out without the stored conversation,
|
|
4472
|
+
* and its save replaces it.
|
|
4360
4473
|
*/
|
|
4361
4474
|
async rehydrate() {
|
|
4362
4475
|
this.#generation += 1;
|
|
4363
4476
|
const generation = this.#generation;
|
|
4364
|
-
this.#
|
|
4365
|
-
|
|
4477
|
+
const threadId = this.#threadId;
|
|
4478
|
+
const store = this.#host.conversationStore();
|
|
4479
|
+
this.#claim = { generation, store, threadId };
|
|
4480
|
+
const checkpoint = store.loadCheckpoint(threadId);
|
|
4481
|
+
const hold = checkpoint === null ? UNHELD : this.#host.holdRun();
|
|
4366
4482
|
try {
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4483
|
+
this.#host.element.setAttribute("data-restoring", "");
|
|
4484
|
+
let messages;
|
|
4485
|
+
try {
|
|
4486
|
+
messages = await store.loadMessages(threadId);
|
|
4487
|
+
} finally {
|
|
4488
|
+
if (generation === this.#generation) {
|
|
4489
|
+
this.#host.element.removeAttribute("data-restoring");
|
|
4490
|
+
}
|
|
4371
4491
|
}
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4492
|
+
if (!this.#restoring(generation)) {
|
|
4493
|
+
this.#forgetStopped(store, threadId, checkpoint);
|
|
4494
|
+
return;
|
|
4495
|
+
}
|
|
4496
|
+
if (messages !== null) {
|
|
4497
|
+
const unfinished = this.#host.strings().callNotFinished;
|
|
4498
|
+
const restored = answerUnansweredCalls(
|
|
4499
|
+
messages,
|
|
4500
|
+
// The shape the store holds for a call the client answered the same
|
|
4501
|
+
// way: its tool message, with the outcome in its metadata.
|
|
4502
|
+
(toolCallId) => ({
|
|
4503
|
+
id: randomUUID3(),
|
|
4504
|
+
role: "tool",
|
|
4505
|
+
content: unfinished,
|
|
4506
|
+
toolCallId,
|
|
4507
|
+
metadata: { outcome: TOOL_OUTCOME.INTERRUPTED }
|
|
4508
|
+
}),
|
|
4509
|
+
new Set(checkpoint === null ? [] : [checkpoint.toolCallId])
|
|
4510
|
+
);
|
|
4511
|
+
this.#restored = restored;
|
|
4512
|
+
for (const message of restored) {
|
|
4513
|
+
this.replay(message);
|
|
4514
|
+
if (!this.#restoring(generation)) {
|
|
4515
|
+
this.#forgetStopped(store, threadId, checkpoint);
|
|
4516
|
+
return;
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4395
4519
|
}
|
|
4520
|
+
if (checkpoint !== null) {
|
|
4521
|
+
await this.#resumeFrom(checkpoint, generation);
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
this.#noticeIfRunUnfinished(messages);
|
|
4525
|
+
} finally {
|
|
4526
|
+
hold.release();
|
|
4396
4527
|
}
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4528
|
+
}
|
|
4529
|
+
/**
|
|
4530
|
+
* Whether the restore that claimed `generation` may go on: nothing has
|
|
4531
|
+
* replaced or cleared the conversation it is restoring, and the element is
|
|
4532
|
+
* still on the page.
|
|
4533
|
+
*
|
|
4534
|
+
* On the page, because leaving it is the one stop that clears nothing: the
|
|
4535
|
+
* element is not reset until it connects again, and connecting restores the
|
|
4536
|
+
* conversation afresh. A restore with nothing to resume went on regardless,
|
|
4537
|
+
* drawing into a node that had gone and running the host's renderers for it.
|
|
4538
|
+
* One that resumes learnt it from the Stop letting go of its hold; nothing
|
|
4539
|
+
* asks the hold that now, so this is what keeps the resume's request in.
|
|
4540
|
+
*
|
|
4541
|
+
* One `&&`, so coverage cannot tell whether either half is held. Removing the
|
|
4542
|
+
* generation fails "draws nothing into a new chat started while it loads" in
|
|
4543
|
+
* `conversation_history.test.ts`, and "drops a stale thread replay when a
|
|
4544
|
+
* newer switch started" in `ag_ui_chat.test.ts`; removing the connection
|
|
4545
|
+
* fails "draws nothing into an element that left the page while it loads".
|
|
4546
|
+
*/
|
|
4547
|
+
#restoring(generation) {
|
|
4548
|
+
return generation === this.#generation && this.#host.element.isConnected;
|
|
4549
|
+
}
|
|
4550
|
+
/**
|
|
4551
|
+
* Forget the checkpoint a stopped restore loaded, when what stopped it also
|
|
4552
|
+
* stopped the run it was about to resume.
|
|
4553
|
+
*
|
|
4554
|
+
* New chat, a thread switch, a `user-key` handover and a newer restore all
|
|
4555
|
+
* move the generation, and each is a Stop for that run. Left in the store,
|
|
4556
|
+
* the checkpoint outlived the Stop: coming back to the conversation later
|
|
4557
|
+
* resumed the navigating call with whatever page was current by then.
|
|
4558
|
+
* Forgotten, the call is answered as not finished there, as any stopped call
|
|
4559
|
+
* is.
|
|
4560
|
+
*
|
|
4561
|
+
* Not while the restore now current claimed the same conversation, which it
|
|
4562
|
+
* has in two cases. It is this restore, when only the element left the page,
|
|
4563
|
+
* which moves no generation: a move or a framework re-render is not a
|
|
4564
|
+
* farewell, and the restore connecting starts has to find the checkpoint to
|
|
4565
|
+
* resume from. Or it is a newer restore of the same thread in the same store
|
|
4566
|
+
* -- `reload()`, switching away and back, or the element connecting again --
|
|
4567
|
+
* which read the checkpoint as it started, or found it gone, and answers for
|
|
4568
|
+
* it from then on: its resume clears it, and when it is stopped in turn this
|
|
4569
|
+
* same rule decides.
|
|
4570
|
+
*
|
|
4571
|
+
* The newer restore's case used to forget it too, on the grounds that the
|
|
4572
|
+
* newer one had read it and would resume once from that copy. It did until
|
|
4573
|
+
* the element left the page while the newer one loaded: that one stood down
|
|
4574
|
+
* keeping the checkpoint for the reconnect, as a departure should, but the
|
|
4575
|
+
* store no longer held it, so the element came back with no run to resume.
|
|
4576
|
+
*
|
|
4577
|
+
* Written to the store and the thread it was read from. After a handover
|
|
4578
|
+
* that is the previous principal's: the built-in store's clear is a removal
|
|
4579
|
+
* from a namespace the handover has already purged, so it changes nothing,
|
|
4580
|
+
* and nothing reaches the next principal's. A host's store is the same object
|
|
4581
|
+
* either side of a handover; one the host replaced is held by "still refuses
|
|
4582
|
+
* a send for the newer restore once the older has stood down".
|
|
4583
|
+
*
|
|
4584
|
+
* Two conditions. Clearing with no checkpoint fails "draws nothing into a new
|
|
4585
|
+
* chat started while it loads", which runs no store code for one. Clearing
|
|
4586
|
+
* what the current restore claimed fails "resumes nothing once the element
|
|
4587
|
+
* leaves the page while the store answers", where the claim is this
|
|
4588
|
+
* restore's own and the re-inserted element resumes, and "resumes once the
|
|
4589
|
+
* element is back when the host reloads and it then leaves the page", where
|
|
4590
|
+
* it is the reload's. The claim's own three are named on `#claimedByCurrent`.
|
|
4591
|
+
*/
|
|
4592
|
+
#forgetStopped(store, threadId, checkpoint) {
|
|
4593
|
+
if (checkpoint !== null && !this.#claimedByCurrent(store, threadId)) {
|
|
4594
|
+
store.saveCheckpoint(threadId, null);
|
|
4400
4595
|
}
|
|
4401
|
-
|
|
4596
|
+
}
|
|
4597
|
+
/**
|
|
4598
|
+
* Whether the restore now current claimed the conversation a stopped restore
|
|
4599
|
+
* read its checkpoint from: the same thread, in the same store.
|
|
4600
|
+
*
|
|
4601
|
+
* A claim from an earlier generation speaks for nobody: the conversation was
|
|
4602
|
+
* cleared after it was made, so whatever it would have resumed was stopped,
|
|
4603
|
+
* and that holds of a stopped restore's own claim as much as of any other.
|
|
4604
|
+
* The store is compared as an object, because a store the host assigned
|
|
4605
|
+
* since is a different record of the conversation, and the one the
|
|
4606
|
+
* checkpoint was read from still has to forget it.
|
|
4607
|
+
*
|
|
4608
|
+
* As an object, a store the element builds for itself -- the built-in one
|
|
4609
|
+
* under a namespace, and the remote one `data-threads-url` wraps around it
|
|
4610
|
+
* -- is a different one after a reconnect, which builds it afresh over the
|
|
4611
|
+
* same storage, so a restore from before the reconnect forgets the
|
|
4612
|
+
* checkpoint there once it answers. The reconnect's own restore read it as
|
|
4613
|
+
* it started and resumes from that copy, as a newer restore always did here;
|
|
4614
|
+
* what that copy cannot survive is the element leaving again before the
|
|
4615
|
+
* load answers.
|
|
4616
|
+
*
|
|
4617
|
+
* One `&&` of three, so coverage cannot tell whether any is held. Dropping
|
|
4618
|
+
* the generation fails "forgets the checkpoint when New chat stops the
|
|
4619
|
+
* resume, so the call ends as not finished", where the claim is the stopped
|
|
4620
|
+
* restore's own; dropping the store fails "still refuses a send for the newer
|
|
4621
|
+
* restore once the older has stood down", whose host replaces the store
|
|
4622
|
+
* between the two; dropping the thread fails "forgets the checkpoint of a
|
|
4623
|
+
* thread switched away from, though the next has one of its own".
|
|
4624
|
+
*/
|
|
4625
|
+
#claimedByCurrent(store, threadId) {
|
|
4626
|
+
const claim = this.#claim;
|
|
4627
|
+
return claim?.generation === this.#generation && claim.store === store && claim.threadId === threadId;
|
|
4402
4628
|
}
|
|
4403
4629
|
/**
|
|
4404
4630
|
* Notice a previous run that never produced a response.
|
|
@@ -4518,12 +4744,15 @@ var ConversationHistory = class {
|
|
|
4518
4744
|
* guard is held by "resumes a checkpointed navigating tool call on mount" in
|
|
4519
4745
|
* `ag_ui_chat.test.ts`, whose transcript never made the call.
|
|
4520
4746
|
*/
|
|
4521
|
-
async #resumeFrom(checkpoint) {
|
|
4747
|
+
async #resumeFrom(checkpoint, generation) {
|
|
4522
4748
|
this.#host.conversationStore().saveCheckpoint(this.#threadId, null);
|
|
4523
4749
|
const client = this.#host.ensureClient();
|
|
4524
4750
|
const landed = JSON.stringify(
|
|
4525
4751
|
this.#host.navigationResult().call(this.#host.element, checkpoint)
|
|
4526
4752
|
);
|
|
4753
|
+
if (!this.#restoring(generation)) {
|
|
4754
|
+
return;
|
|
4755
|
+
}
|
|
4527
4756
|
client.addToolResult(checkpoint.toolCallId, landed);
|
|
4528
4757
|
this.#host.transcript.card(checkpoint.toolCallId)?.settle(TOOL_CALL_STATUS.DONE, landed);
|
|
4529
4758
|
await client.resume();
|
|
@@ -14999,8 +15228,10 @@ var AgUiClient = class {
|
|
|
14999
15228
|
#unfinishedMessage;
|
|
15000
15229
|
#declinedMessage;
|
|
15001
15230
|
#maxToolRounds;
|
|
15002
|
-
// Set by cancel(); reset
|
|
15003
|
-
// a cancel between frontend-tool
|
|
15231
|
+
// Set by cancel(); reset as send() or resume() begins, before either runs
|
|
15232
|
+
// any host code. Checked by the loop so a cancel between frontend-tool
|
|
15233
|
+
// rounds doesn't start another round, and again just before each request,
|
|
15234
|
+
// so a cancel from host code the round itself runs is not sent anyway.
|
|
15004
15235
|
#cancelled = false;
|
|
15005
15236
|
constructor(config) {
|
|
15006
15237
|
this.#agent = config.agent;
|
|
@@ -15056,6 +15287,7 @@ var AgUiClient = class {
|
|
|
15056
15287
|
* {@link messageAttachments}.
|
|
15057
15288
|
*/
|
|
15058
15289
|
async send(content, attachments = []) {
|
|
15290
|
+
this.#cancelled = false;
|
|
15059
15291
|
const message = {
|
|
15060
15292
|
id: randomUUID4(),
|
|
15061
15293
|
role: "user",
|
|
@@ -15071,11 +15303,14 @@ var AgUiClient = class {
|
|
|
15071
15303
|
* can be asked again.
|
|
15072
15304
|
*
|
|
15073
15305
|
* Returns the retained history, or `null` when there is nothing to retry (no
|
|
15074
|
-
* user message has been sent yet).
|
|
15075
|
-
*
|
|
15076
|
-
*
|
|
15077
|
-
*
|
|
15078
|
-
*
|
|
15306
|
+
* user message has been sent yet). The shortened history is saved before it
|
|
15307
|
+
* replaces the current one, so a save that throws changes nothing and the
|
|
15308
|
+
* throw propagates.
|
|
15309
|
+
*
|
|
15310
|
+
* **Truncates only** -- the caller re-renders from the returned list and then
|
|
15311
|
+
* calls {@link resume}, because the transcript belongs to the element and a
|
|
15312
|
+
* client that reached into it would own two things. Running here instead
|
|
15313
|
+
* would stream the new answer in underneath the old one.
|
|
15079
15314
|
*
|
|
15080
15315
|
* Re-running answers the question the agent was last asked, rather than
|
|
15081
15316
|
* telling it its answer was wrong, which is what makes the result a
|
|
@@ -15097,8 +15332,8 @@ var AgUiClient = class {
|
|
|
15097
15332
|
return null;
|
|
15098
15333
|
}
|
|
15099
15334
|
const kept = messages.slice(0, lastUser + 1);
|
|
15335
|
+
this.#onPersist(kept);
|
|
15100
15336
|
this.#agent.setMessages(kept);
|
|
15101
|
-
this.#persist();
|
|
15102
15337
|
return kept;
|
|
15103
15338
|
}
|
|
15104
15339
|
/**
|
|
@@ -15107,6 +15342,7 @@ var AgUiClient = class {
|
|
|
15107
15342
|
* message; it continues the conversation already in history.
|
|
15108
15343
|
*/
|
|
15109
15344
|
async resume() {
|
|
15345
|
+
this.#cancelled = false;
|
|
15110
15346
|
await this.#run();
|
|
15111
15347
|
}
|
|
15112
15348
|
/** Append a frontend tool result to history (used by the resume path). */
|
|
@@ -15120,13 +15356,19 @@ var AgUiClient = class {
|
|
|
15120
15356
|
* disconnect). Safe to call with no run in flight. Partial text already
|
|
15121
15357
|
* streamed stays in history; {@link AgUiClientHandlers.onCancelled} fires
|
|
15122
15358
|
* instead of `onError`, and `onSettled` still follows.
|
|
15359
|
+
*
|
|
15360
|
+
* Also stops a {@link send} or {@link resume} whose request has not been made
|
|
15361
|
+
* yet, when host code it runs on the way calls this: the save of the turn,
|
|
15362
|
+
* the tool and context providers, the save answering a call left open. No
|
|
15363
|
+
* request is made then, and `onCancelled` and `onSettled` fire as for any
|
|
15364
|
+
* other Stop. A cancel with nothing in flight is forgotten when the next
|
|
15365
|
+
* send or resume begins.
|
|
15123
15366
|
*/
|
|
15124
15367
|
cancel() {
|
|
15125
15368
|
this.#cancelled = true;
|
|
15126
15369
|
this.#agent.abortRun();
|
|
15127
15370
|
}
|
|
15128
15371
|
async #run() {
|
|
15129
|
-
this.#cancelled = false;
|
|
15130
15372
|
try {
|
|
15131
15373
|
await this.#runLoop();
|
|
15132
15374
|
if (this.#cancelled) {
|
|
@@ -15284,6 +15526,9 @@ var AgUiClient = class {
|
|
|
15284
15526
|
params.resume = resume;
|
|
15285
15527
|
}
|
|
15286
15528
|
this.#answerUnansweredCalls(resuming);
|
|
15529
|
+
if (this.#cancelled) {
|
|
15530
|
+
return;
|
|
15531
|
+
}
|
|
15287
15532
|
await this.#agent.runAgent(params, this.#buildSubscriber(pending, runState));
|
|
15288
15533
|
resume = void 0;
|
|
15289
15534
|
resuming = /* @__PURE__ */ new Set();
|
|
@@ -16206,9 +16451,12 @@ var StorageScope = class {
|
|
|
16206
16451
|
// remounts, but re-resolves if the host answers the warning with an `id`.
|
|
16207
16452
|
#generatedNs = "";
|
|
16208
16453
|
#generatedFor = "";
|
|
16209
|
-
// The `sessionStorage`-backed store, which the element
|
|
16210
|
-
// on a principal change
|
|
16211
|
-
//
|
|
16454
|
+
// The `sessionStorage`-backed store this scope handed out, which the element
|
|
16455
|
+
// may therefore re-scope on a principal change -- while it is still the store
|
|
16456
|
+
// in use, which rescopeStore asks, since a host can assign its own at any
|
|
16457
|
+
// time. `null` until connecting has met the built-in store at all. A store of
|
|
16458
|
+
// the host's own kind is never held here: the element does not know its
|
|
16459
|
+
// keying and must not guess at it.
|
|
16212
16460
|
#builtinStore = null;
|
|
16213
16461
|
constructor(host) {
|
|
16214
16462
|
this.#host = host;
|
|
@@ -16262,12 +16510,22 @@ var StorageScope = class {
|
|
|
16262
16510
|
return this.#builtinStore;
|
|
16263
16511
|
}
|
|
16264
16512
|
/**
|
|
16265
|
-
* Rebuild the built-in store under `namespace`, or `null`
|
|
16266
|
-
*
|
|
16267
|
-
*
|
|
16513
|
+
* Rebuild the built-in store under `namespace`, or `null` when `current` --
|
|
16514
|
+
* the store the element is using now, unwrapped from any remote it wrapped
|
|
16515
|
+
* it in -- is not the one this scope handed out.
|
|
16516
|
+
*
|
|
16517
|
+
* Asked of the store in use rather than of what connecting remembered,
|
|
16518
|
+
* because the two part company the moment a host assigns a store of its own
|
|
16519
|
+
* to a connected element. Remembering was enough to tell a host's store from
|
|
16520
|
+
* the built-in one at connect; after it, the built-in one was still
|
|
16521
|
+
* remembered, so a key change replaced the host's store with a fresh
|
|
16522
|
+
* `sessionStorage` one -- and a host that had chosen to keep message bodies
|
|
16523
|
+
* off the client found every one of them cached in the tab again. A store
|
|
16524
|
+
* the element did not make holds its data somewhere the element cannot see,
|
|
16525
|
+
* and has to scope itself.
|
|
16268
16526
|
*/
|
|
16269
|
-
rescopeStore(namespace) {
|
|
16270
|
-
if (this.#builtinStore
|
|
16527
|
+
rescopeStore(current, namespace) {
|
|
16528
|
+
if (current !== this.#builtinStore) {
|
|
16271
16529
|
return null;
|
|
16272
16530
|
}
|
|
16273
16531
|
this.#builtinStore = new SessionStorageStore(namespace);
|
|
@@ -16791,6 +17049,13 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16791
17049
|
#queuedRow = document.createElement("div");
|
|
16792
17050
|
/** How far back the composer has been walked; null while the user is typing. */
|
|
16793
17051
|
#recallIndex = null;
|
|
17052
|
+
/**
|
|
17053
|
+
* What the walk last put in the composer, read back from it. A walk is live
|
|
17054
|
+
* only while the composer still holds exactly this, so a write from anywhere
|
|
17055
|
+
* else ends it. Stale while no walk is in progress, which is harmless: the
|
|
17056
|
+
* comparison can only ever end a walk, never start one.
|
|
17057
|
+
*/
|
|
17058
|
+
#recalled = "";
|
|
16794
17059
|
/**
|
|
16795
17060
|
* Re-clamp the dragged launcher when the window changes size. Bound once as
|
|
16796
17061
|
* a field so `removeEventListener` on disconnect gets the same reference.
|
|
@@ -16826,6 +17091,39 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16826
17091
|
* history replay would draw the conversation a second time beneath it.
|
|
16827
17092
|
*/
|
|
16828
17093
|
#connectedBefore = false;
|
|
17094
|
+
/**
|
|
17095
|
+
* The `user-key` whose state the element holds -- the transcript, the recall
|
|
17096
|
+
* history, the Always allow waivers and the shared state -- or `null` before
|
|
17097
|
+
* the first connect, when it holds nobody's.
|
|
17098
|
+
*
|
|
17099
|
+
* Not the same thing as the attribute. The attribute names the principal the
|
|
17100
|
+
* host says is here now, and it can change while the element is out of the
|
|
17101
|
+
* document, where nothing acts on it; this is who the state still belongs
|
|
17102
|
+
* to. Connecting compares the two, which is how a key changed while detached
|
|
17103
|
+
* still gets the handover a live change gets.
|
|
17104
|
+
*/
|
|
17105
|
+
#principalKey = null;
|
|
17106
|
+
/**
|
|
17107
|
+
* The current principal's tenure: replaced on every handover from one
|
|
17108
|
+
* principal to another, and captured by each client as it is built, so that
|
|
17109
|
+
* a client can tell whether the principal it was built for is still the one
|
|
17110
|
+
* this element serves.
|
|
17111
|
+
*
|
|
17112
|
+
* A client outlives the handover that stops it. A stopped run saves what it
|
|
17113
|
+
* had once its request closes, a tool handler cannot be aborted and its
|
|
17114
|
+
* result is kept, and an event already read off the wire is still applied --
|
|
17115
|
+
* all of it after the store was purged and scoped to whoever arrived, so
|
|
17116
|
+
* each one wrote the previous principal's words into the next one's storage
|
|
17117
|
+
* or state. Writing into the previous principal's store instead would only
|
|
17118
|
+
* re-create what the purge just removed, so such a client writes nothing.
|
|
17119
|
+
*
|
|
17120
|
+
* An identity rather than the key itself, for two reasons. The key's first
|
|
17121
|
+
* arrival is an adoption, not a handover, and a run in flight across it is
|
|
17122
|
+
* the same person's and keeps saving. And a key that leaves and comes back
|
|
17123
|
+
* is a second tenure: the first one's stopped run must not re-create a
|
|
17124
|
+
* conversation the handover purged.
|
|
17125
|
+
*/
|
|
17126
|
+
#tenure = {};
|
|
16829
17127
|
/**
|
|
16830
17128
|
* The remote store `data-threads-url` wrapped around the conversation store,
|
|
16831
17129
|
* and the store inside it, so connecting again can wrap that store rather
|
|
@@ -16844,6 +17142,33 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16844
17142
|
// the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
|
|
16845
17143
|
// rounds, but the user must still be able to stop there.
|
|
16846
17144
|
#running = false;
|
|
17145
|
+
/**
|
|
17146
|
+
* The send {@link sendMessage} has taken, or the retry {@link retryLastTurn}
|
|
17147
|
+
* has, whose run has not started yet; null otherwise.
|
|
17148
|
+
*
|
|
17149
|
+
* `running` turns on when the client starts the run, a microtask after it is
|
|
17150
|
+
* asked for one, and a send does two things before it asks: it draws the
|
|
17151
|
+
* turn, and it dispatches the submit event, whose listeners run in the middle
|
|
17152
|
+
* of the send. A send from one of them, or a second call in the same task,
|
|
17153
|
+
* passed every guard and started a second run on the same client -- two
|
|
17154
|
+
* requests, and the listener's turn drawn between the user's turn and its
|
|
17155
|
+
* answer. It is the gap a picked checkpoint closes by counting from the pick,
|
|
17156
|
+
* closed the same way for a send. A retry asks for its run the same way, so
|
|
17157
|
+
* it takes the same hold, and so does a restore that will resume a run, from
|
|
17158
|
+
* before it asks the store for the conversation, through the history host's
|
|
17159
|
+
* `holdRun`.
|
|
17160
|
+
*
|
|
17161
|
+
* Let go of once the run starts, because `running` holds from then until the
|
|
17162
|
+
* run settles, and a send held past that would refuse a host's follow-up from
|
|
17163
|
+
* the run-finished event, which fires as the run settles. Let go of by the
|
|
17164
|
+
* send's, the retry's or the restore's end when its run never starts.
|
|
17165
|
+
*
|
|
17166
|
+
* An object rather than a flag, so a send or a retry ending releases only
|
|
17167
|
+
* what it took. By the time either ends, the turn queued behind it has
|
|
17168
|
+
* usually taken the field for itself, and a Stop lets go of it for the next
|
|
17169
|
+
* send to take; the earlier one ending must release neither.
|
|
17170
|
+
*/
|
|
17171
|
+
#sending = null;
|
|
16847
17172
|
/** The decision a run is suspended on, which a Stop abandons. */
|
|
16848
17173
|
#decision = new PendingDecision();
|
|
16849
17174
|
/**
|
|
@@ -16933,7 +17258,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16933
17258
|
approvalRenderer: () => this.approvalRenderer,
|
|
16934
17259
|
getContext: () => this.getContext(),
|
|
16935
17260
|
conversationStore: () => this.conversationStore,
|
|
16936
|
-
threadId: () => this.#history.threadId
|
|
17261
|
+
threadId: () => this.#history.threadId,
|
|
17262
|
+
// Read live and compared, the way each client compares the one it was
|
|
17263
|
+
// built with: see #tenure.
|
|
17264
|
+
tenure: () => this.#tenure
|
|
16937
17265
|
});
|
|
16938
17266
|
this.#runHandlers = new RunHandlers({
|
|
16939
17267
|
element: this,
|
|
@@ -17069,6 +17397,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17069
17397
|
// A continuation sends only what the composer holds. The tray is not
|
|
17070
17398
|
// read, so nothing rides along to be drawn or announced.
|
|
17071
17399
|
announceTurn: (content) => this.#announceTurn(content, []),
|
|
17400
|
+
recordTurn: (content) => this.#recordTurn(content),
|
|
17072
17401
|
autoGrow: () => autoGrow(this.#input),
|
|
17073
17402
|
continuationEnded: () => this.#flushQueued(),
|
|
17074
17403
|
client: () => this.#client,
|
|
@@ -17077,7 +17406,37 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17077
17406
|
releaseClient: () => {
|
|
17078
17407
|
this.#client = null;
|
|
17079
17408
|
},
|
|
17080
|
-
|
|
17409
|
+
// A held send or retry is as much in flight to a pick as a run is: it has
|
|
17410
|
+
// asked this element's client for a run that has not started, and a
|
|
17411
|
+
// continuation started beside it was a second run in one conversation.
|
|
17412
|
+
// So is a restore that will resume a run, which holds through the same
|
|
17413
|
+
// field from before the store answers.
|
|
17414
|
+
running: () => this.#running || this.#sending !== null,
|
|
17415
|
+
// The hold a send takes, taken for a restore that will resume a run, so
|
|
17416
|
+
// what refuses a send held short of its run refuses one made while the
|
|
17417
|
+
// store is still answering: `sendMessage` returns, Send parks the turn,
|
|
17418
|
+
// a Retry and a pick refuse. The same field rather than a flag beside it,
|
|
17419
|
+
// because what lets go of a send's hold is right for this one too. The
|
|
17420
|
+
// resumed run starting lets go, in `#setRunning`, so there is no gap
|
|
17421
|
+
// after the resume is asked for, and a Stop does, in `#cancelRun`. A flag
|
|
17422
|
+
// the restore cleared itself would still be set when the resumed run
|
|
17423
|
+
// settles and sends the parked turn, so that send would be refused after
|
|
17424
|
+
// the turn had left the queue, and lost.
|
|
17425
|
+
//
|
|
17426
|
+
// Whether the restore was stopped is not read off this hold, because one
|
|
17427
|
+
// with nothing to resume takes none; the restore asks its own generation.
|
|
17428
|
+
holdRun: () => {
|
|
17429
|
+
const hold = {};
|
|
17430
|
+
this.#sending = hold;
|
|
17431
|
+
return {
|
|
17432
|
+
release: () => {
|
|
17433
|
+
if (this.#sending === hold) {
|
|
17434
|
+
this.#sending = null;
|
|
17435
|
+
this.#flushQueued();
|
|
17436
|
+
}
|
|
17437
|
+
}
|
|
17438
|
+
};
|
|
17439
|
+
},
|
|
17081
17440
|
cancelRun: () => this.#cancelRun(),
|
|
17082
17441
|
resetState: () => this.#resetState(),
|
|
17083
17442
|
setRunning: (running) => this.#setRunning(running)
|
|
@@ -17387,6 +17746,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17387
17746
|
}
|
|
17388
17747
|
this.#syncLauncher();
|
|
17389
17748
|
this.#skills.init();
|
|
17749
|
+
const held = this.#principalKey;
|
|
17750
|
+
const changed = held !== null && held !== this.userKey;
|
|
17751
|
+
if (changed) {
|
|
17752
|
+
this.#changePrincipal(held, this.userKey);
|
|
17753
|
+
}
|
|
17754
|
+
this.#principalKey = this.userKey;
|
|
17390
17755
|
this.conversationStore = this.#storage.scopeStore(this.#unwrappedStore(), this.userKey);
|
|
17391
17756
|
window.addEventListener("resize", this.#onViewportResize);
|
|
17392
17757
|
window.visualViewport?.addEventListener("resize", this.#onViewportResize);
|
|
@@ -17398,6 +17763,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17398
17763
|
this.#history.adoptActiveThread();
|
|
17399
17764
|
queueMicrotask(() => this.#startup());
|
|
17400
17765
|
void this.#history.rehydrate();
|
|
17766
|
+
if (changed) {
|
|
17767
|
+
void this.#history.refreshDrawer();
|
|
17768
|
+
}
|
|
17401
17769
|
this.#connected = true;
|
|
17402
17770
|
}
|
|
17403
17771
|
/**
|
|
@@ -17722,23 +18090,49 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17722
18090
|
* logout, because a logout is a navigation (or, in a single-page app, not
|
|
17723
18091
|
* even that) rather than a tab close. Nothing remounts, so the host naming
|
|
17724
18092
|
* the new principal is the only signal the element will ever get.
|
|
18093
|
+
*
|
|
18094
|
+
* Two callers. A live change runs all of it. connectedCallback runs it for a
|
|
18095
|
+
* key that changed while the element was out of the document, and there it
|
|
18096
|
+
* runs before the store is scoped and before `#connected` is set -- which is
|
|
18097
|
+
* what `live` reads, since `isConnected` is already true by then. That call
|
|
18098
|
+
* does the half about what storage and the element still hold. The half
|
|
18099
|
+
* about the principal arriving -- the store scoped under the new key, its
|
|
18100
|
+
* active thread, the replay -- is what connecting goes on to do anyway, and
|
|
18101
|
+
* run here too it would read the old store before the new one exists,
|
|
18102
|
+
* minting a thread pointer in the namespace just purged and replaying the
|
|
18103
|
+
* history twice. Nor would it leave alone a store the host assigned while
|
|
18104
|
+
* the element was out.
|
|
17725
18105
|
*/
|
|
17726
18106
|
#changePrincipal(previousKey, nextKey) {
|
|
18107
|
+
this.#principalKey = nextKey;
|
|
18108
|
+
const live = this.#connected;
|
|
17727
18109
|
const previous = this.#storage.conversationNamespace(previousKey);
|
|
17728
18110
|
const next = this.#storage.conversationNamespace(nextKey);
|
|
17729
18111
|
if (previousKey === "") {
|
|
17730
18112
|
SessionStorageStore.adopt(previous, next);
|
|
17731
|
-
|
|
18113
|
+
if (live) {
|
|
18114
|
+
this.#rescopeStore(next);
|
|
18115
|
+
}
|
|
17732
18116
|
return;
|
|
17733
18117
|
}
|
|
18118
|
+
this.#tenure = {};
|
|
17734
18119
|
SessionStorageStore.purge(previous);
|
|
17735
|
-
|
|
18120
|
+
if (live) {
|
|
18121
|
+
this.#rescopeStore(next);
|
|
18122
|
+
}
|
|
17736
18123
|
this.#dispatch.forgetWaivers();
|
|
17737
18124
|
this.#cancelRun();
|
|
17738
18125
|
this.#resetState();
|
|
17739
18126
|
this.#sharedState = {};
|
|
18127
|
+
this.#input.value = "";
|
|
18128
|
+
this.#onInput();
|
|
17740
18129
|
this.#setRunning(false);
|
|
17741
18130
|
this.#setUnread(0);
|
|
18131
|
+
if (!live) {
|
|
18132
|
+
return;
|
|
18133
|
+
}
|
|
18134
|
+
this.#voice.dispose();
|
|
18135
|
+
this.#voice.wire();
|
|
17742
18136
|
this.#history.adoptActiveThread();
|
|
17743
18137
|
void this.#history.rehydrate();
|
|
17744
18138
|
void this.#history.refreshDrawer();
|
|
@@ -17747,13 +18141,17 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17747
18141
|
* Rebuild the `sessionStorage` store under `namespace`, re-wrapping it for
|
|
17748
18142
|
* `data-threads-url` exactly as connecting did.
|
|
17749
18143
|
*
|
|
17750
|
-
* A store
|
|
17751
|
-
* somewhere the element cannot see has to
|
|
18144
|
+
* A store the host assigned is left alone, whether before connecting or
|
|
18145
|
+
* since: a store that holds its data somewhere the element cannot see has to
|
|
18146
|
+
* scope itself. The exception is a plain `SessionStorageStore` assigned
|
|
18147
|
+
* before connecting, which connecting cannot tell from the default and so
|
|
18148
|
+
* took for the element's own. Hence the unwrapped store in use is what the scope is asked
|
|
18149
|
+
* about, rather than what it remembered when connecting. The transcript on
|
|
17752
18150
|
* screen is still cleared either way — the host swapped principals, and that
|
|
17753
18151
|
* much is the element's to act on.
|
|
17754
18152
|
*/
|
|
17755
18153
|
#rescopeStore(namespace) {
|
|
17756
|
-
const store = this.#storage.rescopeStore(namespace);
|
|
18154
|
+
const store = this.#storage.rescopeStore(this.#unwrappedStore(), namespace);
|
|
17757
18155
|
if (store === null) {
|
|
17758
18156
|
return;
|
|
17759
18157
|
}
|
|
@@ -17876,8 +18274,17 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17876
18274
|
*
|
|
17877
18275
|
* History is truncated to the most recent user message inclusive and the run
|
|
17878
18276
|
* repeats, so the agent answers what it was asked rather than being told its
|
|
17879
|
-
* last answer was wrong. Returns `false` when there is nothing to retry or
|
|
17880
|
-
*
|
|
18277
|
+
* last answer was wrong. Returns `false` when there is nothing to retry, or
|
|
18278
|
+
* while anything that refuses a send is in flight: a run, a send or another
|
|
18279
|
+
* retry whose run has not started yet, a checkpoint continuation, or a
|
|
18280
|
+
* reload's restore that will resume a run. Also `false` when host code it
|
|
18281
|
+
* runs before its run starts -- the store's save, or an activity or tool
|
|
18282
|
+
* renderer the replay draws through -- has stopped it by starting a new chat
|
|
18283
|
+
* or changing `user-key`. A tool or context provider that stops it does so
|
|
18284
|
+
* inside the client's resume, past the last point this can see: nothing is
|
|
18285
|
+
* sent, and this resolves `true`, because both a run starting and a Stop let
|
|
18286
|
+
* go of the hold it checks. Rejects with the store's error when the store
|
|
18287
|
+
* refuses the save, and then the conversation is as it was.
|
|
17881
18288
|
*
|
|
17882
18289
|
* Public because a host with its own message UI wants the same button, and
|
|
17883
18290
|
* because the failed-run notice reaches it from outside the action row.
|
|
@@ -17888,20 +18295,35 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17888
18295
|
* again -- unless the user waived it for this session.
|
|
17889
18296
|
*/
|
|
17890
18297
|
async retryLastTurn() {
|
|
17891
|
-
if (this.#running) {
|
|
17892
|
-
return false;
|
|
17893
|
-
}
|
|
17894
|
-
const client = this.#ensureClient();
|
|
17895
|
-
const kept = client.truncateToLastUser();
|
|
17896
|
-
if (kept === null) {
|
|
18298
|
+
if (this.#running || this.#sending !== null || this.#history.continuation !== null) {
|
|
17897
18299
|
return false;
|
|
17898
18300
|
}
|
|
17899
|
-
|
|
17900
|
-
|
|
17901
|
-
|
|
18301
|
+
const hold = {};
|
|
18302
|
+
this.#sending = hold;
|
|
18303
|
+
try {
|
|
18304
|
+
const client = this.#ensureClient();
|
|
18305
|
+
const kept = client.truncateToLastUser();
|
|
18306
|
+
if (kept === null) {
|
|
18307
|
+
return false;
|
|
18308
|
+
}
|
|
18309
|
+
if (this.#sending !== hold) {
|
|
18310
|
+
return false;
|
|
18311
|
+
}
|
|
18312
|
+
this.#clearTranscript();
|
|
18313
|
+
for (const message of kept) {
|
|
18314
|
+
this.#history.replay(message);
|
|
18315
|
+
if (this.#sending !== hold) {
|
|
18316
|
+
return false;
|
|
18317
|
+
}
|
|
18318
|
+
}
|
|
18319
|
+
await client.resume();
|
|
18320
|
+
return true;
|
|
18321
|
+
} finally {
|
|
18322
|
+
if (this.#sending === hold) {
|
|
18323
|
+
this.#sending = null;
|
|
18324
|
+
this.#flushQueued();
|
|
18325
|
+
}
|
|
17902
18326
|
}
|
|
17903
|
-
await client.resume();
|
|
17904
|
-
return true;
|
|
17905
18327
|
}
|
|
17906
18328
|
/**
|
|
17907
18329
|
* Append a message bubble and return it.
|
|
@@ -18213,6 +18635,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18213
18635
|
* which is what every shell and every coding agent means by this. Arrowing
|
|
18214
18636
|
* forward past the newest empties the composer again rather than sticking on
|
|
18215
18637
|
* it, so the way out is the same key that got you in.
|
|
18638
|
+
*
|
|
18639
|
+
* A walk ends with any write into the composer that is not its own, so that
|
|
18640
|
+
* whatever was written is never replaced by the next step.
|
|
18216
18641
|
*/
|
|
18217
18642
|
#recallHistory(event) {
|
|
18218
18643
|
const back = event.key === "ArrowUp";
|
|
@@ -18223,6 +18648,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18223
18648
|
if (drafts.length === 0) {
|
|
18224
18649
|
return;
|
|
18225
18650
|
}
|
|
18651
|
+
if (this.#input.value !== this.#recalled) {
|
|
18652
|
+
this.#recallIndex = null;
|
|
18653
|
+
}
|
|
18226
18654
|
if (this.#recallIndex === null && (!back || this.#input.value !== "")) {
|
|
18227
18655
|
return;
|
|
18228
18656
|
}
|
|
@@ -18233,6 +18661,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18233
18661
|
event.preventDefault();
|
|
18234
18662
|
this.#recallIndex = next < 0 ? null : next;
|
|
18235
18663
|
this.#input.value = next < 0 ? "" : drafts[next];
|
|
18664
|
+
this.#recalled = this.#input.value;
|
|
18236
18665
|
this.#input.setSelectionRange(this.#input.value.length, this.#input.value.length);
|
|
18237
18666
|
autoGrow(this.#input);
|
|
18238
18667
|
}
|
|
@@ -18243,15 +18672,11 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18243
18672
|
* observes the disconnect).
|
|
18244
18673
|
*/
|
|
18245
18674
|
#cancelRun() {
|
|
18246
|
-
for (const text4 of this.#queued) {
|
|
18247
|
-
if (this.#sentDrafts[0] !== text4) {
|
|
18248
|
-
this.#sentDrafts.unshift(text4);
|
|
18249
|
-
}
|
|
18250
|
-
}
|
|
18251
18675
|
this.#queued.length = 0;
|
|
18252
18676
|
this.#renderQueued();
|
|
18253
18677
|
this.#decision.abort();
|
|
18254
18678
|
this.#client?.cancel();
|
|
18679
|
+
this.#sending = null;
|
|
18255
18680
|
this.#history.stopContinuation();
|
|
18256
18681
|
}
|
|
18257
18682
|
/**
|
|
@@ -18264,6 +18689,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18264
18689
|
#setRunning(running) {
|
|
18265
18690
|
const settled = this.#running && !running;
|
|
18266
18691
|
this.#running = running;
|
|
18692
|
+
if (running) {
|
|
18693
|
+
this.#sending = null;
|
|
18694
|
+
}
|
|
18267
18695
|
const label = running ? this.#strings.stop : this.#strings.send;
|
|
18268
18696
|
this.#send.title = label;
|
|
18269
18697
|
this.#send.setAttribute("aria-label", label);
|
|
@@ -18314,13 +18742,29 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18314
18742
|
this.#queuedRow.appendChild(chip);
|
|
18315
18743
|
}
|
|
18316
18744
|
}
|
|
18745
|
+
/**
|
|
18746
|
+
* Put a turn that has left the composer at the front of the recall history,
|
|
18747
|
+
* and start the next walk from it.
|
|
18748
|
+
*
|
|
18749
|
+
* Every turn the user typed and pressed a key to send comes through here:
|
|
18750
|
+
* sent at once, queued behind a run, or sent to continue a checkpoint. A
|
|
18751
|
+
* host's own `sendMessage` does not, because its text was never in the
|
|
18752
|
+
* composer, and the arrow keys walk back through what the user wrote there.
|
|
18753
|
+
*/
|
|
18754
|
+
#recordTurn(content) {
|
|
18755
|
+
if (content !== "" && this.#sentDrafts[0] !== content) {
|
|
18756
|
+
this.#sentDrafts.unshift(content);
|
|
18757
|
+
}
|
|
18758
|
+
this.#recallIndex = null;
|
|
18759
|
+
}
|
|
18317
18760
|
async #submit() {
|
|
18318
18761
|
const content = this.#input.value.trim();
|
|
18319
18762
|
const attachments = this.#attachments.tray?.readyRefs() ?? [];
|
|
18320
18763
|
if (content === "" && attachments.length === 0) {
|
|
18321
18764
|
return;
|
|
18322
18765
|
}
|
|
18323
|
-
|
|
18766
|
+
this.#recordTurn(content);
|
|
18767
|
+
if (this.#running || this.#sending !== null || this.#history.continuation !== null) {
|
|
18324
18768
|
if (content !== "") {
|
|
18325
18769
|
this.#queued.push(content);
|
|
18326
18770
|
this.#renderQueued();
|
|
@@ -18329,10 +18773,6 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18329
18773
|
}
|
|
18330
18774
|
return;
|
|
18331
18775
|
}
|
|
18332
|
-
if (content !== "" && this.#sentDrafts[0] !== content) {
|
|
18333
|
-
this.#sentDrafts.unshift(content);
|
|
18334
|
-
}
|
|
18335
|
-
this.#recallIndex = null;
|
|
18336
18776
|
this.#input.value = "";
|
|
18337
18777
|
autoGrow(this.#input);
|
|
18338
18778
|
if (this.#attachments.tray?.hasPending() === true) {
|
|
@@ -18360,19 +18800,34 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18360
18800
|
*
|
|
18361
18801
|
* No-ops on an empty message, and while a run or a picked checkpoint's
|
|
18362
18802
|
* continuation is in flight, since a second concurrent run would orphan the
|
|
18363
|
-
* first.
|
|
18364
|
-
*
|
|
18365
|
-
*
|
|
18803
|
+
* first. A send is in flight from the moment it is taken rather than from
|
|
18804
|
+
* its run's start, so this no-ops from a {@link SUBMIT_EVENT} listener and
|
|
18805
|
+
* for a second call in the same task too.
|
|
18806
|
+
* Unlike the built-in Send it does not queue: it returns, and the caller
|
|
18807
|
+
* keeps what it tried to send. Nor does it consult the tray -- what you pass
|
|
18808
|
+
* is what is sent.
|
|
18366
18809
|
*/
|
|
18367
18810
|
async sendMessage(content, attachments = []) {
|
|
18368
|
-
if (this.#running || this.#history.continuation !== null || content === "" && attachments.length === 0) {
|
|
18811
|
+
if (this.#running || this.#sending !== null || this.#history.continuation !== null || content === "" && attachments.length === 0) {
|
|
18369
18812
|
return;
|
|
18370
18813
|
}
|
|
18371
|
-
|
|
18372
|
-
|
|
18814
|
+
const send = {};
|
|
18815
|
+
this.#sending = send;
|
|
18816
|
+
try {
|
|
18817
|
+
if (this.#transcript.isEmpty()) {
|
|
18818
|
+
this.setAttribute("data-composer-settling", "");
|
|
18819
|
+
}
|
|
18820
|
+
this.#announceTurn(content, attachments);
|
|
18821
|
+
if (this.#sending !== send) {
|
|
18822
|
+
return;
|
|
18823
|
+
}
|
|
18824
|
+
await this.#client_send(content, attachments);
|
|
18825
|
+
} finally {
|
|
18826
|
+
if (this.#sending === send) {
|
|
18827
|
+
this.#sending = null;
|
|
18828
|
+
this.#flushQueued();
|
|
18829
|
+
}
|
|
18373
18830
|
}
|
|
18374
|
-
this.#announceTurn(content, attachments);
|
|
18375
|
-
await this.#client_send(content, attachments);
|
|
18376
18831
|
}
|
|
18377
18832
|
/**
|
|
18378
18833
|
* Put a turn the user sent on screen, and tell the host it was sent.
|
|
@@ -18472,6 +18927,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18472
18927
|
*/
|
|
18473
18928
|
#buildClient(seed) {
|
|
18474
18929
|
const threadId = this.#history.threadId;
|
|
18930
|
+
const tenure = this.#tenure;
|
|
18475
18931
|
const agent = this.agentFactory({
|
|
18476
18932
|
endpoint: seed.endpoint,
|
|
18477
18933
|
headers: this.#requestHeaders(),
|
|
@@ -18496,12 +18952,30 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18496
18952
|
// list per thread. For the conversation's own client that is its
|
|
18497
18953
|
// history; a continuation holds only what it adds, and writes the
|
|
18498
18954
|
// conversation it continues ahead of that.
|
|
18955
|
+
//
|
|
18956
|
+
// Every message list a client saves reaches the store through here -- a
|
|
18957
|
+
// send, a round's end, a tool's result, the answers it writes for calls
|
|
18958
|
+
// left open or declined by a Stop, a Stop's last save -- and so does a
|
|
18959
|
+
// `data-threads-url` store's local copy, which is the only thing that
|
|
18960
|
+
// wrapper does on a save. So this is the one gate for them. `onSaved` is
|
|
18961
|
+
// behind it as well: it hands the saved conversation on as the seed of
|
|
18962
|
+
// the next client, which would be the next principal's.
|
|
18499
18963
|
onPersist: (messages) => {
|
|
18964
|
+
if (tenure !== this.#tenure) {
|
|
18965
|
+
return;
|
|
18966
|
+
}
|
|
18500
18967
|
const conversation = [...seed.follows, ...messages];
|
|
18501
18968
|
this.conversationStore.saveMessages(threadId, conversation);
|
|
18502
18969
|
seed.onSaved?.(conversation);
|
|
18503
18970
|
},
|
|
18504
|
-
|
|
18971
|
+
// Shared state is the next principal's first `RunAgentInput.state`, and
|
|
18972
|
+
// the handover has just emptied it. A snapshot already read off the wire
|
|
18973
|
+
// is applied after the Stop, and would put it back.
|
|
18974
|
+
onStateChanged: (state) => {
|
|
18975
|
+
if (tenure === this.#tenure) {
|
|
18976
|
+
this.#onSharedStateChanged(state);
|
|
18977
|
+
}
|
|
18978
|
+
},
|
|
18505
18979
|
connectionLostMessage: this.#strings.connectionLost,
|
|
18506
18980
|
unfinishedMessage: this.#strings.callNotFinished,
|
|
18507
18981
|
declinedMessage: this.#strings.declinedAction,
|
|
@@ -18663,7 +19137,7 @@ function setControlValue(el2, value) {
|
|
|
18663
19137
|
}
|
|
18664
19138
|
|
|
18665
19139
|
// src/version.ts
|
|
18666
|
-
var VERSION = "0.
|
|
19140
|
+
var VERSION = "0.42.0";
|
|
18667
19141
|
export {
|
|
18668
19142
|
ATTACHMENT_EVENT,
|
|
18669
19143
|
AgUiChat,
|