@assistant-ui/mcp-docs-server 0.1.32 → 0.1.34

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 (134) hide show
  1. package/.docs/organized/code-examples/waterfall.md +20 -22
  2. package/.docs/organized/code-examples/with-a2a.md +24 -24
  3. package/.docs/organized/code-examples/with-ag-ui.md +30 -25
  4. package/.docs/organized/code-examples/with-ai-sdk-v6.md +11 -9
  5. package/.docs/organized/code-examples/with-artifacts.md +49 -37
  6. package/.docs/organized/code-examples/with-assistant-transport.md +63 -51
  7. package/.docs/organized/code-examples/with-browser-extension.md +22 -10
  8. package/.docs/organized/code-examples/with-chain-of-thought.md +406 -87
  9. package/.docs/organized/code-examples/with-cloud-standalone.md +25 -24
  10. package/.docs/organized/code-examples/with-cloud.md +11 -9
  11. package/.docs/organized/code-examples/with-custom-thread-list.md +14 -12
  12. package/.docs/organized/code-examples/with-elevenlabs-conversational.md +23 -21
  13. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +14 -13
  14. package/.docs/organized/code-examples/with-expo.md +44 -29
  15. package/.docs/organized/code-examples/with-external-store.md +9 -7
  16. package/.docs/organized/code-examples/with-ffmpeg.md +322 -285
  17. package/.docs/organized/code-examples/with-generative-ui.md +1066 -256
  18. package/.docs/organized/code-examples/with-google-adk.md +10 -8
  19. package/.docs/organized/code-examples/with-heat-graph.md +13 -11
  20. package/.docs/organized/code-examples/with-image-generation.md +19 -17
  21. package/.docs/organized/code-examples/with-interactables.md +317 -239
  22. package/.docs/organized/code-examples/with-langchain.md +14 -12
  23. package/.docs/organized/code-examples/with-langgraph.md +91 -83
  24. package/.docs/organized/code-examples/with-livekit.md +24 -22
  25. package/.docs/organized/code-examples/with-mcp.md +18 -12
  26. package/.docs/organized/code-examples/with-opencode.md +66 -66
  27. package/.docs/organized/code-examples/with-react-hook-form.md +19 -17
  28. package/.docs/organized/code-examples/with-react-ink.md +297 -102
  29. package/.docs/organized/code-examples/with-react-router.md +17 -15
  30. package/.docs/organized/code-examples/with-resumable-stream.md +15 -14
  31. package/.docs/organized/code-examples/with-store.md +78 -76
  32. package/.docs/organized/code-examples/with-tanstack.md +13 -14
  33. package/.docs/organized/code-examples/with-tap-runtime.md +26 -24
  34. package/.docs/raw/docs/(docs)/architecture.mdx +94 -42
  35. package/.docs/raw/docs/(docs)/cli.mdx +1 -2
  36. package/.docs/raw/docs/(docs)/installation.mdx +1 -1
  37. package/.docs/raw/docs/(reference)/api-reference/adapters/persistence.mdx +5 -1
  38. package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +15 -15
  39. package/.docs/raw/docs/(reference)/api-reference/external-store/runtime.mdx +14 -1
  40. package/.docs/raw/docs/(reference)/api-reference/generative-ui/index.mdx +18 -0
  41. package/.docs/raw/docs/(reference)/api-reference/generative-ui/rendering.mdx +33 -0
  42. package/.docs/raw/docs/(reference)/api-reference/generative-ui/spec.mdx +45 -0
  43. package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +41 -41
  44. package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +19 -1
  45. package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +122 -122
  46. package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +43 -2
  47. package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +6 -0
  48. package/.docs/raw/docs/(reference)/api-reference/overview.mdx +4 -6
  49. package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +30 -0
  50. package/.docs/raw/docs/(reference)/api-reference/tools/component-tools.mdx +20 -3
  51. package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +8 -8
  52. package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +52 -4
  53. package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +100 -10
  54. package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +7 -59
  55. package/.docs/raw/docs/cloud/ai-sdk.mdx +0 -2
  56. package/.docs/raw/docs/{(docs)/copilots → copilots}/model-context.mdx +34 -26
  57. package/.docs/raw/docs/{(docs)/copilots → copilots}/motivation.mdx +32 -26
  58. package/.docs/raw/docs/guides/chain-of-thought.mdx +7 -9
  59. package/.docs/raw/docs/guides/context-api.mdx +2 -1
  60. package/.docs/raw/docs/guides/index.mdx +3 -12
  61. package/.docs/raw/docs/guides/mentions.mdx +4 -4
  62. package/.docs/raw/docs/guides/slash-commands.mdx +1 -1
  63. package/.docs/raw/docs/guides/suggestions.mdx +1 -1
  64. package/.docs/raw/docs/ink/adapters.mdx +23 -1
  65. package/.docs/raw/docs/ink/hooks.mdx +101 -85
  66. package/.docs/raw/docs/ink/migration.mdx +1 -1
  67. package/.docs/raw/docs/ink/primitives.mdx +2 -2
  68. package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +1 -1
  69. package/.docs/raw/docs/integrations/index.mdx +2 -2
  70. package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +17 -2
  71. package/.docs/raw/docs/migrations/toolkit-tools.mdx +232 -0
  72. package/.docs/raw/docs/primitives/chain-of-thought.mdx +10 -16
  73. package/.docs/raw/docs/primitives/message.mdx +9 -10
  74. package/.docs/raw/docs/react-native/hooks.mdx +62 -79
  75. package/.docs/raw/docs/react-native/migration.mdx +1 -1
  76. package/.docs/raw/docs/react-native/primitives.mdx +2 -2
  77. package/.docs/raw/docs/runtimes/a2a/client-and-hooks.mdx +3 -0
  78. package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +41 -0
  79. package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +122 -1
  80. package/.docs/raw/docs/runtimes/concepts/architecture.mdx +46 -4
  81. package/.docs/raw/docs/runtimes/concepts/threads.mdx +7 -1
  82. package/.docs/raw/docs/runtimes/custom/assistant-transport.mdx +54 -12
  83. package/.docs/raw/docs/runtimes/custom/external-store.mdx +62 -4
  84. package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +30 -7
  85. package/.docs/raw/docs/runtimes/google-adk/hooks.mdx +108 -38
  86. package/.docs/raw/docs/runtimes/langgraph/generative-ui.mdx +2 -2
  87. package/.docs/raw/docs/runtimes/langgraph/streaming.mdx +14 -1
  88. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +64 -50
  89. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +98 -86
  90. package/.docs/raw/docs/tools/backend.mdx +144 -0
  91. package/.docs/raw/docs/tools/defining-tools.mdx +538 -0
  92. package/.docs/raw/docs/tools/dynamic-tools.mdx +110 -0
  93. package/.docs/raw/docs/tools/generative-ui.mdx +214 -0
  94. package/.docs/raw/docs/tools/index.mdx +71 -0
  95. package/.docs/raw/docs/{guides → tools}/interactables.mdx +1 -1
  96. package/.docs/raw/docs/{integrations/tools → tools}/mcp.mdx +145 -50
  97. package/.docs/raw/docs/{guides → tools}/multi-agent.mdx +15 -17
  98. package/.docs/raw/docs/tools/tool-ui.mdx +967 -0
  99. package/.docs/raw/docs/{integrations/tools/react-mcp.mdx → tools/user-managed-mcp.mdx} +7 -7
  100. package/.docs/raw/docs/ui/directive-text.mdx +3 -3
  101. package/.docs/raw/docs/ui/mcp-config.mdx +4 -4
  102. package/.docs/raw/docs/ui/mermaid.mdx +16 -9
  103. package/.docs/raw/docs/ui/part-grouping.mdx +84 -50
  104. package/.docs/raw/docs/ui/reasoning.mdx +4 -5
  105. package/.docs/raw/docs/ui/tool-fallback.mdx +16 -0
  106. package/.docs/raw/docs/ui/tool-group.mdx +5 -6
  107. package/.docs/raw/docs/utilities/heat-graph.mdx +1 -1
  108. package/.docs/raw/docs/utilities/react-o11y.mdx +2 -2
  109. package/dist/constants.js.map +1 -1
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js.map +1 -1
  112. package/dist/prepare-docs/code-examples.d.ts.map +1 -1
  113. package/dist/prepare-docs/code-examples.js.map +1 -1
  114. package/dist/prepare-docs/copy-raw.d.ts.map +1 -1
  115. package/dist/prepare-docs/copy-raw.js.map +1 -1
  116. package/dist/prepare-docs/prepare.js.map +1 -1
  117. package/dist/stdio.js.map +1 -1
  118. package/dist/tools/docs.js.map +1 -1
  119. package/dist/tools/examples.js.map +1 -1
  120. package/dist/tools/tests/test-setup.js.map +1 -1
  121. package/dist/utils/mdx.js.map +1 -1
  122. package/dist/utils/paths.d.ts.map +1 -1
  123. package/dist/utils/paths.js.map +1 -1
  124. package/package.json +5 -5
  125. package/.docs/organized/code-examples/with-parent-id-grouping.md +0 -596
  126. package/.docs/raw/docs/(docs)/copilots/make-assistant-tool-ui.mdx +0 -151
  127. package/.docs/raw/docs/(docs)/copilots/make-assistant-tool.mdx +0 -230
  128. package/.docs/raw/docs/guides/generative-ui.mdx +0 -142
  129. package/.docs/raw/docs/guides/tool-ui.mdx +0 -858
  130. package/.docs/raw/docs/guides/tools.mdx +0 -736
  131. /package/.docs/raw/docs/{(docs)/copilots → copilots}/assistant-frame.mdx +0 -0
  132. /package/.docs/raw/docs/{(docs)/copilots → copilots}/make-assistant-visible.mdx +0 -0
  133. /package/.docs/raw/docs/{(docs)/copilots → copilots}/use-assistant-instructions.mdx +0 -0
  134. /package/.docs/raw/docs/{guides → tools}/mcp-apps.mdx +0 -0
