@assistant-ui/react 0.10.26 → 0.10.28

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.
Files changed (50) hide show
  1. package/dist/api/AssistantRuntime.d.ts +1 -1
  2. package/dist/api/AssistantRuntime.js.map +1 -1
  3. package/dist/api/ThreadRuntime.d.ts +9 -0
  4. package/dist/api/ThreadRuntime.d.ts.map +1 -1
  5. package/dist/api/ThreadRuntime.js +4 -0
  6. package/dist/api/ThreadRuntime.js.map +1 -1
  7. package/dist/primitives/message/MessagePartsGroupedByParentId.d.ts +122 -0
  8. package/dist/primitives/message/MessagePartsGroupedByParentId.d.ts.map +1 -0
  9. package/dist/primitives/message/MessagePartsGroupedByParentId.js +184 -0
  10. package/dist/primitives/message/MessagePartsGroupedByParentId.js.map +1 -0
  11. package/dist/primitives/message/index.d.ts +1 -0
  12. package/dist/primitives/message/index.d.ts.map +1 -1
  13. package/dist/primitives/message/index.js +3 -1
  14. package/dist/primitives/message/index.js.map +1 -1
  15. package/dist/runtimes/core/BaseThreadRuntimeCore.d.ts +2 -0
  16. package/dist/runtimes/core/BaseThreadRuntimeCore.d.ts.map +1 -1
  17. package/dist/runtimes/core/BaseThreadRuntimeCore.js +4 -0
  18. package/dist/runtimes/core/BaseThreadRuntimeCore.js.map +1 -1
  19. package/dist/runtimes/core/ThreadRuntimeCore.d.ts +2 -0
  20. package/dist/runtimes/core/ThreadRuntimeCore.d.ts.map +1 -1
  21. package/dist/runtimes/external-store/ExternalStoreAdapter.d.ts +3 -1
  22. package/dist/runtimes/external-store/ExternalStoreAdapter.d.ts.map +1 -1
  23. package/dist/runtimes/external-store/ExternalStoreThreadRuntimeCore.d.ts.map +1 -1
  24. package/dist/runtimes/external-store/ExternalStoreThreadRuntimeCore.js +39 -24
  25. package/dist/runtimes/external-store/ExternalStoreThreadRuntimeCore.js.map +1 -1
  26. package/dist/runtimes/external-store/ThreadMessageLike.d.ts +1 -0
  27. package/dist/runtimes/external-store/ThreadMessageLike.d.ts.map +1 -1
  28. package/dist/runtimes/external-store/ThreadMessageLike.js +8 -4
  29. package/dist/runtimes/external-store/ThreadMessageLike.js.map +1 -1
  30. package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.d.ts.map +1 -1
  31. package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.js +3 -0
  32. package/dist/runtimes/remote-thread-list/EMPTY_THREAD_CORE.js.map +1 -1
  33. package/dist/runtimes/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts +2 -0
  34. package/dist/runtimes/remote-thread-list/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  35. package/dist/runtimes/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts +2 -0
  36. package/dist/runtimes/remote-thread-list/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  37. package/dist/types/AssistantTypes.d.ts +4 -0
  38. package/dist/types/AssistantTypes.d.ts.map +1 -1
  39. package/package.json +2 -2
  40. package/src/api/AssistantRuntime.ts +1 -1
  41. package/src/api/ThreadRuntime.ts +14 -0
  42. package/src/primitives/message/MessagePartsGroupedByParentId.tsx +415 -0
  43. package/src/primitives/message/index.ts +1 -0
  44. package/src/runtimes/core/BaseThreadRuntimeCore.tsx +5 -0
  45. package/src/runtimes/core/ThreadRuntimeCore.tsx +3 -0
  46. package/src/runtimes/external-store/ExternalStoreAdapter.tsx +3 -1
  47. package/src/runtimes/external-store/ExternalStoreThreadRuntimeCore.tsx +66 -39
  48. package/src/runtimes/external-store/ThreadMessageLike.tsx +10 -4
  49. package/src/runtimes/remote-thread-list/EMPTY_THREAD_CORE.tsx +4 -0
  50. package/src/types/AssistantTypes.ts +4 -0
