@tanstack/ai 0.9.2 → 0.10.1

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 (28) hide show
  1. package/dist/esm/activities/chat/stream/processor.js +3 -0
  2. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  3. package/dist/esm/index.d.ts +1 -0
  4. package/dist/esm/index.js +3 -0
  5. package/dist/esm/index.js.map +1 -1
  6. package/dist/esm/tool-registry.d.ts +81 -0
  7. package/dist/esm/tool-registry.js +49 -0
  8. package/dist/esm/tool-registry.js.map +1 -0
  9. package/package.json +6 -4
  10. package/skills/ai-core/SKILL.md +59 -0
  11. package/skills/ai-core/adapter-configuration/SKILL.md +283 -0
  12. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +97 -0
  13. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +102 -0
  14. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +77 -0
  15. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +106 -0
  16. package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +82 -0
  17. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +95 -0
  18. package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +99 -0
  19. package/skills/ai-core/ag-ui-protocol/SKILL.md +232 -0
  20. package/skills/ai-core/chat-experience/SKILL.md +506 -0
  21. package/skills/ai-core/custom-backend-integration/SKILL.md +463 -0
  22. package/skills/ai-core/media-generation/SKILL.md +471 -0
  23. package/skills/ai-core/middleware/SKILL.md +336 -0
  24. package/skills/ai-core/structured-outputs/SKILL.md +203 -0
  25. package/skills/ai-core/tool-calling/SKILL.md +411 -0
  26. package/src/activities/chat/stream/processor.ts +7 -0
  27. package/src/index.ts +7 -0
  28. package/src/tool-registry.ts +150 -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