A tool call moves from the model, to your server or the browser, then back as a result. This page shows that path.
Each tool call is a tool-call part in the assistant message. It has a state that changes as the call moves forward. The result arrives as a sibling tool-result part.
Render the two part types next to your text parts:
import type { MessagePart } from "@tanstack/ai-client";
export function ToolStatus({ part }: { part: MessagePart }) {
if (part.type === "tool-call") {
switch (part.state) {
case "awaiting-input":
case "input-streaming":
return <p>Preparing {part.name}...</p>;
case "approval-requested":
return <p>{part.name} waits for your approval</p>;
case "input-complete":
return <p>Running {part.name}...</p>;
default:
// The tool-result part below shows the outcome.
return null;
}
}
if (part.type === "tool-result") {
if (part.state === "error") {
return <p>Tool failed: {part.error}</p>;
}
return <p>Tool done</p>;
}
return null;
}Use it in your message list, for example message.parts.map((part, index) => <ToolStatus key={index} part={part} />). While the model runs your tool, you now see "Running get_weather...", then "Tool done".
To get typed part.name, part.input, and part.output, pass your tools to the chat hook. See Client Tools.
sequenceDiagram
participant User
participant Browser
participant Server
participant LLM Service
User->>Browser: Types message
Browser->>Server: POST /api/chat<br/>{messages, ...}
Server->>Server: Build tool definitions<br/>from tool array
Server->>LLM Service: Send request with:<br/>- messages<br/>- tool definitions<br/>- model config
Note over LLM Service: Model analyzes tools<br/>and decides to use one
LLM Service-->>Server: Stream chunks:<br/>tool_call, content, done
Server-->>Browser: Forward chunks via SSE/HTTP
Browser->>Browser: Parse chunks &<br/>update UI
Browser->>User: Show responseThe state of a tool-call part:
| State | Meaning | What to show |
|---|---|---|
| awaiting-input | The call started. No arguments yet. | A loading state |
| input-streaming | Partial arguments arrive. | Progress |
| input-complete | All arguments are in. | The tool is about to run |
| approval-requested | The call waits for the user. | The approval UI |
| approval-responded | The user approved or denied. | Running, or a denied note |
| complete | The result arrived. | The result |
| error | The call failed or was denied. | The error |
When the result arrives, the call part also gets output.
The state of the sibling tool-result part:
| State | Meaning | What to show |
|---|---|---|
| streaming | Reserved for streamed results. Not used yet. | Progress |
| complete | The result is complete. | The result |
| error | An error occurred before or during the run. part.error has the message. | The error |
A denied or cancelled result is also error. Read part.outcome to tell it apart from a failure. See Tool Approval Flow.
stateDiagram-v2
state "tool-call part (ToolCallState)" as Call {
[*] --> AwaitingInput: tool_call received
AwaitingInput --> InputStreaming: partial arguments
InputStreaming --> InputComplete: all arguments received
InputComplete --> ApprovalRequested: needsApproval=true
ApprovalRequested --> ApprovalResponded: user approves / denies
}
InputComplete --> ResultComplete: needsApproval=false, success
InputComplete --> ResultError: parsing, validation, or execution error
ApprovalResponded --> ResultComplete: approved + success (output set)
ApprovalResponded --> ResultError: approved + error
ApprovalResponded --> Denied: user denied (no execution)
state "tool-result part" as Results {
ResultComplete: complete
ResultError: error
}
ResultComplete --> [*]
ResultError --> [*]
Denied --> [*]sequenceDiagram
participant User
participant Client
participant Server
participant LLM
participant Tool
LLM->>Server: tool_call: send_email
Server->>Server: Check needsApproval
Server->>Client: RUN_FINISHED with an interrupt
Client->>Client: Show approval UI
User->>Client: Clicks "Approve"
Client->>Server: resolveInterrupt, new run resumes
Server->>Tool: execute(args)
Tool-->>Server: result
Server->>LLM: tool_result
LLM-->>Client: Generate responseFor the code, see Tool Approval Flow.
The model can chain tools that run in different places. Here, a server tool reads preferences from the database, and a client tool applies them to the page:
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
import { db } from "./db";
import { i18n } from "./i18n";
// Server: read the preferences from the database
const fetchUserPrefsDef = toolDefinition({
name: "fetch_user_preferences",
description: "Get user preferences from server",
inputSchema: z.object({
userId: z.string(),
}),
});
const fetchUserPreferences = fetchUserPrefsDef.server(async ({ userId }) => {
const prefs = await db.userPreferences.findUnique({ where: { userId } });
return prefs;
});
// Client: apply the preferences to the UI
const applyPrefsDef = toolDefinition({
name: "apply_preferences",
description: "Apply user preferences to the UI",
inputSchema: z.object({
theme: z.string(),
language: z.string(),
}),
});
const applyPreferences = applyPrefsDef.client(async ({ theme, language }) => {
document.body.className = theme;
i18n.changeLanguage(language);
return { applied: true };
});
// The model can call fetch_user_preferences on the server,
// then apply_preferences in the browser with the result.Pass fetchUserPreferences and applyPrefsDef to chat() on the server, and applyPreferences to the chat hook.
The model can call several tools in one turn:
graph TD
A[LLM decides to call 3 tools] --> B[tool_call index: 0]
A --> C[tool_call index: 1]
A --> D[tool_call index: 2]
B --> E[Execute in parallel]
C --> E
D --> E
E --> F[Collect all results]
F --> G[Continue with results]For example, "Compare the weather in NYC, SF, and LA" can give three calls: get_weather for each city. All three run at the same time, then the model writes the comparison.
The server tools of one model turn start together. The model gets their results in the order of its calls. To run them one at a time, see Run tools one at a time.
Tool design:
Security:
Performance:
The streaming protocol under all of this is AG-UI. See the AG-UI docs.