---
description: "The contract between the chat widget and the agent that drives it."
---

# Adapter types

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`](/docs/reference/chat/create-cloud-adapter) and [`createLocalAdapter`](/docs/reference/local-agent/create-local-adapter) hand the chat.

## Import

```ts
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`.

```ts title="echo-adapter.ts"
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`](/docs/reference/chat/call-model-context-tool). |
| `agent` | `WebAgent` | not set | The page's agent ([`defineWebAgent`](/docs/reference/chat/define-web-agent)), 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`](#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-result` or `tool-error` for every `tool-call`, with the same `id`.
- 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 always `ready`. With `subscribe()`, the widget re-reads `status()` whenever you call the listener.

## See also

- [Add the chat](/docs/build/add-the-chat)
- [`createCloudAdapter`](/docs/reference/chat/create-cloud-adapter) and [`createLocalAdapter`](/docs/reference/local-agent/create-local-adapter)
- [`sessionReducer`](/docs/reference/chat/session-reducer)
- [`callModelContextTool`](/docs/reference/chat/call-model-context-tool)
