Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
bb35e7c
docs(plans): remaining products PRs 3–8; ag-ui pages leave the pendin…
blove Sep 6, 2026
9201ab6
docs(ag-ui): generative UI teaches through the running example
blove Sep 6, 2026
8848a15
docs(ag-ui): interrupts teaches through the running example
blove Sep 6, 2026
be29aa5
docs(ag-ui): client tools teaches through the running example
blove Sep 6, 2026
865c463
fix(website): region slices drop inner marker lines
blove Sep 6, 2026
e7ba94d
docs(ag-ui): subagents teaches through the running example
blove Sep 6, 2026
b5a8b1c
docs(ag-ui): tool views teaches through the running example
blove Sep 6, 2026
a2981f1
docs(ag-ui): event mapping reference teaches through the streaming ex…
blove Sep 6, 2026
3b5e86e
test(website): unnamed inner markers are dropped from slices too
blove Sep 6, 2026
3990694
docs(ag-ui): interrupts page — parity and wire-format precision
blove Sep 6, 2026
4254d06
docs(ag-ui): client tools page — why the checkpointer exists, the sch…
blove Sep 6, 2026
204e385
docs(ag-ui): generative UI page — the tools node runs tools; one sent…
blove Sep 6, 2026
3a71cc2
docs(ag-ui): tool views page — why the checkpointer exists
blove Sep 6, 2026
4391e84
docs(ag-ui): event mapping — resume is its own path; run-id gate on R…
blove Sep 6, 2026
e737221
docs(ag-ui): subagents page — a prompt that delegates, the emitter's …
blove Sep 6, 2026
db66e19
docs(ag-ui): interrupts page carries the shared checkpointer reason a…
blove Sep 6, 2026
8bd8a62
test(website): the content-search deep link may land on any docs page…
blove Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 161 additions & 2 deletions apps/website/content/docs/ag-ui/guides/client-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,165 @@ description: Declare a tool in the Angular app, let an AG-UI agent call it, and

# Client Tools

An AG-UI agent can call tools that live in the browser. The Angular app declares the tool, the backend ends its turn when the agent calls it, the browser runs it, and the result is submitted back as a tool message so the run continues. The tool has no server-side implementation.
An AG-UI agent can call tools that live in the browser. The Angular app declares the tool, the backend binds the declaration onto the model without implementing it, and the run ends when the model calls it. The browser executes the tool and submits the result back as a tool message, which starts the next turn. This guide walks the seven files of the running example, from the graph to the components the model fills.

This page is the live example for client tools over AG-UI. Use **Run** to drive the agent, **Code** to read the Angular and Python sources, and **API** for the extracted reference. The browser-side tool contract is documented in the [Chat client tools guide](/docs/chat/guides/client-tools).
## What the demo does

The Run tab shows the prebuilt `<chat>` composition wired to five browser-executed tools. Ask "What is the weather in Denver?" and the model calls `get_weather`, whose handler runs in the page and returns a reading the model then summarizes in one sentence.

Ask it to *show* a weather card and the model calls `weather_card` instead, filling an Angular component's inputs with the readings it invents; the card renders inline in the transcript. Ask it to book something and `confirm_booking` mounts a card with Confirm and Cancel buttons, and whichever you click becomes the tool result that resumes the run. Two further prompts exercise the edges: a terminal snapshot that ends the turn without a spoken summary, and a slow status check you can interrupt with the stop button.

## How it is built

Seven files carry the feature: a graph that binds the browser's tool declarations and ends its turn, the server that exposes it over AG-UI, an application config, the schemas the registry and its components share, the file that declares the registry and passes it to `<chat>`, and the two components the model fills. Open the Code tab to read them in place.

### The graph binds client stubs and ends the turn

The graph has one node. Its `State` declares a `tools` channel alongside `messages`, because that is where the client tool catalog arrives, and `bind_client_tools` reads it there to bind the declarations onto the model for this invocation. There are no server tools, so `route` returns `END` unconditionally.

<ExampleCode file="graph.py" />

