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.

ソース情報

リポジトリ
TanStack/ai
ソースの最終更新活動
2026年10月13日 05:44
検出された SKILL.md の言語
英語
スター
3,199
フォーク
366

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
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 } = await request.json() const stream = chat({ adapter: openaiText('gpt-5.5'), messages, 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'), 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 } = await request.json() const stream = chat({ adapter: openaiText('gpt-5.5'), messages, 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 } = await request.json() const stream = chat({ adapter: openaiText('gpt-5.5'), messages, 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'), 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` remain as deprecated compatibility shims during migration. ```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'), 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({ connection: fetchServerSentEvents('/api/chat'), tools: [sendEmailDef], }) // Payloadless tool-approvals only const approveAll = () => resolveInterrupts(true)
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る