@assistant-ui/mcp-docs-server 0.1.1

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 (109) hide show
  1. package/.docs/organized/code-examples/local-ollama.md +1135 -0
  2. package/.docs/organized/code-examples/search-agent-for-e-commerce.md +1721 -0
  3. package/.docs/organized/code-examples/with-ai-sdk.md +1081 -0
  4. package/.docs/organized/code-examples/with-cloud.md +1164 -0
  5. package/.docs/organized/code-examples/with-external-store.md +1064 -0
  6. package/.docs/organized/code-examples/with-ffmpeg.md +1305 -0
  7. package/.docs/organized/code-examples/with-langgraph.md +1819 -0
  8. package/.docs/organized/code-examples/with-openai-assistants.md +1175 -0
  9. package/.docs/organized/code-examples/with-react-hook-form.md +1727 -0
  10. package/.docs/organized/code-examples/with-vercel-ai-rsc.md +1157 -0
  11. package/.docs/raw/blog/2024-07-29-hello/index.mdx +65 -0
  12. package/.docs/raw/blog/2024-09-11/index.mdx +10 -0
  13. package/.docs/raw/blog/2024-12-15/index.mdx +10 -0
  14. package/.docs/raw/blog/2025-01-31-changelog/index.mdx +129 -0
  15. package/.docs/raw/docs/about-assistantui.mdx +44 -0
  16. package/.docs/raw/docs/api-reference/context-providers/AssistantRuntimeProvider.mdx +30 -0
  17. package/.docs/raw/docs/api-reference/context-providers/TextContentPartProvider.mdx +26 -0
  18. package/.docs/raw/docs/api-reference/integrations/react-hook-form.mdx +103 -0
  19. package/.docs/raw/docs/api-reference/integrations/vercel-ai-sdk.mdx +145 -0
  20. package/.docs/raw/docs/api-reference/overview.mdx +583 -0
  21. package/.docs/raw/docs/api-reference/primitives/ActionBar.mdx +264 -0
  22. package/.docs/raw/docs/api-reference/primitives/AssistantModal.mdx +129 -0
  23. package/.docs/raw/docs/api-reference/primitives/Attachment.mdx +96 -0
  24. package/.docs/raw/docs/api-reference/primitives/BranchPicker.mdx +87 -0
  25. package/.docs/raw/docs/api-reference/primitives/Composer.mdx +204 -0
  26. package/.docs/raw/docs/api-reference/primitives/ContentPart.mdx +173 -0
  27. package/.docs/raw/docs/api-reference/primitives/Error.mdx +70 -0
  28. package/.docs/raw/docs/api-reference/primitives/Message.mdx +181 -0
  29. package/.docs/raw/docs/api-reference/primitives/Thread.mdx +197 -0
  30. package/.docs/raw/docs/api-reference/primitives/composition.mdx +21 -0
  31. package/.docs/raw/docs/api-reference/runtimes/AssistantRuntime.mdx +33 -0
  32. package/.docs/raw/docs/api-reference/runtimes/AttachmentRuntime.mdx +46 -0
  33. package/.docs/raw/docs/api-reference/runtimes/ComposerRuntime.mdx +69 -0
  34. package/.docs/raw/docs/api-reference/runtimes/ContentPartRuntime.mdx +22 -0
  35. package/.docs/raw/docs/api-reference/runtimes/MessageRuntime.mdx +49 -0
  36. package/.docs/raw/docs/api-reference/runtimes/ThreadListItemRuntime.mdx +32 -0
  37. package/.docs/raw/docs/api-reference/runtimes/ThreadListRuntime.mdx +31 -0
  38. package/.docs/raw/docs/api-reference/runtimes/ThreadRuntime.mdx +48 -0
  39. package/.docs/raw/docs/architecture.mdx +92 -0
  40. package/.docs/raw/docs/cloud/authorization.mdx +152 -0
  41. package/.docs/raw/docs/cloud/overview.mdx +55 -0
  42. package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +54 -0
  43. package/.docs/raw/docs/cloud/persistence/langgraph.mdx +123 -0
  44. package/.docs/raw/docs/concepts/architecture.mdx +19 -0
  45. package/.docs/raw/docs/concepts/runtime-layer.mdx +163 -0
  46. package/.docs/raw/docs/concepts/why.mdx +9 -0
  47. package/.docs/raw/docs/copilots/make-assistant-readable.mdx +71 -0
  48. package/.docs/raw/docs/copilots/make-assistant-tool-ui.mdx +76 -0
  49. package/.docs/raw/docs/copilots/make-assistant-tool.mdx +117 -0
  50. package/.docs/raw/docs/copilots/model-context.mdx +135 -0
  51. package/.docs/raw/docs/copilots/motivation.mdx +191 -0
  52. package/.docs/raw/docs/copilots/use-assistant-instructions.mdx +62 -0
  53. package/.docs/raw/docs/getting-started.mdx +1133 -0
  54. package/.docs/raw/docs/guides/Attachments.mdx +640 -0
  55. package/.docs/raw/docs/guides/Branching.mdx +59 -0
  56. package/.docs/raw/docs/guides/Editing.mdx +56 -0
  57. package/.docs/raw/docs/guides/Speech.mdx +43 -0
  58. package/.docs/raw/docs/guides/ToolUI.mdx +663 -0
  59. package/.docs/raw/docs/guides/Tools.mdx +496 -0
  60. package/.docs/raw/docs/index.mdx +7 -0
  61. package/.docs/raw/docs/legacy/styled/AssistantModal.mdx +85 -0
  62. package/.docs/raw/docs/legacy/styled/Decomposition.mdx +633 -0
  63. package/.docs/raw/docs/legacy/styled/Markdown.mdx +86 -0
  64. package/.docs/raw/docs/legacy/styled/Scrollbar.mdx +71 -0
  65. package/.docs/raw/docs/legacy/styled/Thread.mdx +84 -0
  66. package/.docs/raw/docs/legacy/styled/ThreadWidth.mdx +21 -0
  67. package/.docs/raw/docs/mcp-docs-server.mdx +324 -0
  68. package/.docs/raw/docs/migrations/deprecation-policy.mdx +41 -0
  69. package/.docs/raw/docs/migrations/v0-7.mdx +188 -0
  70. package/.docs/raw/docs/migrations/v0-8.mdx +160 -0
  71. package/.docs/raw/docs/migrations/v0-9.mdx +75 -0
  72. package/.docs/raw/docs/react-compatibility.mdx +208 -0
  73. package/.docs/raw/docs/runtimes/ai-sdk/rsc.mdx +226 -0
  74. package/.docs/raw/docs/runtimes/ai-sdk/use-assistant-hook.mdx +195 -0
  75. package/.docs/raw/docs/runtimes/ai-sdk/use-chat-hook.mdx +138 -0
  76. package/.docs/raw/docs/runtimes/ai-sdk/use-chat.mdx +136 -0
  77. package/.docs/raw/docs/runtimes/custom/external-store.mdx +1624 -0
  78. package/.docs/raw/docs/runtimes/custom/local.mdx +1185 -0
  79. package/.docs/raw/docs/runtimes/helicone.mdx +60 -0
  80. package/.docs/raw/docs/runtimes/langgraph/index.mdx +320 -0
  81. package/.docs/raw/docs/runtimes/langgraph/tutorial/index.mdx +11 -0
  82. package/.docs/raw/docs/runtimes/langgraph/tutorial/introduction.mdx +28 -0
  83. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +120 -0
  84. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +336 -0
  85. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +385 -0
  86. package/.docs/raw/docs/runtimes/langserve.mdx +126 -0
  87. package/.docs/raw/docs/runtimes/mastra/full-stack-integration.mdx +218 -0
  88. package/.docs/raw/docs/runtimes/mastra/overview.mdx +17 -0
  89. package/.docs/raw/docs/runtimes/mastra/separate-server-integration.mdx +196 -0
  90. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +222 -0
  91. package/.docs/raw/docs/ui/AssistantModal.mdx +46 -0
  92. package/.docs/raw/docs/ui/AssistantSidebar.mdx +42 -0
  93. package/.docs/raw/docs/ui/Attachment.mdx +82 -0
  94. package/.docs/raw/docs/ui/Markdown.mdx +72 -0
  95. package/.docs/raw/docs/ui/Mermaid.mdx +79 -0
  96. package/.docs/raw/docs/ui/Scrollbar.mdx +59 -0
  97. package/.docs/raw/docs/ui/SyntaxHighlighting.mdx +253 -0
  98. package/.docs/raw/docs/ui/Thread.mdx +47 -0
  99. package/.docs/raw/docs/ui/ThreadList.mdx +49 -0
  100. package/.docs/raw/docs/ui/ToolFallback.mdx +64 -0
  101. package/.docs/raw/docs/ui/primitives/Thread.mdx +197 -0
  102. package/LICENSE +21 -0
  103. package/README.md +128 -0
  104. package/dist/chunk-C7O7EFKU.js +38 -0
  105. package/dist/chunk-CZCDQ3YH.js +420 -0
  106. package/dist/index.js +1 -0
  107. package/dist/prepare-docs/prepare.js +199 -0
  108. package/dist/stdio.js +8 -0
  109. package/package.json +43 -0