`bind_client_tools(llm, [], state)` converts each catalog entry into an OpenAI function-tool dict and calls `llm.bind_tools` with it; the empty list is where your own server tools would go.

<Callout type="info" title="Why the turn ends on a tool call">
The browser, not the graph, produces the result. Ending the run is the signal the adapter waits for: it treats a tool call with no result as pending only once the run is no longer loading. A graph that looped back to a tools node would leave the browser nothing to do.
</Callout>

### Serving the graph over AG-UI

The server is a FastAPI application. `LangGraphAgent` wraps the compiled graph and `add_langgraph_fastapi_endpoint` mounts it at `/agent` as an AG-UI event stream, and `/ok` is a plain health check.

<ExampleCode file="server.py" />

### Providing the agent

`provideAgent()` from `@threadplane/ag-ui` registers the agent at the application root, and `provideChat({})` registers the chat defaults. The example resolves its URL at runtime because the host that serves the demo decides which runtime is attached; your own application passes a `url` directly.

<ExampleCode file="app.config.ts" />

### Declaring the tool registry

`tools()` builds a name-keyed registry, and each entry is one of three declarations from `@threadplane/chat`. `action()` takes a handler that runs in the browser and may return a promise, `view()` takes a component the model fills, and `ask()` takes a component that reports a value back. Every entry carries a description the model reads and a schema authored with `zod/v4`. The `weather_card` and `confirm_booking` entries take their schemas from `schemas.ts`, the file the registry and the two components it points at all import from.

```ts
import { z } from 'zod/v4';

/** Schema for the `weather_card` view tool. */
export const weatherCardSchema = z.object({
location: z.string(),
temperatureF: z.number(),
conditions: z.string(),
humidity: z.number(),
windMph: z.number(),
});

/** Schema for the `confirm_booking` ask tool. */
export const confirmBookingSchema = z.object({ summary: z.string() });
```

<ExampleCode file="client-tools.component.ts" region="tool-registry" title="client-tools.component.ts — the registry" />

The `view` and `ask` overloads are typed against the paired component: every field the schema produces must be a declared `input()` on that component, so a schema change is a compile error rather than a silent runtime gap.

<Callout type="warning" title="Author schemas with zod/v4">
The catalog the model sees is derived by `deriveJsonSchema`, which calls `toJSONSchema` from `zod/v4`. A validator it cannot convert throws with the tool name in the message.
</Callout>

### Passing the registry to chat

The registry reaches the UI through one input. `<chat [clientTools]="clientTools">` builds a coordinator for that registry, which ships the catalog to the agent, runs function tools, mounts view and ask components, and settles each call.

<ExampleCode file="client-tools.component.ts" region="chat-wiring" title="client-tools.component.ts — the chat wiring" />

The catalog is attached to every run the adapter starts, as the `tools` field of the AG-UI run input, so the model sees the browser's tools on the first turn and on every continuation.

### The view component the model fills

`weather_card` is a `view`: the model supplies the props, the card renders inline, and the coordinator acknowledges the call itself with `{ shown: true }`. The component types its inputs through `ClientToolViewProps<typeof weatherCardSchema>`, which is the schema's output plus two framework-supplied props, `status` and `clientTool`.

<ExampleCode file="weather-card.component.ts" />

Inputs are optional because props arrive while the call streams, so the card reads `clientTool()?.phase` and shows a loading badge until the phase is `complete` and the readings have arrived.

### The ask component that resumes the run

`confirm_booking` is an `ask`: the model fills `summary`, and the run waits for a person. The component injects the render host and calls `result({ confirmed })`, which the coordinator matches to the pending call by tool name and settles as that call's result.

<ExampleCode file="confirm-booking.component.ts" />

<Callout type="tip" title="Why the card freezes after the answer">
Settling writes the emitted value onto the local tool call, and the mounted component is re-rendered with the merged props. That is why `confirmed` is declared with a default of `undefined`: while it is undefined the buttons are live, and once the user answers the template branches to a frozen line with no buttons.
</Callout>

### Stopping a tool that is still running

A function handler receives a context object as its second argument, carrying an `AbortSignal`. `slow_status_check` waits three seconds through a helper that rejects the moment the signal aborts.

