Client Tools
Client tools are tools you declare in the browser that the model calls and the browser executes โ no server-side implementation. There are three kinds:
| Helper | Kind | What it does |
|---|---|---|
action() | function | Runs an async handler in the browser; its resolved return value becomes the tool result sent back to the model. |
view() | render-only component | The model fills the component's props from the schema; the card renders inline and the call is auto-acknowledged once it mounts. |
ask() | interactive component | The model fills the component's props; the value the component emits back becomes the tool result (human-in-the-loop). |
Tools are arguments-typed by a Standard Schema (e.g. a Zod object). The catalog is shipped to the model by the adapter; the backend graph binds the client stubs and ends its turn so the browser executes them.
The same declarations work with @threadplane/langgraph and @threadplane/ag-ui โ only the provideAgent/injectAgent imports change.
#Declaring a registry
tools({...}) collects named tools into a frozen registry. Pass it to <chat> via [clientTools]:
The object keys (get_weather, weather_card, confirm_booking) are the tool names the model sees. tools() preserves each tool's precise generic type, so downstream lookups stay typed.
#Typed component props with ViewProps
For view() and ask(), the component's signal inputs are checked against the schema output at compile time โ every field the schema produces must be a declared input() with an assignable type (the component may declare extra inputs the schema doesn't fill). Derive the input types directly from the schema with ViewProps<typeof schema> so the two never drift:
Under strict: true, the typed view/ask overloads report a compile error at the view(...)/ask(...) call site if the component's inputs diverge from the schema โ mismatches become build errors, not silent runtime failures.
#Typed handler args with ToolArgs
For action(), the handler argument type is inferred from the schema automatically. When you want to name that type โ e.g. to write the handler separately โ use ToolArgs<typeof schema> (an alias of the schema's inferred output):
#Terminal tools with followUp: false
By default, resolving a client tool starts a new run so the model can react to the result. Pass followUp: false when a tool ends the turn โ a summary card, a confirmation receipt, anything the model has nothing further to say about. The result is still recorded on the server; the model simply is not asked to respond to it.
Follow-up is decided per tool-call group, not per tool. If the model calls three tools in one turn and any one of them wants a follow-up, the whole group continues in a single run once every result has settled. Only when every tool in the group is terminal does the turn end.
A terminal group has no follow-up run to carry its results, so the adapter writes them to the server directly. On @threadplane/langgraph that uses the transport's updateState. If a custom transport does not implement updateState, flush() rejects when terminal results are staged. The results stay buffered and an ordinary next message can still carry them, but a browser page reload first loses that in-memory fallback and leaves the server thread with an unanswered tool call. If you supply your own transport and use terminal tools, implement updateState.
#Re-running tools safely with idempotent
action() also accepts idempotent. It matters only when you supply a [clientToolExecutionGuard] โ a durable store that claims each tool call before the browser executes it, so a handler with real side effects cannot run twice across a reload or a reconnect.
Tools are treated as non-idempotent by default. Mark a tool idempotent: true only when re-running it is genuinely harmless โ reads, pure computations, lookups.
The guard gives you at-most-once dispatch, not exactly-once effects. If a handler completes its side effect and the browser dies before recording the result, the guard fails closed and reports the call as interrupted. For true end-to-end idempotency, have the handler pass its own idempotency key to the downstream service.
#Stopping and continuation limits
Stop cancels cleanly. Pressing stop while a client tool is running aborts the handler, records a cancelled result so the server never holds an unanswered tool call, and does not start a new run. The cancelled call will not re-execute.
Handlers receive an AbortSignal โ forward it to fetch so in-flight work actually stops:
Runaway loops are capped. A model that keeps calling client tools is stopped after 10 continuation groups per user turn. Tune it with [clientToolContinuationPolicy]:
When the cap trips, tools that already produced a real result keep it; tools that never ran are recorded with a limit error so the thread stays valid. The run does not continue.
#Typed agent state
Tool handlers and components often read agent state. Pair the registry with a typed AgentRef so agent.state() / agent.value() carry your state shape instead of Record<string, unknown> โ see Typed state via AgentRef:
#API reference
| Export | Purpose |
|---|---|
action(description, schema, handler, options?) | Declare a function tool (handler return โ result) |
view(description, schema, component, options?) | Declare a render-only component tool (auto-acknowledged) |
ask(description, schema, component, options?) | Declare an interactive component tool (emitted value โ result) |
tools(map) | Freeze a name-keyed registry for [clientTools] |
ViewProps<S> | Component input prop bag inferred from a schema |
ToolArgs<S> | Handler argument type inferred from a schema |
ClientToolDef / ClientToolRegistry | The tool-definition union and frozen-registry types |
ClientToolContinuationOptions | { followUp? } โ accepted by view() and ask() |
ClientToolExecutionOptions | { followUp?, idempotent? } โ accepted by action() |
ClientToolContinuationPolicy | { maxTurns?, onLimit? } for [clientToolContinuationPolicy] |
ClientToolExecutionStore / ClientToolExecutionGuard | Durable claim store for [clientToolExecutionGuard] |
Component inputs on <chat>:
| Input | Purpose |
|---|---|
[clientTools] | The frozen registry from tools({...}) |
[clientToolContinuationPolicy] | Cap runaway continuation loops (default 10 groups per turn) |
[clientToolExecutionGuard] | Durable claim-before-execute for non-idempotent tools |
The settle / flush / resolve contract behind these features is documented in Writing an Adapter โบ Client Tools.