# Page tools

Give the AI agent tools that act on your own page: open your booking form, read the visitor's cart, jump to a
section, fill in a form. You describe each tool and write its function; the agent decides when to call it, the
widget runs the function in the visitor's browser, and the agent continues the conversation with what it returned.
Every page can offer different tools, so the agent can do on each page what that page is for.

A tool has the [WebMCP](https://webmachinelearning.github.io/webmcp/) shape (the W3C draft for tools a web page
offers to AI agents), so a tool you write for the widget also works with a browser's own WebMCP support, and the
other way round.

```ts
chat.registerTool({
  name: 'open_booking',
  description: 'Open the booking form on this page for a meeting type. Use it whenever the visitor wants to book a call.',
  inputSchema: {
    type: 'object',
    properties: { meetingType: { type: 'string', enum: ['sales', 'support'] } },
    required: ['meetingType'],
  },
  execute({ meetingType }) {
    bookingForm.open(meetingType);
    return { opened: true };
  },
});
```

A visitor writes "I'd like to talk to sales"; the agent calls `open_booking` with `{ meetingType: 'sales' }`, your
booking form opens, and the agent answers "The booking form is open, pick a time that suits you."

## Adding tools

| Where you are    | How                                                                                                                          |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Script tag       | `HelpAI('registerTool', tool)`. Works before the widget has loaded: the loader queues it.                                    |
| npm              | `chat.registerTool(tool, { signal })`, or `init({ tools: [...] })`.                                                          |
| React            | `useHelpAITool(tool)` in the component that owns the feature, or `<HelpAI tools={[...]} />`.                                 |
| `<web-ai-chat>`  | `element.registerTool(tool)`, or the `tools` property (functions can't be attributes).                                       |
| Plain HTML       | A form with `toolname` and `tooldescription` attributes ([Forms as tools](#forms-as-tools)); no script needed.               |
| A WebMCP browser | Tools registered with the browser's own `document.modelContext.registerTool()` are offered too ([WebMCP](#webmcp-browsers)). |

`registerTool` adds the tool for every widget on the page. Pass an `AbortSignal` to remove it again, for example when
a single-page app leaves the route that owns it:

```ts
const route = new AbortController();
chat.registerTool(addToCart, { signal: route.signal });
// …when the visitor leaves the product page:
route.abort();
```

`useHelpAITool` does this for you: the tool is registered while the component is mounted, `execute` always runs the
latest render's function, and React's StrictMode double mount still leaves one registration.

## A tool

| Field                           | What it is                                                                                                                                                                                 |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`                          | Letters, digits, `_` and `-`, 58 characters at most; other characters become `_`. The agent sees `site__<name>`. A name registered again replaces the earlier tool.                        |
| `description`                   | When the agent should use it and what it does, 1024 characters at most. The agent picks tools by their descriptions: say when, not only what. Without one, `title` (then `name`) is used.  |
| `title`                         | Optional. A short human name.                                                                                                                                                              |
| `inputSchema`                   | Optional. [JSON Schema](https://json-schema.org/) of the input, an object. The agent fills it in; use `enum` and `required` to keep the input exact. Without one, the tool takes no input. |
| `annotations.consequentialHint` | Optional. `true` for a tool with real consequences (an order, a booking, a message to someone): the visitor approves each call in the chat first, and the function runs only on Approve.   |
| `execute`                       | `(input, { signal }) => result`. Return a value or a promise of one; throw to report a failure. `signal` aborts when the visitor moves on before the call finishes.                        |

The `ModelContextTool` and `ToolAnnotations` types are exported for TypeScript; `ModelContextTool<{ sku: string }>`
types `execute`'s input. WebMCP's other annotations (`readOnlyHint`, `untrustedContentHint`) are accepted and not used
yet.

Nothing about a tool can break the chat: a tool without a `name` or an `execute` function is left out with a warning
in the browser console, and every message still goes through.

## Forms as tools

A form becomes a tool with two attributes, in every browser:

```html
<form toolname="request_demo" tooldescription="Request a product demo for the visitor." toolautosubmit>
  <label for="email">Work email</label>
  <input id="email" name="email" type="email" required />
  <input name="company" toolparamdescription="The visitor's company name" />
  <select name="plan">
    <option value="starter">Starter</option>
    <option value="pro">Pro</option>
  </select>
  <button>Request demo</button>
</form>
```

- Each named field is an input the agent can fill: a `select` or radio group takes one of its values, a checkbox
  `true` or `false`, a number field a number, anything else text. Hidden, disabled and file fields are left out, and
  so are passwords: the visitor types those, never the chat.
- A field's description is its `toolparamdescription`, else its label, else its `aria-description`; `required`
  fields are required.
- The agent fills the fields the way a visitor would (your framework's change handlers run). With `toolautosubmit`
  the form is then submitted; without it the visitor checks the form and presses its button. When a field still
  blocks the submit (a password, or a value the browser rejects), the form isn't submitted and the agent is told which
  field needs the visitor.

## WebMCP browsers

In a browser with WebMCP (`document.modelContext`: in Chrome, turn on `chrome://flags/#enable-experimental-web-platform-features`
or join the origin trial), the tools the page registered with the browser are offered to the agent too, and the widget
runs them through the browser. A `<form toolname>` is the exception: such a browser lists it too, but the widget fills
it itself, as in every other browser, so your form's own submit handler keeps working and passwords stay the
visitor's. Elsewhere, nothing changes.

## Which tools a message offers

The widget reads the page's tools when the visitor sends each message: the browser's WebMCP tools, then forms, then
the `tools` option, then `registerTool`. For a name in more than one place, the later one wins. When the agent calls a
tool, the widget reads them again and runs the current function of that name, so a single-page app's route change
applies straight away, with nothing to keep in sync. A call to a tool the page no longer has tells the agent the page
doesn't offer it right now. So does a call to a tool the message didn't offer at all (the agent repeating an earlier
call after a route change): the server answers it with "This page doesn't offer that action right now.", tells the agent
not to call it again this turn, and the agent carries on from what it knows or asks the visitor.

## What the agent gets back

- **A return value** becomes the tool's result, as JSON: dates become strings, `undefined` becomes `null`, functions
  are dropped, a page element is named (`"[dialog]"`), a repeated object reads `"[circular]"`, and a result over
  30,000 characters is sent as `{ truncated: true, preview }`. Return what the agent needs to answer well, such as
  `{ opened: true }` or `{ items: 2, total: '$48' }`.
- **A thrown error** (or a rejected promise) reaches the agent as the tool's error, with its message, so it can tell
  the visitor or try another way. For an expected miss ("that product is out of stock") a normal result such as
  `{ ok: false, reason: 'out of stock' }` gives the agent more to work with.

With several calls in one step, the agent waits for all of them, and for any approvals, then continues once.

## Each call runs once

- **Tabs.** Only the tab where the visitor sent the message runs the calls in its reply; another tab of the same
  conversation shows them running, then the agent's answer.
- **Reloads and navigation.** A call that hadn't started when the page reloaded runs once after the reload. A call
  that was running is never run again: the agent is told the page reloaded, so it may or may not have taken effect,
  and checks with the visitor; its card reads "Not completed". A result that was ready but not yet sent is sent after the reload. The widget keeps
  this note per tab in `sessionStorage`, so it also covers a navigation to another page of your site.
- **Leaving the page.** Results go out with `keepalive`, so they arrive even when the page navigates straight after.
  A tool that navigates should return first and leave after:

  ```ts
  execute({ path }) {
    setTimeout(() => location.assign(path), 0); // navigate after the result is sent
    return { navigating: path };
  },
  ```

- **The script loaded twice, or two widgets.** The call still runs once.
- **No time limit.** The agent waits for the result. While a tool runs, the chat shows Stop. If the visitor presses it,
  or sends another message first, the call's `signal` aborts, its result is never sent, and its card reads
  "Not completed". A stopped call never runs again, not even after a reload.

When `sessionStorage` is blocked (some private modes), the note lives in memory, so a reload can't tell a call that
ran from one that didn't; everything else works the same.

## What the visitor sees

A tool that runs on its own appears in the transcript only when the deployment shows tool calls
(`behavior.showToolCalls`), with its input and result: "Running", then "Completed" or "Error" (the tool failed), or
"Not completed" when the visitor stopped it or moved on, or a reload cut it short. A tool with `consequentialHint` always shows an approval card with the input; Reject tells
the agent the visitor declined, and the function never runs.

## Events

Each call this page settles fires `toolResult` with `{ toolCallId, name, status, durationMs }`: `status` is
`completed`, `failed`, `declined` (the visitor rejected it), `stopped` (the visitor pressed Stop) or `interrupted` (a
reload cut it short). It never carries the input or the result.

```ts
chat.on('toolResult', ({ name, status, durationMs }) => analytics.track('help_ai_tool', { name, status, durationMs }));
```

## Security

- Tools run with your page's full privileges, as code you wrote. Anyone can also call your functions from the
  browser's developer tools, so a tool may do only what the visitor could do on the page themselves.
- The agent chooses the input. Check it in `execute` as you would a form submission, and mark tools with real
  consequences (payments, deletions, messages to other people) `consequentialHint: true`, or confirm in your own UI.
- The definitions are sent with each message and the results are stored in the conversation, like any tool's result,
  so never put secrets in a description or a result.

## Testing

`testing/page-tools-manual.md` walks through every case above in a real browser
on the `dev/page-tools.html` playground, with a real model or the mock backend.

## Troubleshooting

- **The agent never uses the tool:** write the description as an instruction ("Use it whenever the visitor wants to
  book a call"), and check the console for a "skipped a tool" warning.
- **The chat waits after a tool:** the function never settled (a promise that doesn't resolve). Return, or throw.
- **"This page doesn't offer … right now":** the call came after the tool was removed (a route change aborted its
  signal), or the agent tried a tool this page doesn't register. Keep a tool registered while the agent may still use
  it.
