Command Palette

Search for a command to run...

Hectal
PHASE 8Advanced ~26 min· topic 3 of 9

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-tool and its name is in toolName.
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.

The state machinediagram
Rendering diagram…

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.

types.ts (real app)whole filets
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. 1

    Print every state a different way

    In the second example, change default: return ""; to default: return "state: " + part.state; and add a case you did not handle, for example remove the output-error case.

    Now the failed order shows state: output-error on the page: the fallback line makes a missing case visible instead of leaving a blank.

  2. 2

    Read the partial input

    In the first example, add if (part.state === "input-streaming") console.log(Object.keys(part.input || {})); inside the if (part) block.

    You will see [] twice (the first time part.input is undefined, 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

Every state, with the input arriving in pieces

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

Press Run to see the page.

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"}
One function draws each state, success and failure

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

Press Run to see the page.

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.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot read properties of undefined (reading 'item')
An error occurred in the <App> component.
 
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.
 
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'click')

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 switch with a default. Libraries add states over time (the approval states are recent). A default that renders nothing keeps old code from crashing on new states.

  • ▸

    isToolUIPart(part) and getToolName(part) work for static and dynamic tools. isStaticToolUIPart narrows to tools you defined in TypeScript, and isToolOutputErrorUIPart narrows to failures. Prefer these over comparing part.type strings by hand.

  • ▸

    Keys: use part.toolCallId as the React key for 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. 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.item while the function is still running, part.output is undefined and React crashes with a white screen. You need to know which fields exist in which state.

  2. 2

    Where the states live. Each assistant message has parts. A tool call is one part. For a tool named lookupOrder its type is "tool-lookupOrder". Tools the app does not know in advance appear as "dynamic-tool" with a toolName field. The helper isToolUIPart(part) is true for both, and getToolName(part) gives the name for both.

  3. 3

    The four main states. input-streaming: the model is still writing the input, so part.input is 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.output is there. output-error: the call failed, part.errorText is a string, and part.output does not exist.

  4. 4

    Two approval states appear when a tool needs human approval (Topic 8.6): approval-requested (waiting for the person, part.approval.id is set) and approval-responded (they answered, part.approval.approved is true or false). A denied call ends in output-denied. The full list is: input-streaming, input-available, approval-requested, approval-responded, output-available, output-error, output-denied.

  5. 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-available in one jump, and a stand-in model may send the whole call at once. So never depend on seeing input-streaming, but be ready for it: part.input may be undefined or missing fields there.

  6. 6

    **Draw with a switch on part.state.** One case per state, each reading only what that state guarantees. Use a default that returns null so new states in a future SDK version do not crash the page. The second example does exactly this for lookupOrder.

  7. 7

    Typed parts. In TypeScript you can tell the message type about your tools, so part.input and part.output have the right types inside each case. Without it, they are unknown and 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

01

List the states of a tool part and say which fields exist in each.

02

Why is input-streaming a state at all, and how should a component treat part.input in it?

03

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?

04

How do you give TypeScript the types of your tool inputs and outputs inside a component?

Practice

01

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?

02

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-error and 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-available is calmer.

Done when you can

  • I can name the seven states and what each one guarantees.

  • I can write a switch that draws a tool part and has a safe default.

  • I never read part.output outside output-available.

  • I can explain why input may be partial or missing.

  • I can type tool parts with InferUITools.