<!-- AvatarFactory Docs · https://avatarfactory.in/docs/sdk/react/app-events · Full map: https://avatarfactory.in/llms.txt -->

# App events & page awareness

Tell the avatar something happened in your app, and let it read the page it is sitting on.

A call does not have to be driven only by speech. `notify()` hands the avatar
something that happened in your UI so it can take a turn about it unprompted, and
`readPage()` lets it see the page it is embedded in.

## notify()

Give the event a stable slug and the avatar decides what to say about it, in
character.

```tsx
const { notify } = useAvatar();

notify?.({
  name: "cart.abandoned",
  data: { items: 3, total: "$82.00" },
  policy: "idle_only",
});
```

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` **(required)** | `string` | — | A stable slug matching ^[a-z0-9][a-z0-9_.]{0,63}$ — never prose. This is what the brain is told happened, so free text here would put your page content straight into a turn instruction. |
| `data` | `Record<string, unknown> \| string` | — | Structured detail, serialized and fenced as untrusted reference data. Capped at 512 characters. |
| `policy` | `AppEventPolicy` | `"idle_only"` | When the event is allowed to take a turn. See the table below. |
| `say` | `string` | — | Speak this verbatim and skip the brain. An escape hatch for scripted lines; the brain-mediated form is what keeps the avatar in persona. Capped at 1000 characters. |
| `cooldownMs` | `number` | `5000` | Per-name gap before this event may fire again. |

### Policies

| Policy | Behaviour |
| --- | --- |
| idle_only | The default. Take a turn only if the avatar is not already busy. An avatar that talks over the user because they scrolled is worse than one that stays quiet. |
| queue | Wait for the current turn to finish, then take one. |
| interrupt | Cut in immediately. Reserve it for things the user genuinely needs to hear now. |
| context_only | Do not speak. Just remember it for later turns. |

### Limits

The SDK gates events before they reach the socket, so a chatty UI cannot flood the
session.

| Limit | Value |
| --- | --- |
| Per-name cooldown | 5000ms (override with cooldownMs) |
| Rate limit | 5 events per 10s window |
| data length | 512 characters |
| say length | 1000 characters |

> A **dropped** event (cooldown or rate limit) is logged, never thrown — the gate
>   doing its job is not your bug. An **invalid** event (bad slug, oversized `data`
>   or `say`) is your bug, and surfaces as an `INVALID_INPUT` error.

## readPage()

Reads the host page into an index of named handles and sends it as page context,
so the avatar can talk about what is on screen.

```tsx
const { readPage } = useAvatar();

const result = readPage?.();
if (result && !result.sent) {
  console.log("not sent:", result.reason);
}
```

It returns `null` when the platform has no page surface, and otherwise a
`PageReadResult`:

```tsx
type PageReadResult = {
  index: PageIndex;
  /** Exactly the text that went up the wire. */
  context: string;
  sent: boolean;
  reason?: "wrong_mode" | "no_session" | "not_granted" | "unchanged";
};
```

> Reading is **manual today** — nothing calls `readPage()` for you. Call it after
>   navigation if you want the avatar to keep up with where the user is.

### Capabilities

What the avatar may do to your page is resolved server-side from the avatar's own
document and delivered when the session starts. `read` is the parent: without it
the server forces the other two off, because acting on a page it cannot see is a
worse feature than not acting at all.

```tsx
type PageCapabilities = {
  read: boolean;
  scroll: boolean;
  navigate: boolean;
};
```

> Extraction is local, but the uplink is gated. A `reason` of `not_granted` means
>   the avatar has no `read` capability — the page was indexed in the browser and
>   then not sent. Grant it on the avatar in the Platform, not in code.