@@ -3,46 +3,137 @@
3
3
  ## app/api/chat/route.ts
4
4
 
5
5
  ```typescript
6
- import { openai } from "@ai-sdk/openai";
6
+ import { createOpenAI } from "@ai-sdk/openai";
7
7
  import {
8
8
  streamText,
9
9
  convertToModelMessages,
10
10
  tool,
11
11
  stepCountIs,
12
12
  zodSchema,
13
+ createUIMessageStream,
14
+ JsonToSseTransformStream,
13
15
  } from "ai";
14
- import type { UIMessage } from "ai";
15
- import { frontendTools } from "@assistant-ui/react-ai-sdk";
16
+ import type { UIMessage, UIMessageStreamWriter } from "ai";
17
+ import { AISDKToolkit } from "@assistant-ui/react-ai-sdk";
16
18
  import { z } from "zod";
19
+ import toolkit from "../../toolkit";
17
20
 
18
21
  export const maxDuration = 30;
19
22
 
23
+ const aiToolkit = new AISDKToolkit({ toolkit });
24
+
25
+ type SearchResult = { id: string; url: string; title: string };
26
+
27
+ const searchSources = (query: string): SearchResult[] => {
28
+ const q = query.toLowerCase();
29
+ if (q.includes("fibonacci")) {
30
+ return [
31
+ {
32
+ id: "src-fib-1",
33
+ url: "https://en.wikipedia.org/wiki/Fibonacci_number",
34
+ title: "Fibonacci number (Wikipedia)",
35
+ },
36
+ {
37
+ id: "src-fib-2",
38
+ url: "https://oeis.org/A000045",
39
+ title: "OEIS A000045: Fibonacci numbers",
40
+ },
41
+ ];
42
+ }
43
+ if (
44
+ q.includes("renewable") ||
45
+ q.includes("energy") ||
46
+ q.includes("climate")
47
+ ) {
48
+ return [
49
+ {
50
+ id: "src-iea",
51
+ url: "https://www.iea.org/reports/renewables-2024",
52
+ title: "IEA Renewables 2024",
53
+ },
54
+ {
55
+ id: "src-irena",
56
+ url: "https://www.irena.org/Publications",
57
+ title: "IRENA Publications",
58
+ },
59
+ ];
60
+ }
61
+ return [
62
+ {
63
+ id: "src-wiki",
64
+ url: `https://en.wikipedia.org/wiki/Special:Search?search=${encodeURIComponent(query)}`,
65
+ title: "Wikipedia search results",
66
+ },
67
+ {
68
+ id: "src-web",
69
+ url: `https://duckduckgo.com/?q=${encodeURIComponent(query)}`,
70
+ title: `Web results for: ${query}`,
71
+ },
72
+ ];
73
+ };
74
+
20
75
  export async function POST(req: Request) {
21
76
  const {
22
77
  messages,
23
78
  tools,
24
79
  }: { messages: UIMessage[]; tools: Record<string, any> } = await req.json();
25
80
 
81
+ const uiStream = createUIMessageStream({
82
+ execute: async ({ writer }) => {
83
+ if (!process.env.OPENAI_API_KEY) {
84
+ await streamFallback(writer, messages);
85
+ return;
86
+ }
87
+ await streamModel(writer, messages, tools);
88
+ },
89
+ });
90
+
91
+ return new Response(
92
+ uiStream
93
+ .pipeThrough(new JsonToSseTransformStream())
94
+ .pipeThrough(new TextEncoderStream()),
95
+ {
96
+ headers: {
97
+ "Content-Type": "text/event-stream",
98
+ "Cache-Control": "no-cache",
99
+ Connection: "keep-alive",
100
+ },
101
+ },
102
+ );
103
+ }
104
+
105
+ async function streamModel(
106
+ writer: UIMessageStreamWriter,
107
+ messages: UIMessage[],
108
+ frontendToolDefs: Record<string, any>,
109
+ ) {
110
+ const toolNameByCall = new Map<string, string>();
111
+ const openai = createOpenAI({
112
+ apiKey: process.env.OPENAI_API_KEY!,
113
+ ...(process.env.OPENAI_BASE_URL && {
114
+ baseURL: process.env.OPENAI_BASE_URL,
115
+ }),
116
+ });
117
+
118
+ const toolkitTools = await aiToolkit.tools({ frontend: frontendToolDefs });
119
+
26
120
  const result = streamText({
27
- // Use a reasoning model to showcase chain of thought
121
+ // Reasoning model so the chain-of-thought group has real content.
28
122
  model: openai("gpt-5.4-mini"),
29
123
  messages: await convertToModelMessages(messages),
30
124
  stopWhen: stepCountIs(10),
31
125
  tools: {
32
- // Frontend tools registered via makeAssistantTool (e.g. execute_js)
33
- ...frontendTools(tools),
34
-
35
- // Backend-only tools
126
+ ...toolkitTools,
36
127
  get_current_weather: tool({
37
128
  description: "Get the current weather for a city",
38
- inputSchema: zodSchema(
39
- z.object({
40
- city: z.string(),
41
- }),
42
- ),
43
- execute: async ({ city }) => {
44
- return `The weather in ${city} is sunny, 72°F`;
45
- },
129
+ inputSchema: zodSchema(z.object({ city: z.string() })),
130
+ execute: async ({ city }) => `The weather in ${city} is sunny, 72°F`,
131
+ }),
132
+ search_web: tool({
133
+ description:
134
+ "Search the web for citations on a topic. Returns a list of source URLs and titles the assistant should consult.",
135
+ inputSchema: zodSchema(z.object({ query: z.string() })),
136
+ execute: async ({ query }) => ({ sources: searchSources(query) }),
46
137
  }),
47
138
  },
48
139
  providerOptions: {
@@ -53,9 +144,106 @@ export async function POST(req: Request) {
53
144
  },
54
145
  });
55
146
 
56
- return result.toUIMessageStreamResponse({ sendReasoning: true });
147
+ // Forward model chunks, then synthesize `source-url` parts from each
148
+ // `search_web` tool output so the UI renders them as `source` parts. OpenAI
149
+ // does not natively emit `source-url` chunks without web search, so the route
150
+ // produces them deterministically from the tool result.
151
+ for await (const chunk of result.toUIMessageStream({ sendReasoning: true })) {
152
+ writer.write(chunk);
153
+
154
+ if (chunk.type === "tool-input-available") {
155
+ toolNameByCall.set(chunk.toolCallId, chunk.toolName);
156
+ }
157
+ if (
158
+ chunk.type === "tool-output-available" &&
159
+ toolNameByCall.get(chunk.toolCallId) === "search_web"
160
+ ) {
161
+ const output = chunk.output as { sources?: SearchResult[] };
162
+ for (const source of output.sources ?? []) {
163
+ writer.write({
164
+ type: "source-url",
165
+ sourceId: `${source.id}-${crypto.randomUUID()}`,
166
+ url: source.url,
167
+ title: source.title,
168
+ });
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ async function streamFallback(
175
+ writer: UIMessageStreamWriter,
176
+ messages: UIMessage[],
177
+ ) {
178
+ const lastUserText =
179
+ messages
180
+ .filter((m) => m.role === "user")
181
+ .at(-1)
182
+ ?.parts.flatMap((p) => (p.type === "text" ? [p.text] : []))
183
+ .join(" ") ?? "your question";
184
+
185
+ const messageId = `msg-${crypto.randomUUID()}`;
186
+ const reasoningId = `r-${crypto.randomUUID()}`;
187
+ const textId = `t-${crypto.randomUUID()}`;
188
+ const toolCallId = `call-${crypto.randomUUID()}`;
189
+ const sources = searchSources(lastUserText);
190
+
191
+ writer.write({ type: "start", messageId });
192
+ writer.write({ type: "start-step" });
193
+
194
+ writer.write({ type: "reasoning-start", id: reasoningId });
195
+ const reasoning =
196
+ "OPENAI_API_KEY is not set, so this response is generated deterministically by the route. " +
197
+ "A real run emits the same chunk shapes from the model: a reasoning step, " +
198
+ "a search_web tool call, source-url parts derived from the tool output, " +
199
+ "and a final answer that summarises the sources.";
200
+ for (const word of reasoning.match(/\S+\s*|\s+/g) ?? [reasoning]) {
201
+ writer.write({ type: "reasoning-delta", id: reasoningId, delta: word });
202
+ await sleep(15);
203
+ }
204
+ writer.write({ type: "reasoning-end", id: reasoningId });
205
+
206
+ writer.write({
207
+ type: "tool-input-available",
208
+ toolCallId,
209
+ toolName: "search_web",
210
+ input: { query: lastUserText },
211
+ });
212
+ await sleep(150);
213
+ writer.write({
214
+ type: "tool-output-available",
215
+ toolCallId,
216
+ output: { sources },
217
+ });
218
+
219
+ for (const source of sources) {
220
+ writer.write({
221
+ type: "source-url",
222
+ sourceId: `${source.id}-${crypto.randomUUID()}`,
223
+ url: source.url,
224
+ title: source.title,
225
+ });
226
+ }
227
+
228
+ writer.write({ type: "text-start", id: textId });
229
+ const answer =
230
+ `\n\nMock answer for: "${lastUserText}". ` +
231
+ "The badges above are structured `source` message parts emitted as " +
232
+ "`source-url` chunks, not URLs scraped from text. Set `OPENAI_API_KEY` " +
233
+ "to switch to a real model run.";
234
+ for (const word of answer.match(/\S+\s*|\s+/g) ?? [answer]) {
235
+ writer.write({ type: "text-delta", id: textId, delta: word });
236
+ await sleep(15);
237
+ }
238
+ writer.write({ type: "text-end", id: textId });
239
+
240
+ writer.write({ type: "finish-step" });
241
+ writer.write({ type: "finish" });
57
242
  }
58
243
 
244
+ const sleep = (ms: number) =>
245
+ new Promise<void>((resolve) => setTimeout(resolve, ms));
246
+
59
247
  ```
60
248
 
61
249
  ## app/globals.css
@@ -194,7 +382,8 @@ import "./globals.css";
194
382
 
195
383
  export const metadata: Metadata = {
196
384
  title: "Chain of Thought Example",
197
- description: "Example using MessagePrimitive.GroupedParts with AI SDK",
385
+ description:
386
+ "Reasoning, tool calls, and source citations rendered as grouped message parts.",
198
387
  };
199
388
 
200
389
  export default function RootLayout({
@@ -218,10 +407,12 @@ export default function RootLayout({
218
407
 
219
408
  import { type FC, type PropsWithChildren, useState } from "react";
220
409
  import { MarkdownText } from "@/components/assistant-ui/markdown-text";
410
+ import { Sources } from "@/components/assistant-ui/sources";
221
411
  import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
222
412
  import { Button } from "@/components/ui/button";
223
413
  import {
224
414
  ComposerPrimitive,
415
+ groupPartByType,
225
416
  MessagePrimitive,
226
417
  SuggestionPrimitive,
227
418
  ThreadPrimitive,
@@ -238,7 +429,7 @@ import {
238
429
  export const MyThread: FC = () => {
239
430
  return (
240
431
  <ThreadPrimitive.Root
241
- className="flex h-full flex-col bg-background"
432
+ className="bg-background flex h-full flex-col"
242
433
  style={{ ["--thread-max-width" as string]: "44rem" }}
243
434
  >
244
435
  <ThreadPrimitive.Viewport className="flex flex-1 flex-col overflow-y-scroll scroll-smooth px-4 pt-8">
@@ -278,7 +469,9 @@ const ThreadWelcome: FC = () => {
278
469
  return (
279
470
  <div className="mx-auto flex w-full max-w-(--thread-max-width) grow flex-col justify-center gap-6">
280
471
  <div>
281
- <h1 className="font-semibold text-2xl">What should we calculate?</h1>
472
+ <h1 className="text-2xl font-semibold">
473
+ What should we think through?
474
+ </h1>
282
475
  </div>
283
476
  <div className="flex flex-wrap gap-2">
284
477
  <ThreadPrimitive.Suggestions>
@@ -295,7 +488,7 @@ const ThreadSuggestionItem: FC = () => {
295
488
  <Button
296
489
  type="button"
297
490
  variant="ghost"
298
- className="h-auto w-full flex-col items-start justify-start gap-1 rounded-2xl border bg-background px-4 py-3 text-start text-sm hover:bg-muted sm:w-[calc(50%-0.25rem)]"
491
+ className="bg-background hover:bg-muted h-auto w-full flex-col items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-start text-sm sm:w-[calc(50%-0.25rem)]"
299
492
  >
300
493
  <SuggestionPrimitive.Title className="font-medium" />
301
494
  <SuggestionPrimitive.Description className="text-muted-foreground empty:hidden" />
@@ -308,7 +501,7 @@ const UserMessage: FC = () => {
308
501
  return (
309
502
  <MessagePrimitive.Root className="mx-auto w-full max-w-(--thread-max-width) py-3">
310
503
  <div className="flex justify-end">
311
- <div className="max-w-[80%] rounded-2xl bg-primary px-4 py-2 text-primary-foreground">
504
+ <div className="bg-primary text-primary-foreground max-w-[80%] rounded-2xl px-4 py-2">
312
505
  <MessagePrimitive.Parts>
313
506
  {({ part }) => {
314
507
  if (part.type === "text") return <Text {...part} />;
@@ -326,13 +519,11 @@ const AssistantMessage: FC = () => {
326
519
  <MessagePrimitive.Root className="mx-auto w-full max-w-(--thread-max-width) py-3">
327
520
  <div className="flex flex-col gap-2 px-2 leading-relaxed">
328
521
  <MessagePrimitive.GroupedParts
329
- groupBy={(part) => {
330
- if (part.type === "reasoning")
331
- return ["group-chainOfThought", "group-reasoning"];
332
- if (part.type === "tool-call")
333
- return ["group-chainOfThought", "group-tool"];
334
- return null;
335
- }}
522
+ groupBy={groupPartByType({
523
+ reasoning: ["group-chainOfThought", "group-reasoning"],
524
+ "tool-call": ["group-chainOfThought", "group-tool"],
525
+ source: ["group-sources"],
526
+ })}
336
527
  >
337
528
  {({ part, children }) => {
338
529
  switch (part.type) {
@@ -344,12 +535,16 @@ const AssistantMessage: FC = () => {
344
535
  return (
345
536
  <PartLayout label="Taking action">{children}</PartLayout>
346
537
  );
538
+ case "group-sources":
539
+ return <SourcesLayout>{children}</SourcesLayout>;
347
540
  case "text":
348
541
  return <MarkdownText />;
349
542
  case "reasoning":
350
543
  return <Reasoning {...part} />;
351
544
  case "tool-call":
352
545
  return <ToolCall {...part} />;
546
+ case "source":
547
+ return <Sources {...part} />;
353
548
  default:
354
549
  return null;
355
550
  }
@@ -367,7 +562,7 @@ const ChainOfThoughtGroup: FC<PropsWithChildren> = ({ children }) => {
367
562
  <div className="my-2 rounded-lg border">
368
563
  <button
369
564
  type="button"
370
- className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 font-medium text-sm hover:bg-muted/50"
565
+ className="hover:bg-muted/50 flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm font-medium"
371
566
  onClick={() => setOpen((o) => !o)}
372
567
  >
373
568
  {open ? (
@@ -382,6 +577,15 @@ const ChainOfThoughtGroup: FC<PropsWithChildren> = ({ children }) => {
382
577
  );
383
578
  };
384
579
 
580
+ const SourcesLayout: FC<PropsWithChildren> = ({ children }) => {
581
+ return (
582
+ <div className="flex flex-wrap items-center gap-1.5">
583
+ <span className="text-muted-foreground mr-1 text-xs">Sources</span>
584
+ {children}
585
+ </div>
586
+ );
587
+ };
588
+
385
589
  const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
386
590
  children,
387
591
  label,
@@ -392,7 +596,7 @@ const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
392
596
  <div className="border-t">
393
597
  <button
394
598
  type="button"
395
- className="flex w-full cursor-pointer items-center gap-2 px-4 py-1.5 text-muted-foreground text-xs hover:bg-muted/50"
599
+ className="text-muted-foreground hover:bg-muted/50 flex w-full cursor-pointer items-center gap-2 px-4 py-1.5 text-xs"
396
600
  onClick={() => setOpen((o) => !o)}
397
601
  >
398
602
  {open ? (
@@ -409,7 +613,7 @@ const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
409
613
 
410
614
  const Reasoning: FC<{ text: string }> = ({ text }) => {
411
615
  return (
412
- <p className="whitespace-pre-wrap px-4 py-2 text-muted-foreground text-sm italic">
616
+ <p className="text-muted-foreground px-4 py-2 text-sm whitespace-pre-wrap italic">
413
617
  {text}
414
618
  </p>
415
619
  );
@@ -436,10 +640,10 @@ const Composer: FC = () => {
436
640
  const isRunning = useThread((state) => state.isRunning);
437
641
 
438
642
  return (
439
- <ComposerPrimitive.Root className="flex w-full flex-col rounded-2xl border border-input bg-background px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20">
643
+ <ComposerPrimitive.Root className="border-input bg-background has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-ring/20 flex w-full flex-col rounded-2xl border px-1 pt-2 transition-shadow outline-none has-[textarea:focus-visible]:ring-2">
440
644
  <ComposerPrimitive.Input
441
645
  placeholder="Send a message..."
442
- className="mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground"
646
+ className="placeholder:text-muted-foreground mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none"
443
647
  rows={1}
444
648
  autoFocus
445
649
  />
@@ -482,63 +686,30 @@ const Composer: FC = () => {
482
686
 
483
687
  import {
484
688
  AssistantRuntimeProvider,
485
- makeAssistantTool,
689
+ Tools,
486
690
  useAui,
487
691
  AuiProvider,
488
692
  Suggestions,
489
693
  } from "@assistant-ui/react";
490
694
  import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
491
- import { z } from "zod";
492
695
  import { MyThread } from "./MyThread";
493
696
  import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
494
-
495
- const ExecuteJsTool = makeAssistantTool({
496
- toolName: "execute_js",
497
- description: "Execute JavaScript code and return the result",
498
- parameters: z.object({
499
- code: z.string().describe("The JavaScript code to execute"),
500
- }),
501
- execute: async ({ code }) => {
502
- try {
503
- // biome-ignore lint/security/noGlobalEval: example code
504
- const result = eval(code);
505
- return { success: true, result: String(result) };
506
- } catch (e) {
507
- return { success: false, error: String(e) };
508
- }
509
- },
510
- render: ({ args, result, status }) => (
511
- <div className="my-2 rounded-lg border bg-muted/30 p-4 text-sm">
512
- <p className="mb-1 font-semibold">execute_js</p>
513
- <pre className="whitespace-pre-wrap rounded bg-background p-2 font-mono text-xs">
514
- {args.code}
515
- </pre>
516
- {status.type !== "running" && result && (
517
- <div className="mt-2 border-t pt-2">
518
- <p className="font-semibold text-muted-foreground">
519
- {result.success ? "Result:" : "Error:"}
520
- </p>
521
- <pre className="whitespace-pre-wrap font-mono text-xs">
522
- {result.success ? result.result : result.error}
523
- </pre>
524
- </div>
525
- )}
526
- </div>
527
- ),
528
- });
697
+ import toolkit from "./toolkit";
529
698
 
530
699
  function MyThreadWithSuggestions() {
531
700
  const aui = useAui({
532
701
  suggestions: Suggestions([
533
702
  {
534
703
  title: "Calculate Fibonacci(20)",
535
- label: "step by step with code",
536
- prompt: "Calculate the 20th Fibonacci number using JavaScript.",
704
+ label: "with chain of thought",
705
+ prompt:
706
+ "Calculate the 20th Fibonacci number using JavaScript and show your reasoning.",
537
707
  },
538
708
  {
539
- title: "What is 2^16?",
540
- label: "show your reasoning",
541
- prompt: "What is 2 to the power of 16? Show your work step by step.",
709
+ title: "Research with citations",
710
+ label: "show sources",
711
+ prompt:
712
+ "Research recent developments in renewable energy and cite your sources.",
542
713
  },
543
714
  ]),
544
715
  });
@@ -553,10 +724,12 @@ export default function Home() {
553
724
  const runtime = useChatRuntime({
554
725
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
555
726
  });
727
+ const aui = useAui({
728
+ tools: Tools({ toolkit }),
729
+ });
556
730
 
557
731
  return (
558
- <AssistantRuntimeProvider runtime={runtime}>
559
- <ExecuteJsTool />
732
+ <AssistantRuntimeProvider aui={aui} runtime={runtime}>
560
733
  <div className="h-full">
561
734
  <MyThreadWithSuggestions />
562
735
  </div>
@@ -566,6 +739,59 @@ export default function Home() {
566
739
 
567
740
  ```
568
741
 
742
+ ## app/toolkit.tsx
743
+
744
+ ```tsx
745
+ "use generative";
746
+
747
+ import { defineToolkit } from "@assistant-ui/react";
748
+ import { z } from "zod";
749
+
750
+ type ExecuteJsResult =
751
+ | { success: true; result: string }
752
+ | { success: false; error: string };
753
+
754
+ export default defineToolkit({
755
+ execute_js: {
756
+ description: "Execute JavaScript code and return the result",
757
+ parameters: z.object({
758
+ code: z.string().describe("The JavaScript code to execute"),
759
+ }),
760
+ execute: async ({ code }) => {
761
+ "use client";
762
+ try {
763
+ const result = eval(code);
764
+ return { success: true, result: String(result) };
765
+ } catch (e) {
766
+ return { success: false, error: String(e) };
767
+ }
768
+ },
769
+ render: ({ args, result, status }) => {
770
+ const output = result as ExecuteJsResult | undefined;
771
+ return (
772
+ <div className="bg-muted/30 my-2 rounded-lg border p-4 text-sm">
773
+ <p className="mb-1 font-semibold">execute_js</p>
774
+ <pre className="bg-background rounded p-2 font-mono text-xs whitespace-pre-wrap">
775
+ {args.code}
776
+ </pre>
777
+ {status.type !== "running" && output && (
778
+ <div className="mt-2 border-t pt-2">
779
+ <p className="text-muted-foreground font-semibold">
780
+ {output.success ? "Result:" : "Error:"}
781
+ </p>
782
+ <pre className="font-mono text-xs whitespace-pre-wrap">
783
+ {output.success ? output.result : output.error}
784
+ </pre>
785
+ </div>
786
+ )}
787
+ </div>
788
+ );
789
+ },
790
+ },
791
+ });
792
+
793
+ ```
794
+
569
795
  ## components.json
570
796
 
571
797
  ```json
@@ -599,12 +825,13 @@ export default function Home() {
599
825
  ## next.config.js
600
826
 
601
827
  ```javascript
828
+ import { withAui } from "@assistant-ui/next";
602
829
  /** @type {import('next').NextConfig} */
603
830
  const nextConfig = {
604
831
  transpilePackages: ["@assistant-ui/react", "@assistant-ui/react-ai-sdk"],
605
832
  };
606
833
 
607
- export default nextConfig;
834
+ export default withAui(nextConfig);
608
835
 
609
836
  ```
610
837
 
@@ -622,26 +849,27 @@ export default nextConfig;
622
849
  "start": "next start"
623
850
  },
624
851
  "dependencies": {
625
- "@ai-sdk/openai": "^3.0.64",
852
+ "@ai-sdk/openai": "^3.0.68",
626
853
  "@assistant-ui/react": "workspace:*",
627
854
  "@assistant-ui/react-ai-sdk": "workspace:*",
628
855
  "@assistant-ui/react-markdown": "workspace:*",
629
856
  "@assistant-ui/ui": "workspace:*",
630
- "ai": "^6.0.188",
857
+ "ai": "^6.0.197",
631
858
  "class-variance-authority": "^0.7.1",
632
859
  "clsx": "^2.1.1",
633
- "lucide-react": "^1.16.0",
634
- "next": "^16.2.6",
635
- "react": "^19.2.6",
636
- "react-dom": "^19.2.6",
860
+ "lucide-react": "^1.17.0",
861
+ "next": "^16.2.7",
862
+ "react": "^19.2.7",
863
+ "react-dom": "^19.2.7",
637
864
  "tailwind-merge": "^3.6.0",
638
865
  "zod": "^4.4.3"
639
866
  },
640
867
  "devDependencies": {
868
+ "@assistant-ui/next": "workspace:*",
641
869
  "@assistant-ui/x-buildutils": "workspace:*",
642
870
  "@tailwindcss/postcss": "^4.3.0",
643
- "@types/node": "^25.9.1",
644
- "@types/react": "^19.2.15",
871
+ "@types/node": "^25.9.2",
872
+ "@types/react": "^19.2.17",
645
873
  "@types/react-dom": "^19.2.3",
646
874
  "postcss": "^8.5.15",
647
875
  "tailwindcss": "^4.3.0",
@@ -652,6 +880,97 @@ export default nextConfig;
652
880
 
653
881
  ```
654
882
 
883
+ ## README.md
884
+
885
+ ```markdown
886
+ # Chain of Thought Example
887
+
888
+ A real chat flow that renders reasoning, tool calls, and source citations as grouped message parts. Demonstrates `MessagePrimitive.GroupedParts` with a nested `groupBy` path, plus structured `source` parts emitted from a search tool.
889
+
890
+ ## Quick start
891
+
892
+ ### Using the CLI
893
+
894
+ ```bash
895
+ npx assistant-ui@latest create my-app --example with-chain-of-thought
896
+ cd my-app
897
+ ```
898
+
899
+ ### Environment variables
900
+
901
+ `OPENAI_API_KEY` is optional. With the key set the route runs a real reasoning model and a `search_web` tool; without it the route streams a deterministic mock response with the same chunk shapes, so the UI is fully demonstrable offline.
902
+
903
+ Create `.env.local`:
904
+
905
+ ```
906
+ OPENAI_API_KEY=sk-...
907
+ ```
908
+
909
+ ### Run
910
+
911
+ ```bash
912
+ npm install
913
+ npm run dev
914
+ ```
915
+
916
+ Open [http://localhost:3000](http://localhost:3000).
917
+
918
+ ## What the example demonstrates
919
+
920
+ | Concern | Where it lives |
921
+ |---|---|
922
+ | Real chat flow with `useChatRuntime` | `app/page.tsx` |
923
+ | Reasoning, tool calls, source citations streamed as message parts | `app/api/chat/route.ts` |
924
+ | Nested adjacency-based grouping with `MessagePrimitive.GroupedParts` | `app/MyThread.tsx` |
925
+ | `Sources` component rendering `source` parts | `app/MyThread.tsx` |
926
+ | Fallback path that runs without an API key | `app/api/chat/route.ts` (`streamFallback`) |
927
+
928
+ ## How parts are grouped
929
+
930
+ The thread groups three part types under one collapsible "Thinking" section, then renders source badges in a row below it. The grouping is configured on `MessagePrimitive.GroupedParts`:
931
+
932
+ ```tsx
933
+ <MessagePrimitive.GroupedParts
934
+ groupBy={(part) => {
935
+ if (part.type === "reasoning") return ["group-chainOfThought", "group-reasoning"];
936
+ if (part.type === "tool-call") return ["group-chainOfThought", "group-tool"];
937
+ if (part.type === "source") return ["group-chainOfThought", "group-sources"];
938
+ return null;
939
+ }}
940
+ >
941
+ ```
942
+
943
+ Each part is mapped to a nested group key path. Adjacent parts that share a path are coalesced and rendered through the matching `group-*` case in the render function. `null` leaves a part ungrouped.
944
+
945
+ This is the canonical pattern. For non-adjacent clustering (parts that share an identifier but appear at different positions in the message), see [`/docs/ui/part-grouping`](https://www.assistant-ui.com/docs/ui/part-grouping).
946
+
947
+ ## How sources are emitted
948
+
949
+ The route emits structured `source-url` chunks; `@assistant-ui/react-ai-sdk` converts them to `source` message parts that the `Sources` component renders as badges. They are not URLs scraped from assistant text.
950
+
951
+ ```ts
952
+ writer.write({
953
+ type: "source-url",
954
+ sourceId: "src-1",
955
+ url: "https://example.com/report",
956
+ title: "Example report",
957
+ });
958
+ ```
959
+
960
+ In the real path the `search_web` tool returns sources in its output, and the route writes one `source-url` chunk per result immediately after the `tool-output-available` chunk. In the fallback path the same shape is written directly. The UI rendering is identical in both modes.
961
+
962
+ ## Why these features live together
963
+
964
+ Chain-of-thought, tool calls, and citation sources are the parts of an assistant turn a user typically wants to inspect together. Grouping them under one collapsible keeps the message tidy by default and reveals the full reasoning trail on demand.
965
+
966
+ ## Related documentation
967
+
968
+ - [Message Part Grouping](https://www.assistant-ui.com/docs/ui/part-grouping)
969
+ - [Sources component](https://www.assistant-ui.com/docs/ui/sources)
970
+ - [AI SDK v6 runtime](https://www.assistant-ui.com/docs/runtimes/ai-sdk/v6)
971
+
972
+ ```
973
+
655
974
  ## tsconfig.json
656
975
 
657
976
  ```json