> For the complete documentation index, see [llms.txt](https://shoppad.gitbook.io/yedric/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://shoppad.gitbook.io/yedric/developers/client-events.md).

# Client Events

Client events let the host application report things that happened in its UI to the assistant: a test run finished, a record was detected as corrupted, a background job completed. The host fires [`Yedric.event(name, payload)`](/yedric/developers/javascript-api.md#yedricevent-name-payload-opts) and the platform decides whether the assistant should react, quietly ignore it, or note it for later.

This is the push-based replacement for patterns where the assistant blocks a chat turn waiting for something to happen in the app (held-open tool calls, polling loops). Instead, the assistant ends its turn and is woken by the event.

## How delivery works

1. **The host emits.** `Yedric.event(name, payload)` is fire and forget. The host never needs to check whether the panel is open or a conversation exists.
2. **The widget gates and batches.** Events are dropped unless the panel is open and the current conversation has at least one user message. An event never opens the panel and never creates a conversation. Surviving events are collected for about 2 seconds and coalesced (a newer event with the same `dedupeKey` replaces the queued one), then posted as a batch.
3. **The server allowlists.** Only event names with a definition on the agent (see below) are accepted; unknown names are dropped and logged. If a chat turn is already streaming for the session, the batch is asked to retry after the stream ends, which also covers multiple open tabs.
4. **Triage returns a verdict.**

   * `respond`: the events are clearly about what the user and assistant are working on right now. The assistant receives the batch as an `[APP EVENT]` message and replies.
   * `context`: plausibly relevant but not worth an unprompted reply. Currently logged only.
   * `ignore`: unrelated, stale, or noise. Dropped.

   Triage is deterministic first: an event whose `triage.mode` is `auto` responds when its `scopeKey` payload value appears in the page URL or an attached resource URI. Everything else falls to a fast LLM pass that fails closed to `ignore`.
5. **The event turn.** On `respond`, the widget starts a normal chat turn. In the thread the event renders as a small muted chip (the definition's `displayLabel`), not a user bubble, and the assistant's reply streams normally. The message is persisted with `kind: 'client_event'`, so reloaded transcripts render the chip too.

## Defining events

Events are defined in the agent's bundle repository, one file per event, next to the plugin's tools and resources:

```
plugins/{server}/events/{event-name}.json
```

Definitions deploy with the rest of the agent bundle (console **Pull from GitHub**). An emitter shipped before its definition is pulled is harmless: the allowlist drops the events until the definition exists.

```json
{
  "schemaVersion": 1,
  "name": "test_run_halt",
  "description": "Fired each time a workflow test run halts: a pause before a write, or a terminal status.",
  "payload": {
    "automation_id": "The workflow's id",
    "run_id": "The halted run's id",
    "task_id": "The paused or failed task, when one exists",
    "state": "paused | passed | failed | stopped | skipped"
  },
  "triage": { "mode": "auto", "scopeKey": "automation_id" },
  "displayLabel": "Test run {state}",
  "reaction": "On paused: fetch the pending write with Get_task_request_details, explain it, advise Allow or Skip, then end your turn. On passed: confirm and offer next steps. On failed: investigate with Debug_error before proposing a fix."
}
```

| Field             | Purpose                                                                                                                                                                          |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`            | Lowercase snake\_case identifier (`^[a-z0-9_]+$`). Must match what the host passes to `Yedric.event`.                                                                            |
| `description`     | What the event means. Shown to the LLM triage pass when deciding relevance.                                                                                                      |
| `payload`         | Documentation map of field name to meaning. Not validated at runtime.                                                                                                            |
| `triage.mode`     | `auto` (respond when scoped, LLM otherwise), `llm` (always LLM triage), or `ignore` (drop).                                                                                      |
| `triage.scopeKey` | Payload field whose value is matched, as a whole token, against the page URL and attached resource URIs.                                                                         |
| `reaction`        | Instructions injected into the assistant's `[APP EVENT]` message when this event fires. Keep per-event policy here rather than in the agent prompt; it loads only when relevant. |
| `attachResources` | Resource URIs attached to the conversation when the event is delivered (e.g. a repair playbook).                                                                                 |
| `displayLabel`    | Chip label template. `{field}` placeholders substitute from the payload, e.g. `Test run {state}`.                                                                                |

## Ground rules

* **Payloads carry identifiers and enums only, never customer data.** Payloads reach the model, the conversation store, and the browser console (the emit is logged), so keep them to ids, states, and type names. Put anything heavier behind a tool the assistant calls in reaction.
* **Events never create conversations.** The widget drops events with no active conversation, and the server independently ignores batches for sessions without a user message.
* **Write reactions as data plus policy, not scripts.** Tell the assistant what to do per state; let it phrase its own reply.
* **The assistant should end its turn while waiting.** Pair emitters with tool and skill guidance like: open the test, tell the user to run it, end your turn; the event wakes you.

## Debugging

The delivery funnel is observable end to end, joined by `batchId`:

* **Browser console.** Every stage logs an always-on `[Yedric events]` line: the host's emit, queue and drop reasons, flush attempts and retries, the server verdict, and the turn handoff.
* **`Yedric.eventStats()`.** Page-lifetime counters of the same funnel: see [JavaScript API](/yedric/developers/javascript-api.md#yedriceventstats).
* **Server logs.** One structured `[client-events] batch resolved` line per batch with the outcome (`respond`, `context`, `ignore` plus reason, `turn_in_flight`, `invalid`), counts, and ids, followed by `event turn starting` and `event turn persisted` lines when a response runs.

Comparing emit lines against `eventStats().enqueued` shows emitter-side loss (panel closed, no conversation); `verdicts.respond` against `turns.started` shows handoff loss; the server's `batch resolved` against `event turn persisted` shows delivery loss.

## Reference example: MESA

MESA's admin uses two events. `test_run_halt` fires on every halt of a workflow test (a simulation pause before a write, or a terminal status) and lets the assistant walk the user through each pause, confirm passes, and jump straight into debugging failures. `workflow_corrupted` fires when the editor detects a corrupted workflow structure; its definition attaches the repair playbook resource so the assistant can fix the workflow immediately. Both use `triage.mode: "auto"` scoped to `automation_id`, so halts for the workflow on screen always respond, while events for other workflows fall to LLM triage.
