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