@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,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