@assistant-ui/mcp-docs-server 0.1.29 → 0.1.31

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