@assistant-ui/mcp-docs-server 0.1.14 → 0.1.16

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 (57) hide show
  1. package/.docs/organized/code-examples/store-example.md +628 -0
  2. package/.docs/organized/code-examples/with-ag-ui.md +792 -178
  3. package/.docs/organized/code-examples/with-ai-sdk-v5.md +762 -209
  4. package/.docs/organized/code-examples/with-assistant-transport.md +707 -254
  5. package/.docs/organized/code-examples/with-cloud.md +848 -202
  6. package/.docs/organized/code-examples/with-custom-thread-list.md +1855 -0
  7. package/.docs/organized/code-examples/with-external-store.md +788 -172
  8. package/.docs/organized/code-examples/with-ffmpeg.md +796 -196
  9. package/.docs/organized/code-examples/with-langgraph.md +864 -230
  10. package/.docs/organized/code-examples/with-parent-id-grouping.md +785 -255
  11. package/.docs/organized/code-examples/with-react-hook-form.md +804 -226
  12. package/.docs/organized/code-examples/with-tanstack.md +1574 -0
  13. package/.docs/raw/blog/2024-07-29-hello/index.mdx +2 -3
  14. package/.docs/raw/docs/api-reference/overview.mdx +6 -6
  15. package/.docs/raw/docs/api-reference/primitives/ActionBar.mdx +85 -4
  16. package/.docs/raw/docs/api-reference/primitives/AssistantIf.mdx +200 -0
  17. package/.docs/raw/docs/api-reference/primitives/Composer.mdx +0 -20
  18. package/.docs/raw/docs/api-reference/primitives/Message.mdx +0 -45
  19. package/.docs/raw/docs/api-reference/primitives/Thread.mdx +0 -50
  20. package/.docs/raw/docs/cli.mdx +396 -0
  21. package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +2 -3
  22. package/.docs/raw/docs/cloud/persistence/langgraph.mdx +2 -3
  23. package/.docs/raw/docs/devtools.mdx +2 -3
  24. package/.docs/raw/docs/getting-started.mdx +37 -1109
  25. package/.docs/raw/docs/guides/Attachments.mdx +3 -25
  26. package/.docs/raw/docs/guides/Branching.mdx +1 -1
  27. package/.docs/raw/docs/guides/Speech.mdx +1 -1
  28. package/.docs/raw/docs/guides/ToolUI.mdx +1 -1
  29. package/.docs/raw/docs/legacy/styled/AssistantModal.mdx +2 -3
  30. package/.docs/raw/docs/legacy/styled/Decomposition.mdx +6 -5
  31. package/.docs/raw/docs/legacy/styled/Markdown.mdx +2 -3
  32. package/.docs/raw/docs/legacy/styled/Thread.mdx +2 -3
  33. package/.docs/raw/docs/react-compatibility.mdx +2 -5
  34. package/.docs/raw/docs/runtimes/ai-sdk/use-chat.mdx +3 -4
  35. package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +3 -6
  36. package/.docs/raw/docs/runtimes/assistant-transport.mdx +891 -0
  37. package/.docs/raw/docs/runtimes/custom/external-store.mdx +2 -3
  38. package/.docs/raw/docs/runtimes/custom/local.mdx +11 -41
  39. package/.docs/raw/docs/runtimes/data-stream.mdx +15 -11
  40. package/.docs/raw/docs/runtimes/langgraph/index.mdx +4 -4
  41. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +1 -1
  42. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +2 -3
  43. package/.docs/raw/docs/runtimes/langserve.mdx +2 -3
  44. package/.docs/raw/docs/runtimes/mastra/full-stack-integration.mdx +2 -3
  45. package/.docs/raw/docs/runtimes/mastra/separate-server-integration.mdx +2 -3
  46. package/.docs/raw/docs/ui/AssistantModal.mdx +3 -25
  47. package/.docs/raw/docs/ui/AssistantSidebar.mdx +2 -24
  48. package/.docs/raw/docs/ui/Attachment.mdx +3 -25
  49. package/.docs/raw/docs/ui/Markdown.mdx +2 -24
  50. package/.docs/raw/docs/ui/Mermaid.mdx +2 -24
  51. package/.docs/raw/docs/ui/Reasoning.mdx +2 -24
  52. package/.docs/raw/docs/ui/Scrollbar.mdx +4 -6
  53. package/.docs/raw/docs/ui/SyntaxHighlighting.mdx +3 -47
  54. package/.docs/raw/docs/ui/Thread.mdx +38 -53
  55. package/.docs/raw/docs/ui/ThreadList.mdx +4 -47
  56. package/.docs/raw/docs/ui/ToolFallback.mdx +2 -24
  57. package/package.json +15 -8
