@assistant-ui/core 0.3.5 → 0.3.6
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 +3 -3
- package/dist/index.js.map +1 -1
- package/dist/internal.js +1 -1
- package/dist/react/AssistantProvider.d.ts +3 -2
- package/dist/react/AssistantProvider.d.ts.map +1 -1
- package/dist/react/AssistantProvider.js +19 -26
- package/dist/react/AssistantProvider.js.map +1 -1
- package/dist/react/AssistantRuntimeProvider.d.ts +7 -2
- package/dist/react/AssistantRuntimeProvider.d.ts.map +1 -1
- package/dist/react/AssistantRuntimeProvider.js +9 -7
- package/dist/react/AssistantRuntimeProvider.js.map +1 -1
- package/dist/react/providers/AttachmentByIndexProvider.d.ts.map +1 -1
- package/dist/react/providers/AttachmentByIndexProvider.js +33 -39
- package/dist/react/providers/AttachmentByIndexProvider.js.map +1 -1
- package/dist/react/providers/ChainOfThoughtByIndicesProvider.d.ts.map +1 -1
- package/dist/react/providers/ChainOfThoughtByIndicesProvider.js +15 -20
- package/dist/react/providers/ChainOfThoughtByIndicesProvider.js.map +1 -1
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +17 -20
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js.map +1 -1
- package/dist/react/providers/MessageByIdProvider.js +28 -38
- package/dist/react/providers/MessageByIdProvider.js.map +1 -1
- package/dist/react/providers/MessageByIndexProvider.js +28 -38
- package/dist/react/providers/MessageByIndexProvider.js.map +1 -1
- package/dist/react/providers/PartByIndexProvider.js +17 -20
- package/dist/react/providers/PartByIndexProvider.js.map +1 -1
- package/dist/react/providers/QueueItemByIndexProvider.js +21 -21
- package/dist/react/providers/QueueItemByIndexProvider.js.map +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.js +24 -32
- package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
- package/dist/react/providers/SuggestionByIndexProvider.js +17 -20
- package/dist/react/providers/SuggestionByIndexProvider.js.map +1 -1
- package/dist/react/providers/TextMessagePartProvider.d.ts.map +1 -1
- package/dist/react/providers/TextMessagePartProvider.js +16 -19
- package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
- package/dist/react/providers/ThreadListItemByIndexProvider.js +17 -20
- package/dist/react/providers/ThreadListItemByIndexProvider.js.map +1 -1
- package/dist/react/providers/ThreadListItemRuntimeProvider.js +15 -18
- package/dist/react/providers/ThreadListItemRuntimeProvider.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +7 -3
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +5 -2
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +3 -2
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.d.ts +3 -1
- package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.js +4 -2
- package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
- package/dist/runtime/api/composer-runtime.d.ts +5 -1
- package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
- package/dist/runtime/api/composer-runtime.js +8 -1
- package/dist/runtime/api/composer-runtime.js.map +1 -1
- package/dist/runtime/api/message-runtime.js +1 -1
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +4 -2
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +2 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +3 -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 +14 -3
- package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/composer-runtime-core.d.ts +2 -1
- package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +3 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/queue/external-thread-queue-adapter.d.ts +19 -6
- package/dist/runtime/queue/external-thread-queue-adapter.d.ts.map +1 -1
- package/dist/runtime/queue/message-queue.d.ts +10 -1
- package/dist/runtime/queue/message-queue.d.ts.map +1 -1
- package/dist/runtime/queue/message-queue.js +138 -36
- package/dist/runtime/queue/message-queue.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +2 -1
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts +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 +7 -6
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-runtime-options.d.ts +13 -0
- package/dist/runtimes/local/local-runtime-options.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +5 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +30 -14
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +6 -3
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +11 -4
- package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
- package/dist/store/scopes/composer.d.ts +7 -3
- package/dist/store/scopes/composer.d.ts.map +1 -1
- package/dist/store/scopes/queue-item.d.ts +10 -0
- package/dist/store/scopes/queue-item.d.ts.map +1 -1
- package/dist/store/scopes/queue-item.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +5 -1
- package/src/react/AssistantProvider.tsx +20 -18
- package/src/react/AssistantRuntimeProvider.test.tsx +55 -1
- package/src/react/AssistantRuntimeProvider.tsx +12 -2
- package/src/react/providers/AttachmentByIndexProvider.tsx +15 -7
- package/src/react/providers/ChainOfThoughtByIndicesProvider.tsx +12 -4
- package/src/react/providers/ChainOfThoughtPartByIndexProvider.tsx +8 -4
- package/src/react/providers/MessageByIdProvider.tsx +8 -4
- package/src/react/providers/MessageByIndexProvider.tsx +8 -4
- package/src/react/providers/PartByIndexProvider.tsx +8 -4
- package/src/react/providers/QueueItemByIndexProvider.tsx +9 -5
- package/src/react/providers/ReadonlyThreadProvider.tsx +9 -4
- package/src/react/providers/SuggestionByIndexProvider.tsx +8 -4
- package/src/react/providers/TextMessagePartProvider.tsx +13 -4
- package/src/react/providers/ThreadListItemByIndexProvider.tsx +8 -4
- package/src/react/providers/ThreadListItemRuntimeProvider.tsx +8 -4
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +19 -4
- package/src/react/runtimes/useLocalRuntime.ts +4 -0
- package/src/runtime/api/composer-runtime.ts +11 -2
- package/src/runtime/api/message-runtime.test.ts +25 -0
- package/src/runtime/api/message-runtime.ts +1 -1
- package/src/runtime/base/base-composer-runtime-core.ts +5 -1
- package/src/runtime/base/default-thread-composer-runtime-core.ts +27 -3
- package/src/runtime/interfaces/composer-runtime-core.ts +2 -1
- package/src/runtime/interfaces/thread-runtime-core.ts +3 -1
- package/src/runtime/queue/external-thread-queue-adapter.ts +19 -3
- package/src/runtime/queue/message-queue.ts +200 -38
- package/src/runtime/utils/thread-message-like.ts +2 -4
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +10 -8
- package/src/runtimes/local/local-runtime-options.ts +15 -0
- package/src/runtimes/local/local-thread-runtime-core.test.ts +43 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +48 -13
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +1 -1
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +1 -1
- package/src/runtimes/remote-thread-list/types.ts +9 -2
- package/src/store/runtime-clients/composer-runtime-client.ts +13 -5
- package/src/store/scopes/composer.ts +6 -4
- package/src/store/scopes/queue-item.ts +8 -1
- package/src/tests/external-store-thread-runtime-core.test.ts +56 -13
- package/src/tests/framework-neutral-boundary.test.ts +221 -0
- package/src/tests/message-queue.test.ts +494 -34
- package/src/tests/thread-message-like.test.ts +66 -0
|
@@ -173,6 +173,31 @@ describe("toMessagePartStatus", () => {
|
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
+
it.each([
|
|
177
|
+
["false", false],
|
|
178
|
+
["zero", 0],
|
|
179
|
+
["an empty string", ""],
|
|
180
|
+
["null", null],
|
|
181
|
+
])("treats %s tool results as complete", (_label, result) => {
|
|
182
|
+
const message = createAssistantMessage(
|
|
183
|
+
[
|
|
184
|
+
{
|
|
185
|
+
type: "tool-call",
|
|
186
|
+
toolCallId: "call-1",
|
|
187
|
+
toolName: "weather",
|
|
188
|
+
args: {},
|
|
189
|
+
argsText: "{}",
|
|
190
|
+
result,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
{ type: "running" },
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(toMessagePartStatus(message, 0, message.content[0]!)).toEqual({
|
|
197
|
+
type: "complete",
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
176
201
|
it("normalizes supplied upstream statuses", () => {
|
|
177
202
|
const upstreamComplete = {
|
|
178
203
|
type: "text",
|
|
@@ -44,7 +44,7 @@ export const toMessagePartStatus = (
|
|
|
44
44
|
if (message.role !== "assistant") return COMPLETE_STATUS;
|
|
45
45
|
|
|
46
46
|
if (part.type === "tool-call") {
|
|
47
|
-
if (
|
|
47
|
+
if (part.result === undefined) {
|
|
48
48
|
return message.status as ToolCallMessagePartStatus;
|
|
49
49
|
} else {
|
|
50
50
|
return COMPLETE_STATUS;
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
SendOptions,
|
|
25
25
|
} from "../interfaces/composer-runtime-core";
|
|
26
26
|
import type { DictationAdapter } from "../../adapters/speech";
|
|
27
|
+
import type { QueuePlacement } from "../queue/external-thread-queue-adapter";
|
|
27
28
|
import {
|
|
28
29
|
EMPTY_QUEUE_ITEMS,
|
|
29
30
|
type QueueItemState,
|
|
@@ -301,7 +302,10 @@ export abstract class BaseComposerRuntimeCore
|
|
|
301
302
|
return EMPTY_QUEUE_ITEMS;
|
|
302
303
|
}
|
|
303
304
|
|
|
304
|
-
public
|
|
305
|
+
public moveQueueItem(
|
|
306
|
+
_queueItemId: string,
|
|
307
|
+
_placement: QueuePlacement,
|
|
308
|
+
): void {}
|
|
305
309
|
public removeQueueItem(_queueItemId: string): void {}
|
|
306
310
|
|
|
307
311
|
protected abstract handleSend(
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ThreadComposerRuntimeCore,
|
|
7
7
|
} from "../interfaces/composer-runtime-core";
|
|
8
8
|
import type { ThreadRuntimeCore } from "../interfaces/thread-runtime-core";
|
|
9
|
+
import type { QueuePlacement } from "../queue/external-thread-queue-adapter";
|
|
9
10
|
import {
|
|
10
11
|
EMPTY_QUEUE_ITEMS,
|
|
11
12
|
type QueueItemState,
|
|
@@ -26,12 +27,35 @@ export class DefaultThreadComposerRuntimeCore
|
|
|
26
27
|
return !this.isEmpty && !this.runtime.isSendDisabled && !this._isSending;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
private _queueCache:
|
|
31
|
+
| {
|
|
32
|
+
steer: readonly QueueItemState[];
|
|
33
|
+
queue: readonly QueueItemState[];
|
|
34
|
+
flat: readonly QueueItemState[];
|
|
35
|
+
}
|
|
36
|
+
| undefined;
|
|
37
|
+
|
|
29
38
|
public override get queue(): readonly QueueItemState[] {
|
|
30
|
-
|
|
39
|
+
const steer = this.runtime.getSteerQueueItems?.() ?? EMPTY_QUEUE_ITEMS;
|
|
40
|
+
const queue = this.runtime.getQueueItems?.() ?? EMPTY_QUEUE_ITEMS;
|
|
41
|
+
const cache = this._queueCache;
|
|
42
|
+
if (cache && cache.steer === steer && cache.queue === queue)
|
|
43
|
+
return cache.flat;
|
|
44
|
+
const flat =
|
|
45
|
+
steer.length === 0
|
|
46
|
+
? queue
|
|
47
|
+
: queue.length === 0
|
|
48
|
+
? steer
|
|
49
|
+
: [...steer, ...queue];
|
|
50
|
+
this._queueCache = { steer, queue, flat };
|
|
51
|
+
return flat;
|
|
31
52
|
}
|
|
32
53
|
|
|
33
|
-
public override
|
|
34
|
-
|
|
54
|
+
public override moveQueueItem(
|
|
55
|
+
queueItemId: string,
|
|
56
|
+
placement: QueuePlacement,
|
|
57
|
+
): void {
|
|
58
|
+
this.runtime.moveQueueItem?.(queueItemId, placement);
|
|
35
59
|
}
|
|
36
60
|
|
|
37
61
|
public override removeQueueItem(queueItemId: string): void {
|
|
@@ -5,6 +5,7 @@ import type { Unsubscribe } from "../../types/unsubscribe";
|
|
|
5
5
|
import type { RunConfig } from "../../types/message";
|
|
6
6
|
import type { DictationAdapter } from "../../adapters/speech";
|
|
7
7
|
import type { QueueItemState } from "../../store/scopes/queue-item";
|
|
8
|
+
import type { QueuePlacement } from "../queue/external-thread-queue-adapter";
|
|
8
9
|
|
|
9
10
|
export type AttachmentAddErrorReason =
|
|
10
11
|
| "no-adapter"
|
|
@@ -82,7 +83,7 @@ export type ComposerRuntimeCore = Readonly<{
|
|
|
82
83
|
cancel: () => void;
|
|
83
84
|
|
|
84
85
|
queue: readonly QueueItemState[];
|
|
85
|
-
|
|
86
|
+
moveQueueItem: (queueItemId: string, placement: QueuePlacement) => void;
|
|
86
87
|
removeQueueItem: (queueItemId: string) => void;
|
|
87
88
|
|
|
88
89
|
dictation: DictationState | undefined;
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
ThreadComposerRuntimeCore,
|
|
18
18
|
} from "./composer-runtime-core";
|
|
19
19
|
import type { QueueItemState } from "../../store/scopes/queue-item";
|
|
20
|
+
import type { QueuePlacement } from "../queue/external-thread-queue-adapter";
|
|
20
21
|
|
|
21
22
|
export type RuntimeCapabilities = {
|
|
22
23
|
readonly switchToBranch: boolean;
|
|
@@ -177,7 +178,8 @@ export type ThreadRuntimeCore = Readonly<{
|
|
|
177
178
|
beginEdit: (messageId: string) => void;
|
|
178
179
|
|
|
179
180
|
getQueueItems?: () => readonly QueueItemState[];
|
|
180
|
-
|
|
181
|
+
getSteerQueueItems?: () => readonly QueueItemState[];
|
|
182
|
+
moveQueueItem?: (queueItemId: string, placement: QueuePlacement) => void;
|
|
181
183
|
removeQueueItem?: (queueItemId: string) => void;
|
|
182
184
|
|
|
183
185
|
speech: SpeechState | undefined;
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import type { AppendMessage } from "../../types/message";
|
|
2
2
|
import type { QueueItemState } from "../../store/scopes/queue-item";
|
|
3
3
|
|
|
4
|
+
export type QueuePlacement = {
|
|
5
|
+
readonly lane?: "queue" | "steer";
|
|
6
|
+
readonly insertAfter?: string | null;
|
|
7
|
+
readonly insertBefore?: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
4
10
|
/**
|
|
5
11
|
* The queue surface a runtime exposes so the composer can stay usable during a
|
|
6
12
|
* run and render the pending messages.
|
|
7
13
|
*/
|
|
8
14
|
export type ExternalThreadQueueAdapter = {
|
|
9
15
|
items: readonly QueueItemState[];
|
|
10
|
-
|
|
11
|
-
|
|
16
|
+
steerItems: readonly QueueItemState[];
|
|
17
|
+
/** Send a message into the queue lane, processed in order. */
|
|
18
|
+
enqueue: (message: AppendMessage) => void;
|
|
19
|
+
/** Send a message into the steer lane, processed next. */
|
|
20
|
+
steer: (message: AppendMessage) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Move a queued message between lanes or within a lane. An unanchored move
|
|
23
|
+
* into the steer lane mid-run interrupts — cancels the live run and
|
|
24
|
+
* dispatches the item — when the runtime supports cancellation; a move with
|
|
25
|
+
* `insertAfter`/`insertBefore` only places the item and never interrupts.
|
|
26
|
+
*/
|
|
27
|
+
move: (queueItemId: string, placement: QueuePlacement) => void;
|
|
28
|
+
edit: (queueItemId: string, message: AppendMessage) => void;
|
|
12
29
|
remove: (queueItemId: string) => void;
|
|
13
|
-
clear: (reason: "edit" | "reload" | "cancel-run") => void;
|
|
14
30
|
};
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AppendMessage,
|
|
3
|
+
FileMessagePart,
|
|
4
|
+
TextMessagePart,
|
|
5
|
+
} from "../../types/message";
|
|
2
6
|
import {
|
|
3
7
|
EMPTY_QUEUE_ITEMS,
|
|
4
8
|
type QueueItemState,
|
|
5
9
|
} from "../../store/scopes/queue-item";
|
|
6
10
|
import { generateId } from "../../utils/id";
|
|
7
11
|
import { getThreadMessageText } from "../../utils/text";
|
|
8
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
ExternalThreadQueueAdapter,
|
|
14
|
+
QueuePlacement,
|
|
15
|
+
} from "./external-thread-queue-adapter";
|
|
9
16
|
|
|
10
17
|
export type MessageQueueDriver = {
|
|
11
18
|
run: (message: AppendMessage, options: { steer: boolean }) => void;
|
|
12
|
-
/** When omitted,
|
|
19
|
+
/** When omitted, steering degrades to "process next" instead of interrupting. */
|
|
13
20
|
cancel?: (() => void) | undefined;
|
|
14
21
|
};
|
|
15
22
|
|
|
@@ -19,95 +26,239 @@ export type MessageQueueController = {
|
|
|
19
26
|
notifyBusy: () => void;
|
|
20
27
|
/** Advances to the next pending message; call on the run's falling edge. */
|
|
21
28
|
notifyIdle: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Pauses queue advance for a user-initiated cancel, so the cancelled run's
|
|
31
|
+
* settle keeps the pending items instead of dispatching the next one; the
|
|
32
|
+
* next explicit send or run start re-arms draining. Call before aborting
|
|
33
|
+
* the run.
|
|
34
|
+
*/
|
|
35
|
+
notifyCancelled: () => void;
|
|
36
|
+
/** Empties both lanes without dispatching. */
|
|
37
|
+
clear: () => void;
|
|
22
38
|
subscribe: (callback: () => void) => () => void;
|
|
23
39
|
};
|
|
24
40
|
|
|
41
|
+
type Lane = "queue" | "steer";
|
|
42
|
+
|
|
43
|
+
const getQueueItemParts = (
|
|
44
|
+
message: AppendMessage,
|
|
45
|
+
): readonly (FileMessagePart | TextMessagePart)[] => {
|
|
46
|
+
// attachment-derived text is the file body, not user-authored prose;
|
|
47
|
+
// project only the file/image content of attachments
|
|
48
|
+
const source = [
|
|
49
|
+
...message.content,
|
|
50
|
+
...(message.attachments ?? []).flatMap((attachment) =>
|
|
51
|
+
(attachment.content ?? []).filter((part) => part.type !== "text"),
|
|
52
|
+
),
|
|
53
|
+
];
|
|
54
|
+
const parts: (FileMessagePart | TextMessagePart)[] = [];
|
|
55
|
+
for (const part of source) {
|
|
56
|
+
if (part.type === "file" || part.type === "text") {
|
|
57
|
+
parts.push(part);
|
|
58
|
+
} else if (part.type === "image") {
|
|
59
|
+
parts.push({
|
|
60
|
+
type: "file",
|
|
61
|
+
data: part.image,
|
|
62
|
+
mimeType: "image/*",
|
|
63
|
+
...(part.filename !== undefined && { filename: part.filename }),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return parts;
|
|
68
|
+
};
|
|
69
|
+
|
|
25
70
|
export const createMessageQueue = (
|
|
26
71
|
driver: MessageQueueDriver,
|
|
27
72
|
): MessageQueueController => {
|
|
28
|
-
let
|
|
73
|
+
let lanes: Record<Lane, readonly QueueItemState[]> = {
|
|
74
|
+
queue: EMPTY_QUEUE_ITEMS,
|
|
75
|
+
steer: EMPTY_QUEUE_ITEMS,
|
|
76
|
+
};
|
|
29
77
|
const messages = new Map<string, AppendMessage>();
|
|
30
78
|
const subscribers = new Set<() => void>();
|
|
31
79
|
|
|
32
80
|
let running = false;
|
|
81
|
+
let paused = false;
|
|
33
82
|
// swallow the cancelled run's settle when steering so it does not double-advance
|
|
34
83
|
let suppressIdle = 0;
|
|
84
|
+
// settles from cancelled runs that must drop `running` without advancing
|
|
85
|
+
let cancelSettles = 0;
|
|
35
86
|
|
|
36
87
|
const notify = () => {
|
|
37
88
|
for (const callback of subscribers) callback();
|
|
38
89
|
};
|
|
39
90
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
adapter.items = next;
|
|
91
|
+
const setLanes = (next: Record<Lane, readonly QueueItemState[]>) => {
|
|
92
|
+
lanes = next;
|
|
93
|
+
adapter.items = next.queue;
|
|
94
|
+
adapter.steerItems = next.steer;
|
|
43
95
|
notify();
|
|
44
96
|
};
|
|
45
97
|
|
|
98
|
+
const toItem = (id: string, message: AppendMessage): QueueItemState => ({
|
|
99
|
+
id,
|
|
100
|
+
prompt: getThreadMessageText(message),
|
|
101
|
+
parts: getQueueItemParts(message),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const laneOf = (queueItemId: string): Lane | undefined => {
|
|
105
|
+
if (lanes.steer.some((item) => item.id === queueItemId)) return "steer";
|
|
106
|
+
if (lanes.queue.some((item) => item.id === queueItemId)) return "queue";
|
|
107
|
+
return undefined;
|
|
108
|
+
};
|
|
109
|
+
|
|
46
110
|
const advance = () => {
|
|
47
|
-
if (running ||
|
|
48
|
-
const
|
|
111
|
+
if (running || paused) return;
|
|
112
|
+
const lane: Lane = lanes.steer.length > 0 ? "steer" : "queue";
|
|
113
|
+
const head = lanes[lane][0];
|
|
114
|
+
if (!head) return;
|
|
49
115
|
const message = messages.get(head.id);
|
|
50
116
|
messages.delete(head.id);
|
|
51
|
-
|
|
117
|
+
setLanes({ ...lanes, [lane]: lanes[lane].slice(1) });
|
|
52
118
|
if (!message) return;
|
|
53
119
|
running = true;
|
|
54
120
|
driver.run(message, { steer: false });
|
|
55
121
|
};
|
|
56
122
|
|
|
57
|
-
const
|
|
123
|
+
const interrupt = (message: AppendMessage) => {
|
|
124
|
+
paused = false;
|
|
125
|
+
// the interrupted run settles exactly once, whether or not it was
|
|
126
|
+
// already cancel-notified
|
|
127
|
+
suppressIdle += Math.max(cancelSettles, 1);
|
|
128
|
+
cancelSettles = 0;
|
|
129
|
+
driver.cancel!();
|
|
130
|
+
running = true;
|
|
131
|
+
driver.run(message, { steer: true });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const push = (lane: Lane, message: AppendMessage) => {
|
|
135
|
+
paused = false;
|
|
58
136
|
const id = generateId();
|
|
59
|
-
const prompt = getThreadMessageText(message);
|
|
60
137
|
messages.set(id, message);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
138
|
+
setLanes({ ...lanes, [lane]: [...lanes[lane], toItem(id, message)] });
|
|
139
|
+
advance();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const enqueue = (message: AppendMessage) => {
|
|
143
|
+
push("queue", message);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const steer = (message: AppendMessage) => {
|
|
147
|
+
if (running && driver.cancel) {
|
|
148
|
+
interrupt(message);
|
|
149
|
+
return;
|
|
66
150
|
}
|
|
151
|
+
push("steer", message);
|
|
67
152
|
};
|
|
68
153
|
|
|
69
|
-
const
|
|
70
|
-
|
|
154
|
+
const move = (queueItemId: string, placement: QueuePlacement) => {
|
|
155
|
+
const fromLane = laneOf(queueItemId);
|
|
156
|
+
if (!fromLane) throw new Error(`Unknown queue item "${queueItemId}".`);
|
|
157
|
+
const toLane = placement.lane ?? fromLane;
|
|
158
|
+
|
|
159
|
+
const item = lanes[fromLane].find((i) => i.id === queueItemId)!;
|
|
160
|
+
const dest = (toLane === fromLane ? lanes[fromLane] : lanes[toLane]).filter(
|
|
161
|
+
(i) => i.id !== queueItemId,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const anchorIndex = (anchor: string) => {
|
|
165
|
+
if (anchor === queueItemId)
|
|
166
|
+
throw new Error(`Queue item "${queueItemId}" cannot anchor itself.`);
|
|
167
|
+
const index = dest.findIndex((i) => i.id === anchor);
|
|
168
|
+
if (index === -1)
|
|
169
|
+
throw new Error(`Unknown anchor "${anchor}" in lane "${toLane}".`);
|
|
170
|
+
return index;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const { insertAfter, insertBefore } = placement;
|
|
174
|
+
let index: number;
|
|
175
|
+
if (insertAfter === undefined && insertBefore === undefined) {
|
|
176
|
+
index =
|
|
177
|
+
toLane === fromLane
|
|
178
|
+
? lanes[fromLane].findIndex((i) => i.id === queueItemId)
|
|
179
|
+
: dest.length;
|
|
180
|
+
} else if (insertAfter !== undefined && insertBefore !== undefined) {
|
|
181
|
+
const after = insertAfter === null ? -1 : anchorIndex(insertAfter);
|
|
182
|
+
const before =
|
|
183
|
+
insertBefore === null ? dest.length : anchorIndex(insertBefore);
|
|
184
|
+
if (before !== after + 1)
|
|
185
|
+
throw new Error(
|
|
186
|
+
`insertAfter "${insertAfter}" and insertBefore "${insertBefore}" are not adjacent in lane "${toLane}".`,
|
|
187
|
+
);
|
|
188
|
+
index = after + 1;
|
|
189
|
+
} else if (insertAfter !== undefined) {
|
|
190
|
+
index = insertAfter === null ? 0 : anchorIndex(insertAfter) + 1;
|
|
191
|
+
} else {
|
|
192
|
+
index = insertBefore === null ? dest.length : anchorIndex(insertBefore!);
|
|
193
|
+
}
|
|
71
194
|
|
|
72
|
-
|
|
195
|
+
// placement and immediate dispatch cannot coexist: an anchored move into
|
|
196
|
+
// the steer lane places without interrupting; only an unanchored one
|
|
197
|
+
// cancels the live run and dispatches
|
|
198
|
+
if (
|
|
199
|
+
insertAfter === undefined &&
|
|
200
|
+
insertBefore === undefined &&
|
|
201
|
+
toLane === "steer" &&
|
|
202
|
+
fromLane !== "steer" &&
|
|
203
|
+
running &&
|
|
204
|
+
driver.cancel
|
|
205
|
+
) {
|
|
73
206
|
const message = messages.get(queueItemId)!;
|
|
74
207
|
messages.delete(queueItemId);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
208
|
+
setLanes({
|
|
209
|
+
queue: lanes.queue.filter((i) => i.id !== queueItemId),
|
|
210
|
+
steer: lanes.steer,
|
|
211
|
+
});
|
|
212
|
+
interrupt(message);
|
|
80
213
|
return;
|
|
81
214
|
}
|
|
82
215
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
216
|
+
const nextDest = [...dest.slice(0, index), item, ...dest.slice(index)];
|
|
217
|
+
const next = { ...lanes, [toLane]: nextDest };
|
|
218
|
+
if (toLane !== fromLane)
|
|
219
|
+
next[fromLane] = lanes[fromLane].filter((i) => i.id !== queueItemId);
|
|
220
|
+
setLanes(next);
|
|
86
221
|
advance();
|
|
87
222
|
};
|
|
88
223
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
224
|
+
const edit = (queueItemId: string, message: AppendMessage) => {
|
|
225
|
+
const lane = laneOf(queueItemId);
|
|
226
|
+
if (!lane) throw new Error(`Unknown queue item "${queueItemId}".`);
|
|
227
|
+
messages.set(queueItemId, message);
|
|
228
|
+
setLanes({
|
|
229
|
+
...lanes,
|
|
230
|
+
[lane]: lanes[lane].map((item) =>
|
|
231
|
+
item.id === queueItemId ? toItem(queueItemId, message) : item,
|
|
232
|
+
),
|
|
233
|
+
});
|
|
92
234
|
};
|
|
93
235
|
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
236
|
+
const remove = (queueItemId: string) => {
|
|
237
|
+
if (!messages.delete(queueItemId)) return;
|
|
238
|
+
setLanes({
|
|
239
|
+
queue: lanes.queue.filter((item) => item.id !== queueItemId),
|
|
240
|
+
steer: lanes.steer.filter((item) => item.id !== queueItemId),
|
|
241
|
+
});
|
|
98
242
|
};
|
|
99
243
|
|
|
100
244
|
const adapter: ExternalThreadQueueAdapter = {
|
|
101
|
-
items,
|
|
245
|
+
items: lanes.queue,
|
|
246
|
+
steerItems: lanes.steer,
|
|
102
247
|
enqueue,
|
|
103
|
-
steer
|
|
248
|
+
steer,
|
|
249
|
+
move,
|
|
250
|
+
edit,
|
|
104
251
|
remove,
|
|
105
|
-
clear,
|
|
106
252
|
};
|
|
107
253
|
|
|
108
254
|
return {
|
|
109
255
|
adapter,
|
|
110
256
|
notifyBusy: () => {
|
|
257
|
+
paused = false;
|
|
258
|
+
// a cancelled run's settle that is still outstanding belongs to a run
|
|
259
|
+
// this new one replaces; swallow it entirely
|
|
260
|
+
suppressIdle += cancelSettles;
|
|
261
|
+
cancelSettles = 0;
|
|
111
262
|
running = true;
|
|
112
263
|
},
|
|
113
264
|
notifyIdle: () => {
|
|
@@ -115,9 +266,20 @@ export const createMessageQueue = (
|
|
|
115
266
|
suppressIdle--;
|
|
116
267
|
return;
|
|
117
268
|
}
|
|
269
|
+
if (cancelSettles > 0) cancelSettles--;
|
|
118
270
|
running = false;
|
|
119
271
|
advance();
|
|
120
272
|
},
|
|
273
|
+
notifyCancelled: () => {
|
|
274
|
+
if (running && cancelSettles === 0) {
|
|
275
|
+
paused = true;
|
|
276
|
+
cancelSettles = 1;
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
clear: () => {
|
|
280
|
+
messages.clear();
|
|
281
|
+
setLanes({ queue: EMPTY_QUEUE_ITEMS, steer: EMPTY_QUEUE_ITEMS });
|
|
282
|
+
},
|
|
121
283
|
subscribe: (callback) => {
|
|
122
284
|
subscribers.add(callback);
|
|
123
285
|
return () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parsePartialJsonObject } from "assistant-stream/utils";
|
|
2
2
|
import { generateId } from "../../utils/id";
|
|
3
|
+
import { parseDataUrl } from "../../utils/data-url";
|
|
3
4
|
import type {
|
|
4
5
|
ReasoningMessagePart,
|
|
5
6
|
SourceMessagePart,
|
|
@@ -130,10 +131,7 @@ export const fromThreadMessageLike = (
|
|
|
130
131
|
...rest
|
|
131
132
|
}: ImageMessagePart): ImageMessagePart | null => {
|
|
132
133
|
if (typeof image !== "string") return null;
|
|
133
|
-
|
|
134
|
-
/^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,(.*)$/i,
|
|
135
|
-
);
|
|
136
|
-
if (dataUri) {
|
|
134
|
+
if (parseDataUrl(image)?.mimeType.startsWith("image/")) {
|
|
137
135
|
return { ...rest, image };
|
|
138
136
|
}
|
|
139
137
|
if (/^(https:\/\/|blob:)/i.test(image)) {
|
|
@@ -27,6 +27,7 @@ import type {
|
|
|
27
27
|
RuntimeCapabilities,
|
|
28
28
|
ThreadRuntimeCore,
|
|
29
29
|
} from "../../runtime/interfaces/thread-runtime-core";
|
|
30
|
+
import type { QueuePlacement } from "../../runtime/queue/external-thread-queue-adapter";
|
|
30
31
|
import { BaseThreadRuntimeCore } from "../../runtime/base/base-thread-runtime-core";
|
|
31
32
|
import type { ModelContextProvider } from "../../model-context/types";
|
|
32
33
|
import {
|
|
@@ -470,7 +471,9 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
470
471
|
// Buffering does not start a run, so the tool-abort below must wait until
|
|
471
472
|
// the queue flushes. By then the prior run (and its tools) has settled.
|
|
472
473
|
if (!isEdit && this._store.queue) {
|
|
473
|
-
|
|
474
|
+
if (message.steer ?? this._store.isRunning ?? false)
|
|
475
|
+
this._store.queue.steer(message);
|
|
476
|
+
else this._store.queue.enqueue(message);
|
|
474
477
|
return;
|
|
475
478
|
}
|
|
476
479
|
|
|
@@ -487,7 +490,6 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
487
490
|
if (isEdit) {
|
|
488
491
|
if (!this._store.onEdit)
|
|
489
492
|
throw new Error("Runtime does not support editing messages.");
|
|
490
|
-
this._store.queue?.clear("edit");
|
|
491
493
|
await this._store.onEdit(message);
|
|
492
494
|
} else {
|
|
493
495
|
await this._store.onNew(message);
|
|
@@ -520,8 +522,12 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
520
522
|
return this._store?.queue?.items ?? EMPTY_QUEUE_ITEMS;
|
|
521
523
|
}
|
|
522
524
|
|
|
523
|
-
public
|
|
524
|
-
this._store?.queue?.
|
|
525
|
+
public getSteerQueueItems() {
|
|
526
|
+
return this._store?.queue?.steerItems ?? EMPTY_QUEUE_ITEMS;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
public moveQueueItem(queueItemId: string, placement: QueuePlacement) {
|
|
530
|
+
this._store?.queue?.move(queueItemId, placement);
|
|
525
531
|
}
|
|
526
532
|
|
|
527
533
|
public removeQueueItem(queueItemId: string) {
|
|
@@ -532,8 +538,6 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
532
538
|
if (!this._store.onReload)
|
|
533
539
|
throw new Error("Runtime does not support reloading messages.");
|
|
534
540
|
|
|
535
|
-
this._store.queue?.clear("reload");
|
|
536
|
-
|
|
537
541
|
// Auto-abort in-flight client-side tool executions when a run reloads;
|
|
538
542
|
// any results that land afterward would target a turn that no longer
|
|
539
543
|
// exists. See `append` above for full rationale.
|
|
@@ -579,8 +583,6 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
579
583
|
if (!this._store.onCancel)
|
|
580
584
|
throw new Error("Runtime does not support cancelling runs.");
|
|
581
585
|
|
|
582
|
-
this._store.queue?.clear("cancel-run");
|
|
583
|
-
|
|
584
586
|
// Abort any in-flight client-side tool executions. Fire-and-forget —
|
|
585
587
|
// the abort resolves once executions settle, but we don't gate the
|
|
586
588
|
// cancel on it.
|
|
@@ -32,4 +32,19 @@ export type LocalRuntimeOptionsBase = {
|
|
|
32
32
|
* `composer.queue` and sent once the run settles. Steering runs it next.
|
|
33
33
|
*/
|
|
34
34
|
unstable_enableMessageQueue?: boolean | undefined;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Auto-clear the message queue when the thread rewinds (message edit).
|
|
38
|
+
* Defaults to `true`.
|
|
39
|
+
* @deprecated Removal after 2026-11-05 — the queue will always survive rewinds.
|
|
40
|
+
*/
|
|
41
|
+
unstable_queueClearOnRewind?: boolean | undefined;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Auto-clear the message queue when the user cancels the run. Defaults to
|
|
45
|
+
* `true`. When `false`, cancel pauses the queue and keeps the pending
|
|
46
|
+
* items; the next send drains them.
|
|
47
|
+
* @deprecated Removal after 2026-11-05 — cancel will always pause the queue and keep the items.
|
|
48
|
+
*/
|
|
49
|
+
unstable_queueClearOnCancel?: boolean | undefined;
|
|
35
50
|
};
|
|
@@ -958,6 +958,49 @@ describe("LocalThreadRuntimeCore tool approval persistence", () => {
|
|
|
958
958
|
});
|
|
959
959
|
});
|
|
960
960
|
|
|
961
|
+
it("does not persist an existing falsy tool result again", async () => {
|
|
962
|
+
const { history, updated } = createHistory();
|
|
963
|
+
const twoHumanTools: ChatModelRunResult = {
|
|
964
|
+
content: [
|
|
965
|
+
{
|
|
966
|
+
...toolCallPart("send_email"),
|
|
967
|
+
toolCallId: "call-1",
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
...toolCallPart("send_email"),
|
|
971
|
+
toolCallId: "call-2",
|
|
972
|
+
},
|
|
973
|
+
],
|
|
974
|
+
status: { type: "requires-action", reason: "tool-calls" },
|
|
975
|
+
};
|
|
976
|
+
const thread = createThread(
|
|
977
|
+
{
|
|
978
|
+
async run() {
|
|
979
|
+
return twoHumanTools;
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
{ history },
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
await thread.append(userMessage("send two emails"));
|
|
986
|
+
await flush();
|
|
987
|
+
|
|
988
|
+
const options = {
|
|
989
|
+
messageId: thread.messages.at(-1)!.id,
|
|
990
|
+
toolName: "send_email",
|
|
991
|
+
toolCallId: "call-1",
|
|
992
|
+
result: false,
|
|
993
|
+
isError: false,
|
|
994
|
+
};
|
|
995
|
+
thread.addToolResult(options);
|
|
996
|
+
await flush();
|
|
997
|
+
expect(updated).toHaveLength(1);
|
|
998
|
+
|
|
999
|
+
thread.addToolResult(options);
|
|
1000
|
+
await flush();
|
|
1001
|
+
expect(updated).toHaveLength(1);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
961
1004
|
it("persists a multi-step run once instead of writing intermediate steps", async () => {
|
|
962
1005
|
const { history, appended, updated } = createHistory();
|
|
963
1006
|
const runs: ChatModelRunOptions[] = [];
|