@@ -0,0 +1,1624 @@
1
+ ---
2
+ title: ExternalStoreRuntime
3
+ ---
4
+
5
+ import { Callout } from "fumadocs-ui/components/callout";
6
+ import { Steps, Step } from "fumadocs-ui/components/steps";
7
+ import { Card, Cards } from "fumadocs-ui/components/card";
8
+ import { ParametersTable } from "@/components/docs";
9
+
10
+ ## Overview
11
+
12
+ `ExternalStoreRuntime` bridges your existing state management with assistant-ui components. It requires an `ExternalStoreAdapter<TMessage>` that handles communication between your state and the UI.
13
+
14
+ **Key differences from `LocalRuntime`:**
15
+
16
+ - **You own the state** - Full control over message state, thread management, and persistence logic
17
+ - **Bring your own state management** - Works with Redux, Zustand, TanStack Query, or any React state library
18
+ - **Custom message formats** - Use your backend's message structure with automatic conversion
19
+
20
+ <Callout type="warn">
21
+ `ExternalStoreRuntime` gives you total control over state (persist, sync,
22
+ share), but you must wire up every callback.
23
+ </Callout>
24
+
25
+ ## Example Implementation
26
+
27
+ ```tsx twoslash title="app/MyRuntimeProvider.tsx"
28
+ type MyMessage = {
29
+ role: "user" | "assistant";
30
+ content: string;
31
+ };
32
+ const backendApi = async (input: string): Promise<MyMessage> => {
33
+ return { role: "assistant", content: "Hello, world!" };
34
+ };
35
+
36
+ // ---cut---
37
+ import { useState, ReactNode } from "react";
38
+ import {
39
+ useExternalStoreRuntime,
40
+ ThreadMessageLike,
41
+ AppendMessage,
42
+ AssistantRuntimeProvider,
43
+ } from "@assistant-ui/react";
44
+
45
+ const convertMessage = (message: MyMessage): ThreadMessageLike => {
46
+ return {
47
+ role: message.role,
48
+ content: [{ type: "text", text: message.content }],
49
+ };
50
+ };
51
+
52
+ export function MyRuntimeProvider({
53
+ children,
54
+ }: Readonly<{
55
+ children: ReactNode;
56
+ }>) {
57
+ const [isRunning, setIsRunning] = useState(false);
58
+ const [messages, setMessages] = useState<MyMessage[]>([]);
59
+
60
+ const onNew = async (message: AppendMessage) => {
61
+ if (message.content[0]?.type !== "text")
62
+ throw new Error("Only text messages are supported");
63
+
64
+ const input = message.content[0].text;
65
+ setMessages((currentConversation) => [
66
+ ...currentConversation,
67
+ { role: "user", content: input },
68
+ ]);
69
+
70
+ setIsRunning(true);
71
+ const assistantMessage = await backendApi(input);
72
+ setMessages((currentConversation) => [
73
+ ...currentConversation,
74
+ assistantMessage,
75
+ ]);
76
+ setIsRunning(false);
77
+ };
78
+
79
+ const runtime = useExternalStoreRuntime({
80
+ isRunning,
81
+ messages,
82
+ convertMessage,
83
+ onNew,
84
+ });
85
+
86
+ return (
87
+ <AssistantRuntimeProvider runtime={runtime}>
88
+ {children}
89
+ </AssistantRuntimeProvider>
90
+ );
91
+ }
92
+ ```
93
+
94
+ ## When to Use
95
+
96
+ Use `ExternalStoreRuntime` if you need:
97
+
98
+ - **Full control over message state** - Manage messages with Redux, Zustand, TanStack Query, or any React state management library
99
+ - **Custom multi-thread implementation** - Build your own thread management system with custom storage
100
+ - **Integration with existing state** - Keep chat state in your existing state management solution
101
+ - **Custom message formats** - Use your backend's message structure with automatic conversion
102
+ - **Complex synchronization** - Sync messages with external data sources, databases, or multiple clients
103
+ - **Custom persistence logic** - Implement your own storage patterns and caching strategies
104
+
105
+ ## Key Features
106
+
107
+ <Cards>
108
+ <Card
109
+ title="State Management Integration"
110
+ description="Works seamlessly with Redux, Zustand, TanStack Query, and more"
111
+ />
112
+ <Card
113
+ title="Message Conversion"
114
+ description="Automatic conversion between your message format and assistant-ui's format"
115
+ />
116
+ <Card
117
+ title="Real-time Streaming"
118
+ description="Built-in support for streaming responses and progressive updates"
119
+ />
120
+ <Card
121
+ title="Thread Management"
122
+ description="Multi-conversation support with archiving and thread switching"
123
+ />
124
+ </Cards>
125
+
126
+ ## Architecture
127
+
128
+ ### How It Works
129
+
130
+ `ExternalStoreRuntime` acts as a bridge between your state management and assistant-ui:
131
+
132
+ ```mermaid
133
+ graph TD
134
+ A[Your State Management] -->|messages| B[ExternalStoreAdapter]
135
+ B --> C[ExternalStoreRuntime]
136
+ C --> D[assistant-ui Components]
137
+ D -->|user actions| B
138
+ B -->|state updates| A
139
+ ```
140
+
141
+ ### Key Concepts
142
+
143
+ 1. **State Ownership** - You own and control all message state
144
+ 2. **Adapter Pattern** - The adapter translates between your state and assistant-ui
145
+ 3. **Capability-Based Features** - UI features are enabled based on which handlers you provide
146
+ 4. **Message Conversion** - Automatic conversion between your message format and assistant-ui's format
147
+ 5. **Optimistic Updates** - Built-in handling for streaming and loading states
148
+
149
+ ## Getting Started
150
+
151
+ <Steps>
152
+ <Step>
153
+ ### Install Dependencies
154
+
155
+ ```sh npm2yarn
156
+ npm install @assistant-ui/react
157
+ ```
158
+
159
+ </Step>
160
+
161
+ <Step>
162
+ ### Create Runtime Provider
163
+
164
+ ```tsx title="app/MyRuntimeProvider.tsx"
165
+ "use client";
166
+
167
+ import { useState } from "react";
168
+ import {
169
+ useExternalStoreRuntime,
170
+ ThreadMessageLike,
171
+ AppendMessage,
172
+ AssistantRuntimeProvider,
173
+ } from "@assistant-ui/react";
174
+
175
+ export function MyRuntimeProvider({ children }) {
176
+ const [messages, setMessages] = useState<ThreadMessageLike[]>([]);
177
+ const [isRunning, setIsRunning] = useState(false);
178
+
179
+ const onNew = async (message: AppendMessage) => {
180
+ // Add user message
181
+ const userMessage: ThreadMessageLike = {
182
+ role: "user",
183
+ content: message.content,
184
+ };
185
+ setMessages(prev => [...prev, userMessage]);
186
+
187
+ // Generate response
188
+ setIsRunning(true);
189
+ const response = await callYourAPI(message);
190
+
191
+ const assistantMessage: ThreadMessageLike = {
192
+ role: "assistant",
193
+ content: response.content,
194
+ };
195
+ setMessages(prev => [...prev, assistantMessage]);
196
+ setIsRunning(false);
197
+ };
198
+
199
+ const runtime = useExternalStoreRuntime({
200
+ messages,
201
+ setMessages,
202
+ isRunning,
203
+ onNew,
204
+ });
205
+
206
+ return (
207
+ <AssistantRuntimeProvider runtime={runtime}>
208
+ {children}
209
+ </AssistantRuntimeProvider>
210
+ );
211
+ }
212
+ ```
213
+
214
+ </Step>
215
+
216
+ <Step>
217
+ ### Use in Your App
218
+
219
+ ```tsx title="app/page.tsx"
220
+ import { Thread } from "@assistant-ui/react";
221
+ import { MyRuntimeProvider } from "./MyRuntimeProvider";
222
+
223
+ export default function Page() {
224
+ return (
225
+ <MyRuntimeProvider>
226
+ <Thread />
227
+ </MyRuntimeProvider>
228
+ );
229
+ }
230
+ ```
231
+
232
+ </Step>
233
+ </Steps>
234
+
235
+ ## Implementation Patterns
236
+
237
+ ### Message Conversion
238
+
239
+ Two approaches for converting your message format:
240
+
241
+ #### 1. Simple Conversion (Recommended)
242
+
243
+ ```tsx
244
+ const convertMessage = (message: MyMessage): ThreadMessageLike => ({
245
+ role: message.role,
246
+ content: [{ type: "text", text: message.text }],
247
+ id: message.id,
248
+ createdAt: new Date(message.timestamp),
249
+ });
250
+
251
+ const runtime = useExternalStoreRuntime({
252
+ messages: myMessages,
253
+ convertMessage,
254
+ onNew,
255
+ });
256
+ ```
257
+
258
+ #### 2. Advanced Conversion with `useExternalMessageConverter`
259
+
260
+ For complex scenarios with performance optimization:
261
+
262
+ ```tsx
263
+ import { useExternalMessageConverter } from "@assistant-ui/react";
264
+
265
+ const convertedMessages = useExternalMessageConverter({
266
+ messages,
267
+ convertMessage: (message: MyMessage): ThreadMessageLike => ({
268
+ role: message.role,
269
+ content: [{ type: "text", text: message.text }],
270
+ id: message.id,
271
+ createdAt: new Date(message.timestamp),
272
+ }),
273
+ joinStrategy: "concat-content", // Merge adjacent assistant messages
274
+ });
275
+
276
+ const runtime = useExternalStoreRuntime({
277
+ messages: convertedMessages,
278
+ onNew,
279
+ // No convertMessage needed - already converted
280
+ });
281
+ ```
282
+
283
+ ### Join Strategy
284
+
285
+ Controls how adjacent assistant messages are combined:
286
+
287
+ - **`concat-content`** (default): Merges adjacent assistant messages into one
288
+ - **`none`**: Keeps all messages separate
289
+
290
+ This is useful when your backend sends multiple message chunks that should appear as a single message in the UI.
291
+
292
+ <Callout type="info">
293
+ `useExternalMessageConverter` provides performance optimization for complex
294
+ message conversion scenarios. For simpler cases, consider using the basic
295
+ `convertMessage` approach shown above.
296
+ </Callout>
297
+
298
+ ### Essential Handlers
299
+
300
+ #### Basic Chat (onNew only)
301
+
302
+ ```tsx
303
+ const runtime = useExternalStoreRuntime({
304
+ messages,
305
+ onNew: async (message) => {
306
+ // Add user message to state
307
+ const userMsg = { role: "user", content: message.content };
308
+ setMessages([...messages, userMsg]);
309
+
310
+ // Get AI response
311
+ const response = await callAI(message);
312
+ setMessages([...messages, userMsg, response]);
313
+ },
314
+ });
315
+ ```
316
+
317
+ #### Full-Featured Chat
318
+
319
+ ```tsx
320
+ const runtime = useExternalStoreRuntime({
321
+ messages,
322
+ setMessages, // Enables branch switching
323
+ onNew, // Required
324
+ onEdit, // Enables message editing
325
+ onReload, // Enables regeneration
326
+ onCancel, // Enables cancellation
327
+ });
328
+ ```
329
+
330
+ <Callout type="info">
331
+ Each handler you provide enables specific UI features: - `setMessages` →
332
+ Branch switching - `onEdit` → Message editing - `onReload` → Regenerate button
333
+ - `onCancel` → Cancel button during generation
334
+ </Callout>
335
+
336
+ ### Streaming Responses
337
+
338
+ Implement real-time streaming with progressive updates:
339
+
340
+ ```tsx
341
+ const onNew = async (message: AppendMessage) => {
342
+ // Add user message
343
+ const userMessage: ThreadMessageLike = {
344
+ role: "user",
345
+ content: message.content,
346
+ id: generateId(),
347
+ };
348
+ setMessages((prev) => [...prev, userMessage]);
349
+
350
+ // Create placeholder for assistant message
351
+ setIsRunning(true);
352
+ const assistantId = generateId();
353
+ const assistantMessage: ThreadMessageLike = {
354
+ role: "assistant",
355
+ content: [{ type: "text", text: "" }],
356
+ id: assistantId,
357
+ };
358
+ setMessages((prev) => [...prev, assistantMessage]);
359
+
360
+ // Stream response
361
+ const stream = await api.streamChat(message);
362
+ for await (const chunk of stream) {
363
+ setMessages((prev) =>
364
+ prev.map((m) =>
365
+ m.id === assistantId
366
+ ? {
367
+ ...m,
368
+ content: [
369
+ {
370
+ type: "text",
371
+ text: (m.content[0] as any).text + chunk,
372
+ },
373
+ ],
374
+ }
375
+ : m,
376
+ ),
377
+ );
378
+ }
379
+ setIsRunning(false);
380
+ };
381
+ ```
382
+
383
+ ### Message Editing
384
+
385
+ Enable message editing by implementing the `onEdit` handler:
386
+
387
+ <Callout type="info">
388
+ You can also implement `onEdit(editedMessage)` and `onRemove(messageId)`
389
+ callbacks to handle user-initiated edits or deletions in your external store.
390
+ This enables features like "edit and re-run" on your backend.
391
+ </Callout>
392
+
393
+ ```tsx
394
+ const onEdit = async (message: AppendMessage) => {
395
+ // Find the index where to insert the edited message
396
+ const index = messages.findIndex((m) => m.id === message.parentId) + 1;
397
+
398
+ // Keep messages up to the parent
399
+ const newMessages = [...messages.slice(0, index)];
400
+
401
+ // Add the edited message
402
+ const editedMessage: ThreadMessageLike = {
403
+ role: "user",
404
+ content: message.content,
405
+ id: message.id || generateId(),
406
+ };
407
+ newMessages.push(editedMessage);
408
+
409
+ setMessages(newMessages);
410
+
411
+ // Generate new response
412
+ setIsRunning(true);
413
+ const response = await api.chat(message);
414
+ newMessages.push({
415
+ role: "assistant",
416
+ content: response.content,
417
+ id: generateId(),
418
+ });
419
+ setMessages(newMessages);
420
+ setIsRunning(false);
421
+ };
422
+ ```
423
+
424
+ ### Tool Calling
425
+
426
+ Support tool calls with proper result handling:
427
+
428
+ ```tsx
429
+ const onAddToolResult = (options: AddToolResultOptions) => {
430
+ setMessages((prev) =>
431
+ prev.map((message) => {
432
+ if (message.id === options.messageId) {
433
+ // Update the specific tool call with its result
434
+ return {
435
+ ...message,
436
+ content: message.content.map((part) => {
437
+ if (
438
+ part.type === "tool-call" &&
439
+ part.toolCallId === options.toolCallId
440
+ ) {
441
+ return {
442
+ ...part,
443
+ result: options.result,
444
+ };
445
+ }
446
+ return part;
447
+ }),
448
+ };
449
+ }
450
+ return message;
451
+ }),
452
+ );
453
+ };
454
+
455
+ const runtime = useExternalStoreRuntime({
456
+ messages,
457
+ onNew,
458
+ onAddToolResult,
459
+ // ... other props
460
+ });
461
+ ```
462
+
463
+ #### Automatic Tool Result Matching
464
+
465
+ The runtime automatically matches tool results with their corresponding tool calls. When messages are converted and joined:
466
+
467
+ 1. **Tool Call Tracking** - The runtime tracks tool calls by their `toolCallId`
468
+ 2. **Result Association** - Tool results are automatically associated with their corresponding calls
469
+ 3. **Message Grouping** - Related tool messages are intelligently grouped together
470
+
471
+ ```tsx
472
+ // Example: Tool call and result in separate messages
473
+ const messages = [
474
+ {
475
+ role: "assistant",
476
+ content: [
477
+ {
478
+ type: "tool-call",
479
+ toolCallId: "call_123",
480
+ toolName: "get_weather",
481
+ args: { location: "San Francisco" },
482
+ },
483
+ ],
484
+ },
485
+ {
486
+ role: "tool",
487
+ content: [
488
+ {
489
+ type: "tool-result",
490
+ toolCallId: "call_123",
491
+ result: { temperature: 72, condition: "sunny" },
492
+ },
493
+ ],
494
+ },
495
+ ];
496
+
497
+ // These are automatically matched and grouped by the runtime
498
+ ```
499
+
500
+ ### File Attachments
501
+
502
+ Enable file uploads with the attachment adapter:
503
+
504
+ ```tsx
505
+ const attachmentAdapter: AttachmentAdapter = {
506
+ accept: "image/*,application/pdf,.txt,.md",
507
+ async add(file) {
508
+ // Upload file to your server
509
+ const formData = new FormData();
510
+ formData.append("file", file);
511
+
512
+ const response = await fetch("/api/upload", {
513
+ method: "POST",
514
+ body: formData,
515
+ });
516
+
517
+ const { id, url } = await response.json();
518
+ return {
519
+ id,
520
+ type: "document",
521
+ name: file.name,
522
+ file,
523
+ url,
524
+ };
525
+ },
526
+ async remove(attachment) {
527
+ // Remove file from server
528
+ await fetch(`/api/upload/${attachment.id}`, {
529
+ method: "DELETE",
530
+ });
531
+ },
532
+ };
533
+
534
+ const runtime = useExternalStoreRuntime({
535
+ messages,
536
+ onNew,
537
+ adapters: {
538
+ attachments: attachmentAdapter,
539
+ },
540
+ });
541
+ ```
542
+
543
+ ### Thread Management
544
+
545
+ #### Managing Thread Context
546
+
547
+ When implementing multi-thread support with `ExternalStoreRuntime`, you need to carefully manage thread context across your application. Here's a comprehensive approach:
548
+
549
+ ```tsx
550
+ // Create a context for thread management
551
+ const ThreadContext = createContext<{
552
+ currentThreadId: string;
553
+ setCurrentThreadId: (id: string) => void;
554
+ threads: Map<string, ThreadMessageLike[]>;
555
+ setThreads: React.Dispatch<
556
+ React.SetStateAction<Map<string, ThreadMessageLike[]>>
557
+ >;
558
+ }>({
559
+ currentThreadId: "default",
560
+ setCurrentThreadId: () => {},
561
+ threads: new Map(),
562
+ setThreads: () => {},
563
+ });
564
+
565
+ // Thread provider component
566
+ export function ThreadProvider({ children }: { children: ReactNode }) {
567
+ const [threads, setThreads] = useState<Map<string, ThreadMessageLike[]>>(
568
+ new Map([["default", []]]),
569
+ );
570
+ const [currentThreadId, setCurrentThreadId] = useState("default");
571
+
572
+ return (
573
+ <ThreadContext.Provider
574
+ value={{ currentThreadId, setCurrentThreadId, threads, setThreads }}
575
+ >
576
+ {children}
577
+ </ThreadContext.Provider>
578
+ );
579
+ }
580
+
581
+ // Hook for accessing thread context
582
+ export function useThreadContext() {
583
+ const context = useContext(ThreadContext);
584
+ if (!context) {
585
+ throw new Error("useThreadContext must be used within ThreadProvider");
586
+ }
587
+ return context;
588
+ }
589
+ ```
590
+
591
+ #### Complete Thread Implementation
592
+
593
+ Here's a full implementation with proper context management:
594
+
595
+ ```tsx
596
+ function ChatWithThreads() {
597
+ const { currentThreadId, setCurrentThreadId, threads, setThreads } =
598
+ useThreadContext();
599
+ const [threadList, setThreadList] = useState<ExternalStoreThreadData[]>([
600
+ { threadId: "default", status: "regular", title: "New Chat" },
601
+ ]);
602
+
603
+ // Get messages for current thread
604
+ const currentMessages = threads.get(currentThreadId) || [];
605
+
606
+ const threadListAdapter: ExternalStoreThreadListAdapter = {
607
+ threadId: currentThreadId,
608
+ threads: threadList.filter((t) => t.status === "regular"),
609
+ archivedThreads: threadList.filter((t) => t.status === "archived"),
610
+
611
+ onSwitchToNewThread: () => {
612
+ const newId = `thread-${Date.now()}`;
613
+ setThreadList((prev) => [
614
+ ...prev,
615
+ {
616
+ threadId: newId,
617
+ status: "regular",
618
+ title: "New Chat",
619
+ },
620
+ ]);
621
+ setThreads((prev) => new Map(prev).set(newId, []));
622
+ setCurrentThreadId(newId);
623
+ },
624
+
625
+ onSwitchToThread: (threadId) => {
626
+ setCurrentThreadId(threadId);
627
+ },
628
+
629
+ onRename: (threadId, newTitle) => {
630
+ setThreadList((prev) =>
631
+ prev.map((t) =>
632
+ t.threadId === threadId ? { ...t, title: newTitle } : t,
633
+ ),
634
+ );
635
+ },
636
+
637
+ onArchive: (threadId) => {
638
+ setThreadList((prev) =>
639
+ prev.map((t) =>
640
+ t.threadId === threadId ? { ...t, status: "archived" } : t,
641
+ ),
642
+ );
643
+ },
644
+
645
+ onDelete: (threadId) => {
646
+ setThreadList((prev) => prev.filter((t) => t.threadId !== threadId));
647
+ setThreads((prev) => {
648
+ const next = new Map(prev);
649
+ next.delete(threadId);
650
+ return next;
651
+ });
652
+ if (currentThreadId === threadId) {
653
+ setCurrentThreadId("default");
654
+ }
655
+ },
656
+ };
657
+
658
+ const runtime = useExternalStoreRuntime({
659
+ messages: currentMessages,
660
+ setMessages: (messages) => {
661
+ setThreads((prev) => new Map(prev).set(currentThreadId, messages));
662
+ },
663
+ onNew: async (message) => {
664
+ // Handle new message for current thread
665
+ // Your implementation here
666
+ },
667
+ adapters: {
668
+ threadList: threadListAdapter,
669
+ },
670
+ });
671
+
672
+ return (
673
+ <AssistantRuntimeProvider runtime={runtime}>
674
+ <ThreadList />
675
+ <Thread />
676
+ </AssistantRuntimeProvider>
677
+ );
678
+ }
679
+
680
+ // App component with proper context wrapping
681
+ export function App() {
682
+ return (
683
+ <ThreadProvider>
684
+ <ChatWithThreads />
685
+ </ThreadProvider>
686
+ );
687
+ }
688
+ ```
689
+
690
+ #### Thread Context Best Practices
691
+
692
+ <Callout type="info">
693
+ **Critical**: When using `ExternalStoreRuntime` with threads, the
694
+ `currentThreadId` must be consistent across all components and handlers.
695
+ Mismatched thread IDs will cause messages to appear in wrong threads or
696
+ disappear entirely.
697
+ </Callout>
698
+
699
+ 1. **Centralize Thread State**: Always use a context or global state management solution to ensure thread ID consistency:
700
+
701
+ ```tsx
702
+ // ❌ Bad: Local state in multiple components
703
+ function ThreadList() {
704
+ const [currentThreadId, setCurrentThreadId] = useState("default");
705
+ // This won't sync with the runtime!
706
+ }
707
+
708
+ // ✅ Good: Shared context
709
+ function ThreadList() {
710
+ const { currentThreadId, setCurrentThreadId } = useThreadContext();
711
+ // Thread ID is synchronized everywhere
712
+ }
713
+ ```
714
+
715
+ 2. **Sync Thread Changes**: Ensure all thread-related operations update both the thread ID and messages:
716
+
717
+ ```tsx
718
+ // ❌ Bad: Only updating thread ID
719
+ onSwitchToThread: (threadId) => {
720
+ setCurrentThreadId(threadId);
721
+ // Messages won't update!
722
+ };
723
+
724
+ // ✅ Good: Complete state update
725
+ onSwitchToThread: (threadId) => {
726
+ setCurrentThreadId(threadId);
727
+ // Messages automatically update via currentMessages = threads.get(currentThreadId)
728
+ };
729
+ ```
730
+
731
+ 3. **Handle Edge Cases**: Always provide fallbacks for missing threads:
732
+
733
+ ```tsx
734
+ // Ensure thread always exists
735
+ const currentMessages = threads.get(currentThreadId) || [];
736
+
737
+ // Initialize new threads properly
738
+ const initializeThread = (threadId: string) => {
739
+ if (!threads.has(threadId)) {
740
+ setThreads((prev) => new Map(prev).set(threadId, []));
741
+ }
742
+ };
743
+ ```
744
+
745
+ 4. **Persist Thread State**: For production apps, sync thread state with your backend:
746
+
747
+ ```tsx
748
+ // Save thread state to backend
749
+ useEffect(() => {
750
+ const saveThread = async () => {
751
+ await api.saveThread(currentThreadId, threads.get(currentThreadId) || []);
752
+ };
753
+
754
+ const debounced = debounce(saveThread, 1000);
755
+ debounced();
756
+
757
+ return () => debounced.cancel();
758
+ }, [currentThreadId, threads]);
759
+ ```
760
+
761
+ ## Integration Examples
762
+
763
+ ### Redux Integration
764
+
765
+ ```tsx title="app/chatSlice.ts"
766
+ // Using Redux Toolkit (recommended)
767
+ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
768
+ import { ThreadMessageLike } from "@assistant-ui/react";
769
+
770
+ interface ChatState {
771
+ messages: ThreadMessageLike[];
772
+ isRunning: boolean;
773
+ }
774
+
775
+ const chatSlice = createSlice({
776
+ name: "chat",
777
+ initialState: {
778
+ messages: [] as ThreadMessageLike[],
779
+ isRunning: false,
780
+ },
781
+ reducers: {
782
+ setMessages: (state, action: PayloadAction<ThreadMessageLike[]>) => {
783
+ state.messages = action.payload;
784
+ },
785
+ addMessage: (state, action: PayloadAction<ThreadMessageLike>) => {
786
+ state.messages.push(action.payload);
787
+ },
788
+ setIsRunning: (state, action: PayloadAction<boolean>) => {
789
+ state.isRunning = action.payload;
790
+ },
791
+ },
792
+ });
793
+
794
+ export const { setMessages, addMessage, setIsRunning } = chatSlice.actions;
795
+ export const selectMessages = (state: RootState) => state.chat.messages;
796
+ export const selectIsRunning = (state: RootState) => state.chat.isRunning;
797
+ export default chatSlice.reducer;
798
+
799
+ // ReduxRuntimeProvider.tsx
800
+ import { useSelector, useDispatch } from "react-redux";
801
+ import {
802
+ selectMessages,
803
+ selectIsRunning,
804
+ addMessage,
805
+ setMessages,
806
+ setIsRunning,
807
+ } from "./chatSlice";
808
+
809
+ export function ReduxRuntimeProvider({ children }) {
810
+ const messages = useSelector(selectMessages);
811
+ const isRunning = useSelector(selectIsRunning);
812
+ const dispatch = useDispatch();
813
+
814
+ const runtime = useExternalStoreRuntime({
815
+ messages,
816
+ isRunning,
817
+ setMessages: (messages) => dispatch(setMessages(messages)),
818
+ onNew: async (message) => {
819
+ // Add user message
820
+ dispatch(
821
+ addMessage({
822
+ role: "user",
823
+ content: message.content,
824
+ id: `msg-${Date.now()}`,
825
+ createdAt: new Date(),
826
+ }),
827
+ );
828
+
829
+ // Generate response
830
+ dispatch(setIsRunning(true));
831
+ const response = await api.chat(message);
832
+ dispatch(
833
+ addMessage({
834
+ role: "assistant",
835
+ content: response.content,
836
+ id: `msg-${Date.now()}`,
837
+ createdAt: new Date(),
838
+ }),
839
+ );
840
+ dispatch(setIsRunning(false));
841
+ },
842
+ });
843
+
844
+ return (
845
+ <AssistantRuntimeProvider runtime={runtime}>
846
+ {children}
847
+ </AssistantRuntimeProvider>
848
+ );
849
+ }
850
+ ```
851
+
852
+ ### Zustand Integration (v5)
853
+
854
+ ```tsx title="app/chatStore.ts"
855
+ // Using Zustand v5 with TypeScript
856
+ import { create } from "zustand";
857
+ import { immer } from "zustand/middleware/immer";
858
+ import { ThreadMessageLike } from "@assistant-ui/react";
859
+
860
+ interface ChatState {
861
+ messages: ThreadMessageLike[];
862
+ isRunning: boolean;
863
+ addMessage: (message: ThreadMessageLike) => void;
864
+ setMessages: (messages: ThreadMessageLike[]) => void;
865
+ setIsRunning: (isRunning: boolean) => void;
866
+ updateMessage: (id: string, updates: Partial<ThreadMessageLike>) => void;
867
+ }
868
+
869
+ // Zustand v5 requires the extra parentheses for TypeScript
870
+ const useChatStore = create<ChatState>()(
871
+ immer((set) => ({
872
+ messages: [],
873
+ isRunning: false,
874
+
875
+ addMessage: (message) =>
876
+ set((state) => {
877
+ state.messages.push(message);
878
+ }),
879
+
880
+ setMessages: (messages) =>
881
+ set((state) => {
882
+ state.messages = messages;
883
+ }),
884
+
885
+ setIsRunning: (isRunning) =>
886
+ set((state) => {
887
+ state.isRunning = isRunning;
888
+ }),
889
+
890
+ updateMessage: (id, updates) =>
891
+ set((state) => {
892
+ const index = state.messages.findIndex((m) => m.id === id);
893
+ if (index !== -1) {
894
+ Object.assign(state.messages[index], updates);
895
+ }
896
+ }),
897
+ })),
898
+ );
899
+
900
+ // ZustandRuntimeProvider.tsx
901
+ import { useShallow } from "zustand/shallow";
902
+
903
+ export function ZustandRuntimeProvider({ children }) {
904
+ // Use useShallow to prevent unnecessary re-renders
905
+ const { messages, isRunning, addMessage, setMessages, setIsRunning } =
906
+ useChatStore(
907
+ useShallow((state) => ({
908
+ messages: state.messages,
909
+ isRunning: state.isRunning,
910
+ addMessage: state.addMessage,
911
+ setMessages: state.setMessages,
912
+ setIsRunning: state.setIsRunning,
913
+ })),
914
+ );
915
+
916
+ const runtime = useExternalStoreRuntime({
917
+ messages,
918
+ isRunning,
919
+ setMessages,
920
+ onNew: async (message) => {
921
+ // Add user message
922
+ addMessage({
923
+ role: "user",
924
+ content: message.content,
925
+ id: `msg-${Date.now()}`,
926
+ createdAt: new Date(),
927
+ });
928
+
929
+ // Generate response
930
+ setIsRunning(true);
931
+ const response = await api.chat(message);
932
+ addMessage({
933
+ role: "assistant",
934
+ content: response.content,
935
+ id: `msg-${Date.now()}-assistant`,
936
+ createdAt: new Date(),
937
+ });
938
+ setIsRunning(false);
939
+ },
940
+ });
941
+
942
+ return (
943
+ <AssistantRuntimeProvider runtime={runtime}>
944
+ {children}
945
+ </AssistantRuntimeProvider>
946
+ );
947
+ }
948
+ ```
949
+
950
+ ### TanStack Query Integration
951
+
952
+ ```tsx title="app/chatQueries.ts"
953
+ // Using TanStack Query v5 with TypeScript
954
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
955
+ import { ThreadMessageLike, AppendMessage } from "@assistant-ui/react";
956
+
957
+ // Query key factory pattern
958
+ export const messageKeys = {
959
+ all: ["messages"] as const,
960
+ thread: (threadId: string) => [...messageKeys.all, threadId] as const,
961
+ };
962
+
963
+ // TanStackQueryRuntimeProvider.tsx
964
+ export function TanStackQueryRuntimeProvider({ children }) {
965
+ const queryClient = useQueryClient();
966
+ const threadId = "main"; // Or from context/props
967
+
968
+ const { data: messages = [] } = useQuery({
969
+ queryKey: messageKeys.thread(threadId),
970
+ queryFn: () => fetchMessages(threadId),
971
+ staleTime: 1000 * 60 * 5, // Consider data fresh for 5 minutes
972
+ });
973
+
974
+ const sendMessage = useMutation({
975
+ mutationFn: api.chat,
976
+
977
+ // Optimistic updates with proper TypeScript types
978
+ onMutate: async (message: AppendMessage) => {
979
+ // Cancel any outgoing refetches
980
+ await queryClient.cancelQueries({
981
+ queryKey: messageKeys.thread(threadId),
982
+ });
983
+
984
+ // Snapshot the previous value
985
+ const previousMessages = queryClient.getQueryData<ThreadMessageLike[]>(
986
+ messageKeys.thread(threadId),
987
+ );
988
+
989
+ // Optimistically update with typed data
990
+ const optimisticMessage: ThreadMessageLike = {
991
+ role: "user",
992
+ content: message.content,
993
+ id: `temp-${Date.now()}`,
994
+ createdAt: new Date(),
995
+ };
996
+
997
+ queryClient.setQueryData<ThreadMessageLike[]>(
998
+ messageKeys.thread(threadId),
999
+ (old = []) => [...old, optimisticMessage],
1000
+ );
1001
+
1002
+ return { previousMessages, tempId: optimisticMessage.id };
1003
+ },
1004
+
1005
+ onSuccess: (response, variables, context) => {
1006
+ // Replace optimistic message with real data
1007
+ queryClient.setQueryData<ThreadMessageLike[]>(
1008
+ messageKeys.thread(threadId),
1009
+ (old = []) => {
1010
+ // Remove temp message and add real ones
1011
+ return old
1012
+ .filter((m) => m.id !== context?.tempId)
1013
+ .concat([
1014
+ {
1015
+ role: "user",
1016
+ content: variables.content,
1017
+ id: `user-${Date.now()}`,
1018
+ createdAt: new Date(),
1019
+ },
1020
+ response,
1021
+ ]);
1022
+ },
1023
+ );
1024
+ },
1025
+
1026
+ onError: (error, variables, context) => {
1027
+ // Rollback to previous messages on error
1028
+ if (context?.previousMessages) {
1029
+ queryClient.setQueryData(
1030
+ messageKeys.thread(threadId),
1031
+ context.previousMessages,
1032
+ );
1033
+ }
1034
+ },
1035
+
1036
+ onSettled: () => {
1037
+ // Always refetch after error or success
1038
+ queryClient.invalidateQueries({
1039
+ queryKey: messageKeys.thread(threadId),
1040
+ });
1041
+ },
1042
+ });
1043
+
1044
+ const runtime = useExternalStoreRuntime({
1045
+ messages,
1046
+ isRunning: sendMessage.isPending,
1047
+ onNew: async (message) => {
1048
+ await sendMessage.mutateAsync(message);
1049
+ },
1050
+ // Enable message editing
1051
+ setMessages: (newMessages) => {
1052
+ queryClient.setQueryData(messageKeys.thread(threadId), newMessages);
1053
+ },
1054
+ });
1055
+
1056
+ return (
1057
+ <AssistantRuntimeProvider runtime={runtime}>
1058
+ {children}
1059
+ </AssistantRuntimeProvider>
1060
+ );
1061
+ }
1062
+ ```
1063
+
1064
+ ## Key Features
1065
+
1066
+ ### Automatic Optimistic Updates
1067
+
1068
+ When `isRunning` becomes true, the runtime automatically shows an optimistic assistant message:
1069
+
1070
+ ```tsx
1071
+ // Your code
1072
+ setIsRunning(true);
1073
+
1074
+ // Runtime automatically:
1075
+ // 1. Shows empty assistant message with "in_progress" status
1076
+ // 2. Displays typing indicator
1077
+ // 3. Updates status to "complete" when isRunning becomes false
1078
+ ```
1079
+
1080
+ ### Message Status Management
1081
+
1082
+ Assistant messages get automatic status updates:
1083
+
1084
+ - `"in_progress"` - When `isRunning` is true
1085
+ - `"complete"` - When `isRunning` becomes false
1086
+ - `"cancelled"` - When cancelled via `onCancel`
1087
+
1088
+ ### Tool Result Matching
1089
+
1090
+ The runtime automatically matches tool results with their calls:
1091
+
1092
+ ```tsx
1093
+ // Tool call and result can be in separate messages
1094
+ const messages = [
1095
+ {
1096
+ role: "assistant",
1097
+ content: [
1098
+ {
1099
+ type: "tool-call",
1100
+ toolCallId: "call_123",
1101
+ toolName: "get_weather",
1102
+ args: { location: "SF" },
1103
+ },
1104
+ ],
1105
+ },
1106
+ {
1107
+ role: "tool",
1108
+ content: [
1109
+ {
1110
+ type: "tool-result",
1111
+ toolCallId: "call_123",
1112
+ result: { temp: 72 },
1113
+ },
1114
+ ],
1115
+ },
1116
+ ];
1117
+ // Runtime automatically associates these
1118
+ ```
1119
+
1120
+ ## Working with External Messages
1121
+
1122
+ ### Converting Back to Your Format
1123
+
1124
+ Use `getExternalStoreMessages` to access your original messages:
1125
+
1126
+ ```tsx
1127
+ import { getExternalStoreMessages } from "@assistant-ui/react";
1128
+
1129
+ const MyComponent = () => {
1130
+ const originalMessages = useMessage((m) => getExternalStoreMessages(m));
1131
+ // originalMessages is MyMessage[] (your original type)
1132
+ };
1133
+ ```
1134
+
1135
+ <Callout type="info">
1136
+ After the chat finishes, use `getExternalStoreMessages(runtime)` to convert
1137
+ back to your domain model. Refer to the API reference for return structures
1138
+ and edge-case behaviors.
1139
+ </Callout>
1140
+
1141
+ <Callout type="warning">
1142
+ `getExternalStoreMessages` may return multiple messages for a single UI
1143
+ message. This happens because assistant-ui merges adjacent assistant and tool
1144
+ messages for display.
1145
+ </Callout>
1146
+
1147
+ ### Content Part Access
1148
+
1149
+ ```tsx
1150
+ const ToolUI = makeAssistantToolUI({
1151
+ render: () => {
1152
+ const originalMessages = useContentPart((p) => getExternalStoreMessages(p));
1153
+ // Access original message data for this content part
1154
+ },
1155
+ });
1156
+ ```
1157
+
1158
+ ## Debugging
1159
+
1160
+ ### Common Debugging Scenarios
1161
+
1162
+ ```tsx
1163
+ // Debug message conversion
1164
+ const convertMessage = (message: MyMessage): ThreadMessageLike => {
1165
+ console.log("Converting message:", message);
1166
+ const converted = {
1167
+ role: message.role,
1168
+ content: [{ type: "text", text: message.content }],
1169
+ };
1170
+ console.log("Converted to:", converted);
1171
+ return converted;
1172
+ };
1173
+
1174
+ // Debug adapter calls
1175
+ const onNew = async (message: AppendMessage) => {
1176
+ console.log("onNew called with:", message);
1177
+ // ... implementation
1178
+ };
1179
+
1180
+ // Enable verbose logging
1181
+ const runtime = useExternalStoreRuntime({
1182
+ messages,
1183
+ onNew: (...args) => {
1184
+ console.log("Runtime onNew:", args);
1185
+ return onNew(...args);
1186
+ },
1187
+ // ... other props
1188
+ });
1189
+ ```
1190
+
1191
+ ## Best Practices
1192
+
1193
+ ### 1. Immutable Updates
1194
+
1195
+ Always create new arrays when updating messages:
1196
+
1197
+ ```tsx
1198
+ // ❌ Wrong - mutating array
1199
+ messages.push(newMessage);
1200
+ setMessages(messages);
1201
+
1202
+ // ✅ Correct - new array
1203
+ setMessages([...messages, newMessage]);
1204
+ ```
1205
+
1206
+ ### 2. Stable Handler References
1207
+
1208
+ Memoize handlers to prevent runtime recreation:
1209
+
1210
+ ```tsx
1211
+ const onNew = useCallback(
1212
+ async (message: AppendMessage) => {
1213
+ // Handle new message
1214
+ },
1215
+ [
1216
+ /* dependencies */
1217
+ ],
1218
+ );
1219
+
1220
+ const runtime = useExternalStoreRuntime({
1221
+ messages,
1222
+ onNew, // Stable reference
1223
+ });
1224
+ ```
1225
+
1226
+ ### 3. Performance Optimization
1227
+
1228
+ ```tsx
1229
+ // For large message lists
1230
+ const recentMessages = useMemo(
1231
+ () => messages.slice(-50), // Show last 50 messages
1232
+ [messages],
1233
+ );
1234
+
1235
+ // For expensive conversions
1236
+ const convertMessage = useCallback((msg) => {
1237
+ // Conversion logic
1238
+ }, []);
1239
+ ```
1240
+
1241
+ ## `LocalRuntime` vs `ExternalStoreRuntime`
1242
+
1243
+ ### When to Choose Which
1244
+
1245
+ | Scenario | Recommendation |
1246
+ | -------------------------------- | ------------------------------------------------------------ |
1247
+ | Quick prototype | `LocalRuntime` |
1248
+ | Using Redux/Zustand | `ExternalStoreRuntime` |
1249
+ | Need Assistant Cloud integration | `LocalRuntime` |
1250
+ | Custom thread storage | Both (`LocalRuntime` with adapter or `ExternalStoreRuntime`) |
1251
+ | Simple single thread | `LocalRuntime` |
1252
+ | Complex state logic | `ExternalStoreRuntime` |
1253
+
1254
+ ### Feature Comparison
1255
+
1256
+ | Feature | `LocalRuntime` | `ExternalStoreRuntime` |
1257
+ | ---------------- | --------------------------- | ---------------------- |
1258
+ | State Management | Built-in | You provide |
1259
+ | Multi-thread | Via Cloud or custom adapter | Via adapter |
1260
+ | Message Format | ThreadMessage | Any (with conversion) |
1261
+ | Setup Complexity | Low | Medium |
1262
+ | Flexibility | Medium | High |
1263
+
1264
+ ## Common Pitfalls
1265
+
1266
+ <Callout type="error">
1267
+ **Features not appearing**: Each UI feature requires its corresponding handler:
1268
+
1269
+ ```tsx
1270
+ // ❌ No edit button
1271
+ const runtime = useExternalStoreRuntime({ messages, onNew });
1272
+
1273
+ // ✅ Edit button appears
1274
+ const runtime = useExternalStoreRuntime({ messages, onNew, onEdit });
1275
+ ```
1276
+
1277
+ </Callout>
1278
+
1279
+ <Callout type="warning">
1280
+
1281
+ **State not updating**: Common causes:
1282
+
1283
+ 1. Mutating arrays instead of creating new ones
1284
+ 2. Missing `setMessages` for branch switching
1285
+ 3. Not handling async operations properly
1286
+ 4. Incorrect message format conversion
1287
+
1288
+ </Callout>
1289
+
1290
+ ### Debugging Checklist
1291
+
1292
+ - Are you creating new arrays when updating messages?
1293
+ - Did you provide all required handlers for desired features?
1294
+ - Is your `convertMessage` returning valid `ThreadMessageLike`?
1295
+ - Are you properly handling `isRunning` state?
1296
+ - For threads: Is your thread list adapter complete?
1297
+
1298
+ ### Thread-Specific Debugging
1299
+
1300
+ Common thread context issues and solutions:
1301
+
1302
+ **Messages disappearing when switching threads:**
1303
+
1304
+ ```tsx
1305
+ // Check 1: Ensure currentThreadId is consistent
1306
+ console.log("Runtime threadId:", threadListAdapter.threadId);
1307
+ console.log("Current threadId:", currentThreadId);
1308
+ console.log("Messages for thread:", threads.get(currentThreadId));
1309
+
1310
+ // Check 2: Verify setMessages uses correct thread
1311
+ setMessages: (messages) => {
1312
+ console.log("Setting messages for thread:", currentThreadId);
1313
+ setThreads((prev) => new Map(prev).set(currentThreadId, messages));
1314
+ };
1315
+ ```
1316
+
1317
+ **Thread list not updating:**
1318
+
1319
+ ```tsx
1320
+ // Ensure threadList state is properly managed
1321
+ onSwitchToNewThread: () => {
1322
+ const newId = `thread-${Date.now()}`;
1323
+ console.log("Creating new thread:", newId);
1324
+
1325
+ // All three updates must happen together
1326
+ setThreadList((prev) => [...prev, newThreadData]);
1327
+ setThreads((prev) => new Map(prev).set(newId, []));
1328
+ setCurrentThreadId(newId);
1329
+ };
1330
+ ```
1331
+
1332
+ **Messages going to wrong thread:**
1333
+
1334
+ ```tsx
1335
+ // Add validation to prevent race conditions
1336
+ const validateThreadContext = () => {
1337
+ const runtimeThread = threadListAdapter.threadId;
1338
+ const contextThread = currentThreadId;
1339
+
1340
+ if (runtimeThread !== contextThread) {
1341
+ console.error("Thread mismatch!", { runtimeThread, contextThread });
1342
+ throw new Error("Thread context mismatch");
1343
+ }
1344
+ };
1345
+
1346
+ // Call before any message operation
1347
+ onNew: async (message) => {
1348
+ validateThreadContext();
1349
+ // ... handle message
1350
+ };
1351
+ ```
1352
+
1353
+ ## API Reference
1354
+
1355
+ ### `ExternalStoreAdapter`
1356
+
1357
+ The main interface for connecting your state to assistant-ui.
1358
+
1359
+ <ParametersTable
1360
+ type="ExternalStoreAdapter<T>"
1361
+ parameters={[
1362
+ {
1363
+ name: "messages",
1364
+ type: "readonly T[]",
1365
+ description: "Array of messages from your state",
1366
+ required: true,
1367
+ },
1368
+ {
1369
+ name: "onNew",
1370
+ type: "(message: AppendMessage) => Promise<void>",
1371
+ description: "Handler for new messages from the user",
1372
+ required: true,
1373
+ },
1374
+ {
1375
+ name: "isRunning",
1376
+ type: "boolean",
1377
+ description:
1378
+ "Whether the assistant is currently generating a response. When true, shows optimistic assistant message",
1379
+ default: "false",
1380
+ },
1381
+ {
1382
+ name: "isDisabled",
1383
+ type: "boolean",
1384
+ description: "Whether the chat input should be disabled",
1385
+ default: "false",
1386
+ },
1387
+ {
1388
+ name: "suggestions",
1389
+ type: "readonly ThreadSuggestion[]",
1390
+ description: "Suggested prompts to display",
1391
+ },
1392
+ {
1393
+ name: "extras",
1394
+ type: "unknown",
1395
+ description: "Additional data accessible via runtime.extras",
1396
+ },
1397
+ {
1398
+ name: "setMessages",
1399
+ type: "(messages: T[]) => void",
1400
+ description: "Update messages (required for branch switching)",
1401
+ },
1402
+ {
1403
+ name: "onEdit",
1404
+ type: "(message: AppendMessage) => Promise<void>",
1405
+ description: "Handler for message edits (required for edit feature)",
1406
+ },
1407
+ {
1408
+ name: "onReload",
1409
+ type: "(parentId: string | null, config: StartRunConfig) => Promise<void>",
1410
+ description:
1411
+ "Handler for regenerating messages (required for reload feature)",
1412
+ },
1413
+ {
1414
+ name: "onCancel",
1415
+ type: "() => Promise<void>",
1416
+ description: "Handler for cancelling the current generation",
1417
+ },
1418
+ {
1419
+ name: "onAddToolResult",
1420
+ type: "(options: AddToolResultOptions) => Promise<void> | void",
1421
+ description: "Handler for adding tool call results",
1422
+ },
1423
+ {
1424
+ name: "convertMessage",
1425
+ type: "(message: T, index: number) => ThreadMessageLike",
1426
+ description:
1427
+ "Convert your message format to assistant-ui format. Not needed if using ThreadMessage type",
1428
+ },
1429
+ {
1430
+ name: "joinStrategy",
1431
+ type: '"concat-content" | "none"',
1432
+ description: "How to join adjacent assistant messages when converting",
1433
+ default: '"concat-content"',
1434
+ },
1435
+ {
1436
+ name: "adapters",
1437
+ type: "object",
1438
+ description: "Feature adapters (same as LocalRuntime)",
1439
+ children: [
1440
+ {
1441
+ type: "adapters",
1442
+ parameters: [
1443
+ {
1444
+ name: "attachments",
1445
+ type: "AttachmentAdapter",
1446
+ description: "Enable file attachments",
1447
+ },
1448
+ {
1449
+ name: "speech",
1450
+ type: "SpeechSynthesisAdapter",
1451
+ description: "Enable text-to-speech",
1452
+ },
1453
+ {
1454
+ name: "feedback",
1455
+ type: "FeedbackAdapter",
1456
+ description: "Enable message feedback",
1457
+ },
1458
+ {
1459
+ name: "threadList",
1460
+ type: "ExternalStoreThreadListAdapter",
1461
+ description: "Enable multi-thread management",
1462
+ },
1463
+ ],
1464
+ },
1465
+ ],
1466
+ },
1467
+ {
1468
+ name: "unstable_capabilities",
1469
+ type: "object",
1470
+ description: "Configure runtime capabilities",
1471
+ children: [
1472
+ {
1473
+ type: "unstable_capabilities",
1474
+ parameters: [
1475
+ {
1476
+ name: "copy",
1477
+ type: "boolean",
1478
+ description: "Enable message copy feature",
1479
+ default: "true",
1480
+ },
1481
+ ],
1482
+ },
1483
+ ],
1484
+ },
1485
+ ]}
1486
+ />
1487
+
1488
+ ### `ThreadMessageLike`
1489
+
1490
+ A flexible message format that can be converted to assistant-ui's internal format.
1491
+
1492
+ <ParametersTable
1493
+ type="ThreadMessageLike"
1494
+ parameters={[
1495
+ {
1496
+ name: "role",
1497
+ type: '"assistant" | "user" | "system"',
1498
+ description: "The role of the message sender",
1499
+ required: true,
1500
+ },
1501
+ {
1502
+ name: "content",
1503
+ type: "string | readonly ContentPart[]",
1504
+ description: "Message content as string or structured content parts",
1505
+ required: true,
1506
+ },
1507
+ {
1508
+ name: "id",
1509
+ type: "string",
1510
+ description: "Unique identifier for the message",
1511
+ },
1512
+ {
1513
+ name: "createdAt",
1514
+ type: "Date",
1515
+ description: "Timestamp when the message was created",
1516
+ },
1517
+ {
1518
+ name: "status",
1519
+ type: "MessageStatus",
1520
+ description:
1521
+ "Status of assistant messages (in_progress, complete, cancelled)",
1522
+ },
1523
+ {
1524
+ name: "attachments",
1525
+ type: "readonly CompleteAttachment[]",
1526
+ description: "File attachments (user messages only)",
1527
+ },
1528
+ {
1529
+ name: "metadata",
1530
+ type: "object",
1531
+ description: "Additional message metadata",
1532
+ children: [
1533
+ {
1534
+ type: "metadata",
1535
+ parameters: [
1536
+ {
1537
+ name: "steps",
1538
+ type: "readonly ThreadStep[]",
1539
+ description: "Tool call steps for assistant messages",
1540
+ },
1541
+ {
1542
+ name: "custom",
1543
+ type: "Record<string, unknown>",
1544
+ description: "Custom metadata for your application",
1545
+ },
1546
+ ],
1547
+ },
1548
+ ],
1549
+ },
1550
+ ]}
1551
+ />
1552
+
1553
+ ### `ExternalStoreThreadListAdapter`
1554
+
1555
+ Enable multi-thread support with custom thread management.
1556
+
1557
+ <ParametersTable
1558
+ type="ExternalStoreThreadListAdapter"
1559
+ parameters={[
1560
+ {
1561
+ name: "threadId",
1562
+ type: "string",
1563
+ description: "ID of the current active thread",
1564
+ },
1565
+ {
1566
+ name: "threads",
1567
+ type: "readonly ExternalStoreThreadData[]",
1568
+ description: "Array of regular threads with { threadId, title }",
1569
+ },
1570
+ {
1571
+ name: "archivedThreads",
1572
+ type: "readonly ExternalStoreThreadData[]",
1573
+ description: "Array of archived threads",
1574
+ },
1575
+ {
1576
+ name: "onSwitchToNewThread",
1577
+ type: "() => Promise<void> | void",
1578
+ description: "Handler for creating a new thread",
1579
+ },
1580
+ {
1581
+ name: "onSwitchToThread",
1582
+ type: "(threadId: string) => Promise<void> | void",
1583
+ description: "Handler for switching to an existing thread",
1584
+ },
1585
+ {
1586
+ name: "onRename",
1587
+ type: "(threadId: string, newTitle: string) => Promise<void> | void",
1588
+ description: "Handler for renaming a thread",
1589
+ },
1590
+ {
1591
+ name: "onArchive",
1592
+ type: "(threadId: string) => Promise<void> | void",
1593
+ description: "Handler for archiving a thread",
1594
+ },
1595
+ {
1596
+ name: "onUnarchive",
1597
+ type: "(threadId: string) => Promise<void> | void",
1598
+ description: "Handler for unarchiving a thread",
1599
+ },
1600
+ {
1601
+ name: "onDelete",
1602
+ type: "(threadId: string) => Promise<void> | void",
1603
+ description: "Handler for deleting a thread",
1604
+ },
1605
+ ]}
1606
+ />
1607
+
1608
+ <Callout type="info">
1609
+ The thread list adapter enables multi-thread support. Without it, the runtime
1610
+ only manages the current conversation.
1611
+ </Callout>
1612
+
1613
+ ### Related Runtime APIs
1614
+
1615
+ - [AssistantRuntime API](/docs/api-reference/runtimes/AssistantRuntime) - Core runtime interface and methods
1616
+ - [ThreadRuntime API](/docs/api-reference/runtimes/ThreadRuntime) - Thread-specific operations and state management
1617
+ - [Runtime Providers](/docs/api-reference/context-providers/AssistantRuntimeProvider) - Context providers for runtime integration
1618
+
1619
+ ## Related Resources
1620
+
1621
+ - [Runtime Layer Concepts](/docs/concepts/runtime-layer)
1622
+ - [Pick a Runtime Guide](/docs/runtimes/pick-a-runtime)
1623
+ - [`LocalRuntime` Documentation](/docs/runtimes/custom/local)
1624
+ - [Examples Repository](https://github.com/assistant-ui/assistant-ui/tree/main/examples/with-external-store)