@tanstack/ai 0.10.0 → 0.10.2
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/dist/esm/activities/chat/index.js +27 -3
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/package.json +6 -4
- package/skills/ai-core/SKILL.md +59 -0
- package/skills/ai-core/adapter-configuration/SKILL.md +283 -0
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +97 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +102 -0
- package/skills/ai-core/adapter-configuration/references/grok-adapter.md +77 -0
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +106 -0
- package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +82 -0
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +95 -0
- package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +99 -0
- package/skills/ai-core/ag-ui-protocol/SKILL.md +232 -0
- package/skills/ai-core/chat-experience/SKILL.md +506 -0
- package/skills/ai-core/custom-backend-integration/SKILL.md +463 -0
- package/skills/ai-core/media-generation/SKILL.md +471 -0
- package/skills/ai-core/middleware/SKILL.md +336 -0
- package/skills/ai-core/structured-outputs/SKILL.md +203 -0
- package/skills/ai-core/tool-calling/SKILL.md +411 -0
- package/src/activities/chat/index.ts +32 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-core/custom-backend-integration
|
|
3
|
+
description: >
|
|
4
|
+
Connect useChat to a non-TanStack-AI backend through custom connection
|
|
5
|
+
adapters. ConnectConnectionAdapter (single async iterable) vs
|
|
6
|
+
SubscribeConnectionAdapter (separate subscribe/send). Customize
|
|
7
|
+
fetchServerSentEvents() and fetchHttpStream() with auth headers,
|
|
8
|
+
custom URLs, and request options. Import from framework package,
|
|
9
|
+
not @tanstack/ai-client.
|
|
10
|
+
type: composition
|
|
11
|
+
library: tanstack-ai
|
|
12
|
+
library_version: '0.10.0'
|
|
13
|
+
sources:
|
|
14
|
+
- 'TanStack/ai:docs/chat/connection-adapters.md'
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Custom Backend Integration
|
|
18
|
+
|
|
19
|
+
This skill builds on ai-core and ai-core/chat-experience. Read them first.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
Connect `useChat` to a custom SSE backend with auth headers:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
27
|
+
|
|
28
|
+
function Chat() {
|
|
29
|
+
const { messages, sendMessage, isLoading } = useChat({
|
|
30
|
+
connection: fetchServerSentEvents('https://my-api.com/chat', {
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${token}`,
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div>
|
|
39
|
+
{messages.map((msg) => (
|
|
40
|
+
<div key={msg.id}>
|
|
41
|
+
<strong>{msg.role}:</strong>
|
|
42
|
+
{msg.parts.map((part, i) => {
|
|
43
|
+
if (part.type === 'text') {
|
|
44
|
+
return <p key={i}>{part.content}</p>
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
})}
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
<button onClick={() => sendMessage('Hello')}>Send</button>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Both `fetchServerSentEvents` and `fetchHttpStream` accept a static URL string
|
|
57
|
+
or a function returning a string (evaluated per request), and a static options
|
|
58
|
+
object or a sync/async function returning options (also evaluated per request).
|
|
59
|
+
This allows dynamic auth tokens and URLs without re-creating the adapter.
|
|
60
|
+
|
|
61
|
+
## Core Patterns
|
|
62
|
+
|
|
63
|
+
### 1. Custom SSE Backend with fetchServerSentEvents
|
|
64
|
+
|
|
65
|
+
Use when your backend speaks SSE (`text/event-stream`) with `data: {json}\n\n`
|
|
66
|
+
framing. This is the recommended default.
|
|
67
|
+
|
|
68
|
+
**Static options:**
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
72
|
+
|
|
73
|
+
const { messages, sendMessage } = useChat({
|
|
74
|
+
connection: fetchServerSentEvents('https://my-api.com/chat', {
|
|
75
|
+
headers: {
|
|
76
|
+
Authorization: `Bearer ${token}`,
|
|
77
|
+
'X-Tenant-Id': tenantId,
|
|
78
|
+
},
|
|
79
|
+
credentials: 'include',
|
|
80
|
+
}),
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Dynamic URL and options (evaluated per request):**
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
88
|
+
|
|
89
|
+
const { messages, sendMessage } = useChat({
|
|
90
|
+
connection: fetchServerSentEvents(
|
|
91
|
+
() => `https://my-api.com/chat?session=${sessionId}`,
|
|
92
|
+
async () => ({
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: `Bearer ${await getAccessToken()}`,
|
|
95
|
+
},
|
|
96
|
+
body: {
|
|
97
|
+
provider: 'openai',
|
|
98
|
+
model: 'gpt-4o',
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The `body` field in options is merged into the POST request body alongside
|
|
106
|
+
`messages` and `data`, so the server receives `{ messages, data, provider, model }`.
|
|
107
|
+
|
|
108
|
+
**Custom fetch client (for proxies, interceptors, retries):**
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
112
|
+
|
|
113
|
+
const { messages, sendMessage } = useChat({
|
|
114
|
+
connection: fetchServerSentEvents('/api/chat', {
|
|
115
|
+
fetchClient: myCustomFetch,
|
|
116
|
+
}),
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2. Custom NDJSON Backend with fetchHttpStream
|
|
121
|
+
|
|
122
|
+
Use when your backend sends newline-delimited JSON (`application/x-ndjson`)
|
|
123
|
+
instead of SSE. Each line is one JSON-encoded `StreamChunk` followed by `\n`.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { useChat, fetchHttpStream } from '@tanstack/ai-react'
|
|
127
|
+
|
|
128
|
+
const { messages, sendMessage } = useChat({
|
|
129
|
+
connection: fetchHttpStream('https://my-api.com/chat', {
|
|
130
|
+
headers: {
|
|
131
|
+
Authorization: `Bearer ${token}`,
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`fetchHttpStream` accepts the same URL and options signatures as
|
|
138
|
+
`fetchServerSentEvents` (static or dynamic, sync or async). The only difference
|
|
139
|
+
is the parsing: no `data:` prefix stripping, no `[DONE]` sentinel -- just one
|
|
140
|
+
JSON object per line.
|
|
141
|
+
|
|
142
|
+
**Dynamic options work identically:**
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { useChat, fetchHttpStream } from '@tanstack/ai-react'
|
|
146
|
+
|
|
147
|
+
const { messages, sendMessage } = useChat({
|
|
148
|
+
connection: fetchHttpStream(
|
|
149
|
+
() => `/api/chat?region=${region}`,
|
|
150
|
+
async () => ({
|
|
151
|
+
headers: { Authorization: `Bearer ${await refreshToken()}` },
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 3. Fully Custom Connection Adapter
|
|
158
|
+
|
|
159
|
+
For protocols that don't fit SSE or NDJSON (WebSockets, gRPC-web, custom binary,
|
|
160
|
+
server functions), implement the `ConnectionAdapter` interface directly.
|
|
161
|
+
|
|
162
|
+
There are two mutually exclusive modes:
|
|
163
|
+
|
|
164
|
+
**ConnectConnectionAdapter (pull-based / async iterable):**
|
|
165
|
+
|
|
166
|
+
Use when the client initiates a request and consumes the response as a stream.
|
|
167
|
+
This is the simpler model and covers most HTTP-based protocols.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { useChat } from '@tanstack/ai-react'
|
|
171
|
+
import type { ConnectionAdapter } from '@tanstack/ai-react'
|
|
172
|
+
import type { StreamChunk, UIMessage } from '@tanstack/ai'
|
|
173
|
+
|
|
174
|
+
const websocketAdapter: ConnectionAdapter = {
|
|
175
|
+
async *connect(
|
|
176
|
+
messages: Array<UIMessage>,
|
|
177
|
+
data?: Record<string, any>,
|
|
178
|
+
abortSignal?: AbortSignal,
|
|
179
|
+
): AsyncGenerator<StreamChunk> {
|
|
180
|
+
const ws = new WebSocket('wss://my-api.com/chat')
|
|
181
|
+
|
|
182
|
+
// Wait for connection
|
|
183
|
+
await new Promise<void>((resolve, reject) => {
|
|
184
|
+
ws.onopen = () => resolve()
|
|
185
|
+
ws.onerror = (e) => reject(e)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// Send messages
|
|
189
|
+
ws.send(JSON.stringify({ messages, ...data }))
|
|
190
|
+
|
|
191
|
+
// Create an async queue to bridge WebSocket events to an async iterable
|
|
192
|
+
const queue: Array<StreamChunk> = []
|
|
193
|
+
let resolve: (() => void) | null = null
|
|
194
|
+
let done = false
|
|
195
|
+
|
|
196
|
+
ws.onmessage = (event) => {
|
|
197
|
+
const chunk: StreamChunk = JSON.parse(event.data)
|
|
198
|
+
queue.push(chunk)
|
|
199
|
+
resolve?.()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
ws.onclose = () => {
|
|
203
|
+
done = true
|
|
204
|
+
resolve?.()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
ws.onerror = () => {
|
|
208
|
+
done = true
|
|
209
|
+
resolve?.()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
abortSignal?.addEventListener('abort', () => {
|
|
213
|
+
ws.close()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// Yield chunks as they arrive
|
|
217
|
+
while (!done || queue.length > 0) {
|
|
218
|
+
if (queue.length > 0) {
|
|
219
|
+
yield queue.shift()!
|
|
220
|
+
} else {
|
|
221
|
+
await new Promise<void>((r) => {
|
|
222
|
+
resolve = r
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function Chat() {
|
|
230
|
+
const { messages, sendMessage } = useChat({
|
|
231
|
+
connection: websocketAdapter,
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// ... render messages
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**SubscribeConnectionAdapter (push-based / separate subscribe + send):**
|
|
239
|
+
|
|
240
|
+
Use for push-based protocols where the server can send data at any time
|
|
241
|
+
(persistent WebSocket connections, MQTT, server push). The `subscribe` method
|
|
242
|
+
returns an `AsyncIterable<StreamChunk>` that stays open, and `send` dispatches
|
|
243
|
+
messages through it.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import type { StreamChunk, UIMessage } from '@tanstack/ai'
|
|
247
|
+
|
|
248
|
+
// SubscribeConnectionAdapter is exported from @tanstack/ai-client
|
|
249
|
+
// (not re-exported by framework packages -- use ConnectionAdapter
|
|
250
|
+
// union type from @tanstack/ai-react for typing)
|
|
251
|
+
const pushAdapter = {
|
|
252
|
+
subscribe(abortSignal?: AbortSignal): AsyncIterable<StreamChunk> {
|
|
253
|
+
// Return a long-lived async iterable that yields chunks
|
|
254
|
+
// whenever the server pushes them
|
|
255
|
+
return createPersistentStream(abortSignal)
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
async send(
|
|
259
|
+
messages: Array<UIMessage>,
|
|
260
|
+
data?: Record<string, any>,
|
|
261
|
+
abortSignal?: AbortSignal,
|
|
262
|
+
): Promise<void> {
|
|
263
|
+
// Dispatch messages; chunks arrive through subscribe()
|
|
264
|
+
await persistentConnection.send(JSON.stringify({ messages, ...data }))
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function Chat() {
|
|
269
|
+
const { messages, sendMessage } = useChat({
|
|
270
|
+
connection: pushAdapter,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// ... render messages
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The `stream()` helper function (re-exported from `@tanstack/ai-react`) provides
|
|
278
|
+
a shorthand for creating a `ConnectConnectionAdapter` from an async generator:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import { useChat, stream } from '@tanstack/ai-react'
|
|
282
|
+
import type { StreamChunk, UIMessage } from '@tanstack/ai'
|
|
283
|
+
|
|
284
|
+
const directAdapter = stream(async function* (
|
|
285
|
+
messages: Array<UIMessage>,
|
|
286
|
+
data?: Record<string, any>,
|
|
287
|
+
): AsyncGenerator<StreamChunk> {
|
|
288
|
+
const response = await fetch('https://my-api.com/chat', {
|
|
289
|
+
method: 'POST',
|
|
290
|
+
headers: { 'Content-Type': 'application/json' },
|
|
291
|
+
body: JSON.stringify({ messages, ...data }),
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
const reader = response.body!.getReader()
|
|
295
|
+
const decoder = new TextDecoder()
|
|
296
|
+
let buffer = ''
|
|
297
|
+
|
|
298
|
+
while (true) {
|
|
299
|
+
const { done, value } = await reader.read()
|
|
300
|
+
if (done) break
|
|
301
|
+
|
|
302
|
+
buffer += decoder.decode(value, { stream: true })
|
|
303
|
+
const lines = buffer.split('\n')
|
|
304
|
+
buffer = lines.pop() || ''
|
|
305
|
+
|
|
306
|
+
for (const line of lines) {
|
|
307
|
+
if (line.trim()) {
|
|
308
|
+
yield JSON.parse(line) as StreamChunk
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const { messages, sendMessage } = useChat({
|
|
315
|
+
connection: directAdapter,
|
|
316
|
+
})
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Common Mistakes
|
|
320
|
+
|
|
321
|
+
### a. HIGH: Providing both connect and subscribe+send in connection adapter
|
|
322
|
+
|
|
323
|
+
The `ConnectionAdapter` interface has two mutually exclusive modes. Providing
|
|
324
|
+
both throws at runtime.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// WRONG -- throws "Connection adapter must provide either connect or both
|
|
328
|
+
// subscribe and send, not both modes"
|
|
329
|
+
const adapter = {
|
|
330
|
+
async *connect(messages) {
|
|
331
|
+
/* ... */
|
|
332
|
+
},
|
|
333
|
+
subscribe(signal) {
|
|
334
|
+
/* ... */
|
|
335
|
+
},
|
|
336
|
+
async send(messages) {
|
|
337
|
+
/* ... */
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// CORRECT -- pick one mode
|
|
342
|
+
// Option A: ConnectConnectionAdapter (pull-based)
|
|
343
|
+
const pullAdapter = {
|
|
344
|
+
async *connect(messages, data, abortSignal) {
|
|
345
|
+
// ... yield StreamChunks
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Option B: SubscribeConnectionAdapter (push-based)
|
|
350
|
+
const pushAdapter = {
|
|
351
|
+
subscribe(abortSignal) {
|
|
352
|
+
return longLivedAsyncIterable
|
|
353
|
+
},
|
|
354
|
+
async send(messages, data, abortSignal) {
|
|
355
|
+
await connection.dispatch({ messages, ...data })
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Source: `ai-client/src/connection-adapters.ts` line 116
|
|
361
|
+
|
|
362
|
+
### b. MEDIUM: SSE browser connection limits
|
|
363
|
+
|
|
364
|
+
Browsers limit SSE connections to 6-8 per domain (the HTTP/1.1 connection
|
|
365
|
+
limit). Multiple chat sessions on the same page, or multiple tabs to the
|
|
366
|
+
same origin, can exhaust this limit. New connections queue indefinitely until
|
|
367
|
+
an existing one closes.
|
|
368
|
+
|
|
369
|
+
Mitigations:
|
|
370
|
+
|
|
371
|
+
- Use HTTP/2 (multiplexes streams over a single TCP connection; no per-domain limit)
|
|
372
|
+
- Use `fetchHttpStream` instead of `fetchServerSentEvents` (each request is a
|
|
373
|
+
standard POST, not a long-lived EventSource)
|
|
374
|
+
- Close idle connections when not actively streaming
|
|
375
|
+
- Use a single persistent WebSocket via `SubscribeConnectionAdapter` instead of
|
|
376
|
+
per-request SSE connections
|
|
377
|
+
|
|
378
|
+
Source: `docs/chat/connection-adapters.md`
|
|
379
|
+
|
|
380
|
+
### c. MEDIUM: HTTP stream without implementing reconnection
|
|
381
|
+
|
|
382
|
+
SSE has built-in browser auto-reconnection via the `EventSource` API. HTTP
|
|
383
|
+
stream (NDJSON via `fetchHttpStream`) does not -- if the connection drops
|
|
384
|
+
mid-stream, the partial response is silently lost with no automatic retry.
|
|
385
|
+
|
|
386
|
+
If your application needs resilience to transient network errors with HTTP
|
|
387
|
+
streaming, implement retry logic in your connection adapter:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { useChat } from '@tanstack/ai-react'
|
|
391
|
+
import type { ConnectionAdapter } from '@tanstack/ai-react'
|
|
392
|
+
import type { StreamChunk, UIMessage } from '@tanstack/ai'
|
|
393
|
+
|
|
394
|
+
const resilientAdapter: ConnectionAdapter = {
|
|
395
|
+
async *connect(
|
|
396
|
+
messages: Array<UIMessage>,
|
|
397
|
+
data?: Record<string, any>,
|
|
398
|
+
abortSignal?: AbortSignal,
|
|
399
|
+
): AsyncGenerator<StreamChunk> {
|
|
400
|
+
const maxRetries = 3
|
|
401
|
+
let attempt = 0
|
|
402
|
+
|
|
403
|
+
while (attempt < maxRetries) {
|
|
404
|
+
try {
|
|
405
|
+
const response = await fetch('https://my-api.com/chat', {
|
|
406
|
+
method: 'POST',
|
|
407
|
+
headers: { 'Content-Type': 'application/json' },
|
|
408
|
+
body: JSON.stringify({ messages, ...data }),
|
|
409
|
+
signal: abortSignal,
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
if (!response.ok) {
|
|
413
|
+
throw new Error(`HTTP ${response.status}`)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const reader = response.body!.getReader()
|
|
417
|
+
const decoder = new TextDecoder()
|
|
418
|
+
let buffer = ''
|
|
419
|
+
|
|
420
|
+
while (true) {
|
|
421
|
+
const { done, value } = await reader.read()
|
|
422
|
+
if (done) break
|
|
423
|
+
|
|
424
|
+
buffer += decoder.decode(value, { stream: true })
|
|
425
|
+
const lines = buffer.split('\n')
|
|
426
|
+
buffer = lines.pop() || ''
|
|
427
|
+
|
|
428
|
+
for (const line of lines) {
|
|
429
|
+
if (line.trim()) {
|
|
430
|
+
yield JSON.parse(line) as StreamChunk
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return // Stream completed successfully
|
|
436
|
+
} catch (err) {
|
|
437
|
+
if (abortSignal?.aborted) throw err
|
|
438
|
+
attempt++
|
|
439
|
+
if (attempt >= maxRetries) throw err
|
|
440
|
+
// Exponential backoff
|
|
441
|
+
await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt))
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const { messages, sendMessage } = useChat({
|
|
448
|
+
connection: resilientAdapter,
|
|
449
|
+
})
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Note: `fetchServerSentEvents` in TanStack AI uses `fetch()` under the hood (not
|
|
453
|
+
the browser `EventSource` API), so it also does not auto-reconnect. The SSE
|
|
454
|
+
auto-reconnection advantage only applies when using the native `EventSource` API
|
|
455
|
+
directly.
|
|
456
|
+
|
|
457
|
+
Source: `docs/protocol/http-stream-protocol.md`
|
|
458
|
+
|
|
459
|
+
## Cross-References
|
|
460
|
+
|
|
461
|
+
- See also: **ai-core/ag-ui-protocol/SKILL.md** -- Understanding the AG-UI protocol helps build compatible custom servers
|
|
462
|
+
- See also: **ai-core/chat-experience/SKILL.md** -- Full chat setup patterns including server-side `chat()` and `toServerSentEventsResponse()`
|
|
463
|
+
- See also: **ai-core/middleware/SKILL.md** -- Use middleware for analytics and lifecycle events on the server side
|