Command Palette

Search for a command to run...

Hectal
PHASE 8Advanced ~27 min· topic 1 of 9

Topic 8.1

Tool Calling from the UI's Side

In one line

A tool call is a loop: the model asks for an action by name, your code runs it, and the result goes back to the model, which then writes the answer. The model never runs anything itself, and the screen has to show each step of the loop.

Think of it like this

A waiter in a restaurant. You say "is the soup vegetarian?". The waiter does not know, so they walk to the kitchen, ask the chef, come back, and tell you. While they are away you see them go, and you know something is happening. The model is the waiter, your code is the kitchen, and the screen is the dining room where the customer should be able to see the waiter walk.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Tool
A function you describe to the model by name, purpose and input shape. The model asks for it. Your code runs it.
Tool call
The model's request to run a tool: the tool's name and an input. The model sends this instead of text.
Tool result
What your function returned. It is added to the conversation so the model can write the answer from it.
Step
One trip to the model. A question that needs one tool takes two steps: one that asks for the tool, one that writes the answer.
Tool part
The piece of the assistant message that stands for one tool call. Its type is tool- plus the tool's name.
execute
The function you write that does the real work of a tool. Without it, the tool runs somewhere else (Topic 8.5).
Stand-in model
The scripted model this course uses so examples run without a key. It says what you tell it to say. In a real app you swap it for a real model.

Step by step

01The loop, drawn

Read the diagram from the top. The browser sends one request, and the server talks to the model twice before the answer comes back. The user sees one answer, but two model calls and one function call happened.

The loop, drawndiagram
Rendering diagram…

02Watch the states in the first example

The first live example asks "Where is ORD-1042?". The server defines one tool, lookupOrder, and gives it a made-up delay of 80 ms so you can see the middle state. Look at the console: the tool part is input-available while the function runs, then output-available, and only after that does the text arrive.

The server line (server) running lookupOrder for ORD-1042 is the function running on your server. It is printed first because the browser only learns about the tool once the model has asked for it.

03What the model is sent on step 2

The second example removes the browser and prints the roles of the messages the model receives on each call. Call 1 gets only the user's question. Call 2 gets three messages: the question, an assistant message holding the tool call, and a tool message holding the result.

That is the whole trick. The "memory" of what the tool returned is just more messages in the list you send. This also means every tool call makes the next call longer, and longer inputs cost more (Topic 8.7).

04When the tool throws

The third example makes execute throw. The tool part ends in output-error, the model is told the tool failed, and it writes an apology. The loop did not crash.

Look at errorText: it says An error occurred., not orders database timed out. By default the SDK hides the real message from the browser, because error text from a server can contain secrets such as table names or addresses. The real message stays in your server logs.

05What you build in this phase

Topics 8.2 to 8.4 define tools and draw them. 8.5 moves a tool into the browser. 8.6 and 8.8 put the person in charge of dangerous actions. 8.7 limits and counts the steps. 8.9 gives Mitra two tools: one that looks up an order, and one that cancels it after approval.

Try it yourself

  1. 1

    Slow the tool down

    In the first example change the delay inside execute from 80 to 800, press Run, and watch the page, not the console.

    For most of a second the page says Tool: lookupOrder (input-available) and nothing else. That is the gap a progress line (Topic 8.4) is for.

  2. 2

    Change what the stand-in model asks for

    In the second example change ORD-1042 in the first step to ORD-9999. The tool still returns shipped, because this example's function does not check the id.

    The model only passes along what it was told. Whether an id makes sense is checked by your code (Topic 8.2).

  3. 3

    Expose the real error message (carefully)

    In the third example, change the last line of the route from result.toUIMessageStreamResponse() to result.toUIMessageStreamResponse({ onError: (e) => e instanceof Error ? e.message : "error" }).

    The console line now ends its first part with errorText: orders database timed out instead of An error occurred.. That is handy while developing and risky in production, where server messages can leak names and addresses.

Code & diagrams

One tool, two steps: ask for the tool, then answer

The model here is a scripted stand-in, so no key is needed. In a real app the one line that changes is `model:`. The last line presses the button for you.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

no tool part yet
(server) running lookupOrder for ORD-1042
lookupOrder is input-available
lookupOrder is output-available
lookupOrder is output-available | text: Your Masala chai 250 g is shipped.
What the model receives on each call

No screen this time: this is the server side of the loop. The first step ends with the reason tool-calls (the model wants something), the second with stop (the model is done).

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

model call 1 receives: user[text]
model call 2 receives: user[text] , assistant[tool-call] , tool[tool-result]
steps: 2 | finish reasons: tool-calls, stop
final text: Shipped.
When the tool throws

A tool that throws is not a failed request: the stream still succeeds. The error is a state of one tool part, and the model still writes an answer. The real message (orders database timed out) stays on the server.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

lookupOrder is output-error | errorText: An error occurred.
lookupOrder is output-error | errorText: An error occurred. | Sorry, I could not look up your order right now. Please try again in a minute.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget stopWhen

In the first example delete the line stopWhen: stepCountIs(3), and press Run.

