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

# Client Events

Report events from your app, like a finished test run or a failed job, to the Yedric assistant with Yedric.event so it can react or note them.

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)`](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](https://docs.yedric.ai/developers/javascript-api/#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.
