@tanstack/ai 0.10.0 → 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.
@@ -0,0 +1,411 @@
1
+ ---
2
+ name: ai-core/tool-calling
3
+ description: >
4
+ Isomorphic tool system: toolDefinition() with Zod schemas,
5
+ .server() and .client() implementations, passing tools to both
6
+ chat() on server and useChat/clientTools on client, tool approval
7
+ flows with needsApproval and addToolApprovalResponse(), lazy tool
8
+ discovery with lazy:true, rendering ToolCallPart and ToolResultPart
9
+ in UI.
10
+ type: sub-skill
11
+ library: tanstack-ai
12
+ library_version: '0.10.0'
13
+ sources:
14
+ - 'TanStack/ai:docs/tools/tools.md'
15
+ - 'TanStack/ai:docs/tools/server-tools.md'
16
+ - 'TanStack/ai:docs/tools/client-tools.md'
17
+ - 'TanStack/ai:docs/tools/tool-approval.md'
18
+ - 'TanStack/ai:docs/tools/lazy-tool-discovery.md'
19
+ ---
20
+
21
+ # Tool Calling
22
+
23
+ This skill builds on ai-core. Read it first for critical rules.
24
+
25
+ ## Setup
26
+
27
+ Complete end-to-end example: shared definition, server tool, client tool, server route, React client.
28
+
29
+ ```typescript
30
+ // tools/definitions.ts
31
+ import { toolDefinition } from '@tanstack/ai'
32
+ import { z } from 'zod'
33
+
34
+ export const getProductsDef = toolDefinition({
35
+ name: 'get_products',
36
+ description: 'Search for products in the catalog',
37
+ inputSchema: z.object({
38
+ query: z.string().meta({ description: 'Search keyword' }),
39
+ limit: z.number().optional().meta({ description: 'Max results' }),
40
+ }),
41
+ outputSchema: z.object({
42
+ products: z.array(
43
+ z.object({ id: z.string(), name: z.string(), price: z.number() }),
44
+ ),
45
+ }),
46
+ })
47
+
48
+ export const updateCartUIDef = toolDefinition({
49
+ name: 'update_cart_ui',
50
+ description: 'Update the shopping cart UI with item count',
51
+ inputSchema: z.object({ itemCount: z.number(), message: z.string() }),
52
+ outputSchema: z.object({ displayed: z.boolean() }),
53
+ })
54
+ ```
55
+
56
+ ```typescript
57
+ // tools/server.ts
58
+ import { getProductsDef } from './definitions'
59
+
60
+ export const getProducts = getProductsDef.server(async ({ query, limit }) => {
61
+ const results = await db.products.search(query, { limit: limit ?? 10 })
62
+ return {
63
+ products: results.map((p) => ({ id: p.id, name: p.name, price: p.price })),
64
+ }
65
+ })
66
+ ```
67
+
68
+ ```typescript
69
+ // api/chat/route.ts
70
+ import { chat, toServerSentEventsResponse } from '@tanstack/ai'
71
+ import { openaiText } from '@tanstack/ai-openai'
72
+ import { getProducts } from '@/tools/server'
73
+ import { updateCartUIDef } from '@/tools/definitions'
74
+
75
+ export async function POST(request: Request) {
76
+ const { messages } = await request.json()
77
+ const stream = chat({
78
+ adapter: openaiText('gpt-4o'),
79
+ messages,
80
+ tools: [getProducts, updateCartUIDef], // server tool + client definition
81
+ })
82
+ return toServerSentEventsResponse(stream)
83
+ }
84
+ ```
85
+
86
+ ```typescript
87
+ // app/chat.tsx
88
+ import {
89
+ useChat,
90
+ fetchServerSentEvents,
91
+ clientTools,
92
+ createChatClientOptions,
93
+ type InferChatMessages,
94
+ } from "@tanstack/ai-react";
95
+ import { updateCartUIDef } from "@/tools/definitions";
96
+ import { useState } from "react";
97
+
98
+ function ChatPage() {
99
+ const [cartCount, setCartCount] = useState(0);
100
+
101
+ const updateCartUI = updateCartUIDef.client((input) => {
102
+ setCartCount(input.itemCount);
103
+ return { displayed: true };
104
+ });
105
+
106
+ const tools = clientTools(updateCartUI);
107
+ const chatOptions = createChatClientOptions({
108
+ connection: fetchServerSentEvents("/api/chat"),
109
+ tools,
110
+ });
111
+ type Messages = InferChatMessages<typeof chatOptions>;
112
+
113
+ const { messages, sendMessage } = useChat(chatOptions);
114
+
115
+ return (
116
+ <div>
117
+ <span>Cart: {cartCount}</span>
118
+ {(messages as Messages).map((msg) => (
119
+ <div key={msg.id}>
120
+ {msg.parts.map((part) => {
121
+ if (part.type === "text") return <p>{part.content}</p>;
122
+ if (part.type === "tool-call") {
123
+ return <div key={part.id}>Tool: {part.name} ({part.state})</div>;
124
+ }
125
+ return null;
126
+ })}
127
+ </div>
128
+ ))}
129
+ </div>
130
+ );
131
+ }
132
+ ```
133
+
134
+ ## Core Patterns
135
+
136
+ ### Pattern 1: Server-Only Tool
137
+
138
+ Define with `toolDefinition()`, implement with `.server()`, pass to `chat({ tools })`.
139
+ The server executes it automatically. The client never runs code for this tool.
140
+
141
+ ```typescript
142
+ import { toolDefinition } from '@tanstack/ai'
143
+ import { z } from 'zod'
144
+
145
+ const getUserDataDef = toolDefinition({
146
+ name: 'get_user_data',
147
+ description: 'Look up user by ID',
148
+ inputSchema: z.object({
149
+ userId: z.string().meta({ description: "The user's ID" }),
150
+ }),
151
+ outputSchema: z.object({ name: z.string(), email: z.string() }),
152
+ })
153
+
154
+ const getUserData = getUserDataDef.server(async ({ userId }) => {
155
+ const user = await db.users.findUnique({ where: { id: userId } })
156
+ return { name: user.name, email: user.email }
157
+ })
158
+
159
+ // In your route handler:
160
+ const stream = chat({
161
+ adapter: openaiText('gpt-4o'),
162
+ messages,
163
+ tools: [getUserData],
164
+ })
165
+ ```
166
+
167
+ ### Pattern 2: Client-Only Tool
168
+
169
+ Pass the bare definition (no `.server()`) to `chat({ tools })` so the LLM knows
170
+ about it. Pass the `.client()` implementation to `useChat` via `clientTools()`.
171
+
172
+ ```typescript
173
+ import { toolDefinition } from '@tanstack/ai'
174
+ import { z } from 'zod'
175
+
176
+ export const showNotificationDef = toolDefinition({
177
+ name: 'show_notification',
178
+ description: 'Display a toast notification to the user',
179
+ inputSchema: z.object({
180
+ message: z.string(),
181
+ type: z.enum(['success', 'error', 'info']),
182
+ }),
183
+ outputSchema: z.object({ shown: z.boolean() }),
184
+ })
185
+ ```
186
+
187
+ Server -- pass definition only (no execute function):
188
+
189
+ ```typescript
190
+ const stream = chat({
191
+ adapter: openaiText('gpt-4o'),
192
+ messages,
193
+ tools: [showNotificationDef],
194
+ })
195
+ ```
196
+
197
+ Client -- pass `.client()` implementation:
198
+
199
+ ```typescript
200
+ import {
201
+ useChat,
202
+ fetchServerSentEvents,
203
+ clientTools,
204
+ createChatClientOptions,
205
+ } from "@tanstack/ai-react";
206
+ import { showNotificationDef } from "@/tools/definitions";
207
+ import { useState } from "react";
208
+
209
+ function ChatPage() {
210
+ const [toast, setToast] = useState<string | null>(null);
211
+
212
+ const showNotification = showNotificationDef.client((input) => {
213
+ setToast(input.message);
214
+ setTimeout(() => setToast(null), 3000);
215
+ return { shown: true };
216
+ });
217
+
218
+ const { messages, sendMessage } = useChat(
219
+ createChatClientOptions({
220
+ connection: fetchServerSentEvents("/api/chat"),
221
+ tools: clientTools(showNotification),
222
+ })
223
+ );
224
+
225
+ return (
226
+ <div>
227
+ {toast && <div className="toast">{toast}</div>}
228
+ {messages.map((msg) => (
229
+ <div key={msg.id}>
230
+ {msg.parts.map((part) =>
231
+ part.type === "text" ? <p>{part.content}</p> : null
232
+ )}
233
+ </div>
234
+ ))}
235
+ </div>
236
+ );
237
+ }
238
+ ```
239
+
240
+ ### Pattern 3: Tool with Approval Flow
241
+
242
+ Set `needsApproval: true` in the definition. Execution pauses until the client
243
+ calls `addToolApprovalResponse()`. The part has `state: "approval-requested"`
244
+ and an `approval` object with an `id`.
245
+
246
+ ```typescript
247
+ import { toolDefinition } from '@tanstack/ai'
248
+ import { z } from 'zod'
249
+
250
+ export const sendEmailDef = toolDefinition({
251
+ name: 'send_email',
252
+ description: 'Send an email to a recipient',
253
+ inputSchema: z.object({
254
+ to: z.string().email(),
255
+ subject: z.string(),
256
+ body: z.string(),
257
+ }),
258
+ outputSchema: z.object({ success: z.boolean(), messageId: z.string() }),
259
+ needsApproval: true,
260
+ })
261
+
262
+ export const sendEmail = sendEmailDef.server(async ({ to, subject, body }) => {
263
+ const result = await emailService.send({ to, subject, body })
264
+ return { success: true, messageId: result.id }
265
+ })
266
+ ```
267
+
268
+ Client -- render approval UI and respond:
269
+
270
+ ```typescript
271
+ import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
272
+
273
+ function ChatPage() {
274
+ const { messages, addToolApprovalResponse } = useChat({
275
+ connection: fetchServerSentEvents("/api/chat"),
276
+ });
277
+
278
+ return (
279
+ <div>
280
+ {messages.map((msg) => (
281
+ <div key={msg.id}>
282
+ {msg.parts.map((part) => {
283
+ if (part.type === "text") return <p>{part.content}</p>;
284
+ if (
285
+ part.type === "tool-call" &&
286
+ part.state === "approval-requested" &&
287
+ part.approval
288
+ ) {
289
+ return (
290
+ <div key={part.id}>
291
+ <p>Approve "{part.name}"?</p>
292
+ <pre>{part.arguments}</pre>
293
+ <button
294
+ onClick={() =>
295
+ addToolApprovalResponse({
296
+ id: part.approval!.id,
297
+ approved: true,
298
+ })
299
+ }
300
+ >
301
+ Approve
302
+ </button>
303
+ <button
304
+ onClick={() =>
305
+ addToolApprovalResponse({
306
+ id: part.approval!.id,
307
+ approved: false,
308
+ })
309
+ }
310
+ >
311
+ Deny
312
+ </button>
313
+ </div>
314
+ );
315
+ }
316
+ return null;
317
+ })}
318
+ </div>
319
+ ))}
320
+ </div>
321
+ );
322
+ }
323
+ ```
324
+
325
+ ### Pattern 4: Lazy Tool Discovery
326
+
327
+ Set `lazy: true` on rarely-needed tools. The LLM sees their names via a synthetic
328
+ `__lazy__tool__discovery__` tool and discovers schemas on demand. Saves tokens.
329
+
330
+ ```typescript
331
+ import {
332
+ toolDefinition,
333
+ chat,
334
+ toServerSentEventsResponse,
335
+ maxIterations,
336
+ } from '@tanstack/ai'
337
+ import { openaiText } from '@tanstack/ai-openai'
338
+ import { z } from 'zod'
339
+
340
+ const getProductsDef = toolDefinition({
341
+ name: 'getProducts',
342
+ description: 'List all products',
343
+ inputSchema: z.object({}),
344
+ outputSchema: z.array(
345
+ z.object({ id: z.number(), name: z.string(), price: z.number() }),
346
+ ),
347
+ })
348
+ const getProducts = getProductsDef.server(async () => db.products.findMany())
349
+
350
+ const compareProductsDef = toolDefinition({
351
+ name: 'compareProducts',
352
+ description: 'Compare two or more products side by side',
353
+ inputSchema: z.object({ productIds: z.array(z.number()).min(2) }),
354
+ lazy: true, // not sent to LLM upfront
355
+ })
356
+ const compareProducts = compareProductsDef.server(async ({ productIds }) => {
357
+ return db.products.findMany({ where: { id: { in: productIds } } })
358
+ })
359
+
360
+ export async function POST(request: Request) {
361
+ const { messages } = await request.json()
362
+ const stream = chat({
363
+ adapter: openaiText('gpt-4o'),
364
+ messages,
365
+ tools: [getProducts, compareProducts],
366
+ agentLoopStrategy: maxIterations(20),
367
+ })
368
+ return toServerSentEventsResponse(stream)
369
+ }
370
+ ```
371
+
372
+ The LLM sees `getProducts` and `__lazy__tool__discovery__` upfront.
373
+ To compare, it first calls `__lazy__tool__discovery__({ toolNames: ["compareProducts"] })`,
374
+ gets the full schema, then calls `compareProducts` directly.
375
+ Once discovered, a tool stays available for the conversation.
376
+ When all lazy tools are discovered, the discovery tool is removed automatically.
377
+
378
+ ## Common Mistakes
379
+
380
+ ### a. HIGH: Not passing tool definitions to both server and client
381
+
382
+ Server tools need `chat({ tools })`. Client tools need their definition in
383
+ `chat({ tools })` AND their `.client()` in `useChat({ tools: clientTools(...) })`.
384
+
385
+ Wrong -- tool only on server, client cannot execute:
386
+
387
+ ```typescript
388
+ chat({ adapter, messages, tools: [myToolDef] })
389
+ useChat({ connection: fetchServerSentEvents('/api/chat') }) // no tools
390
+ ```
391
+
392
+ Wrong -- tool only on client, LLM does not know about it:
393
+
394
+ ```typescript
395
+ chat({ adapter, messages }); // no tools
396
+ useChat({ ..., tools: clientTools(myToolDef.client(() => result)) });
397
+ ```
398
+
399
+ Correct:
400
+
401
+ ```typescript
402
+ chat({ adapter, messages, tools: [myToolDef] });
403
+ useChat({ ..., tools: clientTools(myToolDef.client((input) => ({ success: true }))) });
404
+ ```
405
+
406
+ Source: docs/tools/tools.md
407
+
408
+ ## Cross-References
409
+
410
+ - See also: ai-core/chat-experience/SKILL.md -- Tools are used within chat
411
+ - See also: `@tanstack/ai-code-mode` package skills -- Code Mode is an alternative to tools for complex multi-step operations