terminal
$ Console
── what you'll see ──
no tool part yet
(server) running lookupOrder for ORD-1042
lookupOrder is input-available
lookupOrder is output-available

Myth vs fact

Myth

The model runs the tool.

Fact

The model only writes a request: a name and an input. Your code runs the function, with your permissions. That is why you can, and must, check every request.

Myth

A tool call means a second request from the browser.

Fact

With a server tool, the whole loop runs inside one request and one stream. The browser sees one response containing tool parts and text.

Myth

If a tool throws, the whole chat breaks.

Fact

The SDK turns the error into a tool result of type error, the model writes an answer from it, and the stream goes on. Only that tool part is in the output-error state.

Myth

Tool results are only for the model.

Fact

They are in the message too, so the browser can show them. Anything you return from execute can reach the screen, so return only what the user may see.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Every step re-sends the whole conversation plus all earlier tool results. A three-step answer costs far more input than three times a single question, and each step adds latency. Count steps (Topic 8.7) before you add "just one more" tool.

  • ▸

    Stream order matters for UX: the tool part appears when the model has *asked*, not when the function starts. Between those two moments the input may still be arriving piece by piece (input-streaming, Topic 8.3).

  • ▸

    Return small, plain results from execute. Whatever you return is sent to the model (costing tokens) and to the browser (costing bytes), and can be read by anyone with devtools open. Do not return the whole database row when the answer needs two fields.

  • ▸

    In the examples a stand-in model decides from the messages it is sent ("no tool result yet means ask, a result means answer"). A real model also decides, but from its training. Never rely on it calling a tool: write the screen so the answer still makes sense if it does not.

Remember this

  1. 1

    The problem this solves: A language model only produces text. It cannot read your database, check today's date, or cancel an order. Without tools, it either says "I don't know" or invents an answer. Tool calling lets the model ask your program for real facts and real actions. The UI problem is that this takes extra time and extra steps the user cannot see unless you show them.

  2. 2

    A tool is a function you describe to the model: a name (lookupOrder), a sentence about what it does, and the shape of its input. The model never receives the function's code. It receives the description, and when it decides the description fits the question, it replies with a tool call: the tool's name and a JSON input, such as { "orderId": "ORD-1042" }.

  3. 3

    The loop has four moves. (1) You send the conversation and the tool descriptions to the model. (2) The model answers with a tool call instead of text. (3) Your code runs the function with that input. (4) You send the conversation again, now with the tool result added, and the model writes the final text from it. Each trip to the model is one step. A question that needs one tool takes two steps: ask, then answer.

  4. 4

    Who runs the tool? Usually your server. The model provider only sees text, so it is your code that touches the database. The AI SDK does the loop for you on the server: streamText calls the model, sees the tool call, runs your execute function, calls the model again, and streams everything to the browser as one response. This is why a tool call is not a second HTTP request from the browser. (Topic 8.5 shows the exception: tools that run in the browser.)

  5. 5

    What the browser sees. A tool call becomes a part in the assistant message, just like a text part. Its type is tool-lookupOrder (the word tool- plus the tool name) and its state changes over time: first the input is known, then the output arrives. Topic 8.3 lists every state. Because the message is updated in place, your component re-renders and can show "Looking up your order…" and then the result.

  6. 6

    Why the UI must show it. Tools make answers slower: a lookup can take a second or more, and then the model still has to write the answer. If the screen shows only a blank bubble, the user thinks it is stuck. Worse, tools can *do* things. A person should always be able to see which action the assistant took on their behalf. Hiding tool activity is the same kind of mistake as hiding that an answer was written by an AI (Topic 10.2 returns to honesty in AI interfaces).

  7. 7

    Tools can fail, and so can the model's request. The tool can throw an error. The model can ask for a tool that does not exist, or send input in the wrong shape. The SDK catches all of these and tells the model what went wrong, so it can answer politely. Your job is to make sure the *screen* does too.

Explain it without notes

01

Walk through what happens, step by step, when a user asks "Where is ORD-1042?" and the assistant has a lookup tool.

02

Why does the model never run a tool itself, and what follows from that for security?

03

Why can a tool call take two model calls but only one request from the browser?

04

A tool throws an error. What does the user see, what does the model see, and what stays on the server?

Practice

01

Change the first example so the tool name shown on the page is friendly ("Checking your order…" while the tool runs) instead of the raw lookupOrder (input-available).

02

Add a second tool, returnPolicy, that returns { days: 7 }, and let the stand-in model ask for both in the same step. What does the console show?

Trade-offs

  • ↔

    Tools make answers true (real data, real actions) at the price of latency, cost and a bigger attack surface. Every tool you add is something a confused or tricked model can ask for.

  • ↔

    Running tools on the server keeps keys and data safe and the loop simple. Running them in the browser (Topic 8.5) is the only way to reach the page or the device, but the result then needs one more trip to the server.

Done when you can

  • I can draw the tool loop: question, tool call, function, result, answer.

  • I can explain why the model only asks and my code runs.

  • I can read a tool part's type and state in a message.

  • I can show a tool that throws without breaking the chat.

  • I know what stopWhen is for and what happens without it.