Topic 8.3
Tool Parts and Their States
In one line
Each tool call in a message is a part whose state moves from input-streaming to input-available to output-available or output-error. With approval, two more states appear. Your component draws one thing per state, and must never read an output that is not there yet.
Think of it like this
A parcel with a tracking page: "label created", "in transit", "delivered" or "delivery failed". The page shows something different for each stage, and it would be silly to show a delivery photo while the parcel is still in transit. A tool part has stages in the same way, and each stage has things that exist and things that do not yet.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Part
- One piece of an assistant message: text, a tool call, a reasoning block, a step marker.
- state
- The stage of a tool call: input-streaming, input-available, approval-requested, approval-responded, output-available, output-error or output-denied.
- input
- What the model asked the tool to do. Complete from input-available on. Partial during input-streaming.
- output
- What the tool returned. Exists only in output-available.
- errorText
- A string describing why a tool failed. Exists only in output-error. By default it is a generic text, not the real error.
- Dynamic tool
- A tool the client does not know at build time. Its part type is
dynamic-tooland its name is intoolName. - isToolUIPart
- The helper that is true for any tool part, static or dynamic.
Step by step
01The state machine
A normal call walks left to right. An approval-gated call takes the detour through the approval states. Any call can end in output-error.
02Watch input arrive piece by piece
The first live example builds the model's stream by hand, so the tool input arrives in three pieces, like a real model's. Read the console: input-streaming appears four times with a longer input each time (undefined, {}, {"orderId":"ORD-"}, then the full object), then input-available, then output-available once the function returns.
The two states in the middle are separate on purpose. input-available is the moment you know exactly what is being asked, and it lasts as long as the function takes to run.
03Draw each state
The second example has one function, describe(part), with a switch on the state. It runs twice: for an order that exists, and for one that does not (the function throws, so the part ends in output-error).
Notice that output-error reads part.input carefully (part.input ? part.input.orderId : "the order"): if the failure happened because the input was invalid, the input may be missing.
04Typed parts in TypeScript
Define the tools once, derive the message type from them, and give it to useChat. Inside each case, part.output is now the exact type your execute returns.
import { InferUITools, UIMessage } from "ai";
import { useChat } from "@ai-sdk/react";
// the same object you pass to streamText({ tools })
export const tools = { lookupOrder, returnPolicy };
export type MitraMessage = UIMessage<never, never, InferUITools<typeof tools>>;
// in a component
const { messages } = useChat<MitraMessage>();05Never crash on a state you did not plan
Always end the switch with default: return null (or a neutral line). The state list has grown in recent versions (the approval states are new), and a future version may add more.
Try it yourself
- 1
Print every state a different way
In the second example, change
default: return "";todefault: return "state: " + part.state;and add a case you did not handle, for example remove theoutput-errorcase.Now the failed order shows
state: output-erroron the page: the fallback line makes a missing case visible instead of leaving a blank. - 2
Read the partial input
In the first example, add
if (part.state === "input-streaming") console.log(Object.keys(part.input || {}));inside theif (part)block.You will see
[]twice (the first timepart.inputisundefined, which is why the code says|| {}) and then[ 'orderId' ]while the id is still being typed out ("ORD-"). The partial object grows key by key, so any code that reads it must allow missing fields.
Code & diagrams
The stream here is built by hand with the SDK's test model, to show the streaming of the input. A real model sends the same kinds of pieces.
Preview
Console
Nothing logged yet.Expected console output
tool-lookupOrder | input-streaming | input: undefined
tool-lookupOrder | input-streaming | input: {}
tool-lookupOrder | input-streaming | input: {"orderId":"ORD-"}
tool-lookupOrder | input-streaming | input: {"orderId":"ORD-1042"}
tool-lookupOrder | input-available | input: {"orderId":"ORD-1042"}
tool-lookupOrder | output-available | input: {"orderId":"ORD-1042"} | output: {"orderId":"ORD-1042","status":"shipped"}The first answer, when complete, triggers the second question, so the example never depends on a timer racing the stream. Press either button to try it yourself.
Preview
Console
Nothing logged yet.Expected console output
input-available -> Looking up ORD-1042…
output-available -> ORD-1042: Masala chai 250 g, shipped
input-available -> Looking up ORD-9999…
output-error -> Could not look up ORD-9999.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
Read the output before it exists
In the second example, delete everything in describe except the line return part.input.orderId + ": " + part.output.item + ", " + part.output.status;, and press Run.
Myth vs fact
Myth
A tool part always starts in input-streaming.
Fact
It can start in input-available. Whether you see input-streaming depends on the model and provider streaming the input in pieces.
Myth
part.output is always there once the tool has run.
Fact
If the tool threw, the state is output-error and there is errorText instead. If the person denied it, the state is output-denied and nothing ran.
Myth
errorText tells me what went wrong.
Fact
By default it is the generic text An error occurred.. The real message stays on the server unless you pass your own onError.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Treat states as a closed
switchwith adefault. Libraries add states over time (the approval states are recent). Adefaultthat renders nothing keeps old code from crashing on new states. - ▸
isToolUIPart(part)andgetToolName(part)work for static and dynamic tools.isStaticToolUIPartnarrows to tools you defined in TypeScript, andisToolOutputErrorUIPartnarrows to failures. Prefer these over comparingpart.typestrings by hand. - ▸
Keys: use
part.toolCallIdas the Reactkeyfor tool parts, not the array index. The id stays stable while the part changes state, so React keeps the DOM (an open<details>, a focused button) instead of re-creating it. (The examples log or use the first matching part; real lists need the key.) - ▸
Persist messages with their parts. If you save only the text, a reloaded chat loses its tool cards and approvals, and a pending approval cannot be answered.
Remember this
- 1
The problem this solves: A tool call is not one event. Its input is written by the model piece by piece, then your function runs, then the result arrives, and any of those can fail. If your component reads
part.output.itemwhile the function is still running,part.outputisundefinedand React crashes with a white screen. You need to know which fields exist in which state. - 2
Where the states live. Each assistant message has
parts. A tool call is one part. For a tool namedlookupOrderitstypeis"tool-lookupOrder". Tools the app does not know in advance appear as"dynamic-tool"with atoolNamefield. The helperisToolUIPart(part)istruefor both, andgetToolName(part)gives the name for both. - 3
The four main states.
input-streaming: the model is still writing the input, sopart.inputis a partial object (it grows:{}, then{ orderId: "ORD-" }) or missing.input-available: the input is complete and valid, and your function is running.output-available:part.outputis there.output-error: the call failed,part.errorTextis a string, andpart.outputdoes not exist. - 4
Two approval states appear when a tool needs human approval (Topic 8.6):
approval-requested(waiting for the person,part.approval.idis set) andapproval-responded(they answered,part.approval.approvedistrueorfalse). A denied call ends inoutput-denied. The full list is:input-streaming,input-available,approval-requested,approval-responded,output-available,output-error,output-denied. - 5
Why input-streaming exists. Models write tool input a few characters at a time, like text. The SDK parses the half-written JSON as it grows and hands you the best partial object. A fast tool may go straight from start to
input-availablein one jump, and a stand-in model may send the whole call at once. So never depend on seeinginput-streaming, but be ready for it:part.inputmay beundefinedor missing fields there. - 6
**Draw with a
switchonpart.state.** Onecaseper state, each reading only what that state guarantees. Use adefaultthat returnsnullso new states in a future SDK version do not crash the page. The second example does exactly this forlookupOrder. - 7
Typed parts. In TypeScript you can tell the message type about your tools, so
part.inputandpart.outputhave the right types inside eachcase. Without it, they areunknownand you cast by hand. The snippet in the walkthrough shows the one-line setup. The live examples are plain JavaScript, where nothing is checked, so the discipline is yours.
Explain it without notes
List the states of a tool part and say which fields exist in each.
Why is input-streaming a state at all, and how should a component treat part.input in it?
A component crashes with Cannot read properties of undefined (reading 'item') as soon as a tool starts. What is the likely cause and the fix?
How do you give TypeScript the types of your tool inputs and outputs inside a component?
Practice
Add an approval-requested case to describe that returns "Waiting for your answer". Which line of the second example would never reach it, and why?
Write a helper stateLabel(part) that returns a short word for any tool part, never throws, and returns "working" for states it does not know.
Trade-offs
- ↔
Drawing every state gives the best experience and the most code. Draw
output-available,output-errorand one shared "working" state first. Add the rest when the product needs them. - ↔
Showing partial input early ("Looking up ORD-10…") feels fast, but the half-written text changes many times a second and the user cannot read it. Showing a generic spinner until
input-availableis calmer.
Done when you can
I can name the seven states and what each one guarantees.
I can write a
switchthat draws a tool part and has a safe default.I never read
part.outputoutsideoutput-available.I can explain why
inputmay be partial or missing.I can type tool parts with
InferUITools.