<ExampleCode file="client-tools.component.ts" region="abortable-delay" title="client-tools.component.ts — the abortable delay" />

The executor wraps the agent's `stop`, so pressing stop aborts every in-flight handler, and a cancelled call is recorded without starting a new run.

## The round trip in one pass

<Steps>
<Step title="The catalog ships with the run">
The coordinator converts the registry into specs, each with a name, a description and a JSON Schema, and the adapter attaches them to every run it starts.
</Step>
<Step title="The model calls a tool and the graph ends the turn">
Because the tool has no server implementation, the run finishes with a tool call that carries no result.
</Step>
<Step title="The browser sees the call as pending">
Pending calls are those whose name is in the catalog, whose result is undefined, and which this client has not resolved yet, and only while the agent is not loading.
</Step>
<Step title="The browser produces the result">
A function handler runs, a view is acknowledged on mount, and an ask waits for the user's value.
</Step>
<Step title="The result is submitted and the run continues">
The adapter records the result on the local tool call, adds a tool message addressed to the call id, and starts the next run, which the model answers.
</Step>
</Steps>

## Settle, flush, and resolve

The adapter implements three operations for a produced result, and the coordinator picks between them.

`settle(id, result)` records the result: the value is written onto the local tool call so the transcript freezes, and a tool message is added to the agent's outgoing messages. `resolve(id, result)` settles and then asks the adapter to continue the run, which is the normal path. `flush()` is where an adapter makes settled results durable without continuing; in `@threadplane/ag-ui` it is a deliberate no-op, because `settle` already placed the tool message in the message list that the next run carries.

The choice matters at the edges. A tool declared with `followUp: false`, like `weather_snapshot` in this example, is settled and flushed instead of resolved, so the rendered card is the final answer and no summary follows. A cancelled call takes the same path, because continuing the run is exactly what the user asked not to happen.

<Callout type="info" title="Continuation limit">
Consecutive client-tool continuations are capped per user turn, at ten by default. Set `[clientToolContinuationPolicy]` on `<chat>` to change the maximum or to observe the limit, and use `maxTurns: 0` for no cap. Calls blocked by the limit are settled with an explanatory error so the model never sees an unanswered tool call.
</Callout>

## Requirements on the backend

Three things must hold. The first fails silently, which makes it the one to check first.

<Callout type="warning" title="Declare the tools channel">
The catalog is merged into graph state by `ag-ui-langgraph`. If `State` does not declare a `tools` channel the merged value is dropped, `bind_client_tools` binds nothing, and the model never learns the browser has tools.
</Callout>

The graph in this example compiles with `MemorySaver()` because `ag-ui-langgraph` reads the thread's graph state through a checkpointer; a graph served this way needs one, and outside development it should be durable. The tool result itself does not depend on it: the browser adds the tool message to its own message list, and the continuation run carries the whole list back.

Finally, keep the tool names in the registry and the names in the system prompt aligned. The prompt in this example tells the model which tool to call for each kind of request, and a rename on one side alone is the most common reason a demo goes quiet.

## Mixing server tools and client tools

Nothing about this design is exclusive. Pass your server tools as the second argument of `bind_client_tools(llm, server_tools, state)` and route a server tool call to your tools node while a client tool call still ends the turn. The `route_after_agent` helper in the same middleware module does that split for you.

## What's Next

<CardGroup cols={2}>
<Card title="Tool Views" href="/docs/ag-ui/guides/tool-views">
Render a backend tool call's result through a component the frontend owns.
</Card>
<Card title="Interrupts" href="/docs/ag-ui/guides/interrupts">
Pause a run mid-flight and resume it with a human decision.
</Card>
<Card title="Chat Client Tools" href="/docs/chat/guides/client-tools">
The runtime-neutral contract behind action, view, and ask.
</Card>
<Card title="Event Mapping" href="/docs/ag-ui/reference/event-mapping">
How AG-UI protocol events become the Agent contract the chat UI reads.
</Card>
</CardGroup>
Loading
Loading