@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.
- package/dist/esm/activities/chat/stream/processor.js +3 -0
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tool-registry.d.ts +81 -0
- package/dist/esm/tool-registry.js +49 -0
- package/dist/esm/tool-registry.js.map +1 -0
- 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/stream/processor.ts +7 -0
- package/src/index.ts +7 -0
- package/src/tool-registry.ts +150 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-core/chat-experience
|
|
3
|
+
description: >
|
|
4
|
+
End-to-end chat implementation: server endpoint with chat() and
|
|
5
|
+
toServerSentEventsResponse(), client-side useChat hook with
|
|
6
|
+
fetchServerSentEvents(), message rendering with UIMessage parts,
|
|
7
|
+
multimodal content, thinking/reasoning display. Covers streaming
|
|
8
|
+
states, connection adapters, and message format conversions.
|
|
9
|
+
NOT Vercel AI SDK — uses chat() not streamText().
|
|
10
|
+
type: sub-skill
|
|
11
|
+
library: tanstack-ai
|
|
12
|
+
library_version: '0.10.0'
|
|
13
|
+
sources:
|
|
14
|
+
- 'TanStack/ai:docs/getting-started/quick-start.md'
|
|
15
|
+
- 'TanStack/ai:docs/chat/streaming.md'
|
|
16
|
+
- 'TanStack/ai:docs/chat/connection-adapters.md'
|
|
17
|
+
- 'TanStack/ai:docs/chat/thinking-content.md'
|
|
18
|
+
- 'TanStack/ai:docs/advanced/multimodal-content.md'
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Chat Experience
|
|
22
|
+
|
|
23
|
+
This skill builds on ai-core. Read it first for critical rules.
|
|
24
|
+
|
|
25
|
+
## Setup — Minimal Chat App
|
|
26
|
+
|
|
27
|
+
### Server: API Route (TanStack Start)
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// src/routes/api.chat.ts
|
|
31
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
32
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
33
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
34
|
+
|
|
35
|
+
export const Route = createFileRoute('/api/chat')({
|
|
36
|
+
server: {
|
|
37
|
+
handlers: {
|
|
38
|
+
POST: async ({ request }) => {
|
|
39
|
+
const abortController = new AbortController()
|
|
40
|
+
const body = await request.json()
|
|
41
|
+
const { messages } = body
|
|
42
|
+
|
|
43
|
+
const stream = chat({
|
|
44
|
+
adapter: openaiText('gpt-5.2'),
|
|
45
|
+
messages,
|
|
46
|
+
systemPrompts: ['You are a helpful assistant.'],
|
|
47
|
+
abortController,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return toServerSentEventsResponse(stream, { abortController })
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Client: React Component
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// src/routes/index.tsx
|
|
61
|
+
import { useState } from 'react'
|
|
62
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
63
|
+
import type { UIMessage } from '@tanstack/ai-react'
|
|
64
|
+
|
|
65
|
+
function ChatPage() {
|
|
66
|
+
const [input, setInput] = useState('')
|
|
67
|
+
|
|
68
|
+
const { messages, sendMessage, isLoading, error, stop } = useChat({
|
|
69
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const handleSubmit = () => {
|
|
73
|
+
if (!input.trim()) return
|
|
74
|
+
sendMessage(input.trim())
|
|
75
|
+
setInput('')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<div>
|
|
81
|
+
{messages.map((message: UIMessage) => (
|
|
82
|
+
<div key={message.id}>
|
|
83
|
+
<strong>{message.role}:</strong>
|
|
84
|
+
{message.parts.map((part, i) => {
|
|
85
|
+
if (part.type === 'text') {
|
|
86
|
+
return <p key={i}>{part.content}</p>
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{error && <div>Error: {error.message}</div>}
|
|
95
|
+
|
|
96
|
+
<div>
|
|
97
|
+
<input
|
|
98
|
+
value={input}
|
|
99
|
+
onChange={(e) => setInput(e.target.value)}
|
|
100
|
+
onKeyDown={(e) => {
|
|
101
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
102
|
+
e.preventDefault()
|
|
103
|
+
handleSubmit()
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
disabled={isLoading}
|
|
107
|
+
placeholder="Type a message..."
|
|
108
|
+
/>
|
|
109
|
+
{isLoading ? (
|
|
110
|
+
<button onClick={stop}>Stop</button>
|
|
111
|
+
) : (
|
|
112
|
+
<button onClick={handleSubmit} disabled={!input.trim()}>
|
|
113
|
+
Send
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Vue/Solid/Svelte/Preact have identical patterns with different hook imports
|
|
123
|
+
(e.g., `import { useChat } from '@tanstack/ai-solid'`).
|
|
124
|
+
|
|
125
|
+
## Core Patterns
|
|
126
|
+
|
|
127
|
+
### 1. Streaming Chat with SSE
|
|
128
|
+
|
|
129
|
+
Server returns a streaming SSE Response; client parses it automatically.
|
|
130
|
+
|
|
131
|
+
**Server:**
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
|
|
135
|
+
import { anthropicText } from '@tanstack/ai-anthropic'
|
|
136
|
+
|
|
137
|
+
const stream = chat({
|
|
138
|
+
adapter: anthropicText('claude-sonnet-4-5'),
|
|
139
|
+
messages,
|
|
140
|
+
temperature: 0.7,
|
|
141
|
+
maxTokens: 2000,
|
|
142
|
+
systemPrompts: ['You are a helpful assistant.'],
|
|
143
|
+
abortController,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return toServerSentEventsResponse(stream, { abortController })
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Client:**
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
153
|
+
|
|
154
|
+
const { messages, sendMessage, isLoading, error, stop, status } = useChat({
|
|
155
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
156
|
+
body: { provider: 'anthropic', model: 'claude-sonnet-4-5' },
|
|
157
|
+
onFinish: (message) => {
|
|
158
|
+
console.log('Response complete:', message.id)
|
|
159
|
+
},
|
|
160
|
+
onError: (err) => {
|
|
161
|
+
console.error('Stream error:', err)
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The `body` field is merged into the POST request body alongside `messages`,
|
|
167
|
+
letting the server read `data.provider`, `data.model`, etc.
|
|
168
|
+
|
|
169
|
+
The `status` field tracks the chat lifecycle: `'ready'` | `'submitted'` | `'streaming'` | `'error'`.
|
|
170
|
+
|
|
171
|
+
### 2. Rendering Thinking/Reasoning Content
|
|
172
|
+
|
|
173
|
+
Models with extended thinking (Claude, Gemini) emit `ThinkingPart` in the message parts array.
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import type { UIMessage } from '@tanstack/ai-react'
|
|
177
|
+
|
|
178
|
+
function MessageRenderer({ message }: { message: UIMessage }) {
|
|
179
|
+
return (
|
|
180
|
+
<div>
|
|
181
|
+
{message.parts.map((part, i) => {
|
|
182
|
+
if (part.type === 'thinking') {
|
|
183
|
+
const isComplete = message.parts
|
|
184
|
+
.slice(i + 1)
|
|
185
|
+
.some((p) => p.type === 'text')
|
|
186
|
+
return (
|
|
187
|
+
<details key={i} open={!isComplete}>
|
|
188
|
+
<summary>{isComplete ? 'Thought process' : 'Thinking...'}</summary>
|
|
189
|
+
<pre>{part.content}</pre>
|
|
190
|
+
</details>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (part.type === 'text' && part.content) {
|
|
195
|
+
return <p key={i}>{part.content}</p>
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (part.type === 'tool-call') {
|
|
199
|
+
return (
|
|
200
|
+
<div key={part.id}>
|
|
201
|
+
Tool call: {part.name} ({part.state})
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null
|
|
207
|
+
})}
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Server-side, enable thinking via `modelOptions` on the adapter:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { geminiText } from '@tanstack/ai-gemini'
|
|
217
|
+
|
|
218
|
+
const stream = chat({
|
|
219
|
+
adapter: geminiText('gemini-2.5-flash'),
|
|
220
|
+
messages,
|
|
221
|
+
modelOptions: {
|
|
222
|
+
thinkingConfig: {
|
|
223
|
+
includeThoughts: true,
|
|
224
|
+
thinkingBudget: 100,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 3. Sending Multimodal Content (Images)
|
|
231
|
+
|
|
232
|
+
Use `sendMessage` with a `MultimodalContent` object instead of a plain string.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
236
|
+
import type { ContentPart } from '@tanstack/ai'
|
|
237
|
+
|
|
238
|
+
const { sendMessage } = useChat({
|
|
239
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
function sendImageMessage(text: string, imageBase64: string, mimeType: string) {
|
|
243
|
+
const contentParts: Array<ContentPart> = [
|
|
244
|
+
{ type: 'text', content: text },
|
|
245
|
+
{
|
|
246
|
+
type: 'image',
|
|
247
|
+
source: { type: 'data', value: imageBase64, mimeType },
|
|
248
|
+
},
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
sendMessage({ content: contentParts })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function sendImageUrl(text: string, imageUrl: string) {
|
|
255
|
+
const contentParts: Array<ContentPart> = [
|
|
256
|
+
{ type: 'text', content: text },
|
|
257
|
+
{
|
|
258
|
+
type: 'image',
|
|
259
|
+
source: { type: 'url', value: imageUrl },
|
|
260
|
+
},
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
sendMessage({ content: contentParts })
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Render image parts in received messages:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
if (part.type === 'image') {
|
|
271
|
+
const src =
|
|
272
|
+
part.source.type === 'url'
|
|
273
|
+
? part.source.value
|
|
274
|
+
: `data:${part.source.mimeType};base64,${part.source.value}`
|
|
275
|
+
return <img key={i} src={src} alt="Attached image" />
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 4. HTTP Stream Format (Alternative to SSE)
|
|
280
|
+
|
|
281
|
+
Use `toHttpResponse` + `fetchHttpStream` for newline-delimited JSON instead of SSE.
|
|
282
|
+
|
|
283
|
+
**Server:**
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { chat, toHttpResponse } from '@tanstack/ai'
|
|
287
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
288
|
+
|
|
289
|
+
const stream = chat({
|
|
290
|
+
adapter: openaiText('gpt-5.2'),
|
|
291
|
+
messages,
|
|
292
|
+
abortController,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
return toHttpResponse(stream, { abortController })
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Client:**
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { useChat, fetchHttpStream } from '@tanstack/ai-react'
|
|
302
|
+
|
|
303
|
+
const { messages, sendMessage } = useChat({
|
|
304
|
+
connection: fetchHttpStream('/api/chat'),
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The only difference is swapping `toServerSentEventsResponse` / `fetchServerSentEvents`
|
|
309
|
+
for `toHttpResponse` / `fetchHttpStream`. Everything else stays identical.
|
|
310
|
+
|
|
311
|
+
## Common Mistakes
|
|
312
|
+
|
|
313
|
+
### a. CRITICAL: Using Vercel AI SDK patterns (streamText, generateText)
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// WRONG
|
|
317
|
+
import { streamText } from 'ai'
|
|
318
|
+
import { openai } from '@ai-sdk/openai'
|
|
319
|
+
const result = streamText({ model: openai('gpt-4o'), messages })
|
|
320
|
+
|
|
321
|
+
// CORRECT
|
|
322
|
+
import { chat } from '@tanstack/ai'
|
|
323
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
324
|
+
const stream = chat({ adapter: openaiText('gpt-5.2'), messages })
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### b. CRITICAL: Using Vercel createOpenAI() provider pattern
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// WRONG
|
|
331
|
+
import { createOpenAI } from '@ai-sdk/openai'
|
|
332
|
+
const openai = createOpenAI({ apiKey })
|
|
333
|
+
streamText({ model: openai('gpt-4o'), messages })
|
|
334
|
+
|
|
335
|
+
// CORRECT
|
|
336
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
337
|
+
import { chat } from '@tanstack/ai'
|
|
338
|
+
chat({ adapter: openaiText('gpt-5.2'), messages })
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### c. CRITICAL: Using monolithic openai() instead of openaiText()
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// WRONG
|
|
345
|
+
import { openai } from '@tanstack/ai-openai'
|
|
346
|
+
chat({ adapter: openai(), model: 'gpt-5.2', messages })
|
|
347
|
+
|
|
348
|
+
// CORRECT
|
|
349
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
350
|
+
chat({ adapter: openaiText('gpt-5.2'), messages })
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The monolithic `openai()` adapter is deprecated. Use tree-shakeable adapters:
|
|
354
|
+
`openaiText()`, `openaiImage()`, `openaiSpeech()`, etc.
|
|
355
|
+
|
|
356
|
+
### d. HIGH: Using toResponseStream instead of toServerSentEventsResponse
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
// WRONG
|
|
360
|
+
import { toResponseStream } from '@tanstack/ai'
|
|
361
|
+
return toResponseStream(stream, { abortController })
|
|
362
|
+
|
|
363
|
+
// CORRECT
|
|
364
|
+
import { toServerSentEventsResponse } from '@tanstack/ai'
|
|
365
|
+
return toServerSentEventsResponse(stream, { abortController })
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### e. HIGH: Passing model as separate parameter to chat()
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// WRONG
|
|
372
|
+
chat({ adapter: openaiText(), model: 'gpt-5.2', messages })
|
|
373
|
+
|
|
374
|
+
// CORRECT
|
|
375
|
+
chat({ adapter: openaiText('gpt-5.2'), messages })
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The model is passed to the adapter factory, not to `chat()`.
|
|
379
|
+
|
|
380
|
+
### f. HIGH: Nesting temperature/maxTokens in options object
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// WRONG
|
|
384
|
+
chat({ adapter, messages, options: { temperature: 0.7, maxTokens: 1000 } })
|
|
385
|
+
|
|
386
|
+
// CORRECT
|
|
387
|
+
chat({ adapter, messages, temperature: 0.7, maxTokens: 1000 })
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
All parameters are top-level on the `chat()` options object.
|
|
391
|
+
|
|
392
|
+
### g. HIGH: Using providerOptions instead of modelOptions
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// WRONG
|
|
396
|
+
chat({
|
|
397
|
+
adapter,
|
|
398
|
+
messages,
|
|
399
|
+
providerOptions: { responseFormat: { type: 'json_object' } },
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
// CORRECT
|
|
403
|
+
chat({
|
|
404
|
+
adapter,
|
|
405
|
+
messages,
|
|
406
|
+
modelOptions: { responseFormat: { type: 'json_object' } },
|
|
407
|
+
})
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### h. HIGH: Implementing custom SSE stream instead of using toServerSentEventsResponse
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// WRONG
|
|
414
|
+
const readable = new ReadableStream({
|
|
415
|
+
async start(controller) {
|
|
416
|
+
const encoder = new TextEncoder()
|
|
417
|
+
for await (const chunk of stream) {
|
|
418
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`))
|
|
419
|
+
}
|
|
420
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
421
|
+
controller.close()
|
|
422
|
+
},
|
|
423
|
+
})
|
|
424
|
+
return new Response(readable, {
|
|
425
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// CORRECT
|
|
429
|
+
import { toServerSentEventsResponse } from '@tanstack/ai'
|
|
430
|
+
return toServerSentEventsResponse(stream, { abortController })
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
`toServerSentEventsResponse` handles SSE formatting, abort signals,
|
|
434
|
+
error events (RUN_ERROR), and correct headers automatically.
|
|
435
|
+
|
|
436
|
+
### i. HIGH: Implementing custom onEnd/onFinish callbacks instead of middleware
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
// WRONG
|
|
440
|
+
chat({
|
|
441
|
+
adapter,
|
|
442
|
+
messages,
|
|
443
|
+
onEnd: (result) => {
|
|
444
|
+
trackAnalytics(result)
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
// CORRECT
|
|
449
|
+
import type { ChatMiddleware } from '@tanstack/ai'
|
|
450
|
+
|
|
451
|
+
const analytics: ChatMiddleware = {
|
|
452
|
+
name: 'analytics',
|
|
453
|
+
onFinish(ctx, info) {
|
|
454
|
+
trackAnalytics({ reason: info.finishReason, iterations: ctx.iteration })
|
|
455
|
+
},
|
|
456
|
+
onUsage(ctx, usage) {
|
|
457
|
+
trackTokens(usage.totalTokens)
|
|
458
|
+
},
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
chat({ adapter, messages, middleware: [analytics] })
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
`chat()` has no `onEnd`/`onFinish` option. Use `middleware` for lifecycle events.
|
|
465
|
+
See also: ai-core/middleware/SKILL.md.
|
|
466
|
+
|
|
467
|
+
### j. HIGH: Importing from @tanstack/ai-client instead of framework package
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
// WRONG
|
|
471
|
+
import { fetchServerSentEvents } from '@tanstack/ai-client'
|
|
472
|
+
import { useChat } from '@tanstack/ai-react'
|
|
473
|
+
|
|
474
|
+
// CORRECT
|
|
475
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Framework packages re-export everything needed from `@tanstack/ai-client`.
|
|
479
|
+
Import from `@tanstack/ai-client` only in vanilla JS (no framework).
|
|
480
|
+
|
|
481
|
+
### k. MEDIUM: Not handling RUN_ERROR events in streaming context
|
|
482
|
+
|
|
483
|
+
Streaming errors arrive as `RUN_ERROR` events in the stream, not as thrown
|
|
484
|
+
exceptions. The `useChat` hook surfaces these via the `error` state and
|
|
485
|
+
`onError` callback. If you consume the stream manually (without `useChat`),
|
|
486
|
+
check for `RUN_ERROR` chunks:
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
for await (const chunk of stream) {
|
|
490
|
+
if (chunk.type === 'RUN_ERROR') {
|
|
491
|
+
console.error('Stream error:', chunk.error.message)
|
|
492
|
+
break
|
|
493
|
+
}
|
|
494
|
+
if (chunk.type === 'TEXT_MESSAGE_CONTENT') {
|
|
495
|
+
process.stdout.write(chunk.delta)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
If not handled, the UI appears to hang with no feedback.
|
|
501
|
+
|
|
502
|
+
## Cross-References
|
|
503
|
+
|
|
504
|
+
- See also: **ai-core/tool-calling/SKILL.md** -- Most chats include tools
|
|
505
|
+
- See also: **ai-core/adapter-configuration/SKILL.md** -- Adapter choice affects available features
|
|
506
|
+
- See also: **ai-core/middleware/SKILL.md** -- Use middleware for analytics and lifecycle events
|