@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.
- package/.docs/organized/code-examples/waterfall.md +15 -7
- package/.docs/organized/code-examples/with-a2a.md +9 -21
- package/.docs/organized/code-examples/with-ag-ui.md +11 -8
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +10 -10
- package/.docs/organized/code-examples/with-artifacts.md +12 -10
- package/.docs/organized/code-examples/with-assistant-transport.md +11 -12
- package/.docs/organized/code-examples/with-chain-of-thought.md +83 -54
- package/.docs/organized/code-examples/with-cloud-standalone.md +14 -11
- package/.docs/organized/code-examples/with-cloud.md +9 -10
- package/.docs/organized/code-examples/with-custom-thread-list.md +61 -16
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +17 -12
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +13 -13
- package/.docs/organized/code-examples/with-expo.md +25 -21
- package/.docs/organized/code-examples/with-external-store.md +8 -8
- package/.docs/organized/code-examples/with-ffmpeg.md +17 -12
- package/.docs/organized/code-examples/with-generative-ui.md +9 -9
- package/.docs/organized/code-examples/with-google-adk.md +8 -8
- package/.docs/organized/code-examples/with-heat-graph.md +5 -5
- package/.docs/organized/code-examples/with-interactables.md +10 -25
- package/.docs/organized/code-examples/with-langchain.md +437 -0
- package/.docs/organized/code-examples/with-langgraph.md +16 -16
- package/.docs/organized/code-examples/with-livekit.md +18 -13
- package/.docs/organized/code-examples/with-opencode.md +105 -62
- package/.docs/organized/code-examples/with-parent-id-grouping.md +10 -10
- package/.docs/organized/code-examples/with-react-hook-form.md +220 -148
- package/.docs/organized/code-examples/with-react-ink.md +2 -2
- package/.docs/organized/code-examples/with-react-router.md +12 -12
- package/.docs/organized/code-examples/with-store.md +8 -5
- package/.docs/organized/code-examples/with-tanstack.md +10 -10
- package/.docs/organized/code-examples/with-tap-runtime.md +10 -6
- package/.docs/raw/docs/(docs)/cli.mdx +2 -1
- package/.docs/raw/docs/(docs)/copilots/assistant-frame.mdx +1 -0
- package/.docs/raw/docs/(docs)/copilots/make-assistant-tool-ui.mdx +10 -3
- package/.docs/raw/docs/(docs)/copilots/make-assistant-tool.mdx +8 -3
- package/.docs/raw/docs/(docs)/copilots/make-assistant-visible.mdx +1 -0
- package/.docs/raw/docs/(docs)/copilots/model-context.mdx +1 -0
- package/.docs/raw/docs/(docs)/copilots/motivation.mdx +1 -0
- package/.docs/raw/docs/(docs)/copilots/use-assistant-instructions.mdx +1 -0
- package/.docs/raw/docs/(docs)/devtools.mdx +1 -0
- package/.docs/raw/docs/(docs)/index.mdx +1 -0
- package/.docs/raw/docs/(docs)/installation.mdx +1 -0
- package/.docs/raw/docs/(docs)/rtl.mdx +80 -0
- package/.docs/raw/docs/(reference)/api-reference/adapters/attachments.mdx +34 -0
- package/.docs/raw/docs/(reference)/api-reference/adapters/feedback-speech.mdx +41 -0
- package/.docs/raw/docs/(reference)/api-reference/adapters/index.mdx +26 -0
- package/.docs/raw/docs/(reference)/api-reference/adapters/persistence.mdx +34 -0
- package/.docs/raw/docs/(reference)/api-reference/adapters/runtime.mdx +31 -0
- package/.docs/raw/docs/(reference)/api-reference/context-providers/index.mdx +20 -0
- package/.docs/raw/docs/(reference)/api-reference/hooks/index.mdx +26 -0
- package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +72 -0
- package/.docs/raw/docs/(reference)/api-reference/hooks/runtimes.mdx +41 -0
- package/.docs/raw/docs/(reference)/api-reference/hooks/state.mdx +48 -0
- package/.docs/raw/docs/(reference)/api-reference/hooks/utilities.mdx +30 -0
- package/.docs/raw/docs/(reference)/api-reference/integrations/index.mdx +23 -0
- package/.docs/raw/docs/(reference)/api-reference/overview.mdx +21 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +7 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +149 -40
- package/.docs/raw/docs/(reference)/api-reference/primitives/index.mdx +65 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +2 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +50 -6
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread-list.mdx +15 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +36 -1
- package/.docs/raw/docs/(reference)/api-reference/runtimes/index.mdx +38 -0
- package/.docs/raw/docs/(reference)/api-reference/runtimes/thread-list-runtime.mdx +9 -0
- package/.docs/raw/docs/(reference)/migrations/v0-14.mdx +144 -6
- package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +231 -3
- package/.docs/raw/docs/cloud/ai-sdk.mdx +221 -3
- package/.docs/raw/docs/cloud/langgraph.mdx +274 -2
- package/.docs/raw/docs/{(docs)/guides → guides}/attachments.mdx +41 -36
- package/.docs/raw/docs/guides/branching.mdx +76 -0
- package/.docs/raw/docs/guides/chain-of-thought.mdx +166 -0
- package/.docs/raw/docs/{(docs)/guides → guides}/context-api.mdx +50 -22
- package/.docs/raw/docs/{(docs)/guides → guides}/dictation.mdx +2 -0
- package/.docs/raw/docs/guides/editing.mdx +102 -0
- package/.docs/raw/docs/guides/index.mdx +103 -0
- package/.docs/raw/docs/{(docs)/guides → guides}/interactables.mdx +49 -0
- package/.docs/raw/docs/{(docs)/guides → guides}/latex.mdx +51 -8
- package/.docs/raw/docs/guides/mentions.mdx +520 -0
- package/.docs/raw/docs/{(docs)/guides → guides}/message-timing.mdx +8 -2
- package/.docs/raw/docs/{(docs)/guides → guides}/multi-agent.mdx +64 -4
- package/.docs/raw/docs/{(docs)/guides → guides}/quoting.mdx +10 -17
- package/.docs/raw/docs/guides/slash-commands.mdx +361 -0
- package/.docs/raw/docs/guides/speech.mdx +156 -0
- package/.docs/raw/docs/{(docs)/guides → guides}/suggestions.mdx +21 -83
- package/.docs/raw/docs/{(docs)/guides → guides}/tool-ui.mdx +108 -36
- package/.docs/raw/docs/{(docs)/guides → guides}/tools.mdx +131 -35
- package/.docs/raw/docs/{(docs)/guides → guides}/voice.mdx +39 -0
- package/.docs/raw/docs/ink/index.mdx +1 -3
- package/.docs/raw/docs/ink/migration.mdx +1 -3
- package/.docs/raw/docs/ink/primitives.mdx +37 -1
- package/.docs/raw/docs/integrations/attachments/custom-adapter.mdx +520 -0
- package/.docs/raw/docs/integrations/auth/better-auth.mdx +191 -0
- package/.docs/raw/docs/integrations/auth/clerk.mdx +172 -0
- package/.docs/raw/docs/integrations/auth/next-auth.mdx +196 -0
- package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +79 -0
- package/.docs/raw/docs/integrations/frameworks/mastra/full-stack.mdx +188 -0
- package/.docs/raw/docs/integrations/frameworks/mastra/overview.mdx +57 -0
- package/.docs/raw/docs/integrations/frameworks/mastra/separate-server.mdx +201 -0
- package/.docs/raw/docs/integrations/gateways/index.mdx +157 -0
- package/.docs/raw/docs/integrations/index.mdx +173 -0
- package/.docs/raw/docs/integrations/observability/helicone.mdx +130 -0
- package/.docs/raw/docs/integrations/observability/langfuse.mdx +156 -0
- package/.docs/raw/docs/integrations/observability/langsmith.mdx +146 -0
- package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +712 -0
- package/.docs/raw/docs/integrations/tools/mcp.mdx +267 -0
- package/.docs/raw/docs/primitives/action-bar.mdx +1 -0
- package/.docs/raw/docs/primitives/assistant-modal.mdx +1 -0
- package/.docs/raw/docs/primitives/attachment.mdx +1 -0
- package/.docs/raw/docs/primitives/branch-picker.mdx +1 -0
- package/.docs/raw/docs/primitives/chain-of-thought.mdx +90 -85
- package/.docs/raw/docs/primitives/composer.mdx +96 -63
- package/.docs/raw/docs/primitives/error.mdx +1 -0
- package/.docs/raw/docs/primitives/index.mdx +2 -1
- package/.docs/raw/docs/primitives/message.mdx +68 -5
- package/.docs/raw/docs/primitives/selection-toolbar.mdx +1 -0
- package/.docs/raw/docs/primitives/suggestion.mdx +1 -0
- package/.docs/raw/docs/primitives/thread-list.mdx +39 -0
- package/.docs/raw/docs/primitives/thread.mdx +16 -13
- package/.docs/raw/docs/react-native/index.mdx +1 -3
- package/.docs/raw/docs/react-native/migration.mdx +1 -3
- package/.docs/raw/docs/runtimes/a2a/client-and-hooks.mdx +396 -0
- package/.docs/raw/docs/runtimes/a2a/overview.mdx +60 -0
- package/.docs/raw/docs/runtimes/a2a/quickstart.mdx +216 -0
- package/.docs/raw/docs/runtimes/ag-ui/overview.mdx +70 -0
- package/.docs/raw/docs/runtimes/ag-ui/quickstart.mdx +243 -0
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +123 -0
- package/.docs/raw/docs/runtimes/ai-sdk/overview.mdx +52 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +71 -131
- package/.docs/raw/docs/runtimes/ai-sdk/v5-legacy.mdx +69 -63
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +365 -101
- package/.docs/raw/docs/runtimes/concepts/adapters.mdx +265 -0
- package/.docs/raw/docs/runtimes/concepts/architecture.mdx +125 -0
- package/.docs/raw/docs/runtimes/concepts/stability.mdx +67 -0
- package/.docs/raw/docs/runtimes/concepts/threads.mdx +428 -0
- package/.docs/raw/docs/runtimes/custom/assistant-transport.mdx +703 -0
- package/.docs/raw/docs/runtimes/custom/data-stream.mdx +323 -0
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +253 -1236
- package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +746 -0
- package/.docs/raw/docs/runtimes/custom/overview.mdx +71 -0
- package/.docs/raw/docs/runtimes/google-adk/api.mdx +256 -0
- package/.docs/raw/docs/runtimes/google-adk/hooks.mdx +717 -0
- package/.docs/raw/docs/runtimes/google-adk/overview.mdx +69 -0
- package/.docs/raw/docs/runtimes/google-adk/quickstart.mdx +229 -0
- package/.docs/raw/docs/runtimes/langchain.mdx +533 -0
- package/.docs/raw/docs/runtimes/langgraph/generative-ui.mdx +305 -0
- package/.docs/raw/docs/runtimes/langgraph/interrupts.mdx +104 -0
- package/.docs/raw/docs/runtimes/langgraph/overview.mdx +84 -0
- package/.docs/raw/docs/runtimes/langgraph/quickstart.mdx +496 -0
- package/.docs/raw/docs/runtimes/langgraph/streaming.mdx +127 -0
- package/.docs/raw/docs/runtimes/langgraph/threads.mdx +113 -0
- package/.docs/raw/docs/runtimes/langgraph/tutorial/introduction.mdx +3 -3
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +0 -23
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +1 -1
- package/.docs/raw/docs/runtimes/opencode/hooks.mdx +191 -0
- package/.docs/raw/docs/runtimes/opencode/overview.mdx +48 -0
- package/.docs/raw/docs/runtimes/opencode/quickstart.mdx +119 -0
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +74 -198
- package/.docs/raw/docs/ui/accordion.mdx +1 -0
- package/.docs/raw/docs/ui/assistant-modal.mdx +1 -0
- package/.docs/raw/docs/ui/assistant-sidebar.mdx +1 -0
- package/.docs/raw/docs/ui/attachment.mdx +1 -0
- package/.docs/raw/docs/ui/badge.mdx +1 -0
- package/.docs/raw/docs/ui/composer-trigger-popover.mdx +200 -0
- package/.docs/raw/docs/ui/context-display.mdx +1 -0
- package/.docs/raw/docs/ui/diff-viewer.mdx +1 -0
- package/.docs/raw/docs/ui/directive-text.mdx +114 -0
- package/.docs/raw/docs/ui/file.mdx +1 -0
- package/.docs/raw/docs/ui/image.mdx +1 -0
- package/.docs/raw/docs/ui/markdown.mdx +2 -14
- package/.docs/raw/docs/ui/mermaid.mdx +1 -0
- package/.docs/raw/docs/ui/message-timing.mdx +3 -2
- package/.docs/raw/docs/ui/model-selector.mdx +1 -0
- package/.docs/raw/docs/ui/part-grouping.mdx +325 -313
- package/.docs/raw/docs/ui/quote.mdx +1 -0
- package/.docs/raw/docs/ui/reasoning.mdx +69 -32
- package/.docs/raw/docs/ui/scrollbar.mdx +1 -0
- package/.docs/raw/docs/ui/select.mdx +1 -0
- package/.docs/raw/docs/ui/sources.mdx +1 -0
- package/.docs/raw/docs/ui/streamdown.mdx +1 -0
- package/.docs/raw/docs/ui/syntax-highlighting.mdx +1 -0
- package/.docs/raw/docs/ui/tabs.mdx +1 -0
- package/.docs/raw/docs/ui/thread-list.mdx +17 -0
- package/.docs/raw/docs/ui/thread.mdx +56 -1
- package/.docs/raw/docs/ui/tool-fallback.mdx +1 -0
- package/.docs/raw/docs/ui/tool-group.mdx +39 -11
- package/.docs/raw/docs/ui/voice.mdx +1 -0
- package/.docs/raw/docs/utilities/heat-graph.mdx +1 -0
- package/.docs/raw/docs/utilities/react-o11y.mdx +278 -0
- package/.docs/raw/docs/utilities/tw-shimmer.mdx +1 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/package.json +4 -4
- package/src/tools/tests/path-traversal.test.ts +1 -1
- package/src/utils/logger.ts +1 -1
- package/.docs/raw/docs/(docs)/guides/branching.mdx +0 -65
- package/.docs/raw/docs/(docs)/guides/chain-of-thought.mdx +0 -164
- package/.docs/raw/docs/(docs)/guides/editing.mdx +0 -66
- package/.docs/raw/docs/(docs)/guides/mentions.mdx +0 -406
- package/.docs/raw/docs/(docs)/guides/slash-commands.mdx +0 -275
- package/.docs/raw/docs/(docs)/guides/speech.mdx +0 -38
- package/.docs/raw/docs/runtimes/a2a/index.mdx +0 -298
- package/.docs/raw/docs/runtimes/assistant-transport.mdx +0 -1033
- package/.docs/raw/docs/runtimes/custom/custom-thread-list.mdx +0 -268
- package/.docs/raw/docs/runtimes/custom/local.mdx +0 -1464
- package/.docs/raw/docs/runtimes/data-stream.mdx +0 -422
- package/.docs/raw/docs/runtimes/google-adk/index.mdx +0 -686
- package/.docs/raw/docs/runtimes/helicone.mdx +0 -61
- package/.docs/raw/docs/runtimes/langgraph/index.mdx +0 -607
- package/.docs/raw/docs/runtimes/langgraph/tutorial/index.mdx +0 -12
- package/.docs/raw/docs/runtimes/langserve.mdx +0 -116
- package/.docs/raw/docs/runtimes/mastra/full-stack-integration.mdx +0 -218
- package/.docs/raw/docs/runtimes/mastra/overview.mdx +0 -18
- package/.docs/raw/docs/runtimes/mastra/separate-server-integration.mdx +0 -217
- package/.docs/raw/docs/ui/mention.mdx +0 -168
|
@@ -1,36 +1,50 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: ExternalStoreRuntime
|
|
3
|
-
description: Bring your own
|
|
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
|
-
##
|
|
8
|
+
## When to use it
|
|
8
9
|
|
|
9
|
-
`ExternalStoreRuntime`
|
|
10
|
+
Pick `ExternalStoreRuntime` when:
|
|
10
11
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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((
|
|
63
|
-
...currentConversation,
|
|
64
|
-
{ role: "user", content: input },
|
|
65
|
-
]);
|
|
78
|
+
setMessages((prev) => [...prev, { role: "user", content: input }]);
|
|
66
79
|
|
|
67
80
|
setIsRunning(true);
|
|
68
|
-
const
|
|
69
|
-
setMessages((
|
|
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
|
-
|
|
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
|
-
###
|
|
104
|
+
### Use in your app
|
|
126
105
|
|
|
127
|
-
|
|
106
|
+
```tsx title="app/page.tsx"
|
|
107
|
+
import { Thread } from "@/components/assistant-ui/thread";
|
|
108
|
+
import { MyRuntimeProvider } from "./MyRuntimeProvider";
|
|
128
109
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
110
|
+
export default function Page() {
|
|
111
|
+
return (
|
|
112
|
+
<MyRuntimeProvider>
|
|
113
|
+
<Thread />
|
|
114
|
+
</MyRuntimeProvider>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
136
117
|
```
|
|
137
118
|
|
|
138
|
-
|
|
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
|
-
##
|
|
238
|
-
|
|
239
|
-
### Message Conversion
|
|
122
|
+
## Message conversion
|
|
240
123
|
|
|
241
|
-
Two approaches
|
|
124
|
+
Two approaches.
|
|
242
125
|
|
|
243
|
-
|
|
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
|
-
|
|
143
|
+
### `useExternalMessageConverter` (with join strategy)
|
|
261
144
|
|
|
262
|
-
For
|
|
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", //
|
|
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
|
-
|
|
167
|
+
`joinStrategy` controls how adjacent assistant messages combine: `concat-content` (default) merges them into one; `none` keeps them separate.
|
|
287
168
|
|
|
288
|
-
|
|
169
|
+
## Handler matrix
|
|
289
170
|
|
|
290
|
-
|
|
291
|
-
- **`none`**: Keeps all messages separate
|
|
171
|
+
Each handler enables a specific UI feature.
|
|
292
172
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
193
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
352
194
|
|
|
353
|
-
// Create placeholder for assistant message
|
|
354
195
|
setIsRunning(true);
|
|
355
196
|
const assistantId = generateId();
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
content: [{ type: "text", text: "" }],
|
|
359
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
242
|
+
## Branching
|
|
428
243
|
|
|
429
|
-
The `messages` array
|
|
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
|
-
|
|
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" },
|
|
263
|
+
{ headId: "asst-1" },
|
|
454
264
|
);
|
|
455
265
|
|
|
456
|
-
// Import into the runtime
|
|
457
266
|
runtime.thread.import(repo);
|
|
458
267
|
```
|
|
459
268
|
|
|
460
|
-
|
|
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
|
-
|
|
271
|
+
## Tool calling
|
|
466
272
|
|
|
467
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
301
|
+
The runtime automatically matches tool results to their tool calls by `toolCallId` and groups related messages for display.
|
|
505
302
|
|
|
506
|
-
|
|
303
|
+
## Attachments
|
|
507
304
|
|
|
508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
319
|
+
## Integration examples
|
|
811
320
|
|
|
812
|
-
### Redux
|
|
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
|
-
|
|
845
|
-
export const selectIsRunning = (state: RootState) => state.chat.isRunning;
|
|
846
|
-
export default chatSlice.reducer;
|
|
344
|
+
```
|
|
847
345
|
|
|
848
|
-
|
|
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(
|
|
860
|
-
const isRunning = useSelector(
|
|
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
|
|
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
|
-
|
|
919
|
-
const useChatStore = create<ChatState>()(
|
|
405
|
+
export const useChatStore = create<ChatState>()(
|
|
920
406
|
immer((set) => ({
|
|
921
407
|
messages: [],
|
|
922
408
|
isRunning: false,
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
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((
|
|
957
|
-
messages:
|
|
958
|
-
isRunning:
|
|
959
|
-
addMessage:
|
|
960
|
-
setMessages:
|
|
961
|
-
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()}-
|
|
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
|
|
463
|
+
### TanStack Query
|
|
1000
464
|
|
|
1001
|
-
```tsx
|
|
1002
|
-
// Using TanStack Query v5 with TypeScript
|
|
465
|
+
```tsx
|
|
1003
466
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
1004
|
-
import {
|
|
467
|
+
import { useExternalStoreRuntime } from "@assistant-ui/react";
|
|
1005
468
|
|
|
1006
|
-
|
|
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";
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
.
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
536
|
+
### `getExternalStoreMessages`
|
|
1132
537
|
|
|
1133
|
-
|
|
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
|
-
|
|
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="
|
|
1185
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
570
|
+
This API is experimental and may change without notice.
|
|
1226
571
|
</Callout>
|
|
1227
572
|
|
|
1228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
582
|
+
## Common pitfalls
|
|
1266
583
|
|
|
1267
|
-
|
|
584
|
+
**Edit / regenerate / cancel buttons missing.** Each requires its handler:
|
|
1268
585
|
|
|
1269
586
|
```tsx
|
|
1270
|
-
|
|
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, //
|
|
589
|
+
onNew, // required
|
|
590
|
+
setMessages, // branch switching
|
|
591
|
+
onEdit, // edit
|
|
592
|
+
onReload, // regenerate
|
|
593
|
+
onCancel, // cancel
|
|
1295
594
|
});
|
|
1296
595
|
```
|
|
1297
596
|
|
|
1298
|
-
|
|
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
|
-
|
|
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
|
|
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 |
|
|
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> |
|
|
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
|
|
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
|
|
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:
|
|
1553
|
-
|
|
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:
|
|
1590
|
-
|
|
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 |
|
|
1623
|
-
description:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1673
|
-
|
|
1674
|
-
Enable multi-thread support with custom thread management.
|
|
786
|
+
## Related
|
|
1675
787
|
|
|
1676
|
-
<
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
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>
|