Skip to main content

ai-core-tool-calling

Isomorphic tool system: toolDefinition() with Zod schemas, .server() and .client() implementations, passing tools to both chat() on server and useChat/clientTools on client, tool approval flows with needsApproval and bound interrupts (resolveInterrupt), generic middleware interrupts with defineInterrupt(), lazy tool discovery with lazy:true, rendering ToolCallPart and ToolResultPart in UI.

Quellinformationen

Repository
TanStack/ai
Letzte Quellaktivität
22. Oktober 2026 um 04:34
Erkannte Sprache von SKILL.md
Englisch
Sterne
3.324
Forks
379

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
ai-core/tool-calling
description
Isomorphic tool system: toolDefinition() with Zod schemas, .server() and .client() implementations, passing tools to both chat() on server and useChat/clientTools on client, tool approval flows with needsApproval and bound interrupts (resolveInterrupt), generic middleware interrupts with defineInterrupt(), lazy tool discovery with lazy:true, rendering ToolCallPart and ToolResultPart in UI.
type
sub-skill
library
tanstack-ai
library_version
0.42.0
sources
["TanStack/ai:docs/tools/tools.md","TanStack/ai:docs/tools/server-tools.md","TanStack/ai:docs/tools/client-tools.md","TanStack/ai:docs/tools/tool-approval.md","TanStack/ai:docs/tools/lazy-tool-discovery.md"]
# Tool Calling This skill builds on ai-core. Read it first for critical rules. ## Setup Complete end-to-end example: shared definition, server tool, client tool, server route, React client. The four files below share one scope, so later files use the earlier exports directly. ```typescript group=product-catalog // tools/definitions.ts import { toolDefinition } from '@tanstack/ai' import { z } from 'zod' export const getProductsDef = toolDefinition({ name: 'get_products', description: 'Search for products in the catalog', inputSchema: z.object({ query: z.string().meta({ description: 'Search keyword' }), limit: z.number().optional().meta({ description: 'Max results' }), }), outputSchema: z.object({ products: z.array( z.object({ id: z.string(), name: z.string(), price: z.number() }), ), }), }) export const updateCartUIDef = toolDefinition({ name: 'update_cart_ui', description: 'Update the shopping cart UI with item count', inputSchema: z.object({ itemCount: z.number(), message: z.string() }), outputSchema: z.object({ displayed: z.boolean() }), }) ``` ```typescript group=product-catalog // tools/server.ts (uses getProductsDef from tools/definitions.ts) import { db } from './db' export const getProducts = getProductsDef.server(async ({ query, limit }) => { const results: Array<{ id: string; name: string; price: number }> = await db.products.search(query, { limit: limit ?? 10 }) return { products: results.map((p) => ({ id: p.id, name: p.name, price: p.price })), } }) ``` ```typescript group=product-catalog // api/chat/route.ts (uses getProducts and updateCartUIDef from tools/) import { chat, toServerSentEventsResponse } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' export async function POST(request: Request) { const { messages, threadId } = await request.json() const stream = chat({ adapter: openaiText('gpt-5.5'), messages, threadId, tools: [getProducts, updateCartUIDef], // server tool + client definition }) return toServerSentEventsResponse(stream) } ``` ```tsx group=product-catalog // app/chat.tsx (uses updateCartUIDef from tools/definitions.ts) import { useChat, fetchServerSentEvents, createChatClientOptions, type InferChatMessages, } from '@tanstack/ai-react' import { clientTools } from '@tanstack/ai-client' import { useState } from 'react' function ChatPage() { const [cartCount, setCartCount] = useState(0) const updateCartUI = updateCartUIDef.client((input) => { setCartCount(input.itemCount) return { displayed: true } }) const tools = clientTools(updateCartUI) const chatOptions = createChatClientOptions({ connection: fetchServerSentEvents('/api/chat'), threadId: 'support-42', tools, }) const { messages, sendMessage } = useChat(chatOptions) // InferChatMessages ties part types to the configured tools when needed: // type Messages = InferChatMessages<typeof chatOptions> return ( <div> <span>Cart: {cartCount}</span> {messages.map((msg) => ( <div key={msg.id}> {msg.parts.map((part) => { if (part.type === 'text') return <p>{part.content}</p> if (part.type === 'tool-call') { return ( <div key={part.id}> Tool: {part.name} ({part.state}) </div> ) } return null })} </div> ))} </div> ) } ``` ## Core Patterns ### Generic middleware interrupts Use `defineInterrupt()` when middleware needs typed data from the client. This does not replace `needsApproval`. Tool approval asks whether a tool can run. Generic interrupts ask for application data at a chat lifecycle boundary. Define the interrupt once. Register it with both `chat({ interrupts })` and `useChat({ interrupts })`. Emit it only from `onInterruptBoundary`, then read the typed result in `onInterruptResolution`. ```typescript import { defineInterrupt, type ChatMiddleware } from '@tanstack/ai' import { z } from 'zod' const reviewPlan = defineInterrupt({ id: 'review-plan', payloadSchema: z.object({ title: z.string() }), responseSchema: z.object({ approved: z.boolean() }), }) const reviewMiddleware: ChatMiddleware<unknown, typeof reviewPlan> = { onInterruptBoundary(ctx) { if (ctx.phase !== 'beforeTools') return return { interrupts: [ reviewPlan.interrupt({ key: 'release-plan', reason: 'review-required', message: 'Approve this plan?', payload: { title: 'Release plan' }, }), ], } }, onInterruptResolution(_ctx, resumedInterrupts) { for (const result of resumedInterrupts.for(reviewPlan)) { if (result.status === 'resolved' && !result.response.approved) { return { toolResume: 'stop' } } } }, } ``` Several middleware can request generic interrupts at one boundary. They share one AG-UI interrupt batch with tool approvals. A continuation starts only after the client resolves or cancels every bound item. `stop` is more restrictive than `cancel`, which is more restrictive than `continue`. Do not emit raw AG-UI interrupt events from middleware. Use the boundary hook so the engine creates one terminal event and persistence records the batch. ### Pattern 1: Server-Only Tool Define with `toolDefinition()`, implement with `.server()`, pass to `chat({ tools })`. The server executes it automatically. The client never runs code for this tool. ```typescript import { chat, toolDefinition, toServerSentEventsResponse } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' import { z } from 'zod' import { db } from './db' const getUserDataDef = toolDefinition({ name: 'get_user_data', description: 'Look up user by ID', inputSchema: z.object({ userId: z.string().meta({ description: "The user's ID" }), }), outputSchema: z.object({ name: z.string(), email: z.string() }), }) const getUserData = getUserDataDef.server(async ({ userId }) => { const user = await db.users.findUnique({ where: { id: userId } }) return { name: user.name, email: user.email } }) // In your route handler: export async function POST(request: Request) { const { messages, threadId } = await request.json() const stream = chat({ adapter: openaiText('gpt-5.5'), messages, threadId, tools: [getUserData], }) return toServerSentEventsResponse(stream) } ``` ### Pattern 2: Client-Only Tool Pass the bare definition (no `.server()`) to `chat({ tools })` so the LLM knows about it. Pass the `.client()` implementation to `useChat` via `clientTools()`. ```typescript group=notification-tool // tools/definitions.ts import { toolDefinition } from '@tanstack/ai' import { z } from 'zod' export const showNotificationDef = toolDefinition({ name: 'show_notification', description: 'Display a toast notification to the user', inputSchema: z.object({ message: z.string(), type: z.enum(['success', 'error', 'info']), }), outputSchema: z.object({ shown: z.boolean() }), }) ``` Server -- pass definition only (no execute function): ```typescript group=notification-tool // api/chat/route.ts (uses showNotificationDef from tools/definitions.ts) import { chat, toServerSentEventsResponse } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' export async function POST(request: Request) { const { messages, threadId } = await request.json() const stream = chat({ adapter: openaiText('gpt-5.5'), messages, threadId, tools: [showNotificationDef], }) return toServerSentEventsResponse(stream) } ``` Client -- pass `.client()` implementation: ```tsx group=notification-tool // app/chat.tsx (uses showNotificationDef from tools/definitions.ts) import { useChat, fetchServerSentEvents, createChatClientOptions, } from '@tanstack/ai-react' import { clientTools } from '@tanstack/ai-client' import { useState } from 'react' function ChatPage() { const [toast, setToast] = useState<string | null>(null) const showNotification = showNotificationDef.client((input) => { setToast(input.message) setTimeout(() => setToast(null), 3000) return { shown: true } }) const { messages, sendMessage } = useChat( createChatClientOptions({ connection: fetchServerSentEvents('/api/chat'), threadId: 'support-42', tools: clientTools(showNotification), }), ) return ( <div> {toast && <div className="toast">{toast}</div>} {messages.map((msg) => ( <div key={msg.id}> {msg.parts.map((part) => part.type === 'text' ? <p>{part.content}</p> : null, )} </div> ))} </div> ) } ``` ### Pattern 3: Tool with Approval Flow Set `needsApproval: true` in the definition. Execution pauses with `RUN_FINISHED.outcome.type === 'interrupt'`. The primary client API is bound `interrupts` + `resolveInterrupt` / `resolveInterrupts` / `cancel`. `addToolApprovalResponse` and `pendingInterrupts` were removed in 1.0. ```typescript group=email-approval // tools/email.ts import { toolDefinition } from '@tanstack/ai' import { z } from 'zod' import { emailService } from './email-service' export const sendEmailDef = toolDefinition({ name: 'send_email', description: 'Send an email to a recipient', inputSchema: z.object({ to: z.string().email(), subject: z.string(), body: z.string(), }), outputSchema: z.object({ success: z.boolean(), messageId: z.string() }), needsApproval: true, }) export const sendEmail = sendEmailDef.server(async ({ to, subject, body }) => { const result = await emailService.send({ to, subject, body }) return { success: true, messageId: result.id } }) ``` Server route must forward `resume` / `parentRunId` (via `chatParamsFromRequest` or equivalent). Client -- render bound interrupts: ```tsx group=email-approval // app/chat.tsx (registers sendEmailDef so the approval interrupt is typed) import { useChat, fetchServerSentEvents } from '@tanstack/ai-react' function ChatPage() { const { messages, interrupts, sendMessage } = useChat({ connection: fetchServerSentEvents('/api/chat'), threadId: 'support-42', tools: [sendEmailDef], }) return ( <div> {interrupts.map((interrupt) => { if (interrupt.kind !== 'tool-approval') return null return ( <div key={interrupt.id}> <p>Approve "{interrupt.toolName}"?</p> <pre>{JSON.stringify(interrupt.originalArgs, null, 2)}</pre> <button onClick={() => interrupt.resolveInterrupt(true)}> Approve </button> <button onClick={() => interrupt.resolveInterrupt(false)}> Deny </button> <button onClick={() => interrupt.cancel()}>Cancel</button> </div> ) })} {messages.map((msg) => ( <div key={msg.id}> {msg.parts.map((part) => part.type === 'text' ? ( <p key={part.content}>{part.content}</p> ) : null, )} </div> ))} </div> ) } ``` Batch all pending approvals with `resolveInterrupts` (void — submission is async; watch `resuming` / `interruptErrors`): ```tsx group=email-approval function ApproveAllButton() { const { resolveInterrupts, resuming } = useChat({
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen