> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fiveninelabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The event stream

> The stable event subset you may build on — real wire shapes captured from live runs, guaranteed fields, and the two rules that keep your integration safe.

The stream is the engine's own progress spine, passed through the gateway verbatim —
we do not maintain a second, prettier vocabulary that could drift from reality. This
page blesses the **stable subset** integrators may rely on, and its examples are
**regenerated from captured live streams** (2026-08-27) — where an earlier draft of
this page disagreed with the wire, the wire won. Two rules make it safe to build on:

1. **Unknown phases are liveness.** Any event whose `phase` you don't recognize means
   "the run is alive." Render nothing, never error. New phases are additive and ship
   without a version bump.
2. **Guaranteed fields only.** Each event below lists the fields guaranteed present.
   Other fields may appear and may change without notice.

<Note>
  One honest wart, stated plainly: **event payloads use the engine's `camelCase`**
  (`costUsd`, `elapsedMs`) while REST request/response bodies use `snake_case`
  (`max_spend_usd`, `spend_usd`). The stream is the engine's wire format, verbatim —
  renaming it would mean a translation layer that could lie.
</Note>

## Transport

`GET /v1/sessions/{id}/stream` is `text/event-stream`. Each SSE message: `id:` = the
event's sequence number, `data:` = one JSON event. Resume with `Last-Event-ID`; the
same numbers page `GET …/events?after=`. Sequence numbers are global per session,
monotonic across turns. Full transport semantics:
[Ways to watch a run](/guides/streaming).

The first message of a live run is the meta envelope:

```
id: 0
data: {"kind":"meta","reqId":"sess_9m2kd","sessionId":"sess_9m2kd"}
```

### The paged replay envelope (`GET …/events`)

The paged log serves the **same event objects, flat, with `seq` merged in** — the
event is not nested under a `data` or `event` member:

```json theme={null}
{
  "data": [
    { "seq": 2, "phase": "search-intent", "query": "…", "entity": "record", "confidence": 0.92 },
    { "seq": 3, "phase": "search-resolve", "merchants": [{ "domain": "waxwing.example" }] }
  ],
  "has_more": false,
  "next": 3
}
```

`seq` is the SSE `id:` for that event. Rebuild-then-attach (page `…/events` from 0 to
rebuild state, then connect `…/stream` with `Last-Event-ID` = the last `seq`) is the
[reload/resume recipe](/guides/streaming#reload-recovery-rebuild-then-attach).

## The stable subset

### `search-intent` — what the parse understood (\$0)

Captured live (trimmed):

```json theme={null}
{
  "phase": "search-intent",
  "query": "vinyl records under $30 in austin",
  "entity": "record",
  "merchant": "record store",
  "predicate": [{ "field": "price", "op": "<", "value": 30, "unit": "USD" }],
  "geo": { "region": "Austin, TX", "countryCode": "US" },
  "constraints": 1,
  "confidence": 0.92
}
```

Guaranteed: `phase`, `query`, `entity`, `confidence` (a **number**, 0–1). The wire
shape, as measured:

* **`constraints` is a count** (a number). The parsed constraint objects ride
  **`predicate`** — an array of `{ field, op, value, unit? }` objects. Constraint
  `field` names are per-query vocabulary (the same query may parse to
  `ingredient`/`strength` one run and `vitamin`/`dosage` the next) — treat them as
  data, not an enum.
* **`geo` is an object** (`region`, `countryCode`, …) when present, and may be
  absent.
* `merchant` (a prose merchant descriptor) may appear when the query pins one.

<Warning>
  **`confidence` is a number, 0–1, on every event** — no surface emits the enum words
  (`"high"`/`"low"`) that earlier drafts showed. One known wire inconsistency, stated:
  `POST /v1/sessions/estimate` currently returns `parsed.confidence` as a
  **string-wrapped** float (`"0.92"`). The number form is canonical; the estimate's
  string form is a documented divergence that will be fixed as a dated-version change.
</Warning>

### `search-resolve` — the merchant fan-out plan (\$0)

Captured live (trimmed):

```json theme={null}
{
  "phase": "search-resolve",
  "merchants": [
    { "domain": "waxwing.example", "seedUrl": "https://waxwing.example/" },
    { "domain": "spindlecity.example", "seedUrl": "https://spindlecity.example/" }
  ],
  "ms": 1490
}
```

Guaranteed: `phase`, `merchants` — an **array of merchant objects**, each guaranteed
`domain` (`seedUrl` and an `evidence` provenance string may ride along). The fan-out
count is `merchants.length`. (An earlier draft documented `merchants` as a number;
the wire has always carried the array — the array is the contract.)

### `orchestrate` — a routing decision (why money moved)

Captured live (trimmed):

```json theme={null}
{
  "phase": "orchestrate",
  "action": "dispatched",
  "url": "https://well.ca/brand/cerave.html",
  "domain": "well.ca",
  "reason": "Intent targets CeraVe at well.ca; this is the CeraVe brand catalog page (68 products)…"
}
```

`action` ∈ `dispatched | declined | re-resolved | proposed | stopped` (open enum).
Guaranteed: `phase`, `action`, `reason`. (The field is **`action`** — an earlier
draft called it `decision`; the wire has always said `action`.) A `declined` carries
`by` (`"pool"` | `"duplicate-guard"`).

These events are the audit trail of spend: every dispatch and every decline, with the
reason, in the same stream as the rows. During the quiet stretches of a run they may
be the only substantive traffic — that's normal.

### `search-source` — a source delivered (rows ride the event)

```json theme={null}
{
  "phase": "search-source",
  "domain": "endofanear.shop",
  "url": "https://endofanear.shop/collections/all",
  "delivered": true,
  "records": 142,
  "costUsd": 0.41,
  "elapsedMs": 49000,
  "complete": true,
  "rows": [{ "title": "…", "price": 8.79, "currency": "CAD", "url": "https://…" }],
  "truncated": { "cap": "rows", "limit": 500 }
}
```

Guaranteed: `phase`, `domain`, `delivered`, `records`. `costUsd` may be `null` — cost
basis unavailable, which settles at **\$0**. Like every dollar amount on this API,
`costUsd` is the customer's price, rounded to ≤4 decimals.

<Warning>
  **`delivered` decides whether this pass gets a receipt — `costUsd` does not.** A pass
  with `delivered: true` produces one receipt on `GET …/receipts`. A pass with
  `delivered: false` produces **none**, even when its `costUsd` is non-null and was
  billed — the common case for a pass stopped by a deadline, an anti-bot wall or a dead
  transport. Don't infer a receipt from the presence of a cost:
  [what a receipt covers](/guides/proposals#what-a-receipt-covers--and-what-it-does-not).
</Warning>

`rows` is a bounded preview (500 rows / 1.5 MB per source, stated in `truncated`); the
full corpus is at `GET …/rows`. Stream rows are **flat per-source records** — see
[the three row dialects](#the-three-row-dialects--one-session-three-spellings).

### `cost` — the orchestrator's own model spend

```json theme={null}
{ "phase": "cost", "usd": 0.006 }
```

Guaranteed: `phase`, `usd` (may be `null`).

### `search-done` — the run's terminal (not the turn's)

```json theme={null}
{
  "phase": "search-done",
  "sources": 3,
  "delivered": 2,
  "records": 228,
  "elapsedMs": 262000,
  "stop": "agent-settled",
  "unchecked": [{ "domain": "waxcellar.shop", "stage": "triage" }]
}
```

`stop` ∈ `exhausted | query-budget | spend-budget | agent-settled | stopped` (open
enum). Guaranteed: `phase`, `stop`, `records`.

Two honest details here:

* `unchecked` lists merchants that were **never dispatched** — a fact about us,
  stated, not hidden. Coverage you didn't get is named.
* **A record ending at `search-done` is a turn still judging, not a lost answer.**
  Wait for `search-answer` and the terminal envelope.

### `search-adjudication` — the judging window is never silent

```json theme={null}
{ "phase": "search-adjudication", "started": true, "residual": 183 }
```

A completion event with tallies always follows. Guaranteed: `phase`.

### `search-answer` — the authoritative summary (bounded by construction)

```json theme={null}
{
  "phase": "search-answer",
  "qualified": 41,
  "needsVerification": 6,
  "rejected": 181,
  "rows": [{ "title": "…", "price_listed": 8.79, "currency": "CAD", "product_id": "…" }],
  "truncated": { "cut": "dispositions" }
}
```

Guaranteed: `phase`, `qualified`. Sections shed adaptively by size, each cut named in
`truncated.cut`. Rows the screen couldn't verify are counted under
`needsVerification`, not silently promoted to qualified. Answer rows are flat like
source rows but with **renamed fields** (`price_listed`, `product_id`) — see the
dialect table below.

### `abstain` / `error` — honest endings

```json theme={null}
{ "phase": "abstain", "reason": "no purchasable entity in the query" }
```

```json theme={null}
{ "phase": "error", "message": "model stream error: Forbidden", "code": "model_unavailable" }
```

Guaranteed: `phase`, plus `reason` on an abstain and `message` on an error. A
**terminal** `error` additionally carries `code` — the stable
[stream-terminal vocabulary](/errors#stream-terminal-codes); mid-run fail-soft
`error` events may omit it. The session `status` and the ledger settle accordingly:
an abstain is a \*\*$0 outcome, stated** — only signed receipts settle, and the rest of
the reservation is released. These are statuses, not HTTP errors (see [Errors](/errors), "Failures that are NOT errors"). A query we [decline to spend on](/errors/declined_to_spend) ends this way too — a `201` create,
an `abstain` event, status `abstained`, $0.

### `session-turn` — a turn closed (SSE terminal envelope)

```json theme={null}
{
  "kind": "session-turn",
  "record": {
    "n": 1,
    "text": "vinyl records under $30 in Austin",
    "route": "run",
    "at": "2026-08-26T14:35:12Z"
  },
  "because": "first turn runs by definition"
}
```

`route` ∈ `run | narrow | ask | reaim | unbound`. A `reaim` record carries
`proposal: { estimatedUsd, passes, confirmed }`. This envelope closes the stream for
the turn.

## The three row dialects — one session, three spellings

Rows appear on three surfaces, and today they are **three different shapes**. This is
a documented wart, not a contract we defend — a future dated version should converge
them — but until then, here is the mapping, as measured on live sessions:

| Surface                       | Shape                                                                                          | Price field    | Row id field | Source attribution                                                    |
| ----------------------------- | ---------------------------------------------------------------------------------------------- | -------------- | ------------ | --------------------------------------------------------------------- |
| Stream `search-source.rows[]` | **Flat** per-source record (`{ title, price, currency, url, …per-source keys }`)               | `price`        | `id`         | The enclosing event's `domain`/`url` — rows carry no `source` wrapper |
| Stream `search-answer.rows[]` | Flat, same keys **renamed** on qualification                                                   | `price_listed` | `product_id` | None on the row — join back to source rows by id/title                |
| REST `GET …/rows`             | **Enveloped**: `{ source: { domain, url, receipt_id }, record: { …the flat record } }`         | `record.price` | `record.id`  | `source.domain` / `source.url` / `source.receipt_id` on every row     |
| REST `GET …/result.rows[]`    | **Enveloped**: `{ source: { domain, url }, record: { …the flat record } }`, plus `fulfillment` | `record.price` | `record.id`  | `source.domain` / `source.url` — **no `receipt_id`**                  |

Practical guidance: parse prices from `price ?? price_listed` (both are JSON numbers,
with `currency` alongside); treat all other record keys as per-source schema. `url`
may be an empty string on some rows; no image field is guaranteed to exist.

<Note>
  **The two REST surfaces are not interchangeable, and the difference is the audit
  join.** `GET …/rows` emits **`source.receipt_id`** on every row — it names the receipt
  of the pass that delivered that row, and it is what lets you reconcile a row against
  [the receipts surface](/guides/proposals#where-the-money-story-is-auditable).
  `GET …/result.rows[]` does **not** carry it;
  it carries `fulfillment` (how the row satisfies the predicate, with evidence) and the
  adjudication's `duplicateCount` / `duplicateSources` instead.

  **Page `…/rows` for anything you intend to audit**, and treat `…/result.rows` as a
  convenience view of the adjudicated subset.
</Note>

## Events not in the subset

`harness` heartbeats (per-pass phase pulses), `search-locate`, `search-triage`,
`orchestrate-turn`/`orchestrate-tool` envelopes, acquisition/anti-bot cost events,
and future phases all flow on the wire. Read them if useful; build on the subset.
They can disappear or change shape without notice — the subset cannot.