@@ -0,0 +1,891 @@
1
+ ---
2
+ title: Assistant Transport
3
+ ---
4
+
5
+ import { Callout } from "fumadocs-ui/components/callout";
6
+ import { Tab, Tabs } from "fumadocs-ui/components/tabs";
7
+
8
+ If you've built an agent as a Python or TypeScript script and want to add a UI to it, you need to solve two problems: streaming updates to the frontend and integrating with the UI framework. Assistant Transport handles both.
9
+
10
+ Assistant Transport streams your agent's complete state to the frontend in real-time. Unlike traditional approaches that only stream predefined message types (like text or tool calls), it streams your entire agent state—whatever structure your agent uses internally.
11
+
12
+ It consists of:
13
+
14
+ - **State streaming**: Efficiently streams updates to your agent state (supports any JSON object)
15
+ - **UI integration**: Converts your agent's state into assistant-ui components that render in the browser
16
+ - **Command handling**: Sends user actions (messages, tool executions, custom commands) back to your agent
17
+
18
+ ## When to Use Assistant Transport
19
+
20
+ Use Assistant Transport when:
21
+
22
+ - You don't have a streaming protocol yet and need one
23
+ - You want your agent's native state to be directly accessible in the frontend
24
+ - You're building a custom agent framework or one without a streaming protocol (e.g. OSS LangGraph)
25
+
26
+ ## Mental Model
27
+
28
+ ```mermaid
29
+ graph LR
30
+ Frontend -->|Commands| Agent[Agent Server]
31
+ Agent -->|State Snapshots| Frontend
32
+ ```
33
+
34
+ The frontend receives state snapshots and converts them to React components. The goal is to have the UI be a stateless view on top of the agent framework state.
35
+
36
+ The agent server receives commands from the frontend. When a user interacts with the UI (sends a message, clicks a button, etc.), the frontend queues a command and sends it to the backend. Assistant Transport defines standard commands like `add-message` and `add-tool-result`, and you can define custom commands.
37
+
38
+ ### Command Lifecycle
39
+
40
+ Commands go through the following lifecycle:
41
+
42
+ ```mermaid
43
+ graph LR
44
+ queued -->|sent to backend| in_transit
45
+ in_transit -->|backend processes| applied
46
+ ```
47
+
48
+ The runtime alternates between **idle** (no active backend request) and **sending** (request in flight). When a new command is created while idle, it's immediately sent. Otherwise, it's queued until the current request completes.
49
+
50
+ ```mermaid
51
+ graph LR
52
+ idle -->|new command| sending
53
+ sending -->|request completes| check{check queue}
54
+ check -->|queue has commands| sending
55
+ check -->|queue empty| idle
56
+ ```
57
+
58
+ To implement this architecture, you need to build 2 pieces:
59
+
60
+ 1. **Backend endpoint** on the agent server that accepts commands and returns a stream of state snapshots
61
+ 2. **Frontend-side [state converter](#state-converter)** that converts state snapshots to assistant-ui's data format so that the UI primitives work
62
+
63
+ ## Building a Backend Endpoint
64
+
65
+ Let's build the backend endpoint step by step. You'll need to handle incoming commands, update your agent state, and stream the updates back to the frontend.
66
+
67
+ The backend endpoint receives POST requests with the following payload:
68
+
69
+ ```typescript
70
+ {
71
+ state: T, // The previous state that the frontend has access to
72
+ commands: AssistantTransportCommand[],
73
+ system?: string,
74
+ tools?: ToolDefinition[]
75
+ }
76
+ ```
77
+
78
+ The backend endpoint returns a stream of state snapshots using the `assistant-stream` library ([npm](https://www.npmjs.com/package/assistant-stream) / [PyPI](https://pypi.org/project/assistant-stream/)).
79
+
80
+ ### Handling Commands
81
+
82
+ The backend endpoint processes commands from the `commands` array:
83
+
84
+ ```python
85
+ for command in request.commands:
86
+ if command.type == "add-message":
87
+ # Handle adding a user message
88
+ elif command.type == "add-tool-result":
89
+ # Handle tool execution result
90
+ elif command.type == "my-custom-command":
91
+ # Handle your custom command
92
+ ```
93
+
94
+ ### Streaming Updates
95
+
96
+ To stream state updates, modify `controller.state` within your run callback:
97
+
98
+ ```python
99
+ from assistant_stream import RunController, create_run
100
+ from assistant_stream.serialization import DataStreamResponse
101
+
102
+ @app.post("/assistant")
103
+ async def chat_endpoint(request: ChatRequest):
104
+ async def run_callback(controller: RunController):
105
+ # Emits "set" at path ["message"] with value "Hello"
106
+ controller.state["message"] = "Hello"
107
+
108
+ # Emits "append-text" at path ["message"] with value " World"
109
+ controller.state["message"] += " World"
110
+
111
+ # Create and return the stream
112
+ stream = create_run(run_callback, state=request.state)
113
+ return DataStreamResponse(stream)
114
+ ```
115
+
116
+ The state snapshots are automatically streamed to the frontend using the operations described in [Streaming Protocol](#streaming-protocol).
117
+
118
+ ### Backend Reference Implementation
119
+
120
+ <Tabs items={["Minimal", "Example", "LangGraph"]}>
121
+ <Tab>
122
+
123
+ ```python
124
+ from assistant_stream import RunController, create_run
125
+ from assistant_stream.serialization import DataStreamResponse
126
+
127
+ async def run_callback(controller: RunController):
128
+ # Initialize state
129
+ if controller.state is None:
130
+ controller.state = {}
131
+
132
+ # Process commands
133
+ for command in request.commands:
134
+ # Handle commands...
135
+
136
+ # Run your agent and stream updates
137
+ async for event in agent.stream():
138
+ # update controller.state
139
+ pass
140
+
141
+ # Create and return the stream
142
+ stream = create_run(run_callback, state=request.state)
143
+ return DataStreamResponse(stream)
144
+ ```
145
+
146
+ </Tab>
147
+ <Tab>
148
+
149
+ ```python
150
+ from assistant_stream.serialization import DataStreamResponse
151
+ from assistant_stream import RunController, create_run
152
+
153
+ @app.post("/assistant")
154
+ async def chat_endpoint(request: ChatRequest):
155
+ """Chat endpoint with custom agent streaming."""
156
+
157
+ async def run_callback(controller: RunController):
158
+ # Initialize controller state
159
+ if controller.state is None:
160
+ controller.state = {"messages": []}
161
+
162
+ # Process commands
163
+ for command in request.commands:
164
+ if command.type == "add-message":
165
+ # Add message to messages array
166
+ controller.state["messages"].append(command.message)
167
+
168
+ # Run your custom agent and stream updates
169
+ async for message in your_agent.stream():
170
+ # Push message to messages array
171
+ controller.state["messages"].append(message)
172
+
173
+ # Create streaming response
174
+ stream = create_run(run_callback, state=request.state)
175
+ return DataStreamResponse(stream)
176
+ ```
177
+
178
+ </Tab>
179
+ <Tab>
180
+
181
+ ```python
182
+ from assistant_stream.serialization import DataStreamResponse
183
+ from assistant_stream import RunController, create_run
184
+ from assistant_stream.modules.langgraph import append_langgraph_event
185
+
186
+ @app.post("/assistant")
187
+ async def chat_endpoint(request: ChatRequest):
188
+ """Chat endpoint using LangGraph with streaming."""
189
+
190
+ async def run_callback(controller: RunController):
191
+ # Initialize controller state
192
+ if controller.state is None:
193
+ controller.state = {}
194
+ if "messages" not in controller.state:
195
+ controller.state["messages"] = []
196
+
197
+ input_messages = []
198
+
199
+ # Process commands
200
+ for command in request.commands:
201
+ if command.type == "add-message":
202
+ text_parts = [
203
+ part.text for part in command.message.parts
204
+ if part.type == "text" and part.text
205
+ ]
206
+ if text_parts:
207
+ input_messages.append(HumanMessage(content=" ".join(text_parts)))
208
+
209
+ # Create initial state for LangGraph
210
+ input_state = {"messages": input_messages}
211
+
212
+ # Stream events from LangGraph
213
+ async for namespace, event_type, chunk in graph.astream(
214
+ input_state,
215
+ stream_mode=["messages", "updates"],
216
+ subgraphs=True
217
+ ):
218
+ append_langgraph_event(
219
+ controller.state,
220
+ namespace,
221
+ event_type,
222
+ chunk
223
+ )
224
+
225
+ # Create streaming response
226
+ stream = create_run(run_callback, state=request.state)
227
+ return DataStreamResponse(stream)
228
+ ```
229
+
230
+ </Tab>
231
+ </Tabs>
232
+
233
+ Full example: [`python/assistant-transport-backend-langgraph`](https://github.com/assistant-ui/assistant-ui/tree/main/python/assistant-transport-backend-langgraph)
234
+
235
+ ## Streaming Protocol
236
+
237
+ The assistant-stream state replication protocol allows for streaming updates to an arbitrary JSON object.
238
+
239
+ ### Operations
240
+
241
+ The protocol supports two operations:
242
+
243
+ > **Note:** We've found that these two operations are enough to handle all sorts of complex state operations efficiently. `set` handles value updates and nested structures, while `append-text` enables efficient streaming of text content.
244
+
245
+ #### `set`
246
+
247
+ Sets a value at a specific path in the JSON object.
248
+
249
+ ```json
250
+ // Operation
251
+ { "type": "set", "path": ["status"], "value": "completed" }
252
+
253
+ // Before
254
+ { "status": "pending" }
255
+
256
+ // After
257
+ { "status": "completed" }
258
+ ```
259
+
260
+ #### `append-text`
261
+
262
+ Appends text to an existing string value at a path.
263
+
264
+ ```json
265
+ // Operation
266
+ { "type": "append-text", "path": ["message"], "value": " World" }
267
+
268
+ // Before
269
+ { "message": "Hello" }
270
+
271
+ // After
272
+ { "message": "Hello World" }
273
+ ```
274
+
275
+ ### Wire Format
276
+
277
+ <Callout type="warn">
278
+ The wire format will be migrated to Server-Sent Events (SSE) in a future
279
+ release.
280
+ </Callout>
281
+
282
+ The wire format is inspired by [AI SDK's data stream protocol](https://sdk.vercel.ai/docs/ai-sdk-ui/stream-protocol).
283
+
284
+ **State Update:**
285
+
286
+ ```
287
+ aui-state:ObjectStreamOperation[]
288
+ ```
289
+
290
+ ```
291
+ aui-state:[{"type":"set","path":["status"],"value":"completed"}]
292
+ ```
293
+
294
+ **Error:**
295
+
296
+ ```
297
+ 3:string
298
+ ```
299
+
300
+ ```
301
+ 3:"error message"
302
+ ```
303
+
304
+ ## Building a Frontend
305
+
306
+ Now let's set up the frontend. The state converter is the heart of the integration—it transforms your agent's state into the format assistant-ui expects.
307
+
308
+ The `useAssistantTransportRuntime` hook is used to configure the runtime. It accepts the following config:
309
+
310
+ ```typescript
311
+ {
312
+ initialState: T,
313
+ api: string,
314
+ resumeApi?: string,
315
+ converter: (state: T, connectionMetadata: ConnectionMetadata) => AssistantTransportState,
316
+ headers?: Record<string, string> | (() => Promise<Record<string, string>>),
317
+ body?: object,
318
+ onResponse?: (response: Response) => void,
319
+ onFinish?: () => void,
320
+ onError?: (error: Error) => void,
321
+ onCancel?: () => void
322
+ }
323
+ ```
324
+
325
+ ### State Converter
326
+
327
+ The state converter is the core of your frontend integration. It transforms your agent's state into assistant-ui's message format.
328
+
329
+ ```typescript
330
+ (
331
+ state: T, // Your agent's state
332
+ connectionMetadata: {
333
+ pendingCommands: Command[], // Commands not yet sent to backend
334
+ isSending: boolean // Whether a request is in flight
335
+ }
336
+ ) => {
337
+ messages: ThreadMessage[], // Messages to display
338
+ isRunning: boolean // Whether the agent is running
339
+ }
340
+ ```
341
+
342
+ ### Converting Messages
343
+
344
+ Use the `createMessageConverter` API to transform your agent's messages to assistant-ui format:
345
+
346
+ <Tabs items={["Example", "LangChain"]}>
347
+ <Tab>
348
+
349
+ ```typescript
350
+ import { unstable_createMessageConverter as createMessageConverter } from "@assistant-ui/react";
351
+
352
+ // Define your message type
353
+ type YourMessageType = {
354
+ id: string;
355
+ role: "user" | "assistant";
356
+ content: string;
357
+ timestamp: number;
358
+ };
359
+
360
+ // Define a converter function for a single message
361
+ const exampleMessageConverter = (message: YourMessageType) => {
362
+ // Transform a single message to assistant-ui format
363
+ return {
364
+ role: message.role,
365
+ content: [{ type: "text", text: message.content }]
366
+ };
367
+ };
368
+
369
+ const messageConverter = createMessageConverter(exampleMessageConverter);
370
+
371
+ const converter = (state: YourAgentState) => {
372
+ return {
373
+ messages: messageConverter.toThreadMessages(state.messages),
374
+ isRunning: false
375
+ };
376
+ };
377
+ ```
378
+
379
+ </Tab>
380
+ <Tab>
381
+
382
+ ```typescript
383
+ import { unstable_createMessageConverter as createMessageConverter } from "@assistant-ui/react";
384
+ import { convertLangChainMessages } from "@assistant-ui/react-langgraph";
385
+
386
+ const messageConverter = createMessageConverter(convertLangChainMessages);
387
+
388
+ const converter = (state: YourAgentState) => {
389
+ return {
390
+ messages: messageConverter.toThreadMessages(state.messages),
391
+ isRunning: false
392
+ };
393
+ };
394
+ ```
395
+
396
+ </Tab>
397
+ </Tabs>
398
+
399
+ **Reverse mapping:**
400
+
401
+ The message converter allows you to retrieve the original message format anywhere inside assistant-ui. This lets you access your agent's native message structure from any assistant-ui component:
402
+
403
+ ```typescript
404
+ // Get original message(s) from a ThreadMessage anywhere in assistant-ui
405
+ const originalMessage = messageConverter.toOriginalMessage(threadMessage);
406
+ ```
407
+
408
+ ### Optimistic Updates from Commands
409
+
410
+ The converter also receives `connectionMetadata` which contains pending commands. Use this to show optimistic updates:
411
+
412
+ ```typescript
413
+ const converter = (state: State, connectionMetadata: ConnectionMetadata) => {
414
+ // Extract pending messages from commands
415
+ const optimisticMessages = connectionMetadata.pendingCommands
416
+ .filter((c) => c.type === "add-message")
417
+ .map((c) => c.message);
418
+
419
+ return {
420
+ messages: [...state.messages, ...optimisticMessages],
421
+ isRunning: connectionMetadata.isSending || false
422
+ };
423
+ };
424
+ ```
425
+
426
+ ## Handling Errors and Cancellations
427
+
428
+ The `onError` and `onCancel` callbacks receive an `updateState` function that allows you to update the agent state on the client side without making a server request:
429
+
430
+ ```typescript
431
+ const runtime = useAssistantTransportRuntime({
432
+ // ... other options
433
+ onError: (error, { commands, updateState }) => {
434
+ console.error("Error occurred:", error);
435
+ console.log("Commands in transit:", commands);
436
+
437
+ // Update state to reflect the error
438
+ updateState((currentState) => ({
439
+ ...currentState,
440
+ lastError: error.message,
441
+ }));
442
+ },
443
+ onCancel: ({ commands, updateState }) => {
444
+ console.log("Request cancelled");
445
+ console.log("Commands in transit or queued:", commands);
446
+
447
+ // Update state to reflect cancellation
448
+ updateState((currentState) => ({
449
+ ...currentState,
450
+ status: "cancelled",
451
+ }));
452
+ },
453
+ });
454
+ ```
455
+
456
+ > **Note:** `onError` receives commands that were in transit, while `onCancel` receives both in-transit and queued commands.
457
+
458
+ ## Custom Headers and Body
459
+
460
+ You can pass custom headers and body to the backend endpoint:
461
+
462
+ ```typescript
463
+ const runtime = useAssistantTransportRuntime({
464
+ // ... other options
465
+ headers: {
466
+ "Authorization": "Bearer token",
467
+ "X-Custom-Header": "value",
468
+ },
469
+ body: {
470
+ customField: "value",
471
+ },
472
+ });
473
+ ```
474
+
475
+ ### Dynamic Headers and Body
476
+
477
+ You can also evaluate the header and body payloads on every request by passing an async function:
478
+
479
+ ```typescript
480
+ const runtime = useAssistantTransportRuntime({
481
+ // ... other options
482
+ headers: async () => ({
483
+ "Authorization": `Bearer ${await getAccessToken()}`,
484
+ "X-Request-ID": crypto.randomUUID(),
485
+ }),
486
+ body: {
487
+ customField: "value",
488
+ },
489
+ });
490
+ ```
491
+
492
+ ## Resuming from a Sync Server
493
+
494
+ <Callout type="info">
495
+ We provide a sync server currently only as part of the enterprise plan. Please
496
+ contact us for more information.
497
+ </Callout>
498
+
499
+ To enable resumability, you need to:
500
+
501
+ 1. Pass a `resumeApi` URL to `useAssistantTransportRuntime` that points to your sync server
502
+ 2. Use the `unstable_resumeRun` API to resume a conversation
503
+
504
+ ```typescript
505
+ import { useAssistantApi } from "@assistant-ui/react";
506
+
507
+ const runtime = useAssistantTransportRuntime({
508
+ // ... other options
509
+ api: "http://localhost:8010/assistant",
510
+ resumeApi: "http://localhost:8010/resume", // Sync server endpoint
511
+ // ... other options
512
+ });
513
+
514
+ // Typically called on thread switch or mount to check if sync server has anything to resume
515
+ const api = useAssistantApi();
516
+ api.thread().unstable_resumeRun({
517
+ parentId: null, // Ignored (will be removed in a future version)
518
+ });
519
+ ```
520
+
521
+ ## Accessing Runtime State
522
+
523
+ Use the `useAssistantTransportState` hook to access the current agent state from any component:
524
+
525
+ ```typescript
526
+ import { useAssistantTransportState } from "@assistant-ui/react";
527
+
528
+ function MyComponent() {
529
+ const state = useAssistantTransportState();
530
+
531
+ return <div>{JSON.stringify(state)}</div>;
532
+ }
533
+ ```
534
+
535
+ You can also pass a selector function to extract specific values:
536
+
537
+ ```typescript
538
+ function MyComponent() {
539
+ const messages = useAssistantTransportState((state) => state.messages);
540
+
541
+ return <div>Message count: {messages.length}</div>;
542
+ }
543
+ ```
544
+
545
+ ### Type Safety
546
+
547
+ Use module augmentation to add types for your agent state:
548
+
549
+ ```typescript title="assistant.config.ts"
550
+ import "@assistant-ui/react";
551
+
552
+ declare module "@assistant-ui/react" {
553
+ namespace Assistant {
554
+ interface ExternalState {
555
+ myState: {
556
+ messages: Message[];
557
+ customField: string;
558
+ };
559
+ }
560
+ }
561
+ }
562
+ ```
563
+
564
+ > **Note:** Place this file anywhere in your project (e.g., `src/assistant.config.ts` or at the project root). TypeScript will automatically pick up the type augmentation through module resolution—you don't need to import this file anywhere.
565
+
566
+ After adding the type augmentation, `useAssistantTransportState` will be fully typed:
567
+
568
+ ```typescript
569
+ function MyComponent() {
570
+ // TypeScript knows about your custom fields
571
+ const customField = useAssistantTransportState((state) => state.customField);
572
+
573
+ return <div>{customField}</div>;
574
+ }
575
+ ```
576
+
577
+ ### Accessing the Original Message
578
+
579
+ If you're using `createMessageConverter`, you can access the original message format from any assistant-ui component using the converter's `toOriginalMessage` method:
580
+
581
+ ```typescript
582
+ import { unstable_createMessageConverter as createMessageConverter } from "@assistant-ui/react";
583
+ import { useMessage } from "@assistant-ui/react";
584
+
585
+ const messageConverter = createMessageConverter(yourMessageConverter);
586
+
587
+ function MyMessageComponent() {
588
+ const message = useMessage();
589
+
590
+ // Get the original message(s) from the converted ThreadMessage
591
+ const originalMessage = messageConverter.toOriginalMessage(message);
592
+
593
+ // Access your agent's native message structure
594
+ return <div>{originalMessage.yourCustomField}</div>;
595
+ }
596
+ ```
597
+
598
+ You can also use `toOriginalMessages` to get all original messages when a ThreadMessage was created from multiple source messages:
599
+
600
+ ```typescript
601
+ const originalMessages = messageConverter.toOriginalMessages(message);
602
+ ```
603
+
604
+ ## Frontend Reference Implementation
605
+
606
+ <Tabs items={["Example", "LangGraph"]}>
607
+ <Tab>
608
+
609
+ ```tsx
610
+ "use client";
611
+
612
+ import {
613
+ AssistantRuntimeProvider,
614
+ AssistantTransportConnectionMetadata,
615
+ useAssistantTransportRuntime,
616
+ } from "@assistant-ui/react";
617
+
618
+ type State = {
619
+ messages: Message[];
620
+ };
621
+
622
+ // Converter function: transforms agent state to assistant-ui format
623
+ const converter = (
624
+ state: State,
625
+ connectionMetadata: AssistantTransportConnectionMetadata,
626
+ ) => {
627
+ // Add optimistic updates for pending commands
628
+ const optimisticMessages = connectionMetadata.pendingCommands
629
+ .filter((c) => c.type === "add-message")
630
+ .map((c) => c.message);
631
+
632
+ return {
633
+ messages: [...state.messages, ...optimisticMessages],
634
+ isRunning: connectionMetadata.isSending || false,
635
+ };
636
+ };
637
+
638
+ export function MyRuntimeProvider({ children }) {
639
+ const runtime = useAssistantTransportRuntime({
640
+ initialState: {
641
+ messages: [],
642
+ },
643
+ api: "http://localhost:8010/assistant",
644
+ converter,
645
+ headers: async () => ({
646
+ "Authorization": "Bearer token",
647
+ }),
648
+ body: {
649
+ "custom-field": "custom-value",
650
+ },
651
+ onResponse: (response) => {
652
+ console.log("Response received from server");
653
+ },
654
+ onFinish: () => {
655
+ console.log("Conversation completed");
656
+ },
657
+ onError: (error, { commands, updateState }) => {
658
+ console.error("Assistant transport error:", error);
659
+ console.log("Commands in transit:", commands);
660
+ },
661
+ onCancel: ({ commands, updateState }) => {
662
+ console.log("Request cancelled");
663
+ console.log("Commands in transit or queued:", commands);
664
+ },
665
+ });
666
+
667
+ return (
668
+ <AssistantRuntimeProvider runtime={runtime}>
669
+ {children}
670
+ </AssistantRuntimeProvider>
671
+ );
672
+ }
673
+ ```
674
+
675
+ </Tab>
676
+ <Tab>
677
+
678
+ ```tsx
679
+ "use client";
680
+
681
+ import {
682
+ AssistantRuntimeProvider,
683
+ AssistantTransportConnectionMetadata,
684
+ unstable_createMessageConverter as createMessageConverter,
685
+ useAssistantTransportRuntime,
686
+ } from "@assistant-ui/react";
687
+ import {
688
+ convertLangChainMessages,
689
+ LangChainMessage,
690
+ } from "@assistant-ui/react-langgraph";
691
+
692
+ type State = {
693
+ messages: LangChainMessage[];
694
+ };
695
+
696
+ const LangChainMessageConverter = createMessageConverter(
697
+ convertLangChainMessages,
698
+ );
699
+
700
+ // Converter function: transforms agent state to assistant-ui format
701
+ const converter = (
702
+ state: State,
703
+ connectionMetadata: AssistantTransportConnectionMetadata,
704
+ ) => {
705
+ // Add optimistic updates for pending commands
706
+ const optimisticStateMessages = connectionMetadata.pendingCommands.map(
707
+ (c): LangChainMessage[] => {
708
+ if (c.type === "add-message") {
709
+ return [
710
+ {
711
+ type: "human" as const,
712
+ content: [
713
+ {
714
+ type: "text" as const,
715
+ text: c.message.parts
716
+ .map((p) => (p.type === "text" ? p.text : ""))
717
+ .join("\n"),
718
+ },
719
+ ],
720
+ },
721
+ ];
722
+ }
723
+ return [];
724
+ },
725
+ );
726
+
727
+ const messages = [...state.messages, ...optimisticStateMessages.flat()];
728
+
729
+ return {
730
+ messages: LangChainMessageConverter.toThreadMessages(messages),
731
+ isRunning: connectionMetadata.isSending || false,
732
+ };
733
+ };
734
+
735
+ export function MyRuntimeProvider({ children }) {
736
+ const runtime = useAssistantTransportRuntime({
737
+ initialState: {
738
+ messages: [],
739
+ },
740
+ api: "http://localhost:8010/assistant",
741
+ converter,
742
+ headers: async () => ({
743
+ "Authorization": "Bearer token",
744
+ }),
745
+ body: {
746
+ "custom-field": "custom-value",
747
+ },
748
+ onResponse: (response) => {
749
+ console.log("Response received from server");
750
+ },
751
+ onFinish: () => {
752
+ console.log("Conversation completed");
753
+ },
754
+ onError: (error, { commands, updateState }) => {
755
+ console.error("Assistant transport error:", error);
756
+ console.log("Commands in transit:", commands);
757
+ },
758
+ onCancel: ({ commands, updateState }) => {
759
+ console.log("Request cancelled");
760
+ console.log("Commands in transit or queued:", commands);
761
+ },
762
+ });
763
+
764
+ return (
765
+ <AssistantRuntimeProvider runtime={runtime}>
766
+ {children}
767
+ </AssistantRuntimeProvider>
768
+ );
769
+ }
770
+ ```
771
+
772
+ </Tab>
773
+ </Tabs>
774
+
775
+ Full example: [`examples/with-assistant-transport`](https://github.com/assistant-ui/assistant-ui/tree/main/examples/with-assistant-transport)
776
+
777
+ ## Custom Commands
778
+
779
+ ### Defining Custom Commands
780
+
781
+ Use module augmentation to define a custom command:
782
+
783
+ ```typescript title="assistant.config.ts"
784
+ import "@assistant-ui/react";
785
+
786
+ declare module "@assistant-ui/react" {
787
+ namespace Assistant {
788
+ interface Commands {
789
+ myCustomCommand: {
790
+ type: "my-custom-command";
791
+ data: string;
792
+ };
793
+ }
794
+ }
795
+ }
796
+ ```
797
+
798
+ ### Issuing Commands
799
+
800
+ Use the `useAssistantTransportSendCommand` hook to send custom commands:
801
+
802
+ ```typescript
803
+ import { useAssistantTransportSendCommand } from "@assistant-ui/react";
804
+
805
+ function MyComponent() {
806
+ const sendCommand = useAssistantTransportSendCommand();
807
+
808
+ const handleClick = () => {
809
+ sendCommand({
810
+ type: "my-custom-command",
811
+ data: "Hello, world!",
812
+ });
813
+ };
814
+
815
+ return <button onClick={handleClick}>Send Custom Command</button>;
816
+ }
817
+ ```
818
+
819
+ ### Backend Integration
820
+
821
+ The backend receives custom commands in the `commands` array, just like built-in commands:
822
+
823
+ ```python
824
+ for command in request.commands:
825
+ if command.type == "add-message":
826
+ # Handle add-message command
827
+ elif command.type == "add-tool-result":
828
+ # Handle add-tool-result command
829
+ elif command.type == "my-custom-command":
830
+ # Handle your custom command
831
+ data = command.data
832
+ ```
833
+
834
+ ### Optimistic Updates
835
+
836
+ Update the [state converter](#state-converter) to optimistically handle the custom command:
837
+
838
+ ```typescript
839
+ const converter = (state: State, connectionMetadata: ConnectionMetadata) => {
840
+ // Filter custom commands from pending commands
841
+ const customCommands = connectionMetadata.pendingCommands.filter(
842
+ (c) => c.type === "my-custom-command"
843
+ );
844
+
845
+ // Apply optimistic updates based on custom commands
846
+ const optimisticState = {
847
+ ...state,
848
+ customData: customCommands.map((c) => c.data),
849
+ };
850
+
851
+ return {
852
+ messages: state.messages,
853
+ state: optimisticState,
854
+ isRunning: connectionMetadata.isSending || false,
855
+ };
856
+ };
857
+ ```
858
+
859
+ ### Cancellation and Error Behavior
860
+
861
+ Custom commands follow the same lifecycle as built-in commands. You can update your `onError` and `onCancel` handlers to take custom commands into account:
862
+
863
+ ```typescript
864
+ const runtime = useAssistantTransportRuntime({
865
+ // ... other options
866
+ onError: (error, { commands, updateState }) => {
867
+ // Check if any custom commands were in transit
868
+ const customCommands = commands.filter((c) => c.type === "my-custom-command");
869
+
870
+ if (customCommands.length > 0) {
871
+ // Handle custom command errors
872
+ updateState((state) => ({
873
+ ...state,
874
+ customCommandFailed: true,
875
+ }));
876
+ }
877
+ },
878
+ onCancel: ({ commands, updateState }) => {
879
+ // Check if any custom commands were queued or in transit
880
+ const customCommands = commands.filter((c) => c.type === "my-custom-command");
881
+
882
+ if (customCommands.length > 0) {
883
+ // Handle custom command cancellation
884
+ updateState((state) => ({
885
+ ...state,
886
+ customCommandCancelled: true,
887
+ }));
888
+ }
889
+ },
890
+ });
891
+ ```