본문으로 건너뛰기

클라이언트 도구

클라이언트 도구는 브라우저에서 실행되므로 UI 업데이트, 로컬 저장소 액세스 및 브라우저 API 상호작용이 가능합니다. 서버 도구와 달리 클라이언트 도구는 서버 정의에 execute 함수가 없습니다.

sequenceDiagram
participant LLM Service
participant Server
participant Browser
participant ClientTool

LLM Service->>Server: tool_call chunk<br/>{name: "updateUI", args: {...}}
Server->>Server: Check if tool has<br/>server execute

Note over Server: No execute function<br/>= client tool

Server->>Browser: RUN_FINISHED client-tool<br/>interrupt via SSE/HTTP
Browser->>Browser: Find registered<br/>client tool
Browser->>ClientTool: execute(args)
ClientTool->>ClientTool: Update UI,<br/>localStorage, etc.
ClientTool-->>Browser: Return result
Browser->>Server: POST tool result
Server->>LLM Service: Add tool_result<br/>to conversation

Note over LLM Service: Model uses result<br/>to continue

LLM Service-->>Server: Stream response
Server-->>Browser: Forward chunks

클라이언트 도구를 사용하는 경우​

  • UI 업데이트: 알림을 표시하고, 양식을 업데이트하며, 표시 여부를 전환합니다.
  • 로컬 저장소: 사용자 기본 설정을 저장하고 데이터를 캐시합니다.
  • 브라우저 API: 위치 정보, 카메라, 클립보드에 액세스합니다.
  • 상태 관리: React/Vue/Solid 상태를 업데이트합니다.
  • 탐색: 경로를 변경하고 섹션으로 스크롤합니다.

작동 방식​

  1. LLM의 도구 호출: LLM이 클라이언트 도구를 호출하기로 결정합니다.
  2. 서버 감지: 서버가 도구에 execute 함수가 없음을 확인합니다.
  3. 클라이언트 알림: 서버가 인터럽트 와이어에서 내부 client-tool-execution 일시 중지를 내보냅니다(interrupts의 공개 항목은 아님).
  4. 클라이언트 실행: 브라우저가 도구 이름으로 등록된 .client() 구현을 찾아 파싱된 입력으로 실행합니다.
  5. 결과 반환: 클라이언트가 재개 배치를 통해 결과를 자동 제출합니다.
  6. 서버 업데이트: 결과를 검증하고 대화에 추가합니다.
  7. LLM의 계속 실행: LLM이 결과를 받고 대화를 계속합니다.

네이티브 클라이언트 도구 실행은 원자적 인터럽트 배치 수명 주기를 공유하지만(여러 항목의 제출을 게이트할 수 있음) 자동으로 해결됩니다. 따라서 이에 대해 resolveInterrupt를 호출하지 않습니다. 일시적 수명 주기, 배치 및 과거의 tool-input-available 사용자 지정 이벤트에서의 마이그레이션은 인터럽트를 참조하세요. hydration 후에는 보류 중인 클라이언트 도구 실행이 복원되지만 다시 실행되지는 않습니다. 복구 정책은 클라이언트 영속성을 참조하세요.

승인은 별도의 축입니다​

클라이언트 도구에 승인이 필요할 수도 있으며, 승인은 브라우저 결과와 별개입니다. needsApproval: true를 추가하면 도구가 먼저 tool-approval 인터럽트에서 일시 중지됩니다. 해당 결정만 해결하면 됩니다. 승인되면 클라이언트가 .client() 구현을 자동으로 실행하고 결과를 반환합니다.

const approval = interrupts.find(
(interrupt) =>
interrupt.kind === 'tool-approval' &&
interrupt.toolName === 'delete_local_data',
)

if (
approval?.kind === 'tool-approval' &&
approval.toolName === 'delete_local_data'
) {
approval.resolveInterrupt(true)
}

실행을 수동으로 해결하지 않습니다. 이를 위해 .client() 구현이 사용됩니다. .client() 구현 없이 도구를 등록하고 결과를 직접 제공하려면 addToolResult를 사용하세요(도구의 출력 스키마에 따라 검증됨). 이 방법은 레거시 스트림의 과거 경로도 유지합니다. 승인 양식은 도구 승인 흐름을, 승인 수명 주기는 인터럽트를 참조하세요.

클라이언트 도구 정의​

클라이언트 도구는 동일한 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().meta({ description: "Message to display" }),
type: z.enum(["success", "error", "info"]).meta({ description: "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().meta({ description: "Storage key" }),
value: z.string().meta({ description: "Value to store" }),
}),
outputSchema: z.object({
saved: z.boolean(),
}),
});

클라이언트 도구 사용​

서버 측​

