@assistant-ui/mcp-docs-server 0.1.13 → 0.1.15
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/store-example.md +554 -0
- package/.docs/organized/code-examples/with-ag-ui.md +1639 -0
- package/.docs/organized/code-examples/with-ai-sdk-v5.md +555 -53
- package/.docs/organized/code-examples/with-assistant-transport.md +553 -52
- package/.docs/organized/code-examples/with-cloud.md +637 -42
- package/.docs/organized/code-examples/with-external-store.md +584 -34
- package/.docs/organized/code-examples/with-ffmpeg.md +586 -52
- package/.docs/organized/code-examples/with-langgraph.md +636 -53
- package/.docs/organized/code-examples/with-parent-id-grouping.md +584 -34
- package/.docs/organized/code-examples/with-react-hook-form.md +587 -75
- package/.docs/raw/blog/2024-07-29-hello/index.mdx +0 -1
- package/.docs/raw/docs/cli.mdx +396 -0
- package/.docs/raw/docs/cloud/authorization.mdx +2 -2
- package/.docs/raw/docs/getting-started.mdx +31 -37
- package/.docs/raw/docs/guides/context-api.mdx +5 -5
- package/.docs/raw/docs/migrations/v0-12.mdx +2 -2
- package/.docs/raw/docs/runtimes/assistant-transport.mdx +891 -0
- package/.docs/raw/docs/runtimes/custom/custom-thread-list.mdx +9 -0
- package/.docs/raw/docs/runtimes/custom/local.mdx +77 -4
- package/.docs/raw/docs/runtimes/langgraph/index.mdx +8 -5
- package/.docs/raw/docs/runtimes/mastra/full-stack-integration.mdx +12 -10
- package/.docs/raw/docs/runtimes/mastra/separate-server-integration.mdx +50 -31
- package/.docs/raw/docs/ui/Reasoning.mdx +174 -0
- package/dist/chunk-M2RKUM66.js +3 -3
- package/dist/chunk-NVNFQ5ZO.js +2 -2
- package/package.json +15 -7
|
@@ -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
|
+
```
|