@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/dist/index.js
CHANGED
|
@@ -987,6 +987,7 @@ var DEFAULT_UI_STRINGS = {
|
|
|
987
987
|
notConnected: "This chat isn\u2019t connected to an agent, so the message wasn\u2019t sent.",
|
|
988
988
|
continueNeedsTurn: "Type the next turn in the composer first, then pick a run to continue.",
|
|
989
989
|
continueWhileRunning: "Wait for the current answer or stop it, then pick a run to continue.",
|
|
990
|
+
continueWhileLoading: "Wait for the conversation to finish loading, then pick a run to continue.",
|
|
990
991
|
skillNeeds: "\u201C{title}\u201D needs {fields} \u2014 fill it in below, then send.",
|
|
991
992
|
message: "Message",
|
|
992
993
|
inputPlaceholder: "Ask anything\u2026",
|
|
@@ -1731,11 +1732,23 @@ var ToolDispatch = class {
|
|
|
1731
1732
|
* Execute one frontend tool call the round produced, against its own card:
|
|
1732
1733
|
* refused when the page moved under the round, asked about when a rule gates
|
|
1733
1734
|
* it, and settled with what the handler returned or threw.
|
|
1735
|
+
*
|
|
1736
|
+
* Every await below is a place the run can end, so each is followed by a
|
|
1737
|
+
* check that it has not. A Stop while the host's predicate is pending ends
|
|
1738
|
+
* the call there, and one while the card is open declines it. A handover
|
|
1739
|
+
* during any of the three leaves the call with no transcript to draw in and
|
|
1740
|
+
* no store to write to, so it does neither. Any other reset of the
|
|
1741
|
+
* conversation -- New chat, a drawer switch, `reload()` -- leaves the call
|
|
1742
|
+
* its store and its result but not the transcript, so it still settles and
|
|
1743
|
+
* returns, and does not show the run waiting in a conversation it is not in.
|
|
1734
1744
|
*/
|
|
1735
1745
|
async execute(call) {
|
|
1736
1746
|
if (skillNameFrom(call) !== null) {
|
|
1737
1747
|
return null;
|
|
1738
1748
|
}
|
|
1749
|
+
const tenure = this.#host.tenure();
|
|
1750
|
+
const conversation = this.#host.conversation();
|
|
1751
|
+
const threadId = this.#host.threadId();
|
|
1739
1752
|
const card = this.#host.transcript.cardFor(call);
|
|
1740
1753
|
this.#host.transcript.forgetCard(call.id);
|
|
1741
1754
|
this.#host.transcript.setCardElement(call.id, card.element);
|
|
@@ -1752,7 +1765,12 @@ var ToolDispatch = class {
|
|
|
1752
1765
|
this.#host.transcript.showPending();
|
|
1753
1766
|
return { content: `Error: ${message}`, error: message, outcome: TOOL_OUTCOME.FAILED };
|
|
1754
1767
|
}
|
|
1768
|
+
const asking = this.#host.decision.open();
|
|
1755
1769
|
const rule = await this.#confirmationRule(call, tool);
|
|
1770
|
+
this.#host.decision.close();
|
|
1771
|
+
if (asking.aborted) {
|
|
1772
|
+
return this.#abandoned(card);
|
|
1773
|
+
}
|
|
1756
1774
|
if (rule === "unanswered") {
|
|
1757
1775
|
const message = this.#host.strings().confirmCheckFailed;
|
|
1758
1776
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
@@ -1776,20 +1794,31 @@ var ToolDispatch = class {
|
|
|
1776
1794
|
this.#host.transcript.follow();
|
|
1777
1795
|
const accepted = await decision;
|
|
1778
1796
|
this.#host.decision.close();
|
|
1797
|
+
if (this.#host.tenure() !== tenure) {
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1779
1800
|
card.recordDecision(accepted ? "approved" : "declined");
|
|
1780
1801
|
if (!accepted) {
|
|
1781
1802
|
const message = this.#host.strings().declinedAction;
|
|
1782
1803
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
1783
|
-
this.#host.
|
|
1804
|
+
if (this.#host.conversation() === conversation) {
|
|
1805
|
+
this.#host.transcript.showPending();
|
|
1806
|
+
}
|
|
1784
1807
|
return { content: message, outcome: TOOL_OUTCOME.DENIED };
|
|
1785
1808
|
}
|
|
1809
|
+
if (signal.aborted) {
|
|
1810
|
+
return this.#abandoned(card);
|
|
1811
|
+
}
|
|
1786
1812
|
}
|
|
1787
1813
|
const navigates = isNavigates(tool.parameters) && this.#host.navigate() === null;
|
|
1788
1814
|
if (navigates) {
|
|
1789
|
-
this.#host.conversationStore().saveCheckpoint(
|
|
1815
|
+
this.#host.conversationStore().saveCheckpoint(threadId, { toolCallId: call.id });
|
|
1790
1816
|
}
|
|
1791
1817
|
try {
|
|
1792
1818
|
const result = await tool.handler(call.args, call.id);
|
|
1819
|
+
if (this.#host.tenure() !== tenure) {
|
|
1820
|
+
return null;
|
|
1821
|
+
}
|
|
1793
1822
|
if (tool.render !== void 0) {
|
|
1794
1823
|
this.#host.transcript.renderToolOutput(tool.render, call);
|
|
1795
1824
|
}
|
|
@@ -1799,15 +1828,22 @@ var ToolDispatch = class {
|
|
|
1799
1828
|
}
|
|
1800
1829
|
const content = JSON.stringify(result ?? null);
|
|
1801
1830
|
card.settle(TOOL_CALL_STATUS.DONE, content);
|
|
1802
|
-
this.#host.
|
|
1831
|
+
if (this.#host.conversation() === conversation) {
|
|
1832
|
+
this.#host.transcript.showPending();
|
|
1833
|
+
}
|
|
1803
1834
|
return { content };
|
|
1804
1835
|
} catch (error) {
|
|
1836
|
+
if (this.#host.tenure() !== tenure) {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1805
1839
|
if (navigates) {
|
|
1806
|
-
this.#host.conversationStore().saveCheckpoint(
|
|
1840
|
+
this.#host.conversationStore().saveCheckpoint(threadId, null);
|
|
1807
1841
|
}
|
|
1808
1842
|
const message = error instanceof Error ? error.message : String(error);
|
|
1809
1843
|
card.settle(TOOL_CALL_STATUS.ERROR, message);
|
|
1810
|
-
this.#host.
|
|
1844
|
+
if (this.#host.conversation() === conversation) {
|
|
1845
|
+
this.#host.transcript.showPending();
|
|
1846
|
+
}
|
|
1811
1847
|
return { content: `Error: ${message}`, error: message, outcome: TOOL_OUTCOME.FAILED };
|
|
1812
1848
|
}
|
|
1813
1849
|
}
|
|
@@ -1960,6 +1996,25 @@ var ToolDispatch = class {
|
|
|
1960
1996
|
}
|
|
1961
1997
|
return isDestructive(tool.parameters) ? "destructive" : null;
|
|
1962
1998
|
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Settle a call whose run ended while it waited, and give the loop nothing.
|
|
2001
|
+
*
|
|
2002
|
+
* The card is settled here because nothing else will: dispatch took it out
|
|
2003
|
+
* of the end-of-run sweep before it waited, so left alone it read "running…"
|
|
2004
|
+
* for good. It says the call did not finish, which is true -- nothing ran and
|
|
2005
|
+
* nobody refused it.
|
|
2006
|
+
*
|
|
2007
|
+
* `null` rather than a result, because a stopped run has no next round to
|
|
2008
|
+
* send one in. The loop moves on and finds the run stopped, and the call is
|
|
2009
|
+
* left unanswered like every call after it in the round, so the client
|
|
2010
|
+
* answers it as not finished on the next request -- in the same words as
|
|
2011
|
+
* this card. After a handover the card is in a transcript already cleared,
|
|
2012
|
+
* and settling it there is seen by no one.
|
|
2013
|
+
*/
|
|
2014
|
+
#abandoned(card) {
|
|
2015
|
+
card.settle(TOOL_CALL_STATUS.INTERRUPTED, this.#host.strings().callNotFinished);
|
|
2016
|
+
return null;
|
|
2017
|
+
}
|
|
1963
2018
|
/**
|
|
1964
2019
|
* Whether the page moved since the current round's context was built.
|
|
1965
2020
|
*
|
|
@@ -3029,6 +3084,8 @@ var VoiceInput = class {
|
|
|
3029
3084
|
#capTimer = null;
|
|
3030
3085
|
#hitCap = false;
|
|
3031
3086
|
#disposed = false;
|
|
3087
|
+
/** Identifies the newest start; an older one that settles is stale. */
|
|
3088
|
+
#startToken = {};
|
|
3032
3089
|
constructor(options) {
|
|
3033
3090
|
this.#transcribe = options.transcribe;
|
|
3034
3091
|
this.#onText = options.onText;
|
|
@@ -3058,25 +3115,42 @@ var VoiceInput = class {
|
|
|
3058
3115
|
await this.#start();
|
|
3059
3116
|
}
|
|
3060
3117
|
async #start() {
|
|
3118
|
+
const token = {};
|
|
3119
|
+
this.#startToken = token;
|
|
3061
3120
|
let stream;
|
|
3062
3121
|
try {
|
|
3063
3122
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
3064
3123
|
} catch {
|
|
3124
|
+
stream = null;
|
|
3125
|
+
}
|
|
3126
|
+
if (token !== this.#startToken || this.#disposed) {
|
|
3127
|
+
for (const track of stream?.getTracks() ?? []) {
|
|
3128
|
+
track.stop();
|
|
3129
|
+
}
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
if (stream === null) {
|
|
3065
3133
|
this.#fail(this.#strings.transcriptionFailed);
|
|
3066
3134
|
return;
|
|
3067
3135
|
}
|
|
3068
3136
|
this.#stream = stream;
|
|
3069
3137
|
this.#chunks = [];
|
|
3070
3138
|
this.#hitCap = false;
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3139
|
+
let recorder;
|
|
3140
|
+
try {
|
|
3141
|
+
recorder = new MediaRecorder(stream);
|
|
3142
|
+
recorder.addEventListener("dataavailable", (event) => {
|
|
3143
|
+
this.#chunks.push(event.data);
|
|
3144
|
+
});
|
|
3145
|
+
recorder.addEventListener("stop", () => {
|
|
3146
|
+
void this.#finish(recorder.mimeType);
|
|
3147
|
+
});
|
|
3148
|
+
this.#recorder = recorder;
|
|
3149
|
+
recorder.start();
|
|
3150
|
+
} catch {
|
|
3151
|
+
this.#fail(this.#strings.transcriptionFailed);
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3080
3154
|
this.#capTimer = setTimeout(() => {
|
|
3081
3155
|
this.#hitCap = true;
|
|
3082
3156
|
this.#stop();
|
|
@@ -3095,10 +3169,16 @@ var VoiceInput = class {
|
|
|
3095
3169
|
}
|
|
3096
3170
|
}
|
|
3097
3171
|
/**
|
|
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
|
|
3172
|
+
* Tear the control down, for a host element removed mid-recording or handed
|
|
3173
|
+
* to the next principal. Stops any live `MediaRecorder`, releases the mic
|
|
3174
|
+
* tracks so the browser's recording indicator clears, and suppresses the
|
|
3175
|
+
* pending transcription — a disconnected control must not fire `onText` into
|
|
3176
|
+
* a detached element.
|
|
3177
|
+
*
|
|
3178
|
+
* Disposal is final: nothing un-disposes an instance, and the host's next
|
|
3179
|
+
* wiring builds a new one. So the disposed flag alone tells a continuation
|
|
3180
|
+
* that outlived the control from a live one, and a clip already posted when this runs is
|
|
3181
|
+
* dropped whenever its answer arrives.
|
|
3102
3182
|
*/
|
|
3103
3183
|
dispose() {
|
|
3104
3184
|
this.#disposed = true;
|
|
@@ -3118,6 +3198,9 @@ var VoiceInput = class {
|
|
|
3118
3198
|
const audio = new Blob(this.#chunks, { type: mimeType || "audio/webm" });
|
|
3119
3199
|
try {
|
|
3120
3200
|
const text4 = await this.#transcribe(audio);
|
|
3201
|
+
if (this.#disposed) {
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3121
3204
|
this.#setState("idle");
|
|
3122
3205
|
if (this.#hitCap) {
|
|
3123
3206
|
this.element.title = fillUiString(this.#strings.recordingLimit, {
|
|
@@ -3141,6 +3224,9 @@ var VoiceInput = class {
|
|
|
3141
3224
|
this.#stream = null;
|
|
3142
3225
|
}
|
|
3143
3226
|
#fail(message) {
|
|
3227
|
+
if (this.#disposed) {
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3144
3230
|
this.#releaseStream();
|
|
3145
3231
|
this.#recorder = null;
|
|
3146
3232
|
this.#setState("idle");
|
|
@@ -4119,9 +4205,20 @@ var ConversationHistory = class {
|
|
|
4119
4205
|
* the one it continued is still on screen when it saves.
|
|
4120
4206
|
*/
|
|
4121
4207
|
#cleared = 0;
|
|
4122
|
-
|
|
4123
|
-
|
|
4208
|
+
/**
|
|
4209
|
+
* Which restore may draw: bumped by every rehydrate, and by every clear of
|
|
4210
|
+
* the conversation on screen. A restore whose generation is stale draws
|
|
4211
|
+
* nothing more and resumes nothing: a newer one has started, or New chat, a
|
|
4212
|
+
* thread switch or a `user-key` handover has cleared what it was restoring.
|
|
4213
|
+
*/
|
|
4124
4214
|
#generation = 0;
|
|
4215
|
+
/**
|
|
4216
|
+
* The conversation the latest restore claimed, written by every restore as
|
|
4217
|
+
* it starts; `null` before the first. It speaks for the restore now current
|
|
4218
|
+
* only while its generation is the current one, because clearing the
|
|
4219
|
+
* conversation moves that on. See `#forgetStopped` for the one reader.
|
|
4220
|
+
*/
|
|
4221
|
+
#claim = null;
|
|
4125
4222
|
/** Built lazily from `data-runs-url`; `null` when the host didn't opt in. */
|
|
4126
4223
|
#runIndex = null;
|
|
4127
4224
|
/**
|
|
@@ -4129,6 +4226,17 @@ var ConversationHistory = class {
|
|
|
4129
4226
|
* reaches it. `null` when none is running.
|
|
4130
4227
|
*/
|
|
4131
4228
|
#continuation = null;
|
|
4229
|
+
/**
|
|
4230
|
+
* Settles what a host's send waits on while the restore now current, one
|
|
4231
|
+
* with no run to resume, loads and replays; `null` when no such restore is
|
|
4232
|
+
* under way. Called when that restore ends, and as soon as it is stopped by
|
|
4233
|
+
* anything that moves the generation, so a send waiting on a conversation
|
|
4234
|
+
* that is no longer on screen hears so at once rather than when an
|
|
4235
|
+
* abandoned load answers.
|
|
4236
|
+
*
|
|
4237
|
+
* Also what tells a pick refused for a load from one refused for a run.
|
|
4238
|
+
*/
|
|
4239
|
+
#loading = null;
|
|
4132
4240
|
constructor(host) {
|
|
4133
4241
|
this.#host = host;
|
|
4134
4242
|
}
|
|
@@ -4169,6 +4277,14 @@ var ConversationHistory = class {
|
|
|
4169
4277
|
forgetRestored() {
|
|
4170
4278
|
this.#restored = [];
|
|
4171
4279
|
this.#cleared += 1;
|
|
4280
|
+
this.#generation += 1;
|
|
4281
|
+
this.#endWait();
|
|
4282
|
+
}
|
|
4283
|
+
/** Settle what waits on the restore now loading, if anything does, and forget it. */
|
|
4284
|
+
#endWait() {
|
|
4285
|
+
const end = this.#loading;
|
|
4286
|
+
this.#loading = null;
|
|
4287
|
+
end?.();
|
|
4172
4288
|
}
|
|
4173
4289
|
/** Delete the active thread if nothing was ever sent in it. */
|
|
4174
4290
|
reapUnsent() {
|
|
@@ -4273,7 +4389,10 @@ var ConversationHistory = class {
|
|
|
4273
4389
|
return;
|
|
4274
4390
|
}
|
|
4275
4391
|
if (this.#host.running() || this.#continuation !== null) {
|
|
4276
|
-
this.#
|
|
4392
|
+
const strings = this.#host.strings();
|
|
4393
|
+
this.#refuse(
|
|
4394
|
+
this.#loading === null ? strings.continueWhileRunning : strings.continueWhileLoading
|
|
4395
|
+
);
|
|
4277
4396
|
return;
|
|
4278
4397
|
}
|
|
4279
4398
|
const content = this.#host.input.value.trim();
|
|
@@ -4281,6 +4400,7 @@ var ConversationHistory = class {
|
|
|
4281
4400
|
this.#refuse(this.#host.strings().continueNeedsTurn);
|
|
4282
4401
|
return;
|
|
4283
4402
|
}
|
|
4403
|
+
this.#host.recordTurn(content);
|
|
4284
4404
|
this.#host.input.value = "";
|
|
4285
4405
|
this.#host.autoGrow();
|
|
4286
4406
|
const cleared = this.#cleared;
|
|
@@ -4357,48 +4477,214 @@ var ConversationHistory = class {
|
|
|
4357
4477
|
* because the resume path answers it from the page the reload landed on; that
|
|
4358
4478
|
* exclusion is held by "resumes with the landed page's result, and its card says
|
|
4359
4479
|
* so" in `ag_ui_chat_reload_mid_run.test.ts`.
|
|
4480
|
+
*
|
|
4481
|
+
* **Every restore holds the composer from before the load.** Until the
|
|
4482
|
+
* store answers, the conversation's client can only be seeded from nothing.
|
|
4483
|
+
* With a remote store that is a real request, and a send, a Retry or a pick
|
|
4484
|
+
* in it was refused by nothing: each built the conversation's client before
|
|
4485
|
+
* the conversation had loaded, so a send went out carrying none of it, its
|
|
4486
|
+
* first save replaced the stored one, and the replay then drew the stored
|
|
4487
|
+
* conversation under the new turn. The client stayed cached without it, so
|
|
4488
|
+
* every later turn went out without it too. So the composer is held as a
|
|
4489
|
+
* send holds it: what was typed parks and goes out after the replay, and a
|
|
4490
|
+
* Retry and a pick are refused -- a Retry has nothing on screen to retry, and
|
|
4491
|
+
* after the load it would re-ask the previous visit's last question.
|
|
4492
|
+
*
|
|
4493
|
+
* **A host's send waits for a restore with no run to resume.** It is
|
|
4494
|
+
* usually a prompt fired as the element mounts, which no host could time
|
|
4495
|
+
* against a load nothing announces, and refusing it would lose the turn.
|
|
4496
|
+
* So it waits for this restore to end, taking the hold over as it does,
|
|
4497
|
+
* and then sends with the conversation drawn above it; what the built-in
|
|
4498
|
+
* Send parked goes out after its run. Stopped while it waits, it is dropped,
|
|
4499
|
+
* as a send stopped by its own submit listener is.
|
|
4500
|
+
*
|
|
4501
|
+
* **A restore that resumes refuses a host's send instead.** The resume is a
|
|
4502
|
+
* run, and the restore ends by starting it, so a send that waited would only
|
|
4503
|
+
* reach that run and be refused there. The resume gives way to nothing: what
|
|
4504
|
+
* was typed parks behind the resumed run and goes out after its answer.
|
|
4505
|
+
*
|
|
4506
|
+
* The other way round, the resume standing down for the send, was rejected.
|
|
4507
|
+
* The send that got in first had already gone out without the conversation,
|
|
4508
|
+
* so standing down kept that loss, and it also dropped the run the navigation
|
|
4509
|
+
* was part of, which the user asked for before the reload: the agent stopped
|
|
4510
|
+
* halfway through the task, with the navigating call never answered.
|
|
4511
|
+
*
|
|
4512
|
+
* **A restore stops where it was stopped, held or not.** New chat, a thread
|
|
4513
|
+
* switch, a `user-key` handover, a newer restore or the element leaving the
|
|
4514
|
+
* page can each come while the store answers, and from host code the restore
|
|
4515
|
+
* runs itself: the activity and tool renderers each replayed message draws
|
|
4516
|
+
* through, and `navigationResult`. From then on it draws nothing more and
|
|
4517
|
+
* resumes nothing. The hold cannot say so, because a host's send that
|
|
4518
|
+
* waited may hold it by then. So the restore asks its generation, which clearing
|
|
4519
|
+
* the conversation moves on as a newer restore does, and asks it after the
|
|
4520
|
+
* load, after each replayed message -- as a Retry asks after each one -- and
|
|
4521
|
+
* once more before the resume. Stopped by anything but the element leaving
|
|
4522
|
+
* the page, it also forgets the checkpoint it loaded, since that was a Stop
|
|
4523
|
+
* for the run it would have resumed -- unless what replaced it is a restore
|
|
4524
|
+
* of the same conversation, still current, which answers for that run
|
|
4525
|
+
* instead; see `#forgetStopped`.
|
|
4360
4526
|
*/
|
|
4361
4527
|
async rehydrate() {
|
|
4362
4528
|
this.#generation += 1;
|
|
4363
4529
|
const generation = this.#generation;
|
|
4364
|
-
this.#
|
|
4365
|
-
|
|
4530
|
+
const threadId = this.#threadId;
|
|
4531
|
+
const store = this.#host.conversationStore();
|
|
4532
|
+
this.#claim = { generation, store, threadId };
|
|
4533
|
+
const checkpoint = store.loadCheckpoint(threadId);
|
|
4534
|
+
const restored = checkpoint === null ? new Promise((resolve) => {
|
|
4535
|
+
this.#loading = resolve;
|
|
4536
|
+
}) : null;
|
|
4537
|
+
const waiting = this.#loading;
|
|
4538
|
+
const hold = this.#host.holdRun(restored);
|
|
4366
4539
|
try {
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4540
|
+
this.#host.element.setAttribute("data-restoring", "");
|
|
4541
|
+
let messages;
|
|
4542
|
+
try {
|
|
4543
|
+
messages = await store.loadMessages(threadId);
|
|
4544
|
+
} finally {
|
|
4545
|
+
if (generation === this.#generation) {
|
|
4546
|
+
this.#host.element.removeAttribute("data-restoring");
|
|
4547
|
+
}
|
|
4371
4548
|
}
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4549
|
+
if (!this.#restoring(generation)) {
|
|
4550
|
+
this.#forgetStopped(store, threadId, checkpoint);
|
|
4551
|
+
return;
|
|
4552
|
+
}
|
|
4553
|
+
if (messages !== null) {
|
|
4554
|
+
const unfinished = this.#host.strings().callNotFinished;
|
|
4555
|
+
const restored2 = answerUnansweredCalls(
|
|
4556
|
+
messages,
|
|
4557
|
+
// The shape the store holds for a call the client answered the same
|
|
4558
|
+
// way: its tool message, with the outcome in its metadata.
|
|
4559
|
+
(toolCallId) => ({
|
|
4560
|
+
id: randomUUID3(),
|
|
4561
|
+
role: "tool",
|
|
4562
|
+
content: unfinished,
|
|
4563
|
+
toolCallId,
|
|
4564
|
+
metadata: { outcome: TOOL_OUTCOME.INTERRUPTED }
|
|
4565
|
+
}),
|
|
4566
|
+
new Set(checkpoint === null ? [] : [checkpoint.toolCallId])
|
|
4567
|
+
);
|
|
4568
|
+
this.#restored = restored2;
|
|
4569
|
+
for (const message of restored2) {
|
|
4570
|
+
this.replay(message);
|
|
4571
|
+
if (!this.#restoring(generation)) {
|
|
4572
|
+
this.#forgetStopped(store, threadId, checkpoint);
|
|
4573
|
+
return;
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
if (checkpoint !== null) {
|
|
4578
|
+
await this.#resumeFrom(checkpoint, generation);
|
|
4579
|
+
return;
|
|
4580
|
+
}
|
|
4581
|
+
this.#noticeIfRunUnfinished(messages);
|
|
4582
|
+
} finally {
|
|
4583
|
+
hold.release();
|
|
4584
|
+
if (this.#loading === waiting) {
|
|
4585
|
+
this.#endWait();
|
|
4395
4586
|
}
|
|
4396
4587
|
}
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4588
|
+
}
|
|
4589
|
+
/**
|
|
4590
|
+
* Whether the restore that claimed `generation` may go on: nothing has
|
|
4591
|
+
* replaced or cleared the conversation it is restoring, and the element is
|
|
4592
|
+
* still on the page.
|
|
4593
|
+
*
|
|
4594
|
+
* On the page, because leaving it is the one stop that clears nothing: the
|
|
4595
|
+
* element is not reset until it connects again, and connecting restores the
|
|
4596
|
+
* conversation afresh. A restore with nothing to resume went on regardless,
|
|
4597
|
+
* drawing into a node that had gone and running the host's renderers for it.
|
|
4598
|
+
* One that resumes learnt it from the Stop letting go of its hold; nothing
|
|
4599
|
+
* asks the hold that now, so this is what keeps the resume's request in.
|
|
4600
|
+
*
|
|
4601
|
+
* One `&&`, so coverage cannot tell whether either half is held. Removing the
|
|
4602
|
+
* generation fails "draws nothing into a new chat started while it loads" in
|
|
4603
|
+
* `conversation_history.test.ts`, and "drops a stale thread replay when a
|
|
4604
|
+
* newer switch started" in `ag_ui_chat.test.ts`; removing the connection
|
|
4605
|
+
* fails "draws nothing into an element that left the page while it loads".
|
|
4606
|
+
*/
|
|
4607
|
+
#restoring(generation) {
|
|
4608
|
+
return generation === this.#generation && this.#host.element.isConnected;
|
|
4609
|
+
}
|
|
4610
|
+
/**
|
|
4611
|
+
* Forget the checkpoint a stopped restore loaded, when what stopped it also
|
|
4612
|
+
* stopped the run it was about to resume.
|
|
4613
|
+
*
|
|
4614
|
+
* New chat, a thread switch, a `user-key` handover and a newer restore all
|
|
4615
|
+
* move the generation, and each is a Stop for that run. Left in the store,
|
|
4616
|
+
* the checkpoint outlived the Stop: coming back to the conversation later
|
|
4617
|
+
* resumed the navigating call with whatever page was current by then.
|
|
4618
|
+
* Forgotten, the call is answered as not finished there, as any stopped call
|
|
4619
|
+
* is.
|
|
4620
|
+
*
|
|
4621
|
+
* Not while the restore now current claimed the same conversation, which it
|
|
4622
|
+
* has in two cases. It is this restore, when only the element left the page,
|
|
4623
|
+
* which moves no generation: a move or a framework re-render is not a
|
|
4624
|
+
* farewell, and the restore connecting starts has to find the checkpoint to
|
|
4625
|
+
* resume from. Or it is a newer restore of the same thread in the same store
|
|
4626
|
+
* -- `reload()`, switching away and back, or the element connecting again --
|
|
4627
|
+
* which read the checkpoint as it started, or found it gone, and answers for
|
|
4628
|
+
* it from then on: its resume clears it, and when it is stopped in turn this
|
|
4629
|
+
* same rule decides.
|
|
4630
|
+
*
|
|
4631
|
+
* The newer restore's case used to forget it too, on the grounds that the
|
|
4632
|
+
* newer one had read it and would resume once from that copy. It did until
|
|
4633
|
+
* the element left the page while the newer one loaded: that one stood down
|
|
4634
|
+
* keeping the checkpoint for the reconnect, as a departure should, but the
|
|
4635
|
+
* store no longer held it, so the element came back with no run to resume.
|
|
4636
|
+
*
|
|
4637
|
+
* Written to the store and the thread it was read from. After a handover
|
|
4638
|
+
* that is the previous principal's: the built-in store's clear is a removal
|
|
4639
|
+
* from a namespace the handover has already purged, so it changes nothing,
|
|
4640
|
+
* and nothing reaches the next principal's. A host's store is the same object
|
|
4641
|
+
* either side of a handover; one the host replaced is held by "still refuses
|
|
4642
|
+
* a send for the newer restore once the older has stood down".
|
|
4643
|
+
*
|
|
4644
|
+
* Two conditions. Clearing with no checkpoint fails "draws nothing into a new
|
|
4645
|
+
* chat started while it loads", which runs no store code for one. Clearing
|
|
4646
|
+
* what the current restore claimed fails "resumes nothing once the element
|
|
4647
|
+
* leaves the page while the store answers", where the claim is this
|
|
4648
|
+
* restore's own and the re-inserted element resumes, and "resumes once the
|
|
4649
|
+
* element is back when the host reloads and it then leaves the page", where
|
|
4650
|
+
* it is the reload's. The claim's own three are named on `#claimedByCurrent`.
|
|
4651
|
+
*/
|
|
4652
|
+
#forgetStopped(store, threadId, checkpoint) {
|
|
4653
|
+
if (checkpoint !== null && !this.#claimedByCurrent(store, threadId)) {
|
|
4654
|
+
store.saveCheckpoint(threadId, null);
|
|
4400
4655
|
}
|
|
4401
|
-
|
|
4656
|
+
}
|
|
4657
|
+
/**
|
|
4658
|
+
* Whether the restore now current claimed the conversation a stopped restore
|
|
4659
|
+
* read its checkpoint from: the same thread, in the same store.
|
|
4660
|
+
*
|
|
4661
|
+
* A claim from an earlier generation speaks for nobody: the conversation was
|
|
4662
|
+
* cleared after it was made, so whatever it would have resumed was stopped,
|
|
4663
|
+
* and that holds of a stopped restore's own claim as much as of any other.
|
|
4664
|
+
* The store is compared as an object, because a store the host assigned
|
|
4665
|
+
* since is a different record of the conversation, and the one the
|
|
4666
|
+
* checkpoint was read from still has to forget it.
|
|
4667
|
+
*
|
|
4668
|
+
* As an object, a store the element builds for itself -- the built-in one
|
|
4669
|
+
* under a namespace, and the remote one `data-threads-url` wraps around it
|
|
4670
|
+
* -- is a different one after a reconnect, which builds it afresh over the
|
|
4671
|
+
* same storage, so a restore from before the reconnect forgets the
|
|
4672
|
+
* checkpoint there once it answers. The reconnect's own restore read it as
|
|
4673
|
+
* it started and resumes from that copy, as a newer restore always did here;
|
|
4674
|
+
* what that copy cannot survive is the element leaving again before the
|
|
4675
|
+
* load answers.
|
|
4676
|
+
*
|
|
4677
|
+
* One `&&` of three, so coverage cannot tell whether any is held. Dropping
|
|
4678
|
+
* the generation fails "forgets the checkpoint when New chat stops the
|
|
4679
|
+
* resume, so the call ends as not finished", where the claim is the stopped
|
|
4680
|
+
* restore's own; dropping the store fails "still refuses a send for the newer
|
|
4681
|
+
* restore once the older has stood down", whose host replaces the store
|
|
4682
|
+
* between the two; dropping the thread fails "forgets the checkpoint of a
|
|
4683
|
+
* thread switched away from, though the next has one of its own".
|
|
4684
|
+
*/
|
|
4685
|
+
#claimedByCurrent(store, threadId) {
|
|
4686
|
+
const claim = this.#claim;
|
|
4687
|
+
return claim?.generation === this.#generation && claim.store === store && claim.threadId === threadId;
|
|
4402
4688
|
}
|
|
4403
4689
|
/**
|
|
4404
4690
|
* Notice a previous run that never produced a response.
|
|
@@ -4518,12 +4804,15 @@ var ConversationHistory = class {
|
|
|
4518
4804
|
* guard is held by "resumes a checkpointed navigating tool call on mount" in
|
|
4519
4805
|
* `ag_ui_chat.test.ts`, whose transcript never made the call.
|
|
4520
4806
|
*/
|
|
4521
|
-
async #resumeFrom(checkpoint) {
|
|
4807
|
+
async #resumeFrom(checkpoint, generation) {
|
|
4522
4808
|
this.#host.conversationStore().saveCheckpoint(this.#threadId, null);
|
|
4523
4809
|
const client = this.#host.ensureClient();
|
|
4524
4810
|
const landed = JSON.stringify(
|
|
4525
4811
|
this.#host.navigationResult().call(this.#host.element, checkpoint)
|
|
4526
4812
|
);
|
|
4813
|
+
if (!this.#restoring(generation)) {
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
4527
4816
|
client.addToolResult(checkpoint.toolCallId, landed);
|
|
4528
4817
|
this.#host.transcript.card(checkpoint.toolCallId)?.settle(TOOL_CALL_STATUS.DONE, landed);
|
|
4529
4818
|
await client.resume();
|
|
@@ -14999,8 +15288,10 @@ var AgUiClient = class {
|
|
|
14999
15288
|
#unfinishedMessage;
|
|
15000
15289
|
#declinedMessage;
|
|
15001
15290
|
#maxToolRounds;
|
|
15002
|
-
// Set by cancel(); reset
|
|
15003
|
-
// a cancel between frontend-tool
|
|
15291
|
+
// Set by cancel(); reset as send() or resume() begins, before either runs
|
|
15292
|
+
// any host code. Checked by the loop so a cancel between frontend-tool
|
|
15293
|
+
// rounds doesn't start another round, and again just before each request,
|
|
15294
|
+
// so a cancel from host code the round itself runs is not sent anyway.
|
|
15004
15295
|
#cancelled = false;
|
|
15005
15296
|
constructor(config) {
|
|
15006
15297
|
this.#agent = config.agent;
|
|
@@ -15056,6 +15347,7 @@ var AgUiClient = class {
|
|
|
15056
15347
|
* {@link messageAttachments}.
|
|
15057
15348
|
*/
|
|
15058
15349
|
async send(content, attachments = []) {
|
|
15350
|
+
this.#cancelled = false;
|
|
15059
15351
|
const message = {
|
|
15060
15352
|
id: randomUUID4(),
|
|
15061
15353
|
role: "user",
|
|
@@ -15071,11 +15363,14 @@ var AgUiClient = class {
|
|
|
15071
15363
|
* can be asked again.
|
|
15072
15364
|
*
|
|
15073
15365
|
* Returns the retained history, or `null` when there is nothing to retry (no
|
|
15074
|
-
* user message has been sent yet).
|
|
15075
|
-
*
|
|
15076
|
-
*
|
|
15077
|
-
*
|
|
15078
|
-
*
|
|
15366
|
+
* user message has been sent yet). The shortened history is saved before it
|
|
15367
|
+
* replaces the current one, so a save that throws changes nothing and the
|
|
15368
|
+
* throw propagates.
|
|
15369
|
+
*
|
|
15370
|
+
* **Truncates only** -- the caller re-renders from the returned list and then
|
|
15371
|
+
* calls {@link resume}, because the transcript belongs to the element and a
|
|
15372
|
+
* client that reached into it would own two things. Running here instead
|
|
15373
|
+
* would stream the new answer in underneath the old one.
|
|
15079
15374
|
*
|
|
15080
15375
|
* Re-running answers the question the agent was last asked, rather than
|
|
15081
15376
|
* telling it its answer was wrong, which is what makes the result a
|
|
@@ -15097,8 +15392,8 @@ var AgUiClient = class {
|
|
|
15097
15392
|
return null;
|
|
15098
15393
|
}
|
|
15099
15394
|
const kept = messages.slice(0, lastUser + 1);
|
|
15395
|
+
this.#onPersist(kept);
|
|
15100
15396
|
this.#agent.setMessages(kept);
|
|
15101
|
-
this.#persist();
|
|
15102
15397
|
return kept;
|
|
15103
15398
|
}
|
|
15104
15399
|
/**
|
|
@@ -15107,6 +15402,7 @@ var AgUiClient = class {
|
|
|
15107
15402
|
* message; it continues the conversation already in history.
|
|
15108
15403
|
*/
|
|
15109
15404
|
async resume() {
|
|
15405
|
+
this.#cancelled = false;
|
|
15110
15406
|
await this.#run();
|
|
15111
15407
|
}
|
|
15112
15408
|
/** Append a frontend tool result to history (used by the resume path). */
|
|
@@ -15120,13 +15416,19 @@ var AgUiClient = class {
|
|
|
15120
15416
|
* disconnect). Safe to call with no run in flight. Partial text already
|
|
15121
15417
|
* streamed stays in history; {@link AgUiClientHandlers.onCancelled} fires
|
|
15122
15418
|
* instead of `onError`, and `onSettled` still follows.
|
|
15419
|
+
*
|
|
15420
|
+
* Also stops a {@link send} or {@link resume} whose request has not been made
|
|
15421
|
+
* yet, when host code it runs on the way calls this: the save of the turn,
|
|
15422
|
+
* the tool and context providers, the save answering a call left open. No
|
|
15423
|
+
* request is made then, and `onCancelled` and `onSettled` fire as for any
|
|
15424
|
+
* other Stop. A cancel with nothing in flight is forgotten when the next
|
|
15425
|
+
* send or resume begins.
|
|
15123
15426
|
*/
|
|
15124
15427
|
cancel() {
|
|
15125
15428
|
this.#cancelled = true;
|
|
15126
15429
|
this.#agent.abortRun();
|
|
15127
15430
|
}
|
|
15128
15431
|
async #run() {
|
|
15129
|
-
this.#cancelled = false;
|
|
15130
15432
|
try {
|
|
15131
15433
|
await this.#runLoop();
|
|
15132
15434
|
if (this.#cancelled) {
|
|
@@ -15284,6 +15586,9 @@ var AgUiClient = class {
|
|
|
15284
15586
|
params.resume = resume;
|
|
15285
15587
|
}
|
|
15286
15588
|
this.#answerUnansweredCalls(resuming);
|
|
15589
|
+
if (this.#cancelled) {
|
|
15590
|
+
return;
|
|
15591
|
+
}
|
|
15287
15592
|
await this.#agent.runAgent(params, this.#buildSubscriber(pending, runState));
|
|
15288
15593
|
resume = void 0;
|
|
15289
15594
|
resuming = /* @__PURE__ */ new Set();
|
|
@@ -16206,9 +16511,12 @@ var StorageScope = class {
|
|
|
16206
16511
|
// remounts, but re-resolves if the host answers the warning with an `id`.
|
|
16207
16512
|
#generatedNs = "";
|
|
16208
16513
|
#generatedFor = "";
|
|
16209
|
-
// The `sessionStorage`-backed store, which the element
|
|
16210
|
-
// on a principal change
|
|
16211
|
-
//
|
|
16514
|
+
// The `sessionStorage`-backed store this scope handed out, which the element
|
|
16515
|
+
// may therefore re-scope on a principal change -- while it is still the store
|
|
16516
|
+
// in use, which rescopeStore asks, since a host can assign its own at any
|
|
16517
|
+
// time. `null` until connecting has met the built-in store at all. A store of
|
|
16518
|
+
// the host's own kind is never held here: the element does not know its
|
|
16519
|
+
// keying and must not guess at it.
|
|
16212
16520
|
#builtinStore = null;
|
|
16213
16521
|
constructor(host) {
|
|
16214
16522
|
this.#host = host;
|
|
@@ -16262,12 +16570,22 @@ var StorageScope = class {
|
|
|
16262
16570
|
return this.#builtinStore;
|
|
16263
16571
|
}
|
|
16264
16572
|
/**
|
|
16265
|
-
* Rebuild the built-in store under `namespace`, or `null`
|
|
16266
|
-
*
|
|
16267
|
-
*
|
|
16573
|
+
* Rebuild the built-in store under `namespace`, or `null` when `current` --
|
|
16574
|
+
* the store the element is using now, unwrapped from any remote it wrapped
|
|
16575
|
+
* it in -- is not the one this scope handed out.
|
|
16576
|
+
*
|
|
16577
|
+
* Asked of the store in use rather than of what connecting remembered,
|
|
16578
|
+
* because the two part company the moment a host assigns a store of its own
|
|
16579
|
+
* to a connected element. Remembering was enough to tell a host's store from
|
|
16580
|
+
* the built-in one at connect; after it, the built-in one was still
|
|
16581
|
+
* remembered, so a key change replaced the host's store with a fresh
|
|
16582
|
+
* `sessionStorage` one -- and a host that had chosen to keep message bodies
|
|
16583
|
+
* off the client found every one of them cached in the tab again. A store
|
|
16584
|
+
* the element did not make holds its data somewhere the element cannot see,
|
|
16585
|
+
* and has to scope itself.
|
|
16268
16586
|
*/
|
|
16269
|
-
rescopeStore(namespace) {
|
|
16270
|
-
if (this.#builtinStore
|
|
16587
|
+
rescopeStore(current, namespace) {
|
|
16588
|
+
if (current !== this.#builtinStore) {
|
|
16271
16589
|
return null;
|
|
16272
16590
|
}
|
|
16273
16591
|
this.#builtinStore = new SessionStorageStore(namespace);
|
|
@@ -16791,6 +17109,13 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16791
17109
|
#queuedRow = document.createElement("div");
|
|
16792
17110
|
/** How far back the composer has been walked; null while the user is typing. */
|
|
16793
17111
|
#recallIndex = null;
|
|
17112
|
+
/**
|
|
17113
|
+
* What the walk last put in the composer, read back from it. A walk is live
|
|
17114
|
+
* only while the composer still holds exactly this, so a write from anywhere
|
|
17115
|
+
* else ends it. Stale while no walk is in progress, which is harmless: the
|
|
17116
|
+
* comparison can only ever end a walk, never start one.
|
|
17117
|
+
*/
|
|
17118
|
+
#recalled = "";
|
|
16794
17119
|
/**
|
|
16795
17120
|
* Re-clamp the dragged launcher when the window changes size. Bound once as
|
|
16796
17121
|
* a field so `removeEventListener` on disconnect gets the same reference.
|
|
@@ -16826,6 +17151,59 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16826
17151
|
* history replay would draw the conversation a second time beneath it.
|
|
16827
17152
|
*/
|
|
16828
17153
|
#connectedBefore = false;
|
|
17154
|
+
/**
|
|
17155
|
+
* The `user-key` whose state the element holds -- the transcript, the recall
|
|
17156
|
+
* history, the Always allow waivers and the shared state -- or `null` before
|
|
17157
|
+
* the first connect, when it holds nobody's.
|
|
17158
|
+
*
|
|
17159
|
+
* Not the same thing as the attribute. The attribute names the principal the
|
|
17160
|
+
* host says is here now, and it can change while the element is out of the
|
|
17161
|
+
* document, where nothing acts on it; this is who the state still belongs
|
|
17162
|
+
* to. Connecting compares the two, which is how a key changed while detached
|
|
17163
|
+
* still gets the handover a live change gets.
|
|
17164
|
+
*/
|
|
17165
|
+
#principalKey = null;
|
|
17166
|
+
/**
|
|
17167
|
+
* The current principal's tenure: replaced on every handover from one
|
|
17168
|
+
* principal to another, and captured by each client as it is built, so that
|
|
17169
|
+
* a client can tell whether the principal it was built for is still the one
|
|
17170
|
+
* this element serves.
|
|
17171
|
+
*
|
|
17172
|
+
* A client outlives the handover that stops it. A stopped run saves what it
|
|
17173
|
+
* had once its request closes, a tool handler cannot be aborted and its
|
|
17174
|
+
* result is kept, and an event already read off the wire is still applied --
|
|
17175
|
+
* all of it after the store was purged and scoped to whoever arrived, so
|
|
17176
|
+
* each one wrote the previous principal's words into the next one's storage
|
|
17177
|
+
* or state. Writing into the previous principal's store instead would only
|
|
17178
|
+
* re-create what the purge just removed, so such a client writes nothing.
|
|
17179
|
+
*
|
|
17180
|
+
* An identity rather than the key itself, for two reasons. The key's first
|
|
17181
|
+
* arrival is an adoption, not a handover, and a run in flight across it is
|
|
17182
|
+
* the same person's and keeps saving. And a key that leaves and comes back
|
|
17183
|
+
* is a second tenure: the first one's stopped run must not re-create a
|
|
17184
|
+
* conversation the handover purged.
|
|
17185
|
+
*/
|
|
17186
|
+
#tenure = {};
|
|
17187
|
+
/**
|
|
17188
|
+
* The conversation on screen: replaced every time the transcript is cleared
|
|
17189
|
+
* for another one -- New chat, a drawer switch, deleting the active thread,
|
|
17190
|
+
* `reload()`, re-insertion and a handover all pass through
|
|
17191
|
+
* {@link #resetConversation} -- and captured by each frontend tool call as it
|
|
17192
|
+
* is dispatched, so the call can tell whether the conversation it was drawn
|
|
17193
|
+
* in is still the one showing.
|
|
17194
|
+
*
|
|
17195
|
+
* A call outlives the reset that stops its run. A handler cannot be aborted,
|
|
17196
|
+
* and a card open at the time is declined, so the call resumes once the new
|
|
17197
|
+
* conversation is showing; drawing the run's pending indicator then put it
|
|
17198
|
+
* in a conversation with no run to take it down.
|
|
17199
|
+
*
|
|
17200
|
+
* Not {@link #tenure}, which only a handover replaces. A reset is not a
|
|
17201
|
+
* change of principal: the call still belongs to someone this element
|
|
17202
|
+
* serves, so it still settles its card, clears its checkpoint and returns
|
|
17203
|
+
* its result, and only the drawing is skipped. The tenure checks end the
|
|
17204
|
+
* call before any of that.
|
|
17205
|
+
*/
|
|
17206
|
+
#conversation = {};
|
|
16829
17207
|
/**
|
|
16830
17208
|
* The remote store `data-threads-url` wrapped around the conversation store,
|
|
16831
17209
|
* and the store inside it, so connecting again can wrap that store rather
|
|
@@ -16844,6 +17222,42 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16844
17222
|
// the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
|
|
16845
17223
|
// rounds, but the user must still be able to stop there.
|
|
16846
17224
|
#running = false;
|
|
17225
|
+
/**
|
|
17226
|
+
* The send {@link sendMessage} has taken, or the retry {@link retryLastTurn}
|
|
17227
|
+
* has, whose run has not started yet; null otherwise.
|
|
17228
|
+
*
|
|
17229
|
+
* `running` turns on when the client starts the run, a microtask after it is
|
|
17230
|
+
* asked for one, and a send does two things before it asks: it draws the
|
|
17231
|
+
* turn, and it dispatches the submit event, whose listeners run in the middle
|
|
17232
|
+
* of the send. A send from one of them, or a second call in the same task,
|
|
17233
|
+
* passed every guard and started a second run on the same client -- two
|
|
17234
|
+
* requests, and the listener's turn drawn between the user's turn and its
|
|
17235
|
+
* answer. It is the gap a picked checkpoint closes by counting from the pick,
|
|
17236
|
+
* closed the same way for a send. A retry asks for its run the same way, so
|
|
17237
|
+
* it takes the same hold, and so does every restore, from before it asks the
|
|
17238
|
+
* store for the conversation, through the history host's `holdRun`: until
|
|
17239
|
+
* the store answers, the client can only be built from nothing.
|
|
17240
|
+
*
|
|
17241
|
+
* Let go of once the run starts, because `running` holds from then until the
|
|
17242
|
+
* run settles, and a send held past that would refuse a host's follow-up from
|
|
17243
|
+
* the run-finished event, which fires as the run settles. Let go of by the
|
|
17244
|
+
* send's, the retry's or the restore's end when its run never starts.
|
|
17245
|
+
*
|
|
17246
|
+
* An object rather than a flag, so a send or a retry ending releases only
|
|
17247
|
+
* what it took. By the time either ends, the turn queued behind it has
|
|
17248
|
+
* usually taken the field for itself, and a Stop lets go of it for the next
|
|
17249
|
+
* send to take; the earlier one ending must release neither. It is also how
|
|
17250
|
+
* {@link sendMessage} tells the one hold it waits on from all the others it
|
|
17251
|
+
* refuses behind: see {@link #restoreHold}.
|
|
17252
|
+
*/
|
|
17253
|
+
#sending = null;
|
|
17254
|
+
/**
|
|
17255
|
+
* The hold the latest restore with no run to resume took, and what settles
|
|
17256
|
+
* once that restore has ended or been stopped; `null` when the latest
|
|
17257
|
+
* restore will resume a run. Only meaningful while `#sending` is that hold:
|
|
17258
|
+
* then a host's {@link sendMessage} waits for it rather than refusing.
|
|
17259
|
+
*/
|
|
17260
|
+
#restoreHold = null;
|
|
16847
17261
|
/** The decision a run is suspended on, which a Stop abandons. */
|
|
16848
17262
|
#decision = new PendingDecision();
|
|
16849
17263
|
/**
|
|
@@ -16933,7 +17347,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16933
17347
|
approvalRenderer: () => this.approvalRenderer,
|
|
16934
17348
|
getContext: () => this.getContext(),
|
|
16935
17349
|
conversationStore: () => this.conversationStore,
|
|
16936
|
-
threadId: () => this.#history.threadId
|
|
17350
|
+
threadId: () => this.#history.threadId,
|
|
17351
|
+
// Read live and compared, the way each client compares the one it was
|
|
17352
|
+
// built with: see #tenure.
|
|
17353
|
+
tenure: () => this.#tenure,
|
|
17354
|
+
// The same, for the conversation on screen: see #conversation.
|
|
17355
|
+
conversation: () => this.#conversation
|
|
16937
17356
|
});
|
|
16938
17357
|
this.#runHandlers = new RunHandlers({
|
|
16939
17358
|
element: this,
|
|
@@ -17069,6 +17488,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17069
17488
|
// A continuation sends only what the composer holds. The tray is not
|
|
17070
17489
|
// read, so nothing rides along to be drawn or announced.
|
|
17071
17490
|
announceTurn: (content) => this.#announceTurn(content, []),
|
|
17491
|
+
recordTurn: (content) => this.#recordTurn(content),
|
|
17072
17492
|
autoGrow: () => autoGrow(this.#input),
|
|
17073
17493
|
continuationEnded: () => this.#flushQueued(),
|
|
17074
17494
|
client: () => this.#client,
|
|
@@ -17077,7 +17497,41 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17077
17497
|
releaseClient: () => {
|
|
17078
17498
|
this.#client = null;
|
|
17079
17499
|
},
|
|
17080
|
-
|
|
17500
|
+
// A held send or retry is as much in flight to a pick as a run is: it has
|
|
17501
|
+
// asked this element's client for a run that has not started, and a
|
|
17502
|
+
// continuation started beside it was a second run in one conversation.
|
|
17503
|
+
// So is a restore, which holds through the same field from before the
|
|
17504
|
+
// store answers.
|
|
17505
|
+
running: () => this.#running || this.#sending !== null,
|
|
17506
|
+
// The hold a send takes, taken for every restore, so what refuses a send
|
|
17507
|
+
// held short of its run holds one made while the store is still
|
|
17508
|
+
// answering: Send parks the turn, a Retry and a pick refuse, and
|
|
17509
|
+
// `sendMessage` refuses behind a restore that will resume a run and waits
|
|
17510
|
+
// for one that will not -- `restored` is what it waits on, given only for
|
|
17511
|
+
// that kind. The same field rather than a flag beside it, because what
|
|
17512
|
+
// lets go of a send's hold is right for this one too. The resumed run
|
|
17513
|
+
// starting lets go, in `#setRunning`, so there is no gap after the resume
|
|
17514
|
+
// is asked for, and a Stop does, in `#cancelRun`. A flag the restore
|
|
17515
|
+
// cleared itself would still be set when the resumed run settles and
|
|
17516
|
+
// sends the parked turn, so that send would be refused after the turn had
|
|
17517
|
+
// left the queue, and lost.
|
|
17518
|
+
//
|
|
17519
|
+
// Whether the restore was stopped is not read off this hold, because a
|
|
17520
|
+
// host's send that waited takes it over; the restore asks its own
|
|
17521
|
+
// generation.
|
|
17522
|
+
holdRun: (restored) => {
|
|
17523
|
+
const hold = {};
|
|
17524
|
+
this.#sending = hold;
|
|
17525
|
+
this.#restoreHold = restored === null ? null : { hold, restored };
|
|
17526
|
+
return {
|
|
17527
|
+
release: () => {
|
|
17528
|
+
if (this.#sending === hold) {
|
|
17529
|
+
this.#sending = null;
|
|
17530
|
+
this.#flushQueued();
|
|
17531
|
+
}
|
|
17532
|
+
}
|
|
17533
|
+
};
|
|
17534
|
+
},
|
|
17081
17535
|
cancelRun: () => this.#cancelRun(),
|
|
17082
17536
|
resetState: () => this.#resetState(),
|
|
17083
17537
|
setRunning: (running) => this.#setRunning(running)
|
|
@@ -17387,6 +17841,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17387
17841
|
}
|
|
17388
17842
|
this.#syncLauncher();
|
|
17389
17843
|
this.#skills.init();
|
|
17844
|
+
const held = this.#principalKey;
|
|
17845
|
+
const changed = held !== null && held !== this.userKey;
|
|
17846
|
+
if (changed) {
|
|
17847
|
+
this.#changePrincipal(held, this.userKey);
|
|
17848
|
+
}
|
|
17849
|
+
this.#principalKey = this.userKey;
|
|
17390
17850
|
this.conversationStore = this.#storage.scopeStore(this.#unwrappedStore(), this.userKey);
|
|
17391
17851
|
window.addEventListener("resize", this.#onViewportResize);
|
|
17392
17852
|
window.visualViewport?.addEventListener("resize", this.#onViewportResize);
|
|
@@ -17398,6 +17858,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17398
17858
|
this.#history.adoptActiveThread();
|
|
17399
17859
|
queueMicrotask(() => this.#startup());
|
|
17400
17860
|
void this.#history.rehydrate();
|
|
17861
|
+
if (changed) {
|
|
17862
|
+
void this.#history.refreshDrawer();
|
|
17863
|
+
}
|
|
17401
17864
|
this.#connected = true;
|
|
17402
17865
|
}
|
|
17403
17866
|
/**
|
|
@@ -17722,23 +18185,49 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17722
18185
|
* logout, because a logout is a navigation (or, in a single-page app, not
|
|
17723
18186
|
* even that) rather than a tab close. Nothing remounts, so the host naming
|
|
17724
18187
|
* the new principal is the only signal the element will ever get.
|
|
18188
|
+
*
|
|
18189
|
+
* Two callers. A live change runs all of it. connectedCallback runs it for a
|
|
18190
|
+
* key that changed while the element was out of the document, and there it
|
|
18191
|
+
* runs before the store is scoped and before `#connected` is set -- which is
|
|
18192
|
+
* what `live` reads, since `isConnected` is already true by then. That call
|
|
18193
|
+
* does the half about what storage and the element still hold. The half
|
|
18194
|
+
* about the principal arriving -- the store scoped under the new key, its
|
|
18195
|
+
* active thread, the replay -- is what connecting goes on to do anyway, and
|
|
18196
|
+
* run here too it would read the old store before the new one exists,
|
|
18197
|
+
* minting a thread pointer in the namespace just purged and replaying the
|
|
18198
|
+
* history twice. Nor would it leave alone a store the host assigned while
|
|
18199
|
+
* the element was out.
|
|
17725
18200
|
*/
|
|
17726
18201
|
#changePrincipal(previousKey, nextKey) {
|
|
18202
|
+
this.#principalKey = nextKey;
|
|
18203
|
+
const live = this.#connected;
|
|
17727
18204
|
const previous = this.#storage.conversationNamespace(previousKey);
|
|
17728
18205
|
const next = this.#storage.conversationNamespace(nextKey);
|
|
17729
18206
|
if (previousKey === "") {
|
|
17730
18207
|
SessionStorageStore.adopt(previous, next);
|
|
17731
|
-
|
|
18208
|
+
if (live) {
|
|
18209
|
+
this.#rescopeStore(next);
|
|
18210
|
+
}
|
|
17732
18211
|
return;
|
|
17733
18212
|
}
|
|
18213
|
+
this.#tenure = {};
|
|
17734
18214
|
SessionStorageStore.purge(previous);
|
|
17735
|
-
|
|
18215
|
+
if (live) {
|
|
18216
|
+
this.#rescopeStore(next);
|
|
18217
|
+
}
|
|
17736
18218
|
this.#dispatch.forgetWaivers();
|
|
17737
18219
|
this.#cancelRun();
|
|
17738
18220
|
this.#resetState();
|
|
17739
18221
|
this.#sharedState = {};
|
|
18222
|
+
this.#input.value = "";
|
|
18223
|
+
this.#onInput();
|
|
17740
18224
|
this.#setRunning(false);
|
|
17741
18225
|
this.#setUnread(0);
|
|
18226
|
+
if (!live) {
|
|
18227
|
+
return;
|
|
18228
|
+
}
|
|
18229
|
+
this.#voice.dispose();
|
|
18230
|
+
this.#voice.wire();
|
|
17742
18231
|
this.#history.adoptActiveThread();
|
|
17743
18232
|
void this.#history.rehydrate();
|
|
17744
18233
|
void this.#history.refreshDrawer();
|
|
@@ -17747,13 +18236,17 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17747
18236
|
* Rebuild the `sessionStorage` store under `namespace`, re-wrapping it for
|
|
17748
18237
|
* `data-threads-url` exactly as connecting did.
|
|
17749
18238
|
*
|
|
17750
|
-
* A store
|
|
17751
|
-
* somewhere the element cannot see has to
|
|
18239
|
+
* A store the host assigned is left alone, whether before connecting or
|
|
18240
|
+
* since: a store that holds its data somewhere the element cannot see has to
|
|
18241
|
+
* scope itself. The exception is a plain `SessionStorageStore` assigned
|
|
18242
|
+
* before connecting, which connecting cannot tell from the default and so
|
|
18243
|
+
* took for the element's own. Hence the unwrapped store in use is what the scope is asked
|
|
18244
|
+
* about, rather than what it remembered when connecting. The transcript on
|
|
17752
18245
|
* screen is still cleared either way — the host swapped principals, and that
|
|
17753
18246
|
* much is the element's to act on.
|
|
17754
18247
|
*/
|
|
17755
18248
|
#rescopeStore(namespace) {
|
|
17756
|
-
const store = this.#storage.rescopeStore(namespace);
|
|
18249
|
+
const store = this.#storage.rescopeStore(this.#unwrappedStore(), namespace);
|
|
17757
18250
|
if (store === null) {
|
|
17758
18251
|
return;
|
|
17759
18252
|
}
|
|
@@ -17849,6 +18342,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17849
18342
|
#resetConversation() {
|
|
17850
18343
|
this.#client = null;
|
|
17851
18344
|
this.#runHandlers.detach();
|
|
18345
|
+
this.#conversation = {};
|
|
17852
18346
|
this.#clearTranscript();
|
|
17853
18347
|
this.#history.forgetRestored();
|
|
17854
18348
|
}
|
|
@@ -17876,8 +18370,20 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17876
18370
|
*
|
|
17877
18371
|
* History is truncated to the most recent user message inclusive and the run
|
|
17878
18372
|
* 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
|
-
*
|
|
18373
|
+
* last answer was wrong. Returns `false` when there is nothing to retry, or
|
|
18374
|
+
* while anything that holds a send is in flight: a run, a send or another
|
|
18375
|
+
* retry whose run has not started yet, a checkpoint continuation, or a
|
|
18376
|
+
* restore of the stored conversation that is still loading. It does not wait
|
|
18377
|
+
* for a restore as a send does: nothing is on screen to retry yet, and after
|
|
18378
|
+
* the load it would re-ask the previous visit's last question. Also `false`
|
|
18379
|
+
* when host code it
|
|
18380
|
+
* runs before its run starts -- the store's save, or an activity or tool
|
|
18381
|
+
* renderer the replay draws through -- has stopped it by starting a new chat
|
|
18382
|
+
* or changing `user-key`. A tool or context provider that stops it does so
|
|
18383
|
+
* inside the client's resume, past the last point this can see: nothing is
|
|
18384
|
+
* sent, and this resolves `true`, because both a run starting and a Stop let
|
|
18385
|
+
* go of the hold it checks. Rejects with the store's error when the store
|
|
18386
|
+
* refuses the save, and then the conversation is as it was.
|
|
17881
18387
|
*
|
|
17882
18388
|
* Public because a host with its own message UI wants the same button, and
|
|
17883
18389
|
* because the failed-run notice reaches it from outside the action row.
|
|
@@ -17888,20 +18394,35 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17888
18394
|
* again -- unless the user waived it for this session.
|
|
17889
18395
|
*/
|
|
17890
18396
|
async retryLastTurn() {
|
|
17891
|
-
if (this.#running) {
|
|
17892
|
-
return false;
|
|
17893
|
-
}
|
|
17894
|
-
const client = this.#ensureClient();
|
|
17895
|
-
const kept = client.truncateToLastUser();
|
|
17896
|
-
if (kept === null) {
|
|
18397
|
+
if (this.#running || this.#sending !== null || this.#history.continuation !== null) {
|
|
17897
18398
|
return false;
|
|
17898
18399
|
}
|
|
17899
|
-
|
|
17900
|
-
|
|
17901
|
-
|
|
18400
|
+
const hold = {};
|
|
18401
|
+
this.#sending = hold;
|
|
18402
|
+
try {
|
|
18403
|
+
const client = this.#ensureClient();
|
|
18404
|
+
const kept = client.truncateToLastUser();
|
|
18405
|
+
if (kept === null) {
|
|
18406
|
+
return false;
|
|
18407
|
+
}
|
|
18408
|
+
if (this.#sending !== hold) {
|
|
18409
|
+
return false;
|
|
18410
|
+
}
|
|
18411
|
+
this.#clearTranscript();
|
|
18412
|
+
for (const message of kept) {
|
|
18413
|
+
this.#history.replay(message);
|
|
18414
|
+
if (this.#sending !== hold) {
|
|
18415
|
+
return false;
|
|
18416
|
+
}
|
|
18417
|
+
}
|
|
18418
|
+
await client.resume();
|
|
18419
|
+
return true;
|
|
18420
|
+
} finally {
|
|
18421
|
+
if (this.#sending === hold) {
|
|
18422
|
+
this.#sending = null;
|
|
18423
|
+
this.#flushQueued();
|
|
18424
|
+
}
|
|
17902
18425
|
}
|
|
17903
|
-
await client.resume();
|
|
17904
|
-
return true;
|
|
17905
18426
|
}
|
|
17906
18427
|
/**
|
|
17907
18428
|
* Append a message bubble and return it.
|
|
@@ -18213,6 +18734,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18213
18734
|
* which is what every shell and every coding agent means by this. Arrowing
|
|
18214
18735
|
* forward past the newest empties the composer again rather than sticking on
|
|
18215
18736
|
* it, so the way out is the same key that got you in.
|
|
18737
|
+
*
|
|
18738
|
+
* A walk ends with any write into the composer that is not its own, so that
|
|
18739
|
+
* whatever was written is never replaced by the next step.
|
|
18216
18740
|
*/
|
|
18217
18741
|
#recallHistory(event) {
|
|
18218
18742
|
const back = event.key === "ArrowUp";
|
|
@@ -18223,6 +18747,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18223
18747
|
if (drafts.length === 0) {
|
|
18224
18748
|
return;
|
|
18225
18749
|
}
|
|
18750
|
+
if (this.#input.value !== this.#recalled) {
|
|
18751
|
+
this.#recallIndex = null;
|
|
18752
|
+
}
|
|
18226
18753
|
if (this.#recallIndex === null && (!back || this.#input.value !== "")) {
|
|
18227
18754
|
return;
|
|
18228
18755
|
}
|
|
@@ -18233,6 +18760,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18233
18760
|
event.preventDefault();
|
|
18234
18761
|
this.#recallIndex = next < 0 ? null : next;
|
|
18235
18762
|
this.#input.value = next < 0 ? "" : drafts[next];
|
|
18763
|
+
this.#recalled = this.#input.value;
|
|
18236
18764
|
this.#input.setSelectionRange(this.#input.value.length, this.#input.value.length);
|
|
18237
18765
|
autoGrow(this.#input);
|
|
18238
18766
|
}
|
|
@@ -18243,15 +18771,11 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18243
18771
|
* observes the disconnect).
|
|
18244
18772
|
*/
|
|
18245
18773
|
#cancelRun() {
|
|
18246
|
-
for (const text4 of this.#queued) {
|
|
18247
|
-
if (this.#sentDrafts[0] !== text4) {
|
|
18248
|
-
this.#sentDrafts.unshift(text4);
|
|
18249
|
-
}
|
|
18250
|
-
}
|
|
18251
18774
|
this.#queued.length = 0;
|
|
18252
18775
|
this.#renderQueued();
|
|
18253
18776
|
this.#decision.abort();
|
|
18254
18777
|
this.#client?.cancel();
|
|
18778
|
+
this.#sending = null;
|
|
18255
18779
|
this.#history.stopContinuation();
|
|
18256
18780
|
}
|
|
18257
18781
|
/**
|
|
@@ -18264,6 +18788,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18264
18788
|
#setRunning(running) {
|
|
18265
18789
|
const settled = this.#running && !running;
|
|
18266
18790
|
this.#running = running;
|
|
18791
|
+
if (running) {
|
|
18792
|
+
this.#sending = null;
|
|
18793
|
+
}
|
|
18267
18794
|
const label = running ? this.#strings.stop : this.#strings.send;
|
|
18268
18795
|
this.#send.title = label;
|
|
18269
18796
|
this.#send.setAttribute("aria-label", label);
|
|
@@ -18314,13 +18841,29 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18314
18841
|
this.#queuedRow.appendChild(chip);
|
|
18315
18842
|
}
|
|
18316
18843
|
}
|
|
18844
|
+
/**
|
|
18845
|
+
* Put a turn that has left the composer at the front of the recall history,
|
|
18846
|
+
* and start the next walk from it.
|
|
18847
|
+
*
|
|
18848
|
+
* Every turn the user typed and pressed a key to send comes through here:
|
|
18849
|
+
* sent at once, queued behind a run, or sent to continue a checkpoint. A
|
|
18850
|
+
* host's own `sendMessage` does not, because its text was never in the
|
|
18851
|
+
* composer, and the arrow keys walk back through what the user wrote there.
|
|
18852
|
+
*/
|
|
18853
|
+
#recordTurn(content) {
|
|
18854
|
+
if (content !== "" && this.#sentDrafts[0] !== content) {
|
|
18855
|
+
this.#sentDrafts.unshift(content);
|
|
18856
|
+
}
|
|
18857
|
+
this.#recallIndex = null;
|
|
18858
|
+
}
|
|
18317
18859
|
async #submit() {
|
|
18318
18860
|
const content = this.#input.value.trim();
|
|
18319
18861
|
const attachments = this.#attachments.tray?.readyRefs() ?? [];
|
|
18320
18862
|
if (content === "" && attachments.length === 0) {
|
|
18321
18863
|
return;
|
|
18322
18864
|
}
|
|
18323
|
-
|
|
18865
|
+
this.#recordTurn(content);
|
|
18866
|
+
if (this.#running || this.#sending !== null || this.#history.continuation !== null) {
|
|
18324
18867
|
if (content !== "") {
|
|
18325
18868
|
this.#queued.push(content);
|
|
18326
18869
|
this.#renderQueued();
|
|
@@ -18329,10 +18872,6 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18329
18872
|
}
|
|
18330
18873
|
return;
|
|
18331
18874
|
}
|
|
18332
|
-
if (content !== "" && this.#sentDrafts[0] !== content) {
|
|
18333
|
-
this.#sentDrafts.unshift(content);
|
|
18334
|
-
}
|
|
18335
|
-
this.#recallIndex = null;
|
|
18336
18875
|
this.#input.value = "";
|
|
18337
18876
|
autoGrow(this.#input);
|
|
18338
18877
|
if (this.#attachments.tray?.hasPending() === true) {
|
|
@@ -18358,21 +18897,77 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18358
18897
|
* `attachments` are durable {@link AttachmentRef}s — what {@link attachFile}
|
|
18359
18898
|
* resolves to and what {@link ATTACHMENT_EVENT} reports.
|
|
18360
18899
|
*
|
|
18361
|
-
*
|
|
18362
|
-
*
|
|
18363
|
-
*
|
|
18364
|
-
*
|
|
18365
|
-
*
|
|
18900
|
+
* Resolves `true` once the turn has been handed to the client for its run,
|
|
18901
|
+
* which is when the interaction settles, frontend-tool rounds included; a
|
|
18902
|
+
* run a tool or context provider then stops still counts, as it does for
|
|
18903
|
+
* {@link retryLastTurn}. Resolves `false` when nothing was sent.
|
|
18904
|
+
*
|
|
18905
|
+
* Nothing is sent for an empty message, nor while a run or a picked
|
|
18906
|
+
* checkpoint's continuation is in flight, since a second concurrent run
|
|
18907
|
+
* would orphan the first. A send is in flight from the moment it is taken
|
|
18908
|
+
* rather than from its run's start, so nothing is sent from a
|
|
18909
|
+
* {@link SUBMIT_EVENT} listener or for a second call in the same task
|
|
18910
|
+
* either. Nor with no `endpoint`, which says so in the transcript. Nor
|
|
18911
|
+
* behind a reload's restore that will resume a run a navigating tool
|
|
18912
|
+
* interrupted, since that restore ends by starting the run.
|
|
18913
|
+
*
|
|
18914
|
+
* **While the element restores a stored conversation with no run to resume
|
|
18915
|
+
* -- on connect, {@link reload}, a thread picked from the drawer, a change
|
|
18916
|
+
* from one `user-key` to another -- it waits, then sends.** The turn has to follow the
|
|
18917
|
+
* conversation it is part of: sent at once, it went out without it, the
|
|
18918
|
+
* conversation was then drawn under it, and its save replaced the stored
|
|
18919
|
+
* one. So the bubble, the {@link SUBMIT_EVENT} and the request all come after
|
|
18920
|
+
* the replay, by the length of the store's load. A New chat, a thread
|
|
18921
|
+
* switch, a change from one `user-key` to another, a reload or the element
|
|
18922
|
+
* leaving the page while it waits drops it, resolving `false`: the
|
|
18923
|
+
* conversation it was addressed to is no longer on screen. Leaving the page
|
|
18924
|
+
* resolves it only once the load answers or the element is back, since
|
|
18925
|
+
* that is when the restore learns it was stopped. A first `user-key` on an
|
|
18926
|
+
* anonymous conversation adopts it rather than replacing it, and stops
|
|
18927
|
+
* nothing. A second call while one waits is
|
|
18928
|
+
* refused, as a second call in the same task is.
|
|
18929
|
+
*
|
|
18930
|
+
* Unlike the built-in Send it does not queue: it returns, and the caller
|
|
18931
|
+
* keeps what it tried to send. Nor does it consult the tray -- what you pass
|
|
18932
|
+
* is what is sent.
|
|
18366
18933
|
*/
|
|
18367
18934
|
async sendMessage(content, attachments = []) {
|
|
18368
|
-
if (
|
|
18369
|
-
return;
|
|
18935
|
+
if (content === "" && attachments.length === 0) {
|
|
18936
|
+
return false;
|
|
18937
|
+
}
|
|
18938
|
+
const restore = this.#restoreHold;
|
|
18939
|
+
if (restore !== null && this.#sending === restore.hold) {
|
|
18940
|
+
const waiting = {};
|
|
18941
|
+
this.#sending = waiting;
|
|
18942
|
+
await restore.restored;
|
|
18943
|
+
if (this.#sending !== waiting) {
|
|
18944
|
+
return false;
|
|
18945
|
+
}
|
|
18946
|
+
this.#sending = null;
|
|
18947
|
+
return this.sendMessage(content, attachments);
|
|
18370
18948
|
}
|
|
18371
|
-
if (this.#
|
|
18372
|
-
|
|
18949
|
+
if (this.#running || this.#sending !== null || this.#history.continuation !== null) {
|
|
18950
|
+
return false;
|
|
18951
|
+
}
|
|
18952
|
+
const send = {};
|
|
18953
|
+
this.#sending = send;
|
|
18954
|
+
try {
|
|
18955
|
+
if (this.#transcript.isEmpty()) {
|
|
18956
|
+
this.setAttribute("data-composer-settling", "");
|
|
18957
|
+
}
|
|
18958
|
+
this.#announceTurn(content, attachments);
|
|
18959
|
+
if (this.#sending !== send) {
|
|
18960
|
+
return false;
|
|
18961
|
+
}
|
|
18962
|
+
const connected = this.endpoint !== "";
|
|
18963
|
+
await this.#client_send(content, attachments);
|
|
18964
|
+
return connected;
|
|
18965
|
+
} finally {
|
|
18966
|
+
if (this.#sending === send) {
|
|
18967
|
+
this.#sending = null;
|
|
18968
|
+
this.#flushQueued();
|
|
18969
|
+
}
|
|
18373
18970
|
}
|
|
18374
|
-
this.#announceTurn(content, attachments);
|
|
18375
|
-
await this.#client_send(content, attachments);
|
|
18376
18971
|
}
|
|
18377
18972
|
/**
|
|
18378
18973
|
* Put a turn the user sent on screen, and tell the host it was sent.
|
|
@@ -18472,6 +19067,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18472
19067
|
*/
|
|
18473
19068
|
#buildClient(seed) {
|
|
18474
19069
|
const threadId = this.#history.threadId;
|
|
19070
|
+
const tenure = this.#tenure;
|
|
18475
19071
|
const agent = this.agentFactory({
|
|
18476
19072
|
endpoint: seed.endpoint,
|
|
18477
19073
|
headers: this.#requestHeaders(),
|
|
@@ -18496,12 +19092,30 @@ var AgUiChat = class extends HTMLElement {
|
|
|
18496
19092
|
// list per thread. For the conversation's own client that is its
|
|
18497
19093
|
// history; a continuation holds only what it adds, and writes the
|
|
18498
19094
|
// conversation it continues ahead of that.
|
|
19095
|
+
//
|
|
19096
|
+
// Every message list a client saves reaches the store through here -- a
|
|
19097
|
+
// send, a round's end, a tool's result, the answers it writes for calls
|
|
19098
|
+
// left open or declined by a Stop, a Stop's last save -- and so does a
|
|
19099
|
+
// `data-threads-url` store's local copy, which is the only thing that
|
|
19100
|
+
// wrapper does on a save. So this is the one gate for them. `onSaved` is
|
|
19101
|
+
// behind it as well: it hands the saved conversation on as the seed of
|
|
19102
|
+
// the next client, which would be the next principal's.
|
|
18499
19103
|
onPersist: (messages) => {
|
|
19104
|
+
if (tenure !== this.#tenure) {
|
|
19105
|
+
return;
|
|
19106
|
+
}
|
|
18500
19107
|
const conversation = [...seed.follows, ...messages];
|
|
18501
19108
|
this.conversationStore.saveMessages(threadId, conversation);
|
|
18502
19109
|
seed.onSaved?.(conversation);
|
|
18503
19110
|
},
|
|
18504
|
-
|
|
19111
|
+
// Shared state is the next principal's first `RunAgentInput.state`, and
|
|
19112
|
+
// the handover has just emptied it. A snapshot already read off the wire
|
|
19113
|
+
// is applied after the Stop, and would put it back.
|
|
19114
|
+
onStateChanged: (state) => {
|
|
19115
|
+
if (tenure === this.#tenure) {
|
|
19116
|
+
this.#onSharedStateChanged(state);
|
|
19117
|
+
}
|
|
19118
|
+
},
|
|
18505
19119
|
connectionLostMessage: this.#strings.connectionLost,
|
|
18506
19120
|
unfinishedMessage: this.#strings.callNotFinished,
|
|
18507
19121
|
declinedMessage: this.#strings.declinedAction,
|
|
@@ -18663,7 +19277,7 @@ function setControlValue(el2, value) {
|
|
|
18663
19277
|
}
|
|
18664
19278
|
|
|
18665
19279
|
// src/version.ts
|
|
18666
|
-
var VERSION = "0.
|
|
19280
|
+
var VERSION = "0.43.0";
|
|
18667
19281
|
export {
|
|
18668
19282
|
ATTACHMENT_EVENT,
|
|
18669
19283
|
AgUiChat,
|