LLM이 클라이언트 도구에 액세스하도록 하려면 채팅을 생성할 때 도구 구현이 아닌 도구 정의를 서버에 전달합니다.

// api/chat/route.ts
import { chat, toServerSentEventsResponse } 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.5"),
messages,
tools: [updateUIDef, saveToLocalStorageDef], // Pass definitions
});

return toServerSentEventsResponse(stream);
}

보안: 위와 같이 정의를 정적으로 등록하는 것이 안전한 기본값입니다. 서버만 모델에 표시할 도구를 결정하므로 클라이언트가 승인하지 않은 도구를 알릴 수 없습니다. 대신 클라이언트가 AG-UI RunAgentInput.tools를 통해 요청마다 도구를 선언하도록 하려면 mergeAgentTools를 사용하세요. params.tools는 클라이언트가 제어하므로 먼저 보안 참고 사항을 읽어야 합니다.

클라이언트 측​

자동 실행과 완전한 타입 안전성을 갖춘 클라이언트 구현을 생성합니다.

// app/chat.tsx
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import {
createChatClientOptions,
type InferChatMessages,
type ToolCallPart,
type MessagePart,
} from "@tanstack/ai-client";
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";

const updateUIDef = toolDefinition({
name: "update_ui",
description: "Update the UI with new information",
inputSchema: z.object({
message: z.string().meta({ description: "Message to display" }),
type: z.enum(["success", "error", "info"]).meta({ description: "Message type" }),
}),
outputSchema: z.object({ success: z.boolean() }),
});

const saveToLocalStorageDef = toolDefinition({
name: "save_to_local_storage",
description: "Save data to browser local storage",
inputSchema: z.object({
key: z.string().meta({ description: "Storage key" }),
value: z.string().meta({ description: "Value to store" }),
}),
outputSchema: z.object({ saved: z.boolean() }),
});

// Step 1: Create client implementations (module scope)
const updateUI = updateUIDef.client((input) => {
// Update UI state - fully typed!
console.log(input.message, input.type);
return { success: true };
});

const saveToLocalStorage = saveToLocalStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});

// Step 2: A plain array is all you need — literal tool names, inputs and
// outputs are inferred without any wrapper or `as const`.
const tools = [updateUI, saveToLocalStorage];

const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});

// Step 3: Infer message types for full type safety
type ChatMessages = InferChatMessages<typeof chatOptions>;

function ChatComponent() {
const { messages, sendMessage, isLoading } = useChat(chatOptions);

// Step 4: Render with full type safety
return (
<div>
{messages.map((message) => (
<MessageComponent key={message.id} message={message} />
))}
</div>
);
}

// Messages component with full type safety
function MessageComponent({ message }: { message: ChatMessages[number] }) {
return (
<div>
{message.parts.map((part: MessagePart) => {
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>
);
}
}
return null;
})}
</div>
);
}

자동 실행​

모델이 클라이언트 도구를 호출하면 클라이언트 도구가 자동으로 실행됩니다. 흐름은 다음과 같습니다.

  1. LLM이 클라이언트 도구를 호출합니다.
  2. 서버가 브라우저에 client-tool-execution 인터럽트를 보냅니다.
  3. 클라이언트가 일치하는 도구 구현을 자동으로 실행합니다.
  4. 결과가 서버로 다시 전송됩니다.
  5. 대화가 결과와 함께 계속됩니다.

클라이언트 런타임 컨텍스트​

클라이언트 도구는 두 번째 인수로 타입이 지정된 런타임 컨텍스트를 받을 수 있습니다. 이 컨텍스트는 ChatClient 또는 프레임워크 훅 인스턴스에 로컬이며 서버로 직렬화되지 않습니다.

import { createChatClientOptions } from "@tanstack/ai-client";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import { toolDefinition } from "@tanstack/ai";
import { toast } from "./toast";

const activeProjectId = "";

type ClientContext = {
activeProjectId: string;
toast(message: string): void;
};

const showToast = toolDefinition({
name: "show_toast",
description: "Show a browser notification",
}).client<ClientContext>((_input, ctx) => {
ctx.context.toast(`Project ${ctx.context.activeProjectId} updated`);
return { ok: true };
});

const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools: [showToast],
context: {
activeProjectId,
toast: (message) => toast(message),
},
});

const chat = useChat(chatOptions);

로컬 브라우저 종속성에는 context를 사용합니다. 서버에도 클라이언트의 값이 필요한 경우 forwardedProps로 전송하고, 라우트에서 검증한 후 서버의 chat({ context })에 명시적으로 매핑합니다. 전체 패턴은 런타임 컨텍스트를 참조하세요.

clientTools() 헬퍼(선택 사항)​

