@assistant-ui/core 0.3.11 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +6 -5
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +6 -5
- package/dist/internal.js +5 -4
- package/dist/react/client/interactable-model-context.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.js +5 -1
- package/dist/react/client/interactable-model-context.js.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts +2 -3
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +2 -16
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +5 -1
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +25 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +64 -11
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +14 -0
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +31 -0
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.js +1 -6
- package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -4
- package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
- package/dist/runtime/internal.d.ts +4 -4
- package/dist/runtime/internal.js +4 -4
- package/dist/runtime/queue/external-thread-queue-adapter.d.ts +15 -0
- package/dist/runtime/queue/external-thread-queue-adapter.d.ts.map +1 -1
- package/dist/runtime/queue/message-queue.js +23 -10
- package/dist/runtime/queue/message-queue.js.map +1 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts +7 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +3 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +49 -13
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +2 -1
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/store/clients/external-thread.d.ts +61 -0
- package/dist/store/clients/external-thread.d.ts.map +1 -0
- package/dist/store/clients/external-thread.js +1445 -0
- package/dist/store/clients/external-thread.js.map +1 -0
- package/dist/store/clients/runtime-adapter.d.ts.map +1 -1
- package/dist/store/clients/runtime-adapter.js +5 -2
- package/dist/store/clients/runtime-adapter.js.map +1 -1
- package/dist/store/clients/single-thread-list.d.ts +9 -0
- package/dist/store/clients/single-thread-list.d.ts.map +1 -0
- package/dist/store/clients/single-thread-list.js +175 -0
- package/dist/store/clients/single-thread-list.js.map +1 -0
- package/dist/store/clients/suggestions.d.ts +3 -1
- package/dist/store/clients/suggestions.d.ts.map +1 -1
- package/dist/store/clients/suggestions.js +65 -37
- package/dist/store/clients/suggestions.js.map +1 -1
- package/dist/store/index.d.ts +5 -3
- package/dist/store/index.js +4 -2
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +64 -50
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +98 -82
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scope-registration.d.ts +2 -2
- package/dist/store/scopes/suggestions.d.ts +6 -1
- package/dist/store/scopes/suggestions.d.ts.map +1 -1
- package/dist/store/scopes/thread.d.ts +17 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/types/augmentations.d.ts +33 -0
- package/dist/types/augmentations.d.ts.map +1 -0
- package/dist/types/augmentations.js +0 -0
- package/dist/types/error.d.ts +15 -1
- package/dist/types/error.d.ts.map +1 -1
- package/dist/types/error.js +18 -1
- package/dist/types/error.js.map +1 -1
- package/dist/types/message.d.ts +2 -0
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/normalizePartStatus.d.ts +3 -2
- package/dist/utils/normalizePartStatus.d.ts.map +1 -1
- package/dist/utils/normalizePartStatus.js +15 -1
- package/dist/utils/normalizePartStatus.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +7 -0
- package/src/internal.ts +1 -1
- package/src/react/client/interactable-model-context.ts +6 -1
- package/src/react/runtimes/useExternalStoreRuntime.suggestions.test.tsx +114 -0
- package/src/runtime/api/message-runtime.test.ts +1 -1
- package/src/runtime/api/message-runtime.ts +2 -35
- package/src/runtime/api/thread-runtime.ts +8 -1
- package/src/runtime/base/base-composer-runtime-core.ts +100 -8
- package/src/runtime/base/base-thread-runtime-core.ts +36 -0
- package/src/runtime/base/default-edit-composer-runtime-core.ts +1 -15
- package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -11
- package/src/runtime/queue/external-thread-queue-adapter.ts +17 -0
- package/src/runtime/queue/message-queue.ts +26 -9
- package/src/runtimes/external-store/external-store-adapter.ts +7 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +100 -26
- package/src/runtimes/local/local-thread-runtime-core.ts +4 -1
- package/src/store/clients/external-thread.ts +1255 -0
- package/src/store/clients/runtime-adapter.ts +5 -2
- package/src/store/clients/single-thread-list.ts +116 -0
- package/src/store/clients/suggestions.ts +39 -13
- package/src/store/index.ts +6 -0
- package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -28
- package/src/store/runtime-clients/thread-runtime-client.ts +5 -0
- package/src/store/scopes/suggestions.ts +6 -0
- package/src/store/scopes/thread.ts +17 -0
- package/src/tests/append-interactable-snapshots.test.ts +350 -0
- package/src/tests/base-composer-runtime-core-send.test.ts +126 -0
- package/src/tests/base-composer-runtime-core.test.ts +108 -1
- package/src/tests/default-edit-composer-runtime-core.test.ts +0 -68
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +300 -0
- package/src/tests/external-thread-attachments.test.tsx +584 -0
- package/src/tests/external-thread-branches.test.tsx +157 -0
- package/src/tests/external-thread-feedback.test.tsx +204 -0
- package/src/tests/external-thread-parity.test.tsx +548 -0
- package/src/tests/external-thread-refetch.test.tsx +117 -0
- package/src/tests/external-thread-speech.test.tsx +328 -0
- package/src/tests/external-thread-suggestions.test.tsx +59 -0
- package/src/tests/message-queue.test.ts +26 -0
- package/src/tests/thread-message-like.test.ts +39 -0
- package/src/tests/thread-switch-events.test.tsx +136 -0
- package/src/types/augmentations.ts +33 -0
- package/src/types/error.ts +24 -0
- package/src/types/message.ts +2 -0
- package/src/utils/normalizePartStatus.ts +27 -0
- package/src/tests/default-thread-composer-runtime-core.test.ts +0 -82
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type PendingAttachment,
|
|
7
7
|
} from "../../types/attachment";
|
|
8
8
|
import type { MessageRole, AppendMessage } from "../../types/message";
|
|
9
|
+
import { isMessageNotSentError } from "../../types/error";
|
|
9
10
|
import type { QuoteInfo } from "../../types/quote";
|
|
10
11
|
import type { Unsubscribe } from "../../types/unsubscribe";
|
|
11
12
|
import type { RunConfig } from "../../types/message";
|
|
@@ -118,15 +119,22 @@ export abstract class BaseComposerRuntimeCore
|
|
|
118
119
|
if (this._text === value) return;
|
|
119
120
|
|
|
120
121
|
this._text = value;
|
|
121
|
-
|
|
122
|
-
this._dictationBaseText = value;
|
|
123
|
-
this._currentInterimText = "";
|
|
124
|
-
const { status, inputDisabled } = this._dictation;
|
|
125
|
-
this._dictation = inputDisabled ? { status, inputDisabled } : { status };
|
|
126
|
-
}
|
|
122
|
+
this._rebaseDictation(value);
|
|
127
123
|
this._notifySubscribers();
|
|
128
124
|
}
|
|
129
125
|
|
|
126
|
+
// A live dictation session appends to the text it last saw, so any write
|
|
127
|
+
// that bypasses `setText` has to move that baseline or the next transcript
|
|
128
|
+
// overwrites what was just written.
|
|
129
|
+
private _rebaseDictation(value: string) {
|
|
130
|
+
if (!this._dictation) return;
|
|
131
|
+
|
|
132
|
+
this._dictationBaseText = value;
|
|
133
|
+
this._currentInterimText = "";
|
|
134
|
+
const { status, inputDisabled } = this._dictation;
|
|
135
|
+
this._dictation = inputDisabled ? { status, inputDisabled } : { status };
|
|
136
|
+
}
|
|
137
|
+
|
|
130
138
|
public setRole(role: MessageRole) {
|
|
131
139
|
if (this._role === role) return;
|
|
132
140
|
|
|
@@ -245,6 +253,7 @@ export abstract class BaseComposerRuntimeCore
|
|
|
245
253
|
if (generation === this._sendGeneration) {
|
|
246
254
|
if (!this.text.trim() && this._quote === undefined) {
|
|
247
255
|
this._text = text;
|
|
256
|
+
this._rebaseDictation(text);
|
|
248
257
|
this._quote = quote;
|
|
249
258
|
this._notifySubscribers();
|
|
250
259
|
}
|
|
@@ -289,11 +298,94 @@ export abstract class BaseComposerRuntimeCore
|
|
|
289
298
|
metadata: { custom: { ...(quote ? { quote } : {}) } },
|
|
290
299
|
};
|
|
291
300
|
|
|
292
|
-
const
|
|
293
|
-
|
|
301
|
+
const draft = { text, quote, attachments: finalAttachments };
|
|
302
|
+
let sendTask: void | Promise<void>;
|
|
303
|
+
try {
|
|
304
|
+
sendTask = this.handleSend(message, options);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
this._restoreUnsentDraft(error, generation, draft);
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
if (sendTask)
|
|
310
|
+
void sendTask.catch((error) => {
|
|
311
|
+
this._restoreUnsentDraft(error, generation, draft);
|
|
312
|
+
});
|
|
294
313
|
this._notifyEventSubscribers("send", {});
|
|
295
314
|
}
|
|
296
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Take a message back into the composer when it has nowhere else to live:
|
|
318
|
+
* a send the runtime never dispatched, or a message a cancelled run is
|
|
319
|
+
* removing from the thread. Reports whether the composer accepted it, so a
|
|
320
|
+
* caller that is also removing the message can keep it instead of dropping
|
|
321
|
+
* it. Refused, and left untouched, while the composer holds anything of its
|
|
322
|
+
* own.
|
|
323
|
+
*/
|
|
324
|
+
public restoreDraft(draft: {
|
|
325
|
+
text: string;
|
|
326
|
+
quote?: QuoteInfo | undefined;
|
|
327
|
+
attachments?: readonly Attachment[] | undefined;
|
|
328
|
+
}): boolean {
|
|
329
|
+
if (
|
|
330
|
+
this._text.trim() ||
|
|
331
|
+
this._quote !== undefined ||
|
|
332
|
+
this._attachments.length > 0
|
|
333
|
+
)
|
|
334
|
+
return false;
|
|
335
|
+
|
|
336
|
+
this._text = draft.text;
|
|
337
|
+
this._rebaseDictation(draft.text);
|
|
338
|
+
this._quote = draft.quote;
|
|
339
|
+
this._attachments = draft.attachments ?? [];
|
|
340
|
+
this._notifySubscribers();
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Inverse of `restoreDraft`: clears the composer while it still holds
|
|
346
|
+
* exactly the given draft. A draft the user has edited since is left
|
|
347
|
+
* untouched.
|
|
348
|
+
*/
|
|
349
|
+
public retractDraft(draft: {
|
|
350
|
+
text: string;
|
|
351
|
+
quote?: QuoteInfo | undefined;
|
|
352
|
+
attachments?: readonly Attachment[] | undefined;
|
|
353
|
+
}): void {
|
|
354
|
+
const attachmentsUntouched =
|
|
355
|
+
draft.attachments !== undefined
|
|
356
|
+
? this._attachments === draft.attachments
|
|
357
|
+
: this._attachments.length === 0;
|
|
358
|
+
if (
|
|
359
|
+
this._text !== draft.text ||
|
|
360
|
+
this._quote !== draft.quote ||
|
|
361
|
+
!attachmentsUntouched
|
|
362
|
+
)
|
|
363
|
+
return;
|
|
364
|
+
|
|
365
|
+
this._text = "";
|
|
366
|
+
this._rebaseDictation("");
|
|
367
|
+
this._quote = undefined;
|
|
368
|
+
this._attachments = [];
|
|
369
|
+
this._notifySubscribers();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// The generation check is what a reset and a later send use to invalidate a
|
|
373
|
+
// draft, so of several queued drafts only the most recent one is still
|
|
374
|
+
// restorable.
|
|
375
|
+
private _restoreUnsentDraft(
|
|
376
|
+
error: unknown,
|
|
377
|
+
generation: number,
|
|
378
|
+
draft: {
|
|
379
|
+
text: string;
|
|
380
|
+
quote: QuoteInfo | undefined;
|
|
381
|
+
attachments: readonly CompleteAttachment[];
|
|
382
|
+
},
|
|
383
|
+
) {
|
|
384
|
+
if (!isMessageNotSentError(error)) return;
|
|
385
|
+
if (generation !== this._sendGeneration) return;
|
|
386
|
+
this.restoreDraft(draft);
|
|
387
|
+
}
|
|
388
|
+
|
|
297
389
|
public cancel() {
|
|
298
390
|
this.handleCancel();
|
|
299
391
|
}
|
|
@@ -35,6 +35,7 @@ import type { AttachmentAdapter } from "../../adapters/attachment";
|
|
|
35
35
|
import type { RealtimeVoiceAdapter } from "../../adapters/voice";
|
|
36
36
|
import type { ThreadMessageLike } from "../utils/thread-message-like";
|
|
37
37
|
import { notifyEventListeners } from "../../utils/notify-event-listeners";
|
|
38
|
+
import { gateInteractableComposerMetadata } from "../../model-context/interactable-composer-metadata";
|
|
38
39
|
|
|
39
40
|
type BaseThreadAdapters = {
|
|
40
41
|
speech?: SpeechSynthesisAdapter | undefined;
|
|
@@ -123,6 +124,41 @@ export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
|
|
|
123
124
|
return this._contextProvider.getModelContext();
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Stamps provider-contributed composer metadata onto an outgoing message.
|
|
129
|
+
* Called at dispatch rather than in the composer, so programmatic sends are
|
|
130
|
+
* covered too, and exactly once per message: a queued send is stamped when
|
|
131
|
+
* it leaves the lane, never when it enters.
|
|
132
|
+
*
|
|
133
|
+
* Only user messages are stamped, matching the readers: both the version
|
|
134
|
+
* fold and the model injection skip every other role.
|
|
135
|
+
*
|
|
136
|
+
* @param anchorId Message the gated branch prefix ends at. A queued send
|
|
137
|
+
* passes the current tail, having waited through a run that grew the prefix
|
|
138
|
+
* past the parent it was created with.
|
|
139
|
+
*/
|
|
140
|
+
protected enrichAppendMetadata(
|
|
141
|
+
message: AppendMessage,
|
|
142
|
+
anchorId: string | null = message.parentId,
|
|
143
|
+
): AppendMessage {
|
|
144
|
+
if (message.role !== "user") return message;
|
|
145
|
+
const messages = this.messages;
|
|
146
|
+
const parentIndex =
|
|
147
|
+
anchorId === null ? -1 : messages.findIndex((m) => m.id === anchorId);
|
|
148
|
+
const composerMetadata = gateInteractableComposerMetadata(
|
|
149
|
+
this.getModelContext().unstable_composerMetadata,
|
|
150
|
+
messages.slice(0, parentIndex + 1),
|
|
151
|
+
);
|
|
152
|
+
if (!composerMetadata) return message;
|
|
153
|
+
return {
|
|
154
|
+
...message,
|
|
155
|
+
metadata: {
|
|
156
|
+
...message.metadata,
|
|
157
|
+
custom: { ...message.metadata?.custom, ...composerMetadata },
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
126
162
|
private _editComposers = new Map<string, DefaultEditComposerRuntimeCore>();
|
|
127
163
|
public getEditComposer(messageId: string) {
|
|
128
164
|
return this._editComposers.get(messageId);
|
|
@@ -7,7 +7,6 @@ import type { DictationAdapter } from "../../adapters/speech";
|
|
|
7
7
|
import type { SendOptions } from "../interfaces/composer-runtime-core";
|
|
8
8
|
import type { ThreadRuntimeCore } from "../interfaces/thread-runtime-core";
|
|
9
9
|
import { BaseComposerRuntimeCore } from "./base-composer-runtime-core";
|
|
10
|
-
import { gateInteractableComposerMetadata } from "../../model-context/interactable-composer-metadata";
|
|
11
10
|
|
|
12
11
|
export class DefaultEditComposerRuntimeCore extends BaseComposerRuntimeCore {
|
|
13
12
|
public get canCancel() {
|
|
@@ -97,21 +96,8 @@ export class DefaultEditComposerRuntimeCore extends BaseComposerRuntimeCore {
|
|
|
97
96
|
...this._nonTextPassthrough,
|
|
98
97
|
] as AppendMessage["content"])
|
|
99
98
|
: message.content;
|
|
100
|
-
// Gate live state against the new branch's prefix (messages up to the
|
|
101
|
-
// parent): an unchanged interactable re-stamps the prior baseline, an
|
|
102
|
-
// interactable edited since the original message stamps its newest state.
|
|
103
|
-
const messages = this.runtime.messages;
|
|
104
|
-
const parentIndex =
|
|
105
|
-
this._parentId === null
|
|
106
|
-
? -1
|
|
107
|
-
: messages.findIndex((m) => m.id === this._parentId);
|
|
108
|
-
const composerMetadata = gateInteractableComposerMetadata(
|
|
109
|
-
this.runtime.getModelContext().unstable_composerMetadata,
|
|
110
|
-
messages.slice(0, parentIndex + 1),
|
|
111
|
-
);
|
|
112
|
-
const enriched = this.enrichWithComposerMetadata(message, composerMetadata);
|
|
113
99
|
const appendTask = this.runtime.append({
|
|
114
|
-
...
|
|
100
|
+
...message,
|
|
115
101
|
content,
|
|
116
102
|
parentId: this._parentId,
|
|
117
103
|
sourceId: this._sourceId,
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
type QueueItemState,
|
|
13
13
|
} from "../../store/scopes/queue-item";
|
|
14
14
|
import { BaseComposerRuntimeCore } from "./base-composer-runtime-core";
|
|
15
|
-
import { gateInteractableComposerMetadata } from "../../model-context/interactable-composer-metadata";
|
|
16
15
|
|
|
17
16
|
export class DefaultThreadComposerRuntimeCore
|
|
18
17
|
extends BaseComposerRuntimeCore
|
|
@@ -119,17 +118,8 @@ export class DefaultThreadComposerRuntimeCore
|
|
|
119
118
|
message: Omit<AppendMessage, "parentId" | "sourceId">,
|
|
120
119
|
options?: SendOptions,
|
|
121
120
|
) {
|
|
122
|
-
// Merge provider-contributed metadata onto the outgoing user message
|
|
123
|
-
// (same metadata.custom append path quotes ride). The interactables gate
|
|
124
|
-
// runs here because it needs thread history, unavailable to the provider.
|
|
125
|
-
const composerMetadata = gateInteractableComposerMetadata(
|
|
126
|
-
this.runtime.getModelContext().unstable_composerMetadata,
|
|
127
|
-
this.runtime.messages,
|
|
128
|
-
);
|
|
129
|
-
const enriched = this.enrichWithComposerMetadata(message, composerMetadata);
|
|
130
|
-
|
|
131
121
|
return this.runtime.append({
|
|
132
|
-
...(
|
|
122
|
+
...(message as AppendMessage),
|
|
133
123
|
parentId: this.runtime.messages.at(-1)?.id ?? null,
|
|
134
124
|
sourceId: null,
|
|
135
125
|
startRun: options?.startRun,
|
|
@@ -27,4 +27,21 @@ export type ExternalThreadQueueAdapter = {
|
|
|
27
27
|
move: (queueItemId: string, placement: QueuePlacement) => void;
|
|
28
28
|
edit: (queueItemId: string, message: AppendMessage) => void;
|
|
29
29
|
remove: (queueItemId: string) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Installed by the owning runtime, applied to a message as it leaves a lane.
|
|
32
|
+
* A message can wait through an entire run, so state the runtime attaches at
|
|
33
|
+
* send time is recomputed against the thread it actually lands on. Adapters
|
|
34
|
+
* from `createMessageQueue` provide this; a hand-rolled adapter that omits
|
|
35
|
+
* it dispatches whatever was enqueued.
|
|
36
|
+
*/
|
|
37
|
+
__internal_setDispatchTransform?:
|
|
38
|
+
| ((transform: (message: AppendMessage) => AppendMessage) => void)
|
|
39
|
+
| undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Called by the owning runtime when the user cancels the run, before the run
|
|
42
|
+
* is aborted. Pauses the queue so the cancelled run's settle keeps the
|
|
43
|
+
* pending items instead of dispatching the next one; the next send re-arms
|
|
44
|
+
* draining. A hand-rolled adapter that omits it drains on cancel.
|
|
45
|
+
*/
|
|
46
|
+
__internal_notifyCancelled?: (() => void) | undefined;
|
|
30
47
|
};
|
|
@@ -79,10 +79,12 @@ export const createMessageQueue = (
|
|
|
79
79
|
|
|
80
80
|
let running = false;
|
|
81
81
|
let paused = false;
|
|
82
|
+
let dispatchTransform: (message: AppendMessage) => AppendMessage = (m) => m;
|
|
82
83
|
// swallow the cancelled run's settle when steering so it does not double-advance
|
|
83
84
|
let suppressIdle = 0;
|
|
84
85
|
// settles from cancelled runs that must drop `running` without advancing
|
|
85
86
|
let cancelSettles = 0;
|
|
87
|
+
let interrupting = false;
|
|
86
88
|
|
|
87
89
|
const notify = () => {
|
|
88
90
|
for (const callback of subscribers) callback();
|
|
@@ -117,7 +119,7 @@ export const createMessageQueue = (
|
|
|
117
119
|
setLanes({ ...lanes, [lane]: lanes[lane].slice(1) });
|
|
118
120
|
if (!message) return;
|
|
119
121
|
running = true;
|
|
120
|
-
driver.run(message, { steer: false });
|
|
122
|
+
driver.run(dispatchTransform(message), { steer: false });
|
|
121
123
|
};
|
|
122
124
|
|
|
123
125
|
const interrupt = (message: AppendMessage) => {
|
|
@@ -126,9 +128,17 @@ export const createMessageQueue = (
|
|
|
126
128
|
// already cancel-notified
|
|
127
129
|
suppressIdle += Math.max(cancelSettles, 1);
|
|
128
130
|
cancelSettles = 0;
|
|
129
|
-
driver
|
|
131
|
+
// a driver whose cancel routes through the runtime notifies this queue
|
|
132
|
+
// back; the interrupt already accounted for that settle and is dispatching
|
|
133
|
+
// in its place
|
|
134
|
+
interrupting = true;
|
|
135
|
+
try {
|
|
136
|
+
driver.cancel!();
|
|
137
|
+
} finally {
|
|
138
|
+
interrupting = false;
|
|
139
|
+
}
|
|
130
140
|
running = true;
|
|
131
|
-
driver.run(message, { steer: true });
|
|
141
|
+
driver.run(dispatchTransform(message), { steer: true });
|
|
132
142
|
};
|
|
133
143
|
|
|
134
144
|
const push = (lane: Lane, message: AppendMessage) => {
|
|
@@ -241,6 +251,14 @@ export const createMessageQueue = (
|
|
|
241
251
|
});
|
|
242
252
|
};
|
|
243
253
|
|
|
254
|
+
const notifyCancelled = () => {
|
|
255
|
+
if (interrupting) return;
|
|
256
|
+
if (running && cancelSettles === 0) {
|
|
257
|
+
paused = true;
|
|
258
|
+
cancelSettles = 1;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
244
262
|
const adapter: ExternalThreadQueueAdapter = {
|
|
245
263
|
items: lanes.queue,
|
|
246
264
|
steerItems: lanes.steer,
|
|
@@ -249,6 +267,10 @@ export const createMessageQueue = (
|
|
|
249
267
|
move,
|
|
250
268
|
edit,
|
|
251
269
|
remove,
|
|
270
|
+
__internal_setDispatchTransform: (transform) => {
|
|
271
|
+
dispatchTransform = transform;
|
|
272
|
+
},
|
|
273
|
+
__internal_notifyCancelled: notifyCancelled,
|
|
252
274
|
};
|
|
253
275
|
|
|
254
276
|
return {
|
|
@@ -270,12 +292,7 @@ export const createMessageQueue = (
|
|
|
270
292
|
running = false;
|
|
271
293
|
advance();
|
|
272
294
|
},
|
|
273
|
-
notifyCancelled
|
|
274
|
-
if (running && cancelSettles === 0) {
|
|
275
|
-
paused = true;
|
|
276
|
-
cancelSettles = 1;
|
|
277
|
-
}
|
|
278
|
-
},
|
|
295
|
+
notifyCancelled,
|
|
279
296
|
clear: () => {
|
|
280
297
|
messages.clear();
|
|
281
298
|
setLanes({ queue: EMPTY_QUEUE_ITEMS, steer: EMPTY_QUEUE_ITEMS });
|
|
@@ -109,6 +109,13 @@ type ExternalStoreAdapterBase<T> = {
|
|
|
109
109
|
state?: ReadonlyJSONValue | undefined;
|
|
110
110
|
extras?: unknown;
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Applies a message list the runtime rewrote, and is what tells the runtime
|
|
114
|
+
* a removal it makes will survive the next snapshot. Without it, cancelling a
|
|
115
|
+
* run leaves a trailing user message in the thread and the composer
|
|
116
|
+
* untouched; an adapter that removes that message itself owns handing it
|
|
117
|
+
* back, because the runtime cannot see a removal it did not make.
|
|
118
|
+
*/
|
|
112
119
|
setMessages?: ((messages: readonly T[]) => void) | undefined;
|
|
113
120
|
/**
|
|
114
121
|
* Fires when the user explicitly switches branches via the runtime's
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AppendMessage, ThreadMessage } from "../../types/message";
|
|
2
|
+
import type { Attachment } from "../../types/attachment";
|
|
2
3
|
import type {
|
|
3
4
|
AddToolResultOptions,
|
|
4
5
|
ResumeRunConfig,
|
|
@@ -27,7 +28,10 @@ import type {
|
|
|
27
28
|
RuntimeCapabilities,
|
|
28
29
|
ThreadRuntimeCore,
|
|
29
30
|
} from "../../runtime/interfaces/thread-runtime-core";
|
|
30
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
ExternalThreadQueueAdapter,
|
|
33
|
+
QueuePlacement,
|
|
34
|
+
} from "../../runtime/queue/external-thread-queue-adapter";
|
|
31
35
|
import { BaseThreadRuntimeCore } from "../../runtime/base/base-thread-runtime-core";
|
|
32
36
|
import type { ModelContextProvider } from "../../model-context/types";
|
|
33
37
|
import {
|
|
@@ -37,6 +41,7 @@ import {
|
|
|
37
41
|
import { generateId } from "../../utils/id";
|
|
38
42
|
import { ToolInvocationTracker } from "../tool-invocations/ToolInvocationTracker";
|
|
39
43
|
import { EMPTY_QUEUE_ITEMS } from "../../store/scopes/queue-item";
|
|
44
|
+
import type { QuoteInfo } from "../../types/quote";
|
|
40
45
|
|
|
41
46
|
const EMPTY_ARRAY: readonly ThreadSuggestion[] = Object.freeze([]);
|
|
42
47
|
|
|
@@ -117,6 +122,8 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
117
122
|
|
|
118
123
|
private _store!: ExternalStoreAdapter<any>;
|
|
119
124
|
|
|
125
|
+
private _transformedQueue: ExternalThreadQueueAdapter | undefined;
|
|
126
|
+
|
|
120
127
|
/**
|
|
121
128
|
* Client-side tool-invocations pipeline. Constructed lazily on first
|
|
122
129
|
* snapshot — only when `adapter.unstable_enableToolInvocations === true`.
|
|
@@ -147,6 +154,19 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
147
154
|
|
|
148
155
|
const oldStore = this._store as ExternalStoreAdapter<any> | undefined;
|
|
149
156
|
this._store = store;
|
|
157
|
+
if (oldStore?.queue !== store.queue) {
|
|
158
|
+
this._transformedQueue = undefined;
|
|
159
|
+
store.queue?.__internal_setDispatchTransform?.((message) => {
|
|
160
|
+
// Re-point at the tail, as LocalThreadRuntimeCore's driver does, so
|
|
161
|
+
// the prefix gated against is the one the message lands on whatever
|
|
162
|
+
// the host routes by. Queuing only ever accepts a tail append, so a
|
|
163
|
+
// later tail is the same intent.
|
|
164
|
+
const parentId = this.messages.at(-1)?.id ?? null;
|
|
165
|
+
return this.enrichAppendMetadata({ ...message, parentId }, parentId);
|
|
166
|
+
});
|
|
167
|
+
if (store.queue?.__internal_setDispatchTransform)
|
|
168
|
+
this._transformedQueue = store.queue;
|
|
169
|
+
}
|
|
150
170
|
if (this.extras !== store.extras) {
|
|
151
171
|
this.extras = store.extras;
|
|
152
172
|
}
|
|
@@ -465,22 +485,30 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
465
485
|
});
|
|
466
486
|
}
|
|
467
487
|
|
|
468
|
-
public async append(
|
|
488
|
+
public async append(rawMessage: AppendMessage): Promise<void> {
|
|
469
489
|
// sourceId marks an edit send; the parent may coincide with the head
|
|
470
490
|
// after a resync (e.g. cancelRun dropped the edited message).
|
|
471
491
|
const isEdit =
|
|
472
|
-
|
|
473
|
-
|
|
492
|
+
rawMessage.sourceId != null ||
|
|
493
|
+
rawMessage.parentId !== (this.messages.at(-1)?.id ?? null);
|
|
474
494
|
|
|
475
495
|
// Buffering does not start a run, so the tool-abort below must wait until
|
|
476
496
|
// the queue flushes. By then the prior run (and its tools) has settled.
|
|
477
497
|
if (!isEdit && this._store.queue) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
498
|
+
// Skip only for the queue this core actually installed on: another
|
|
499
|
+
// core's transform would gate against its own thread's messages.
|
|
500
|
+
const queued =
|
|
501
|
+
this._store.queue === this._transformedQueue
|
|
502
|
+
? rawMessage
|
|
503
|
+
: this.enrichAppendMetadata(rawMessage);
|
|
504
|
+
if (queued.steer ?? this._store.isRunning ?? false)
|
|
505
|
+
this._store.queue.steer(queued);
|
|
506
|
+
else this._store.queue.enqueue(queued);
|
|
481
507
|
return;
|
|
482
508
|
}
|
|
483
509
|
|
|
510
|
+
const message = this.enrichAppendMetadata(rawMessage);
|
|
511
|
+
|
|
484
512
|
// Auto-abort in-flight client-side tool executions when a new run is
|
|
485
513
|
// about to start. Without this, a tool that finishes after the new turn
|
|
486
514
|
// begins would feed a stale result into `onAddToolResult`, racing with
|
|
@@ -592,37 +620,83 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
592
620
|
// cancel on it.
|
|
593
621
|
void this._toolInvocations?.abort();
|
|
594
622
|
|
|
623
|
+
// Before the run is aborted, so the settle it produces keeps the pending
|
|
624
|
+
// items instead of dispatching the next one at the moment the user
|
|
625
|
+
// stopped.
|
|
626
|
+
this._store.queue?.__internal_notifyCancelled?.();
|
|
627
|
+
|
|
595
628
|
this._store.onCancel();
|
|
596
629
|
|
|
597
|
-
|
|
598
|
-
// partially-streamed one is kept and re-supplied by the store on resync.
|
|
599
|
-
const head = this.repository.getMessages().at(-1);
|
|
600
|
-
if (head && head.metadata.isOptimistic && head.content.length === 0) {
|
|
601
|
-
this.repository.deleteMessage(head.id);
|
|
602
|
-
}
|
|
630
|
+
this.dropEmptyOptimisticHead();
|
|
603
631
|
|
|
604
|
-
|
|
632
|
+
const messages = this.repository.getMessages();
|
|
605
633
|
const previousMessage = messages[messages.length - 1];
|
|
606
|
-
|
|
634
|
+
const trailingUserLeaf =
|
|
635
|
+
this._store.setMessages !== undefined &&
|
|
607
636
|
previousMessage?.role === "user" &&
|
|
608
|
-
previousMessage.id === messages.at(-1)?.id // ensure the previous message is a leaf node
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
637
|
+
previousMessage.id === messages.at(-1)?.id && // ensure the previous message is a leaf node
|
|
638
|
+
previousMessage.content.every((part) => part.type === "text")
|
|
639
|
+
? previousMessage
|
|
640
|
+
: undefined;
|
|
641
|
+
|
|
642
|
+
// Handing the message to the composer and taking it out of the thread are
|
|
643
|
+
// one move: the composer refuses while the user is writing, and removing
|
|
644
|
+
// the message then would leave it nowhere. A message the composer cannot
|
|
645
|
+
// hold whole, carrying content parts it has no home for, is not moved.
|
|
646
|
+
let movedLeaf:
|
|
647
|
+
| {
|
|
648
|
+
id: string;
|
|
649
|
+
draft: {
|
|
650
|
+
text: string;
|
|
651
|
+
attachments: readonly Attachment[];
|
|
652
|
+
quote: QuoteInfo | undefined;
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
| undefined;
|
|
656
|
+
if (trailingUserLeaf) {
|
|
657
|
+
const draft = {
|
|
658
|
+
text: getThreadMessageText(trailingUserLeaf),
|
|
659
|
+
attachments: trailingUserLeaf.attachments,
|
|
660
|
+
quote: trailingUserLeaf.metadata.custom.quote as QuoteInfo | undefined,
|
|
661
|
+
};
|
|
662
|
+
if (this.composer.restoreDraft(draft)) {
|
|
663
|
+
this.repository.deleteMessage(trailingUserLeaf.id);
|
|
664
|
+
movedLeaf = { id: trailingUserLeaf.id, draft };
|
|
613
665
|
}
|
|
614
|
-
|
|
615
|
-
messages = this.repository.getMessages();
|
|
616
|
-
} else {
|
|
617
|
-
this._notifySubscribers();
|
|
618
666
|
}
|
|
667
|
+
if (!movedLeaf) this._notifySubscribers();
|
|
619
668
|
|
|
620
|
-
// resync
|
|
669
|
+
// The resync commits what the cancel left (a kept optimistic message, the
|
|
670
|
+
// restored branch) back to the store a macrotask later. The store may move
|
|
671
|
+
// in that gap; a server settling the cancelled turn lands in the same
|
|
672
|
+
// tick. Read the repository at flush time and re-apply the rollbacks to
|
|
673
|
+
// it, instead of stamping a snapshot captured above over the newer state.
|
|
621
674
|
setTimeout(() => {
|
|
622
|
-
this.
|
|
675
|
+
this.dropEmptyOptimisticHead();
|
|
676
|
+
if (movedLeaf) {
|
|
677
|
+
const current = this.repository.getMessages();
|
|
678
|
+
if (current.at(-1)?.id === movedLeaf.id) {
|
|
679
|
+
// Unanswered tail: the removal has not reached the store yet.
|
|
680
|
+
this.repository.deleteMessage(movedLeaf.id);
|
|
681
|
+
} else if (current.some((m) => m.id === movedLeaf.id)) {
|
|
682
|
+
// The store kept the turn in the thread; take the untouched draft
|
|
683
|
+
// back so the same content does not sit in both places.
|
|
684
|
+
this.composer.retractDraft(movedLeaf.draft);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
this.updateMessages(this.repository.getMessages());
|
|
623
688
|
}, 0);
|
|
624
689
|
}
|
|
625
690
|
|
|
691
|
+
// Placeholder or pre-stream message; a partially-streamed one is kept and
|
|
692
|
+
// committed to the store by the cancel resync.
|
|
693
|
+
private dropEmptyOptimisticHead(): void {
|
|
694
|
+
const head = this.repository.getMessages().at(-1);
|
|
695
|
+
if (head && head.metadata.isOptimistic && head.content.length === 0) {
|
|
696
|
+
this.repository.deleteMessage(head.id);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
626
700
|
public addToolResult(options: AddToolResultOptions) {
|
|
627
701
|
if (!this._store.onAddToolResult)
|
|
628
702
|
throw new Error("Runtime does not support tool results.");
|
|
@@ -312,7 +312,10 @@ export class LocalThreadRuntimeCore
|
|
|
312
312
|
this._queue?.adapter.remove(queueItemId);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
private async _runAppend(
|
|
315
|
+
private async _runAppend(rawMessage: AppendMessage): Promise<void> {
|
|
316
|
+
// Stamped here rather than in `append` so a queued message is gated after
|
|
317
|
+
// the flush re-pointed its parentId at the current tail.
|
|
318
|
+
const message = this.enrichAppendMetadata(rawMessage);
|
|
316
319
|
this.ensureInitialized();
|
|
317
320
|
|
|
318
321
|
const initPromise = this._getInitializePromise?.();
|