@assistant-ui/core 0.2.6 → 0.2.8
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/adapters/attachment.d.ts.map +1 -1
- package/dist/adapters/speech.d.ts.map +1 -1
- package/dist/adapters/speech.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -0
- package/dist/internal/duplicate-detection.d.ts +5 -0
- package/dist/internal/duplicate-detection.d.ts.map +1 -0
- package/dist/internal/duplicate-detection.js +11 -0
- package/dist/internal/duplicate-detection.js.map +1 -0
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +2 -2
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/registry.d.ts.map +1 -1
- package/dist/model-context/tool.d.ts.map +1 -1
- package/dist/react/AssistantProvider.d.ts.map +1 -1
- package/dist/react/AssistantProvider.js.map +1 -1
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/Tools.js +26 -15
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/index.d.ts +5 -4
- package/dist/react/index.js +2 -2
- package/dist/react/model-context/toolbox.d.ts +29 -2
- package/dist/react/model-context/toolbox.d.ts.map +1 -1
- package/dist/react/model-context/toolbox.js +18 -0
- package/dist/react/model-context/toolbox.js.map +1 -0
- package/dist/react/model-context/useAssistantTool.d.ts.map +1 -1
- package/dist/react/model-context/useAssistantTool.js +6 -3
- package/dist/react/model-context/useAssistantTool.js.map +1 -1
- package/dist/react/model-context/useAssistantToolUI.d.ts +6 -0
- package/dist/react/model-context/useAssistantToolUI.d.ts.map +1 -1
- package/dist/react/model-context/useAssistantToolUI.js +4 -2
- package/dist/react/model-context/useAssistantToolUI.js.map +1 -1
- package/dist/react/model-context/useInlineRender.js.map +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +49 -7
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +28 -3
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +2 -7
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RuntimeAdapterProvider.d.ts.map +1 -1
- package/dist/react/runtimes/RuntimeAdapterProvider.js +6 -5
- package/dist/react/runtimes/RuntimeAdapterProvider.js.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/external-message-converter.d.ts +1 -1
- package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +1 -0
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts +7 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts.map +1 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.js +21 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -0
- package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/types/scopes/tools.d.ts +19 -2
- package/dist/react/types/scopes/tools.d.ts.map +1 -1
- package/dist/react/utils/groupParts.d.ts +32 -11
- package/dist/react/utils/groupParts.d.ts.map +1 -1
- package/dist/react/utils/groupParts.js +13 -6
- package/dist/react/utils/groupParts.js.map +1 -1
- package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
- package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
- package/dist/runtime/api/attachment-runtime.js.map +1 -1
- package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/base/base-assistant-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +8 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts +9 -1
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +34 -14
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.d.ts +1 -0
- 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 +31 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-shared-options.d.ts +8 -0
- package/dist/runtimes/external-store/external-store-shared-options.d.ts.map +1 -0
- package/dist/runtimes/external-store/external-store-shared-options.js +11 -0
- package/dist/runtimes/external-store/external-store-shared-options.js.map +1 -0
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +25 -2
- 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 +106 -26
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/external-store/thread-message-converter.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/adapter/in-memory.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +168 -0
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -0
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +449 -0
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -0
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
- package/dist/types/message.d.ts +6 -0
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/composite-context-provider.d.ts.map +1 -1
- package/dist/utils/id.d.ts +1 -3
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +1 -4
- package/dist/utils/id.js.map +1 -1
- package/package.json +10 -10
- package/src/adapters/index.ts +1 -4
- package/src/adapters/speech.ts +0 -1
- package/src/index.ts +12 -0
- package/src/internal/duplicate-detection.ts +26 -0
- package/src/internal.ts +0 -2
- package/src/model-context/frame/host.ts +0 -1
- package/src/model-context/frame/provider.ts +0 -1
- package/src/react/AssistantProvider.tsx +2 -3
- package/src/react/client/Interactables.ts +0 -1
- package/src/react/client/Tools.ts +50 -25
- package/src/react/index.ts +9 -8
- package/src/react/model-context/toolbox.ts +46 -1
- package/src/react/model-context/useAssistantTool.ts +8 -3
- package/src/react/model-context/useAssistantToolUI.ts +9 -2
- package/src/react/model-context/useInlineRender.ts +0 -1
- package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -2
- package/src/react/primitives/message/MessageAttachments.test.tsx +1 -1
- package/src/react/primitives/message/MessageGroupedParts.tsx +102 -13
- package/src/react/primitives/message/MessageParts.tsx +4 -7
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +0 -3
- package/src/react/runtimes/RuntimeAdapterProvider.tsx +12 -7
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +0 -3
- package/src/react/runtimes/external-message-converter.ts +5 -1
- package/src/react/runtimes/useExternalStoreSharedOptions.ts +23 -0
- package/src/react/runtimes/useLocalRuntime.ts +0 -10
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +0 -6
- package/src/react/types/scopes/tools.ts +20 -1
- package/src/react/utils/groupParts.ts +49 -18
- package/src/runtime/api/attachment-runtime.ts +1 -2
- package/src/runtime/interfaces/thread-runtime-core.ts +8 -0
- package/src/runtime/internal.ts +1 -4
- package/src/runtime/utils/message-repository.ts +57 -16
- package/src/runtime/utils/thread-message-like.ts +2 -0
- package/src/runtimes/external-store/external-store-adapter.ts +33 -0
- package/src/runtimes/external-store/external-store-shared-options.ts +18 -0
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +1 -3
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +179 -37
- package/src/runtimes/tool-invocations/EDGE_CASES.md +194 -0
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +1054 -0
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +782 -0
- package/src/subscribable/subscribable.ts +3 -3
- package/src/tests/MessageRepository.test.ts +83 -52
- package/src/tests/OptimisticState-delete-crash.test.ts +2 -0
- package/src/tests/OptimisticState-list-race.test.ts +2 -4
- package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +5 -5
- package/src/tests/auiV0Encode.test.ts +1 -1
- package/src/tests/composer-can-send.test.ts +8 -4
- package/src/tests/duplicate-detection.test.ts +34 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +1 -1
- package/src/tests/external-store-thread-runtime-core.test.ts +112 -79
- package/src/tests/groupParts.test.ts +70 -0
- package/src/tests/no-unsafe-process-env.test.ts +1 -0
- package/src/tests/remote-thread-list-isLoading.test.ts +2 -5
- package/src/tests/thread-message-like.test.ts +4 -1
- package/src/types/index.ts +1 -4
- package/src/types/message.ts +6 -0
- package/src/utils/id.ts +0 -4
- package/dist/react/runtimes/useToolInvocations.d.ts +0 -53
- package/dist/react/runtimes/useToolInvocations.d.ts.map +0 -1
- package/dist/react/runtimes/useToolInvocations.js +0 -380
- package/dist/react/runtimes/useToolInvocations.js.map +0 -1
- package/src/react/runtimes/useToolInvocations.ts +0 -694
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThreadMessage } from "../../types/message";
|
|
2
2
|
import type { RunConfig } from "../../types/message";
|
|
3
|
-
import { generateId
|
|
3
|
+
import { generateId } from "../../utils/id";
|
|
4
4
|
import type { ThreadMessageLike } from "./thread-message-like";
|
|
5
5
|
import { getAutoStatus } from "./auto-status";
|
|
6
6
|
import { fromThreadMessageLike } from "./thread-message-like";
|
|
@@ -259,20 +259,6 @@ export class MessageRepository {
|
|
|
259
259
|
};
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
appendOptimisticMessage(parentId: string | null, message: ThreadMessageLike) {
|
|
263
|
-
let optimisticId: string;
|
|
264
|
-
do {
|
|
265
|
-
optimisticId = generateOptimisticId();
|
|
266
|
-
} while (this.messages.has(optimisticId));
|
|
267
|
-
|
|
268
|
-
this.addOrUpdateMessage(
|
|
269
|
-
parentId,
|
|
270
|
-
fromThreadMessageLike(message, optimisticId, { type: "running" }),
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
return optimisticId;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
262
|
deleteMessage(messageId: string, replacementId?: string | null | undefined) {
|
|
277
263
|
const message = this.messages.get(messageId);
|
|
278
264
|
|
|
@@ -322,6 +308,45 @@ export class MessageRepository {
|
|
|
322
308
|
return children;
|
|
323
309
|
}
|
|
324
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Evicts optimistic messages (`metadata.isOptimistic`) the head just moved
|
|
313
|
+
* away from. Since eviction runs on every head move, the only optimistic
|
|
314
|
+
* messages in the repository live on the branch the head previously pointed
|
|
315
|
+
* at — so we walk just that branch rather than the whole repository. Keeps a
|
|
316
|
+
* client→server id swap from leaving a phantom sibling, and drops off-branch
|
|
317
|
+
* placeholders.
|
|
318
|
+
*/
|
|
319
|
+
private evictOffBranchOptimisticMessages(
|
|
320
|
+
previousHead: RepositoryMessage | null,
|
|
321
|
+
currentHead: RepositoryMessage | null,
|
|
322
|
+
) {
|
|
323
|
+
if (!previousHead) return;
|
|
324
|
+
|
|
325
|
+
const onHeadBranch = new Set<string>();
|
|
326
|
+
for (let current = currentHead; current; current = current.prev) {
|
|
327
|
+
onHeadBranch.add(current.current.id);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const stale: string[] = [];
|
|
331
|
+
for (
|
|
332
|
+
let current: RepositoryMessage | null = previousHead;
|
|
333
|
+
current;
|
|
334
|
+
current = current.prev
|
|
335
|
+
) {
|
|
336
|
+
// Stop at the first node shared with the current head branch: every
|
|
337
|
+
// ancestor above it is shared too, so nothing further can be off-branch.
|
|
338
|
+
if (onHeadBranch.has(current.current.id)) break;
|
|
339
|
+
if (current.current.metadata?.isOptimistic) {
|
|
340
|
+
stale.push(current.current.id);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const id of stale) {
|
|
345
|
+
// A prior deletion may have already removed this node.
|
|
346
|
+
if (this.messages.has(id)) this.deleteMessage(id);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
325
350
|
switchToBranch(messageId: string) {
|
|
326
351
|
const message = this.messages.get(messageId);
|
|
327
352
|
if (!message)
|
|
@@ -329,11 +354,14 @@ export class MessageRepository {
|
|
|
329
354
|
"MessageRepository(switchToBranch): Branch not found. This is likely an internal bug in assistant-ui.",
|
|
330
355
|
);
|
|
331
356
|
|
|
357
|
+
const previousHead = this.head;
|
|
332
358
|
const prevOrRoot = message.prev ?? this.root;
|
|
333
359
|
prevOrRoot.next = message;
|
|
334
360
|
|
|
335
361
|
this.head = findHead(message);
|
|
336
362
|
|
|
363
|
+
this.evictOffBranchOptimisticMessages(previousHead, this.head);
|
|
364
|
+
|
|
337
365
|
this._messages.dirty();
|
|
338
366
|
}
|
|
339
367
|
|
|
@@ -349,6 +377,8 @@ export class MessageRepository {
|
|
|
349
377
|
"MessageRepository(resetHead): Branch not found. This is likely an internal bug in assistant-ui.",
|
|
350
378
|
);
|
|
351
379
|
|
|
380
|
+
const previousHead = this.head;
|
|
381
|
+
|
|
352
382
|
if (message.children.length > 0) {
|
|
353
383
|
const deleteDescendants = (msg: RepositoryMessage) => {
|
|
354
384
|
for (const childId of msg.children) {
|
|
@@ -378,6 +408,8 @@ export class MessageRepository {
|
|
|
378
408
|
}
|
|
379
409
|
}
|
|
380
410
|
|
|
411
|
+
this.evictOffBranchOptimisticMessages(previousHead, this.head);
|
|
412
|
+
|
|
381
413
|
this._messages.dirty();
|
|
382
414
|
}
|
|
383
415
|
|
|
@@ -394,15 +426,24 @@ export class MessageRepository {
|
|
|
394
426
|
export(): ExportedMessageRepository {
|
|
395
427
|
const exportItems: ExportedMessageRepository["messages"] = [];
|
|
396
428
|
|
|
429
|
+
// Optimistic messages are ephemeral and never persisted. They're always
|
|
430
|
+
// leaf nodes, so skipping them can't orphan a persisted child.
|
|
397
431
|
for (const [, message] of this.messages) {
|
|
432
|
+
if (message.current.metadata?.isOptimistic) continue;
|
|
398
433
|
exportItems.push({
|
|
399
434
|
message: message.current,
|
|
400
435
|
parentId: message.prev?.current.id ?? null,
|
|
401
436
|
});
|
|
402
437
|
}
|
|
403
438
|
|
|
439
|
+
// The head may itself be optimistic; walk up to the nearest persisted ancestor.
|
|
440
|
+
let head = this.head;
|
|
441
|
+
while (head?.current.metadata?.isOptimistic) {
|
|
442
|
+
head = head.prev;
|
|
443
|
+
}
|
|
444
|
+
|
|
404
445
|
return {
|
|
405
|
-
headId:
|
|
446
|
+
headId: head?.current.id ?? null,
|
|
406
447
|
messages: exportItems,
|
|
407
448
|
};
|
|
408
449
|
}
|
|
@@ -81,6 +81,7 @@ export type ThreadMessageLike = {
|
|
|
81
81
|
readonly steps?: readonly ThreadStep[] | undefined;
|
|
82
82
|
readonly timing?: MessageTiming | undefined;
|
|
83
83
|
readonly submittedFeedback?: { readonly type: "positive" | "negative" };
|
|
84
|
+
readonly isOptimistic?: boolean | undefined;
|
|
84
85
|
readonly custom?: Record<string, unknown> | undefined;
|
|
85
86
|
}
|
|
86
87
|
| undefined;
|
|
@@ -210,6 +211,7 @@ export const fromThreadMessageLike = (
|
|
|
210
211
|
...(metadata?.submittedFeedback && {
|
|
211
212
|
submittedFeedback: metadata.submittedFeedback,
|
|
212
213
|
}),
|
|
214
|
+
...(metadata?.isOptimistic && { isOptimistic: true }),
|
|
213
215
|
},
|
|
214
216
|
} satisfies ThreadAssistantMessage;
|
|
215
217
|
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
} from "../../runtime/interfaces/thread-runtime-core";
|
|
17
17
|
import type { ExportedMessageRepository } from "../../runtime/utils/message-repository";
|
|
18
18
|
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
19
|
+
import type { ToolExecutionStatus } from "../tool-invocations/ToolInvocationTracker";
|
|
19
20
|
|
|
20
21
|
export type ExternalStoreThreadData<TState extends "regular" | "archived"> = {
|
|
21
22
|
status: TState;
|
|
@@ -131,6 +132,38 @@ type ExternalStoreAdapterBase<T> = {
|
|
|
131
132
|
copy?: boolean | undefined;
|
|
132
133
|
}
|
|
133
134
|
| undefined;
|
|
135
|
+
/**
|
|
136
|
+
* Opt in to the built-in client-side tool-invocations pipeline
|
|
137
|
+
* (`streamCall` / `execute` / tool-status tracking) for this thread.
|
|
138
|
+
*
|
|
139
|
+
* Defaults to `false` — the runtime does *not* drive client-side tool
|
|
140
|
+
* callbacks on its own. Set to `true` to have the runtime construct a
|
|
141
|
+
* `ToolInvocationTracker` and feed every snapshot through it, so tool
|
|
142
|
+
* callbacks fire automatically for tool-call parts in `messages`.
|
|
143
|
+
*
|
|
144
|
+
* Opt-in by default because most external-store runtimes either run
|
|
145
|
+
* tools entirely server-side, or already wire their own client-side
|
|
146
|
+
* dispatch path. Enabling the embedded tracker on top of an existing
|
|
147
|
+
* dispatch path would cause tool callbacks to run twice.
|
|
148
|
+
*
|
|
149
|
+
* When enabled, client-side tool results (from `execute()` returning,
|
|
150
|
+
* or from `streamCall` resolving) flow back through
|
|
151
|
+
* `adapter.onAddToolResult` like any other tool result, with
|
|
152
|
+
* `modelContent` populated when present.
|
|
153
|
+
*/
|
|
154
|
+
unstable_enableToolInvocations?: boolean | undefined;
|
|
155
|
+
/**
|
|
156
|
+
* Receives the current per-tool-call execution status map whenever it
|
|
157
|
+
* changes. Only invoked when `unstable_enableToolInvocations` is `true`
|
|
158
|
+
* — the runtime maintains the map via the embedded tracker.
|
|
159
|
+
*
|
|
160
|
+
* Wire this into local React state and feed it into the converter's
|
|
161
|
+
* `metadata.toolStatuses` so the UI can render `executing` spinners
|
|
162
|
+
* and human-input prompts.
|
|
163
|
+
*/
|
|
164
|
+
setToolStatuses?:
|
|
165
|
+
| ((statuses: Record<string, ToolExecutionStatus>) => void)
|
|
166
|
+
| undefined;
|
|
134
167
|
};
|
|
135
168
|
|
|
136
169
|
export type ExternalStoreAdapter<T = ThreadMessage> =
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ExternalStoreAdapter } from "./external-store-adapter";
|
|
2
|
+
|
|
3
|
+
export type ExternalStoreSharedOptions = Pick<
|
|
4
|
+
ExternalStoreAdapter,
|
|
5
|
+
"isDisabled" | "isSendDisabled" | "unstable_capabilities" | "suggestions"
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export const pickExternalStoreSharedOptions = (
|
|
9
|
+
options: ExternalStoreSharedOptions,
|
|
10
|
+
): ExternalStoreSharedOptions =>
|
|
11
|
+
({
|
|
12
|
+
isDisabled: options.isDisabled,
|
|
13
|
+
isSendDisabled: options.isSendDisabled,
|
|
14
|
+
unstable_capabilities: options.unstable_capabilities,
|
|
15
|
+
suggestions: options.suggestions,
|
|
16
|
+
}) satisfies {
|
|
17
|
+
[K in keyof Required<ExternalStoreSharedOptions>]: ExternalStoreSharedOptions[K];
|
|
18
|
+
};
|
|
@@ -22,9 +22,7 @@ const DEFAULT_THREAD_DATA = Object.freeze({
|
|
|
22
22
|
[DEFAULT_THREAD_ID]: DEFAULT_THREAD,
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
export class ExternalStoreThreadListRuntimeCore
|
|
26
|
-
implements ThreadListRuntimeCore
|
|
27
|
-
{
|
|
25
|
+
export class ExternalStoreThreadListRuntimeCore implements ThreadListRuntimeCore {
|
|
28
26
|
private _mainThreadId: string = DEFAULT_THREAD_ID;
|
|
29
27
|
private _threads: readonly string[] = DEFAULT_THREADS;
|
|
30
28
|
private _archivedThreads: readonly string[] = EMPTY_ARRAY;
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
ExportedMessageRepository,
|
|
31
31
|
MessageRepository,
|
|
32
32
|
} from "../../runtime/utils/message-repository";
|
|
33
|
+
import { generateId } from "../../utils/id";
|
|
34
|
+
import { ToolInvocationTracker } from "../tool-invocations/ToolInvocationTracker";
|
|
33
35
|
|
|
34
36
|
const EMPTY_ARRAY: readonly ThreadSuggestion[] = Object.freeze([]);
|
|
35
37
|
|
|
@@ -53,9 +55,6 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
53
55
|
extends BaseThreadRuntimeCore
|
|
54
56
|
implements ThreadRuntimeCore
|
|
55
57
|
{
|
|
56
|
-
private _assistantOptimisticId: string | null = null;
|
|
57
|
-
private _lastSyncedMessageIds = new Set<string>();
|
|
58
|
-
|
|
59
58
|
private _capabilities: RuntimeCapabilities = {
|
|
60
59
|
switchToBranch: false,
|
|
61
60
|
switchBranchDuringRun: false,
|
|
@@ -105,6 +104,12 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
105
104
|
|
|
106
105
|
private _store!: ExternalStoreAdapter<any>;
|
|
107
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Client-side tool-invocations pipeline. Constructed lazily on first
|
|
109
|
+
* snapshot — only when `adapter.unstable_enableToolInvocations === true`.
|
|
110
|
+
*/
|
|
111
|
+
private _toolInvocations: ToolInvocationTracker | null = null;
|
|
112
|
+
|
|
108
113
|
public override beginEdit(messageId: string) {
|
|
109
114
|
if (!this._store.onEdit)
|
|
110
115
|
throw new Error("Runtime does not support editing.");
|
|
@@ -169,10 +174,7 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
169
174
|
return;
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
// Clear and import the message repository
|
|
173
177
|
this.repository.clear();
|
|
174
|
-
this._assistantOptimisticId = null;
|
|
175
|
-
this._lastSyncedMessageIds = new Set();
|
|
176
178
|
this.repository.import(store.messageRepository);
|
|
177
179
|
|
|
178
180
|
messages = this.repository.getMessages();
|
|
@@ -225,12 +227,6 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
225
227
|
return newMessage;
|
|
226
228
|
});
|
|
227
229
|
|
|
228
|
-
const nextIds = new Set(messages.map((m) => m.id));
|
|
229
|
-
for (const prevId of this._lastSyncedMessageIds) {
|
|
230
|
-
if (!nextIds.has(prevId)) this.repository.deleteMessage(prevId);
|
|
231
|
-
}
|
|
232
|
-
this._lastSyncedMessageIds = nextIds;
|
|
233
|
-
|
|
234
230
|
for (let i = 0; i < messages.length; i++) {
|
|
235
231
|
const message = messages[i]!;
|
|
236
232
|
const parent = messages[i - 1];
|
|
@@ -253,29 +249,132 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
253
249
|
}
|
|
254
250
|
}
|
|
255
251
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
252
|
+
// Append an optimistic placeholder while running but before a trailing
|
|
253
|
+
// assistant message exists. resetHead evicts off-branch optimistic messages
|
|
254
|
+
// (prior placeholders, mid-run id-swap siblings); export() never persists them.
|
|
255
|
+
let optimisticId: string | null = null;
|
|
261
256
|
if (hasUpcomingMessage(isRunning, messages)) {
|
|
262
|
-
|
|
257
|
+
optimisticId = generateId();
|
|
258
|
+
this.repository.addOrUpdateMessage(
|
|
263
259
|
messages.at(-1)?.id ?? null,
|
|
264
|
-
|
|
265
|
-
role: "assistant",
|
|
266
|
-
|
|
267
|
-
|
|
260
|
+
fromThreadMessageLike(
|
|
261
|
+
{ role: "assistant", content: [], metadata: { isOptimistic: true } },
|
|
262
|
+
optimisticId,
|
|
263
|
+
{ type: "running" },
|
|
264
|
+
),
|
|
268
265
|
);
|
|
269
266
|
}
|
|
270
267
|
|
|
271
|
-
this.repository.resetHead(
|
|
272
|
-
this._assistantOptimisticId ?? messages.at(-1)?.id ?? null,
|
|
273
|
-
);
|
|
268
|
+
this.repository.resetHead(optimisticId ?? messages.at(-1)?.id ?? null);
|
|
274
269
|
|
|
275
270
|
this._messages = this.repository.getMessages();
|
|
271
|
+
|
|
272
|
+
this._driveToolInvocations();
|
|
273
|
+
|
|
276
274
|
this._notifySubscribers();
|
|
277
275
|
}
|
|
278
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Feed the current message snapshot into the tool-invocations tracker.
|
|
279
|
+
* Opt-in via `adapter.unstable_enableToolInvocations: true`. The tracker
|
|
280
|
+
* itself is fail-silent — see ToolInvocationTracker for the
|
|
281
|
+
* state-transition contract.
|
|
282
|
+
*/
|
|
283
|
+
private _driveToolInvocations(): void {
|
|
284
|
+
if (!this._store.unstable_enableToolInvocations) {
|
|
285
|
+
// Adapter did not opt in (default). If a tracker was previously
|
|
286
|
+
// constructed (e.g. the adapter just toggled the flag off via a
|
|
287
|
+
// dynamic swap), drop it so subsequent snapshots are no-ops.
|
|
288
|
+
if (this._toolInvocations) {
|
|
289
|
+
this._toolInvocations.reset();
|
|
290
|
+
this._toolInvocations = null;
|
|
291
|
+
this._store.setToolStatuses?.({});
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!this._toolInvocations) {
|
|
297
|
+
this._toolInvocations = new ToolInvocationTracker(
|
|
298
|
+
() => this.getModelContext().tools,
|
|
299
|
+
{
|
|
300
|
+
onResult: (command) => {
|
|
301
|
+
try {
|
|
302
|
+
const messageId = this._findMessageIdForToolCall(
|
|
303
|
+
command.toolCallId,
|
|
304
|
+
);
|
|
305
|
+
if (messageId === undefined) {
|
|
306
|
+
// The tool call no longer exists in the snapshot (e.g.
|
|
307
|
+
// rolled back). Drop the result.
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this._store.onAddToolResult?.({
|
|
311
|
+
messageId,
|
|
312
|
+
toolCallId: command.toolCallId,
|
|
313
|
+
toolName: command.toolName,
|
|
314
|
+
result: command.result,
|
|
315
|
+
isError: command.isError,
|
|
316
|
+
...(command.artifact !== undefined && {
|
|
317
|
+
artifact: command.artifact,
|
|
318
|
+
}),
|
|
319
|
+
...(command.modelContent !== undefined && {
|
|
320
|
+
modelContent: command.modelContent,
|
|
321
|
+
}),
|
|
322
|
+
});
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.error(
|
|
325
|
+
"[ExternalStoreThreadRuntimeCore] onAddToolResult dispatch failed",
|
|
326
|
+
err,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
onStatusesChange: (statuses) => {
|
|
331
|
+
this._store.setToolStatuses?.(Object.fromEntries(statuses));
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this._toolInvocations.setState({
|
|
338
|
+
messages: this._messages,
|
|
339
|
+
isRunning: this._store.isRunning ?? false,
|
|
340
|
+
...(this._store.isLoading !== undefined && {
|
|
341
|
+
isLoading: this._store.isLoading,
|
|
342
|
+
}),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Lookup table from `toolCallId` to the owning assistant message's `id`,
|
|
348
|
+
* rebuilt lazily when `_messages` changes (see `_messagesForToolCallIndex`).
|
|
349
|
+
*/
|
|
350
|
+
private _toolCallToMessageId = new Map<string, string>();
|
|
351
|
+
private _messagesForToolCallIndex: readonly ThreadMessage[] | null = null;
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Look up the assistant message that owns a tool-call part. Lazily builds
|
|
355
|
+
* (and caches) a `toolCallId → messageId` map keyed off the current
|
|
356
|
+
* `_messages` reference, so onResult dispatches stay O(1) instead of
|
|
357
|
+
* walking the full thread on every result.
|
|
358
|
+
*/
|
|
359
|
+
private _findMessageIdForToolCall(toolCallId: string): string | undefined {
|
|
360
|
+
if (this._messagesForToolCallIndex !== this._messages) {
|
|
361
|
+
this._toolCallToMessageId.clear();
|
|
362
|
+
const visit = (messages: readonly ThreadMessage[]): void => {
|
|
363
|
+
for (const message of messages) {
|
|
364
|
+
if (!Array.isArray(message.content)) continue;
|
|
365
|
+
for (const part of message.content) {
|
|
366
|
+
if (!part || part.type !== "tool-call") continue;
|
|
367
|
+
this._toolCallToMessageId.set(part.toolCallId, message.id);
|
|
368
|
+
if (part.messages) visit(part.messages);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
visit(this._messages);
|
|
373
|
+
this._messagesForToolCallIndex = this._messages;
|
|
374
|
+
}
|
|
375
|
+
return this._toolCallToMessageId.get(toolCallId);
|
|
376
|
+
}
|
|
377
|
+
|
|
279
378
|
public override switchToBranch(branchId: string): void {
|
|
280
379
|
if (!this._store.setMessages)
|
|
281
380
|
throw new Error("Runtime does not support switching branches.");
|
|
@@ -290,6 +389,16 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
290
389
|
}
|
|
291
390
|
|
|
292
391
|
public async append(message: AppendMessage): Promise<void> {
|
|
392
|
+
// Auto-abort in-flight client-side tool executions when a new run is
|
|
393
|
+
// about to start. Without this, a tool that finishes after the new turn
|
|
394
|
+
// begins would feed a stale result into `onAddToolResult`, racing with
|
|
395
|
+
// the new turn the user just initiated. `startRun` defaults to true for
|
|
396
|
+
// user messages — matches the satellites' historical opt-in cancel
|
|
397
|
+
// behavior, which is now built in.
|
|
398
|
+
if (message.startRun ?? message.role === "user") {
|
|
399
|
+
await this._toolInvocations?.abort();
|
|
400
|
+
}
|
|
401
|
+
|
|
293
402
|
if (message.parentId !== (this.messages.at(-1)?.id ?? null)) {
|
|
294
403
|
if (!this._store.onEdit)
|
|
295
404
|
throw new Error("Runtime does not support editing messages.");
|
|
@@ -303,6 +412,11 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
303
412
|
if (!this._store.onReload)
|
|
304
413
|
throw new Error("Runtime does not support reloading messages.");
|
|
305
414
|
|
|
415
|
+
// Auto-abort in-flight client-side tool executions when a run reloads;
|
|
416
|
+
// any results that land afterward would target a turn that no longer
|
|
417
|
+
// exists. See `append` above for full rationale.
|
|
418
|
+
await this._toolInvocations?.abort();
|
|
419
|
+
|
|
306
420
|
await this._store.onReload(config.parentId, config);
|
|
307
421
|
}
|
|
308
422
|
|
|
@@ -324,6 +438,18 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
324
438
|
if (!this._store.onLoadExternalState)
|
|
325
439
|
throw new Error("Runtime does not support importing external states.");
|
|
326
440
|
|
|
441
|
+
// Re-arm the tracker so the next adapter snapshot (containing the
|
|
442
|
+
// imported state) is treated as historical — no streamCall/execute
|
|
443
|
+
// fires for the loaded tool calls. The adapter is expected to update
|
|
444
|
+
// its messages in response to onLoadExternalState; that update flows
|
|
445
|
+
// back here via __internal_setAdapter. We only clear adapter-side
|
|
446
|
+
// tool statuses when the tracker is the source of truth — otherwise
|
|
447
|
+
// we'd wipe statuses the adapter is managing on its own.
|
|
448
|
+
if (this._toolInvocations) {
|
|
449
|
+
this._toolInvocations.reset();
|
|
450
|
+
this._store.setToolStatuses?.({});
|
|
451
|
+
}
|
|
452
|
+
|
|
327
453
|
this._store.onLoadExternalState(state);
|
|
328
454
|
}
|
|
329
455
|
|
|
@@ -331,11 +457,18 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
331
457
|
if (!this._store.onCancel)
|
|
332
458
|
throw new Error("Runtime does not support cancelling runs.");
|
|
333
459
|
|
|
460
|
+
// Abort any in-flight client-side tool executions. Fire-and-forget —
|
|
461
|
+
// the abort resolves once executions settle, but we don't gate the
|
|
462
|
+
// cancel on it.
|
|
463
|
+
void this._toolInvocations?.abort();
|
|
464
|
+
|
|
334
465
|
this._store.onCancel();
|
|
335
466
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
467
|
+
// Drop an empty optimistic head (placeholder or pre-stream message); a
|
|
468
|
+
// partially-streamed one is kept and re-supplied by the store on resync.
|
|
469
|
+
const head = this.repository.getMessages().at(-1);
|
|
470
|
+
if (head && head.metadata.isOptimistic && head.content.length === 0) {
|
|
471
|
+
this.repository.deleteMessage(head.id);
|
|
339
472
|
}
|
|
340
473
|
|
|
341
474
|
let messages = this.repository.getMessages();
|
|
@@ -345,7 +478,6 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
345
478
|
previousMessage.id === messages.at(-1)?.id // ensure the previous message is a leaf node
|
|
346
479
|
) {
|
|
347
480
|
this.repository.deleteMessage(previousMessage.id);
|
|
348
|
-
this._lastSyncedMessageIds.delete(previousMessage.id);
|
|
349
481
|
if (!this.composer.text.trim()) {
|
|
350
482
|
this.composer.setText(getThreadMessageText(previousMessage));
|
|
351
483
|
}
|
|
@@ -362,15 +494,29 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
362
494
|
}
|
|
363
495
|
|
|
364
496
|
public addToolResult(options: AddToolResultOptions) {
|
|
365
|
-
if (!this._store.onAddToolResult
|
|
497
|
+
if (!this._store.onAddToolResult)
|
|
366
498
|
throw new Error("Runtime does not support tool results.");
|
|
367
499
|
this._store.onAddToolResult?.(options);
|
|
368
500
|
}
|
|
369
501
|
|
|
370
502
|
public resumeToolCall(options: ResumeToolCallOptions) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
503
|
+
// Tracker owns its own human-input handlers — let it resume in-process
|
|
504
|
+
// tool calls without round-tripping through the adapter. Falls back to
|
|
505
|
+
// the adapter's onResumeToolCall (if any) for tool calls the tracker
|
|
506
|
+
// doesn't know about.
|
|
507
|
+
const handled =
|
|
508
|
+
this._toolInvocations?.resume(options.toolCallId, options.payload) ??
|
|
509
|
+
false;
|
|
510
|
+
if (handled) return;
|
|
511
|
+
|
|
512
|
+
if (this._store.onResumeToolCall) {
|
|
513
|
+
this._store.onResumeToolCall(options);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
throw new Error(
|
|
518
|
+
`Tool call ${options.toolCallId} is not waiting for resume.`,
|
|
519
|
+
);
|
|
374
520
|
}
|
|
375
521
|
|
|
376
522
|
public respondToToolApproval(options: RespondToToolApprovalOptions) {
|
|
@@ -380,16 +526,12 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
380
526
|
}
|
|
381
527
|
|
|
382
528
|
public override reset(initialMessages?: readonly ThreadMessageLike[]) {
|
|
383
|
-
this._lastSyncedMessageIds = new Set();
|
|
384
529
|
const repo = new MessageRepository();
|
|
385
530
|
repo.import(ExportedMessageRepository.fromArray(initialMessages ?? []));
|
|
386
531
|
this.updateMessages(repo.getMessages());
|
|
387
532
|
}
|
|
388
533
|
|
|
389
534
|
public override import(data: ExportedMessageRepository) {
|
|
390
|
-
this._assistantOptimisticId = null;
|
|
391
|
-
this._lastSyncedMessageIds = new Set();
|
|
392
|
-
|
|
393
535
|
super.import(data);
|
|
394
536
|
|
|
395
537
|
if (this._store.onImport) {
|