일반 배열인 tools: [toolA, toolB]만 전달하면 됩니다. 래퍼나 as const 없이 도구 이름, 입력 및 출력이 추론됩니다. clientTools()는 동일한 캡처를 명시적으로 수행하는 선택적 identity 헬퍼입니다. 훅/옵션 호출 외부에서 공유 가능하고 재사용할 수 있는 도구 튜플을 빌드하려는 경우에만 사용하세요.

import { clientTools } from "@tanstack/ai-client";
import { toolDefinition } from "@tanstack/ai";

const notify = toolDefinition({
name: "notify",
description: "Show a notification",
}).client(() => ({ ok: true }));

// Equivalent to `const tools = [notify]` — just captured explicitly.
const tools = clientTools(notify);

타입 안전성의 이점​

동형 아키텍처는 종단 간 완전한 타입 안전성을 제공합니다.

import type { UIMessage } from "@tanstack/ai-client";

const messages: UIMessage[] = [];

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!
}
}
});
});

도구 상태​

tool-call 파트는 UI에 진행 상태를 표시할 수 있도록 관찰 가능한 소수의 ToolCallState 값을 거칩니다:

  • awaiting-input — 모델이 도구를 호출하려 하지만 아직 인수가 도착하지 않았습니다.
  • input-streaming — 모델이 도구 인수를 스트리밍하고 있습니다(일부 입력을 사용할 수 있을 수 있음).
  • input-complete — 모든 인수를 수신했으며 도구를 실행할 수 있습니다.
  • approval-requested — 도구가 실행되기 전에 사용자 승인을 기다리고 있습니다.
  • approval-responded — 사용자가 도구 호출을 승인하거나 거부했습니다.

ToolCallState 유니온에는 complete 값이 포함되지만 런타임은 tool-call 파트를 이 값으로 전환하지 않습니다. 완료된 호출은 input-complete 상태로 마무리됩니다. 도구가 실행되면 결과가 두 가지 방식으로 나타납니다. tool-call 파트에 part.output이 채워지고, 자체 state가 complete 또는 error인 형제 tool-result 파트가 생성됩니다(error인 경우 part.error를 포함합니다). 로딩/스트리밍 진행 상태에는 tool-call 상태를 사용하고, 최종 성공/오류 피드백에는 tool-result 파트를 사용하세요.

import type { ToolCallPart } from "@tanstack/ai-client";

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, running tool...</div>;
}

// Completion shows up as a populated `part.output` (and as a sibling
// `tool-result` part whose state is `complete` / `error`).
if (part.output) {
return <div>✅ Tool complete</div>;
}

return null;
}

하이브리드 도구​

도구는 서버와 클라이언트 모두에 구현하여 유연하게 실행할 수 있습니다.

import { toolDefinition, chat } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { z } from "zod";
import { db } from "./db";

// 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.5'), messages: [], tools: [addToCartDef] }); // Client will execute

// Or pass server implementation for server execution
chat({ adapter: openaiText('gpt-5.5'), messages: [], tools: [addToCartServer] }); // Server will execute

모범 사례​

  • 클라이언트 도구를 단순하게 유지 - 클라이언트 도구는 브라우저에서 실행되므로 번들 크기를 불필요하게 키울 수 있는 무거운 계산이나 큰 종속성을 피합니다.
  • 오류를 적절하게 처리 - 도구 구현에서 명확한 오류 처리를 정의하고 출력 스키마에 의미 있는 오류 메시지를 반환합니다.
  • UI를 반응형으로 업데이트 - 도구 실행에 따라 UI를 업데이트하려면 프레임워크의 상태 관리(예: React/Vue/Solid)를 사용합니다.
  • 민감한 데이터 보호 - API 키나 개인 정보와 같은 민감한 데이터를 로컬 저장소에 저장하거나 클라이언트 도구를 통해 노출하지 않습니다.
  • 피드백 제공 - 도구 상태를 사용하여 진행 중인 작업과 클라이언트 도구 실행 결과(로딩 스피너, 성공 메시지, 오류 알림)를 사용자에게 알립니다.
  • 모든 항목에 타입 지정 - 도구 정의부터 구현 및 사용까지 완전한 타입 안전성을 위해 TypeScript와 Zod 스키마를 활용합니다.

일반적인 사용 사례​

  • UI 업데이트 - 알림 표시, 양식 업데이트, 표시 여부 전환
  • 로컬 저장소 - 사용자 기본 설정 저장, 데이터 캐시
  • 브라우저 API - 위치 정보, 카메라, 클립보드 액세스
  • 상태 관리 - React/Vue/Solid 상태 업데이트
  • 탐색 - 경로 변경, 섹션으로 스크롤
  • 분석 - 사용자 상호작용 추적

다음 단계​