客户端工具在浏览器中执行,能够实现 UI 更新、本地存储访问和浏览器 API 交互。与服务器工具不同,客户端工具在其服务器定义中没有 execute 函数。
客户端工具使用相同的 toolDefinition() API,但使用 .client() 方法
// tools/definitions.ts - Shared between server and client
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
export const updateUIDef = toolDefinition({
name: "update_ui",
description: "Update the UI with new information",
inputSchema: z.object({
message: z.string().describe("Message to display"),
type: z.enum(["success", "error", "info"]).describe("Message type"),
}),
outputSchema: z.object({
success: z.boolean(),
}),
});
export const saveToLocalStorageDef = toolDefinition({
name: "save_to_local_storage",
description: "Save data to browser local storage",
inputSchema: z.object({
key: z.string().describe("Storage key"),
value: z.string().describe("Value to store"),
}),
outputSchema: z.object({
saved: z.boolean(),
}),
});
为了让 LLM 访问客户端工具,请在创建聊天时将工具定义(而非实现)传递给服务器
// api/chat/route.ts
import { chat, toServerSentEventsStream } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { updateUIDef, saveToLocalStorageDef } from "@/tools/definitions";
export async function POST(request: Request) {
const { messages } = await request.json();
const stream = chat({
adapter: openaiText("gpt-5.2"),
messages,
tools: [updateUIDef, saveToLocalStorageDef], // Pass definitions
});
return toServerSentEventsStream(stream);
}
创建具有自动执行和完全类型安全的客户端实现
// app/chat.tsx
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import {
clientTools,
createChatClientOptions,
type InferChatMessages
} from "@tanstack/ai-client";
import { updateUIDef, saveToLocalStorageDef } from "@/tools/definitions";
import { useState } from "react";
function ChatComponent() {
const [notification, setNotification] = useState(null);
// Step 1: Create client implementations
const updateUI = updateUIDef.client((input) => {
// Update React state - fully typed!
setNotification({ message: input.message, type: input.type });
return { success: true };
});
const saveToLocalStorage = saveToLocalStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});
// Step 2: Create typed tools array (no 'as const' needed!)
const tools = clientTools(updateUI, saveToLocalStorage);
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
// Step 3: Infer message types for full type safety
type ChatMessages = InferChatMessages<typeof chatOptions>;
const { messages, sendMessage, isLoading } = useChat(chatOptions);
// Step 4: Render with full type safety
return (
<div>
{messages.map((message) => (
<MessageComponent key={message.id} message={message} />
))}
{notification && (
<div className={`notification ${notification.type}`}>
{notification.message}
</div>
)}
</div>
);
}
// Messages component with full type safety
function MessageComponent({ message }: { message: ChatMessages[number] }) {
return (
<div>
{message.parts.map((part) => {
if (part.type === "text") {
return <p>{part.content}</p>;
}
if (part.type === "tool-call") {
// ✅ part.name is narrowed to specific tool names
if (part.name === "update_ui") {
// ✅ part.input is typed as { message: string, type: "success" | "error" | "info" }
// ✅ part.output is typed as { success: boolean } | undefined
return (
<div>
Tool: {part.name}
{part.output && <span>✓ Success</span>}
</div>
);
}
}
})}
</div>
);
}
客户端工具在模型调用它们时会自动执行。无需手动 onToolCall 回调!流程如下:
同构架构提供完整的端到端类型安全
messages.forEach((message) => {
message.parts.forEach((part) => {
if (part.type === "tool-call" && part.name === "update_ui") {
// ✅ TypeScript knows part.name is literally "update_ui"
// ✅ part.input is typed as { message: string, type: "success" | "error" | "info" }
// ✅ part.output is typed as { success: boolean } | undefined
console.log(part.input.message); // ✅ Fully typed!
if (part.output) {
console.log(part.output.success); // ✅ Fully typed!
}
}
});
});
客户端工具会经历一小部分可观察的生命周期状态,你可以将其显示在 UI 中以指示进度
使用这些状态来显示加载指示器、流式传输进度以及最终的成功/错误反馈。下面的示例将每个状态映射到简单的 UI 消息。
function ToolCallDisplay({ part }: { part: ToolCallPart }) {
if (part.state === "awaiting-input") {
return <div>🔄 Waiting for arguments...</div>;
}
if (part.state === "input-streaming") {
return <div>📥 Receiving arguments...</div>;
}
if (part.state === "input-complete") {
return <div>✓ Arguments received, executing...</div>;
}
if (part.output) {
return <div>✅ Tool completed successfully</div>;
}
return null;
}
工具可以同时为服务器和客户端实现,从而实现灵活的执行
// Define once
const addToCartDef = toolDefinition({
name: "add_to_cart",
description: "Add item to shopping cart",
inputSchema: z.object({
itemId: z.string(),
quantity: z.number(),
}),
outputSchema: z.object({
success: z.boolean(),
cartId: z.string(),
}),
});
// Server implementation - Store in database
const addToCartServer = addToCartDef.server(async (input) => {
const cart = await db.carts.create({
data: { itemId: input.itemId, quantity: input.quantity },
});
return { success: true, cartId: cart.id };
});
// Client implementation - Update local wishlist
const addToCartClient = addToCartDef.client((input) => {
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
wishlist.push(input.itemId);
localStorage.setItem("wishlist", JSON.stringify(wishlist));
return { success: true, cartId: "local" };
});
// Server: Pass definition for client execution
chat({ adapter: openaiText('gpt-5.2'), messages: [], tools: [addToCartDef] }); // Client will execute
// Or pass server implementation for server execution
chat({ adapter: openaiText('gpt-5.2'), messages: [], tools: [addToCartServer] }); // Server will execute