Adapter types
The contract between the chat widget and the agent that drives it.
The contract between the chat widget and the agent that drives it. An adapter owns the model and tool loop and reports what happens as a stream of parts; the widget only renders them. Read this page to write your own adapter, or to understand what createCloudAdapter and createLocalAdapter hand the chat.
Import
import type {
AdapterStatus,
AgentAdapter,
AgentMessage,
AgentPart,
AgentRunRequest,
AgentStreamPart,
ModelChoice,
ToolCallState,
ToolDescriptor,
ToolOutput,
} from "@webmcp-agents/chat";
Example
A minimal adapter with no model: it calls the page’s first tool through req.callTool, then reports what it found. It shows the shape every adapter follows: message-start, the parts, then finish.
import type { AgentAdapter } from "@webmcp-agents/chat";
// A minimal adapter: no model. It calls the page's first tool, then
// says what happened. Pass it to <AgentChat adapter={echo} />.
export const echo: AgentAdapter = {
id: "echo",
label: "Echo",
status: () => ({ state: "ready" }),
async *run(req) {
const messageId = crypto.randomUUID();
yield { type: "message-start", messageId };
// Read tools fresh: they change as the page changes.
const tool = req.tools[0];
if (tool) {
const id = crypto.randomUUID();
const call = { id, name: tool.name, input: {} };
yield { type: "tool-call", messageId, ...call };
// One tool at a time (one page, one cursor). Never rejects.
const output = await req.callTool(call);
const text = output.content[0]?.text ?? "";
yield output.isError
? { type: "tool-error", id, error: text }
: { type: "tool-result", id, output };
}
const delta = `This page has ${req.tools.length} tools.`;
yield { type: "text-delta", messageId, delta };
yield { type: "finish", reason: "stop" };
},
};
Options
Each type’s members, one table per type. The ones you implement are AgentAdapter (and its run); the others are what you receive or yield.
AgentAdapter
An agent the chat can talk to: the model and tool loop behind a stream of parts.
| Name | Type | Default | Description |
|---|---|---|---|
id |
string |
required | Stable id; the widget remembers the chosen adapter by it. |
label |
string |
required | Shown in the adapter switcher, for example “Claude (cloud)”. |
run |
(req: AgentRunRequest) => AsyncIterable<AgentStreamPart> |
required | Handle one user message: yield stream parts and call req.callTool() for each tool call. |
status |
() => AdapterStatus |
always ready | Current availability. |
subscribe |
(listener: () => void) => () => void |
not set | Called on status changes (a download progressing, the cloud check finishing). Returns an unsubscribe function. |
prepare |
(signal?: AbortSignal) => Promise<void> |
not set | Opt-in heavy setup, such as a model download, started by the person’s click. Aborting signal cancels it. |
models |
() => readonly ModelChoice[] |
not set | The models the person can pick from. The widget shows a picker when there is more than one. |
selectModel |
(id: string) => Promise<void> |
not set | Switch to one of models(); the status then describes what the new model needs. Rejects while a reply or a download is in progress. |
A ModelChoice describes one model in the picker:
| Name | Type | Default | Description |
|---|---|---|---|
id |
string |
required | The id selectModel takes. |
label |
string |
required | Short name, for example “Qwen3 0.6B”. |
brief |
string |
required | One sentence on when to pick it. |
bytes |
number |
not set | Download size in bytes, when known. |
cached |
boolean |
not said | Already on this device: no download. |
recommended |
boolean |
not said | The adapter’s default. |
selected |
boolean |
not said | The one in use. |
AdapterStatus
Whether an adapter can run right now. The composer explains any state but ready.
| State | Members | Description |
|---|---|---|
ready |
It can run. | |
unavailable |
reason: string |
It cannot, and why. |
needs-download |
bytes?: number, note?: string |
A download must start first. note is a caveat for the download card, for example “No WebGPU here: a smaller, slower model runs on the CPU.” |
downloading |
progress: number |
Downloading; progress is 0 to 1. |
loading |
progress?: number |
Starting up. Without progress it is a connection check (the cloud adapter); with progress it is a model already on this device being read from disk, which the widget words as a load, never as a download. |
error |
error: string |
Setup failed. |
AgentRunRequest
What an adapter gets for one run: one user message, possibly many model steps.
| Name | Type | Default | Description |
|---|---|---|---|
messages |
AgentMessage[] |
required | The conversation so far, ending with the new user message (on Retry, with the last finished tool call). |
tools |
ToolDescriptor[] |
required | The page’s tools right now, already filtered through agent. A getter: tools change as the page changes, so read it fresh before every model call. |
signal |
AbortSignal |
required | Aborts when the person presses Stop. |
callTool |
(call: { id: string; name: string; input: unknown }) => Promise<ToolOutput> |
required | Run a tool on the page, cursor and all. Never rejects: failures come back as isError results. See callModelContextTool. |
agent |
WebAgent |
not set | The page’s agent (defineWebAgent), when set. In-browser adapters read its instructions and maxSteps; the cloud adapter sends only its id. |
AgentStreamPart
What an adapter reports while it runs, in order. Start each assistant message with message-start; text, reasoning and tool calls refer to it by messageId. End the run with finish or error.
type |
Members | Description |
|---|---|---|
message-start |
messageId |
A new assistant message begins. |
text-delta |
messageId, delta |
More text for the message. |
reasoning |
messageId, text, providerMetadata? |
The model’s reasoning, kept for the next request, not rendered. |
tool-call |
messageId, id, name, input, providerMetadata? |
The model calls a tool. providerMetadata is provider data that must travel with the call (Gemini thought signatures, OpenAI item ids). |
tool-result |
id, output |
The call finished. |
tool-error |
id, error |
The call failed. A text starting Error [cancelled] shows as cancelled, not failed. |
step-finish |
step |
One model step (and its tool calls) is done. |
finish |
reason: "stop" | "max-steps" | "aborted" |
The run is over. |
error |
error, code?: "chat_disabled" | "rate_limited" | "bad_request" | "network" |
The run failed; code lets the widget explain it. |
AgentMessage and AgentPart
One turn of the conversation: { id: string; role: "user" | "assistant"; parts: AgentPart[] }. A part is one of:
type |
Members | Description |
|---|---|---|
text |
text |
Plain text. |
reasoning |
text, providerMetadata? |
The model’s reasoning, kept so the next request can send it back (Claude requires its signed thinking alongside its tool calls). Not rendered. |
tool |
id, name, input, state, output?, error?, prompt?, agent?, providerMetadata? |
A tool call. state is a ToolCallState. prompt is what the person should click while the call awaits confirmation. agent is { id, messages }: a sub-agent’s nested run behind a delegate_to_<id> call, rendered nested. providerMetadata travels with the call like reasoning’s. |
ToolCallState
"running" | "awaiting-confirmation" | "done" | "error" | "cancelled": where a tool call is. awaiting-confirmation means it is parked on a control the person must click.
ToolDescriptor and ToolOutput
A ToolDescriptor is a page tool as the model sees it:
| Name | Type | Default | Description |
|---|---|---|---|
name |
string |
required | The tool’s name. |
description |
string |
required | What it does; the model decides from this. |
inputSchema |
JSONSchema |
required | Its parameters as JSON Schema. |
A ToolOutput is a tool call’s result: an MCP result, { content, isError?, structuredContent? } (ModelContextToolResult from @webmcp-agents/core).
Returns
An adapter’s run returns an AsyncIterable<AgentStreamPart>: the parts above, in order, ending with finish or error.
Notes
- Run tool calls one at a time: one page, one cursor. The session matches confirmation events to calls by tool name.
- Yield
tool-resultortool-errorfor everytool-call, with the sameid. - Create an adapter once, at module scope, not during render: the session and the status hook key on the adapter object.
- An adapter without
status()is alwaysready. Withsubscribe(), the widget re-readsstatus()whenever you call the listener.