@@ -9,6 +9,7 @@ import {
9
9
  } from "../runtimes/core/ThreadRuntimeCore";
10
10
  import { ExportedMessageRepository } from "../runtimes/utils/MessageRepository";
11
11
  import { AppendMessage, ThreadMessage, Unsubscribe } from "../types";
12
+ import { ThreadMessageLike } from "../runtimes/external-store";
12
13
  import {
13
14
  MessageRuntime,
14
15
  MessageRuntimeImpl,
@@ -266,6 +267,14 @@ export type ThreadRuntime = {
266
267
 
267
268
  export(): ExportedMessageRepository;
268
269
  import(repository: ExportedMessageRepository): void;
270
+
271
+ /**
272
+ * Reset the thread with optional initial messages.
273
+ *
274
+ * @param initialMessages - Optional array of initial messages to populate the thread
275
+ */
276
+ reset(initialMessages?: readonly ThreadMessageLike[]): void;
277
+
269
278
  getMesssageByIndex(idx: number): MessageRuntime;
270
279
  getMesssageById(messageId: string): MessageRuntime;
271
280
 
@@ -340,6 +349,7 @@ export class ThreadRuntimeImpl implements ThreadRuntime {
340
349
  this.stopSpeaking = this.stopSpeaking.bind(this);
341
350
  this.export = this.export.bind(this);
342
351
  this.import = this.import.bind(this);
352
+ this.reset = this.reset.bind(this);
343
353
  this.getMesssageByIndex = this.getMesssageByIndex.bind(this);
344
354
  this.getMesssageById = this.getMesssageById.bind(this);
345
355
  this.subscribe = this.subscribe.bind(this);
@@ -403,6 +413,10 @@ export class ThreadRuntimeImpl implements ThreadRuntime {
403
413
  this._threadBinding.getState().import(data);
404
414
  }
405
415
 
416
+ public reset(initialMessages?: readonly ThreadMessageLike[]) {
417
+ this._threadBinding.getState().reset(initialMessages);
418
+ }
419
+
406
420
  public getMesssageByIndex(idx: number) {
407
421
  if (idx < 0) throw new Error("Message index must be >= 0");
408
422
 
@@ -0,0 +1,415 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ComponentType,
5
+ type FC,
6
+ memo,
7
+ PropsWithChildren,
8
+ useMemo,
9
+ } from "react";
10
+ import {
11
+ TextMessagePartProvider,
12
+ useMessagePart,
13
+ useMessagePartRuntime,
14
+ useToolUIs,
15
+ } from "../../context";
16
+ import {
17
+ useMessage,
18
+ useMessageRuntime,
19
+ } from "../../context/react/MessageContext";
20
+ import { MessagePartRuntimeProvider } from "../../context/providers/MessagePartRuntimeProvider";
21
+ import { MessagePartPrimitiveText } from "../messagePart/MessagePartText";
22
+ import { MessagePartPrimitiveImage } from "../messagePart/MessagePartImage";
23
+ import type {
24
+ Unstable_AudioMessagePartComponent,
25
+ EmptyMessagePartComponent,
26
+ TextMessagePartComponent,
27
+ ImageMessagePartComponent,
28
+ SourceMessagePartComponent,
29
+ ToolCallMessagePartComponent,
30
+ ToolCallMessagePartProps,
31
+ FileMessagePartComponent,
32
+ ReasoningMessagePartComponent,
33
+ } from "../../types/MessagePartComponentTypes";
34
+ import { MessagePartPrimitiveInProgress } from "../messagePart/MessagePartInProgress";
35
+ import { MessagePartStatus } from "../../types/AssistantTypes";
36
+
37
+ type MessagePartGroup = {
38
+ parentId: string | undefined;
39
+ indices: number[];
40
+ };
41
+
42
+ /**
43
+ * Groups message parts by their parent ID.
44
+ * Parts without a parent ID appear after grouped parts and remain ungrouped.
45
+ * The position of groups is based on the first occurrence of each parent ID.
46
+ */
47
+ const groupMessagePartsByParentId = (
48
+ parts: readonly any[],
49
+ ): MessagePartGroup[] => {
50
+ const groups: MessagePartGroup[] = [];
51
+ const parentIdToGroupIndex = new Map<string | undefined, number>();
52
+ const processedIndices = new Set<number>();
53
+
54
+ // First pass: process all parts with parent IDs
55
+ for (let i = 0; i < parts.length; i++) {
56
+ const part = parts[i];
57
+ const parentId = part?.parentId as string | undefined;
58
+
59
+ if (parentId !== undefined) {
60
+ let groupIndex = parentIdToGroupIndex.get(parentId);
61
+
62
+ if (groupIndex === undefined) {
63
+ // Create new group for this parent ID
64
+ groupIndex = groups.length;
65
+ groups.push({ parentId, indices: [] });
66
+ parentIdToGroupIndex.set(parentId, groupIndex);
67
+ }
68
+
69
+ groups[groupIndex]!.indices.push(i);
70
+ processedIndices.add(i);
71
+ }
72
+ }
73
+
74
+ // Second pass: add ungrouped parts (those without parent ID)
75
+ for (let i = 0; i < parts.length; i++) {
76
+ if (!processedIndices.has(i)) {
77
+ // Add individual group for parts without parent ID
78
+ groups.push({ parentId: undefined, indices: [i] });
79
+ }
80
+ }
81
+
82
+ return groups;
83
+ };
84
+
85
+ const useMessagePartsGroupedByParentId = (): MessagePartGroup[] => {
86
+ const parts = useMessage((m) => m.content);
87
+
88
+ return useMemo(() => {
89
+ if (parts.length === 0) {
90
+ return [];
91
+ }
92
+ return groupMessagePartsByParentId(parts);
93
+ }, [parts]);
94
+ };
95
+
96
+ export namespace MessagePrimitiveUnstable_PartsGroupedByParentId {
97
+ export type Props = {
98
+ /**
99
+ * Component configuration for rendering different types of message content.
100
+ *
101
+ * You can provide custom components for each content type (text, image, file, etc.)
102
+ * and configure tool rendering behavior. If not provided, default components will be used.
103
+ */
104
+ components?:
105
+ | {
106
+ /** Component for rendering empty messages */
107
+ Empty?: EmptyMessagePartComponent | undefined;
108
+ /** Component for rendering text content */
109
+ Text?: TextMessagePartComponent | undefined;
110
+ /** Component for rendering reasoning content (typically hidden) */
111
+ Reasoning?: ReasoningMessagePartComponent | undefined;
112
+ /** Component for rendering source content */
113
+ Source?: SourceMessagePartComponent | undefined;
114
+ /** Component for rendering image content */
115
+ Image?: ImageMessagePartComponent | undefined;
116
+ /** Component for rendering file content */
117
+ File?: FileMessagePartComponent | undefined;
118
+ /** Component for rendering audio content (experimental) */
119
+ Unstable_Audio?: Unstable_AudioMessagePartComponent | undefined;
120
+ /** Configuration for tool call rendering */
121
+ tools?:
122
+ | {
123
+ /** Map of tool names to their specific components */
124
+ by_name?:
125
+ | Record<string, ToolCallMessagePartComponent | undefined>
126
+ | undefined;
127
+ /** Fallback component for unregistered tools */
128
+ Fallback?: ComponentType<ToolCallMessagePartProps> | undefined;
129
+ }
130
+ | {
131
+ /** Override component that handles all tool calls */
132
+ Override: ComponentType<ToolCallMessagePartProps>;
133
+ }
134
+ | undefined;
135
+
136
+ /**
137
+ * Component for rendering grouped message parts with the same parent ID.
138
+ *
139
+ * When provided, this component will automatically wrap message parts that share
140
+ * the same parent ID, allowing you to create collapsible sections, custom styling,
141
+ * or other grouped presentations.
142
+ *
143
+ * The component receives:
144
+ * - `parentId`: The parent ID shared by all parts in the group (or undefined for ungrouped parts)
145
+ * - `indices`: Array of indices for the parts in this group
146
+ * - `children`: The rendered message part components
147
+ *
148
+ * @example
149
+ * ```tsx
150
+ * // Collapsible parent ID group
151
+ * Group: ({ parentId, indices, children }) => {
152
+ * if (!parentId) return <>{children}</>;
153
+ * return (
154
+ * <details className="parent-group">
155
+ * <summary>
156
+ * Group {parentId} ({indices.length} parts)
157
+ * </summary>
158
+ * <div className="parent-group-content">
159
+ * {children}
160
+ * </div>
161
+ * </details>
162
+ * );
163
+ * }
164
+ * ```
165
+ *
166
+ * @example
167
+ * ```tsx
168
+ * // Custom styled parent ID group
169
+ * Group: ({ parentId, indices, children }) => {
170
+ * if (!parentId) return <>{children}</>;
171
+ * return (
172
+ * <div className="border rounded-lg p-4 my-2">
173
+ * <div className="text-sm text-gray-600 mb-2">
174
+ * Related content ({parentId})
175
+ * </div>
176
+ * <div className="space-y-2">
177
+ * {children}
178
+ * </div>
179
+ * </div>
180
+ * );
181
+ * }
182
+ * ```
183
+ *
184
+ * @param parentId - The parent ID shared by all parts in this group (undefined for ungrouped parts)
185
+ * @param indices - Array of indices for the parts in this group
186
+ * @param children - Rendered message part components to display within the group
187
+ */
188
+ Group?: ComponentType<
189
+ PropsWithChildren<{
190
+ parentId: string | undefined;
191
+ indices: number[];
192
+ }>
193
+ >;
194
+ }
195
+ | undefined;
196
+ };
197
+ }
198
+
199
+ const ToolUIDisplay = ({
200
+ Fallback,
201
+ ...props
202
+ }: {
203
+ Fallback: ToolCallMessagePartComponent | undefined;
204
+ } & ToolCallMessagePartProps) => {
205
+ const Render = useToolUIs((s) => s.getToolUI(props.toolName)) ?? Fallback;
206
+ if (!Render) return null;
207
+ return <Render {...props} />;
208
+ };
209
+
210
+ const defaultComponents = {
211
+ Text: () => (
212
+ <p style={{ whiteSpace: "pre-line" }}>
213
+ <MessagePartPrimitiveText />
214
+ <MessagePartPrimitiveInProgress>
215
+ <span style={{ fontFamily: "revert" }}>{" \u25CF"}</span>
216
+ </MessagePartPrimitiveInProgress>
217
+ </p>
218
+ ),
219
+ Reasoning: () => null,
220
+ Source: () => null,
221
+ Image: () => <MessagePartPrimitiveImage />,
222
+ File: () => null,
223
+ Unstable_Audio: () => null,
224
+ Group: ({ children }) => children,
225
+ } satisfies MessagePrimitiveUnstable_PartsGroupedByParentId.Props["components"];
226
+
227
+ type MessagePartComponentProps = {
228
+ components: MessagePrimitiveUnstable_PartsGroupedByParentId.Props["components"];
229
+ };
230
+
231
+ const MessagePartComponent: FC<MessagePartComponentProps> = ({
232
+ components: {
233
+ Text = defaultComponents.Text,
234
+ Reasoning = defaultComponents.Reasoning,
235
+ Image = defaultComponents.Image,
236
+ Source = defaultComponents.Source,
237
+ File = defaultComponents.File,
238
+ Unstable_Audio: Audio = defaultComponents.Unstable_Audio,
239
+ tools = {},
240
+ } = {},
241
+ }) => {
242
+ const MessagePartRuntime = useMessagePartRuntime();
243
+
244
+ const part = useMessagePart();
245
+
246
+ const type = part.type;
247
+ if (type === "tool-call") {
248
+ const addResult = (result: any) => MessagePartRuntime.addToolResult(result);
249
+ if ("Override" in tools)
250
+ return <tools.Override {...part} addResult={addResult} />;
251
+ const Tool = tools.by_name?.[part.toolName] ?? tools.Fallback;
252
+ return <ToolUIDisplay {...part} Fallback={Tool} addResult={addResult} />;
253
+ }
254
+
255
+ if (part.status.type === "requires-action")
256
+ throw new Error("Encountered unexpected requires-action status");
257
+
258
+ switch (type) {
259
+ case "text":
260
+ return <Text {...part} />;
261
+
262
+ case "reasoning":
263
+ return <Reasoning {...part} />;
264
+
265
+ case "source":
266
+ return <Source {...part} />;
267
+
268
+ case "image":
269
+ // eslint-disable-next-line jsx-a11y/alt-text
270
+ return <Image {...part} />;
271
+
272
+ case "file":
273
+ return <File {...part} />;
274
+
275
+ case "audio":
276
+ return <Audio {...part} />;
277
+
278
+ default:
279
+ const unhandledType: never = type;
280
+ throw new Error(`Unknown message part type: ${unhandledType}`);
281
+ }
282
+ };
283
+
284
+ type MessagePartProps = {
285
+ partIndex: number;
286
+ components: MessagePrimitiveUnstable_PartsGroupedByParentId.Props["components"];
287
+ };
288
+
289
+ const MessagePartImpl: FC<MessagePartProps> = ({ partIndex, components }) => {
290
+ const messageRuntime = useMessageRuntime();
291
+ const runtime = useMemo(
292
+ () => messageRuntime.getMessagePartByIndex(partIndex),
293
+ [messageRuntime, partIndex],
294
+ );
295
+
296
+ return (
297
+ <MessagePartRuntimeProvider runtime={runtime}>
298
+ <MessagePartComponent components={components} />
299
+ </MessagePartRuntimeProvider>
300
+ );
301
+ };
302
+
303
+ const MessagePart = memo(
304
+ MessagePartImpl,
305
+ (prev, next) =>
306
+ prev.partIndex === next.partIndex &&
307
+ prev.components?.Text === next.components?.Text &&
308
+ prev.components?.Reasoning === next.components?.Reasoning &&
309
+ prev.components?.Source === next.components?.Source &&
310
+ prev.components?.Image === next.components?.Image &&
311
+ prev.components?.File === next.components?.File &&
312
+ prev.components?.Unstable_Audio === next.components?.Unstable_Audio &&
313
+ prev.components?.tools === next.components?.tools &&
314
+ prev.components?.Group === next.components?.Group,
315
+ );
316
+
317
+ const COMPLETE_STATUS: MessagePartStatus = Object.freeze({
318
+ type: "complete",
319
+ });
320
+
321
+ const EmptyPartFallback: FC<{
322
+ status: MessagePartStatus;
323
+ component: TextMessagePartComponent;
324
+ }> = ({ status, component: Component }) => {
325
+ return (
326
+ <TextMessagePartProvider text="" isRunning={status.type === "running"}>
327
+ <Component type="text" text="" status={status} />
328
+ </TextMessagePartProvider>
329
+ );
330
+ };
331
+
332
+ const EmptyPartsImpl: FC<MessagePartComponentProps> = ({ components }) => {
333
+ const status =
334
+ useMessage((s) => s.status as MessagePartStatus) ?? COMPLETE_STATUS;
335
+
336
+ if (components?.Empty) return <components.Empty status={status} />;
337
+
338
+ return (
339
+ <EmptyPartFallback
340
+ status={status}
341
+ component={components?.Text ?? defaultComponents.Text}
342
+ />
343
+ );
344
+ };
345
+
346
+ const EmptyParts = memo(
347
+ EmptyPartsImpl,
348
+ (prev, next) =>
349
+ prev.components?.Empty === next.components?.Empty &&
350
+ prev.components?.Text === next.components?.Text,
351
+ );
352
+
353
+ /**
354
+ * Renders the parts of a message grouped by their parent ID.
355
+ *
356
+ * This component automatically groups message parts that share the same parent ID,
357
+ * allowing you to create hierarchical or related content presentations. Parts without
358
+ * a parent ID appear after grouped parts and remain ungrouped.
359
+ *
360
+ * @example
361
+ * ```tsx
362
+ * <MessagePrimitive.Unstable_PartsGroupedByParentId
363
+ * components={{
364
+ * Text: ({ text }) => <p className="message-text">{text}</p>,
365
+ * Image: ({ image }) => <img src={image} alt="Message image" />,
366
+ * Group: ({ parentId, indices, children }) => {
367
+ * if (!parentId) return <>{children}</>;
368
+ * return (
369
+ * <div className="parent-group border rounded p-4">
370
+ * <h4>Related Content</h4>
371
+ * {children}
372
+ * </div>
373
+ * );
374
+ * }
375
+ * }}
376
+ * />
377
+ * ```
378
+ */
379
+ export const MessagePrimitiveUnstable_PartsGroupedByParentId: FC<
380
+ MessagePrimitiveUnstable_PartsGroupedByParentId.Props
381
+ > = ({ components }) => {
382
+ const contentLength = useMessage((s) => s.content.length);
383
+ const messageGroups = useMessagePartsGroupedByParentId();
384
+
385
+ const partsElements = useMemo(() => {
386
+ if (contentLength === 0) {
387
+ return <EmptyParts components={components} />;
388
+ }
389
+
390
+ return messageGroups.map((group, groupIndex) => {
391
+ const GroupComponent = components?.Group ?? defaultComponents.Group;
392
+
393
+ return (
394
+ <GroupComponent
395
+ key={`group-${groupIndex}-${group.parentId ?? "ungrouped"}`}
396
+ parentId={group.parentId}
397
+ indices={group.indices}
398
+ >
399
+ {group.indices.map((partIndex) => (
400
+ <MessagePart
401
+ key={partIndex}
402
+ partIndex={partIndex}
403
+ components={components}
404
+ />
405
+ ))}
406
+ </GroupComponent>
407
+ );
408
+ });
409
+ }, [messageGroups, components, contentLength]);
410
+
411
+ return <>{partsElements}</>;
412
+ };
413
+
414
+ MessagePrimitiveUnstable_PartsGroupedByParentId.displayName =
415
+ "MessagePrimitive.Unstable_PartsGroupedByParentId";
@@ -4,3 +4,4 @@ export { MessagePrimitiveParts as Content } from "./MessageParts";
4
4
  export { MessagePrimitiveIf as If } from "./MessageIf";
5
5
  export { MessagePrimitiveAttachments as Attachments } from "./MessageAttachments";
6
6
  export { MessagePrimitiveError as Error } from "./MessageError";
7
+ export { MessagePrimitiveUnstable_PartsGroupedByParentId as Unstable_PartsGroupedByParentId } from "./MessagePartsGroupedByParentId";
@@ -22,6 +22,7 @@ import { FeedbackAdapter } from "../adapters/feedback/FeedbackAdapter";
22
22
  import { AttachmentAdapter } from "../adapters/attachment";
23
23
  import { getThreadMessageText } from "../../utils/getThreadMessageText";
24
24
  import { ModelContextProvider } from "../../model-context";
25
+ import { ThreadMessageLike } from "../external-store";
25
26
 
26
27
  type BaseThreadAdapters = {
27
28
  speech?: SpeechSynthesisAdapter | undefined;
@@ -194,6 +195,10 @@ export abstract class BaseThreadRuntimeCore implements ThreadRuntimeCore {
194
195
  this._notifySubscribers();
195
196
  }
196
197
 
198
+ public reset(initialMessages?: readonly ThreadMessageLike[]) {
199
+ this.import(ExportedMessageRepository.fromArray(initialMessages ?? []));
200
+ }
201
+
197
202
  private _eventSubscribers = new Map<
198
203
  ThreadRuntimeEventType,
199
204
  Set<() => void>
@@ -6,6 +6,7 @@ import type { Unsubscribe } from "../../types/Unsubscribe";
6
6
  import { SpeechSynthesisAdapter } from "../adapters/speech/SpeechAdapterTypes";
7
7
  import { ChatModelRunOptions, ChatModelRunResult } from "../local";
8
8
  import { ExportedMessageRepository } from "../utils/MessageRepository";
9
+ import { ThreadMessageLike } from "../external-store";
9
10
  import {
10
11
  ComposerRuntimeCore,
11
12
  ThreadComposerRuntimeCore,
@@ -118,5 +119,7 @@ export type ThreadRuntimeCore = Readonly<{
118
119
  import(repository: ExportedMessageRepository): void;
119
120
  export(): ExportedMessageRepository;
120
121
 
122
+ reset(initialMessages?: readonly ThreadMessageLike[]): void;
123
+
121
124
  unstable_on(event: ThreadRuntimeEventType, callback: () => void): Unsubscribe;
122
125
  }>;
@@ -8,6 +8,7 @@ import {
8
8
  import { FeedbackAdapter } from "../adapters/feedback/FeedbackAdapter";
9
9
  import { SpeechSynthesisAdapter } from "../adapters/speech/SpeechAdapterTypes";
10
10
  import { ThreadMessageLike } from "./ThreadMessageLike";
11
+ import { ExportedMessageRepository } from "../utils/MessageRepository";
11
12
 
12
13
  export type ExternalStoreThreadData<TState extends "regular" | "archived"> = {
13
14
  status: TState;
@@ -53,7 +54,8 @@ type ExternalStoreAdapterBase<T> = {
53
54
  isDisabled?: boolean | undefined;
54
55
  isRunning?: boolean | undefined;
55
56
  isLoading?: boolean | undefined;
56
- messages: readonly T[];
57
+ messages?: readonly T[];
58
+ messageRepository?: ExportedMessageRepository;
57
59
  suggestions?: readonly ThreadSuggestion[] | undefined;
58
60
  extras?: unknown;
59
61
 
@@ -108,46 +108,79 @@ export class ExternalStoreThreadRuntimeCore
108
108
  feedback: !!this._store.adapters?.feedback,
109
109
  };
110
110
 
111
- if (oldStore) {
112
- // flush the converter cache when the convertMessage prop changes
113
- if (oldStore.convertMessage !== store.convertMessage) {
114
- this._converter = new ThreadMessageConverter();
115
- } else if (
111
+ let messages: readonly ThreadMessage[];
112
+
113
+ if (store.messageRepository) {
114
+ // Handle messageRepository
115
+ if (
116
+ oldStore &&
116
117
  oldStore.isRunning === store.isRunning &&
117
- oldStore.messages === store.messages
118
+ oldStore.messageRepository === store.messageRepository
118
119
  ) {
119
120
  this._notifySubscribers();
120
- // no conversion update
121
121
  return;
122
122
  }
123
- }
124
123
 
125
- const messages = !store.convertMessage
126
- ? store.messages
127
- : this._converter.convertMessages(store.messages, (cache, m, idx) => {
128
- if (!store.convertMessage) return m;
129
-
130
- const isLast = idx === store.messages.length - 1;
131
- const autoStatus = getAutoStatus(isLast, isRunning);
132
-
133
- if (
134
- cache &&
135
- (cache.role !== "assistant" ||
136
- !isAutoStatus(cache.status) ||
137
- cache.status === autoStatus)
138
- )
139
- return cache;
140
-
141
- const messageLike = store.convertMessage(m, idx);
142
- const newMessage = fromThreadMessageLike(
143
- messageLike,
144
- idx.toString(),
145
- autoStatus,
146
- );
147
- (newMessage as any)[symbolInnerMessage] = m;
148
- return newMessage;
149
- });
124
+ // Clear and import the message repository
125
+ this.repository.clear();
126
+ this.repository.import(store.messageRepository);
127
+
128
+ messages = this.repository.getMessages();
129
+ } else if (store.messages) {
130
+ // Handle messages array
131
+
132
+ if (oldStore) {
133
+ // flush the converter cache when the convertMessage prop changes
134
+ if (oldStore.convertMessage !== store.convertMessage) {
135
+ this._converter = new ThreadMessageConverter();
136
+ } else if (
137
+ oldStore.isRunning === store.isRunning &&
138
+ oldStore.messages === store.messages
139
+ ) {
140
+ this._notifySubscribers();
141
+ // no conversion update
142
+ return;
143
+ }
144
+ }
145
+
146
+ messages = !store.convertMessage
147
+ ? store.messages
148
+ : this._converter.convertMessages(store.messages, (cache, m, idx) => {
149
+ if (!store.convertMessage) return m;
150
+
151
+ const isLast = idx === store.messages!.length - 1;
152
+ const autoStatus = getAutoStatus(isLast, isRunning);
153
+
154
+ if (
155
+ cache &&
156
+ (cache.role !== "assistant" ||
157
+ !isAutoStatus(cache.status) ||
158
+ cache.status === autoStatus)
159
+ )
160
+ return cache;
161
+
162
+ const messageLike = store.convertMessage(m, idx);
163
+ const newMessage = fromThreadMessageLike(
164
+ messageLike,
165
+ idx.toString(),
166
+ autoStatus,
167
+ );
168
+ (newMessage as any)[symbolInnerMessage] = m;
169
+ return newMessage;
170
+ });
171
+
172
+ for (let i = 0; i < messages.length; i++) {
173
+ const message = messages[i]!;
174
+ const parent = messages[i - 1];
175
+ this.repository.addOrUpdateMessage(parent?.id ?? null, message);
176
+ }
177
+ } else {
178
+ throw new Error(
179
+ "ExternalStoreAdapter must provide either 'messages' or 'messageRepository'",
180
+ );
181
+ }
150
182
 
183
+ // Common logic for both paths
151
184
  if (messages.length > 0) this.ensureInitialized();
152
185
 
153
186
  if (oldStore?.isRunning ?? false !== store.isRunning ?? false) {
@@ -158,12 +191,6 @@ export class ExternalStoreThreadRuntimeCore
158
191
  }
159
192
  }
160
193
 
161
- for (let i = 0; i < messages.length; i++) {
162
- const message = messages[i]!;
163
- const parent = messages[i - 1];
164
- this.repository.addOrUpdateMessage(parent?.id ?? null, message);
165
- }
166
-
167
194
  if (this.assistantOptimisticId) {
168
195
  this.repository.deleteMessage(this.assistantOptimisticId);
169
196
  this.assistantOptimisticId = null;
@@ -41,6 +41,7 @@ export type ThreadMessageLike = {
41
41
  readonly artifact?: any;
42
42
  readonly result?: any | undefined;
43
43
  readonly isError?: boolean | undefined;
44
+ readonly parentId?: string | undefined;
44
45
  }
45
46
  )[];
46
47
  readonly id?: string | undefined;
@@ -104,17 +105,22 @@ export const fromThreadMessageLike = (
104
105
  return part;
105
106
 
106
107
  case "tool-call": {
108
+ const { parentId, ...basePart } = part;
109
+ const commonProps = {
110
+ ...basePart,
111
+ toolCallId: part.toolCallId ?? "tool-" + generateId(),
112
+ ...(parentId !== undefined && { parentId }),
113
+ };
114
+
107
115
  if (part.args) {
108
116
  return {
109
- ...part,
110
- toolCallId: part.toolCallId ?? "tool-" + generateId(),
117
+ ...commonProps,
111
118
  args: part.args,
112
119
  argsText: JSON.stringify(part.args),
113
120
  };
114
121
  }
115
122
  return {
116
- ...part,
117
- toolCallId: part.toolCallId ?? "tool-" + generateId(),
123
+ ...commonProps,
118
124
  args:
119
125
  part.args ??
120
126
  parsePartialJsonObject(part.argsText ?? "") ??
@@ -163,6 +163,10 @@ export const EMPTY_THREAD_CORE: ThreadRuntimeCore = {
163
163
  return { messages: [] };
164
164
  },
165
165
 
166
+ reset() {
167
+ throw EMPTY_THREAD_ERROR;
168
+ },
169
+
166
170
  unstable_on() {
167
171
  return () => {};
168
172
  },