- Unknown phases are liveness. Any event whose
phaseyou don’t recognize means “the run is alive.” Render nothing, never error. New phases are additive and ship without a version bump. - Guaranteed fields only. Each event below lists the fields guaranteed present. Other fields may appear and may change without notice.
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.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.
The first message of a live run is the meta envelope:
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:
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.
The stable subset
search-intent — what the parse understood ($0)
Captured live (trimmed):
phase, query, entity, confidence (a number, 0–1). The wire
shape, as measured:
constraintsis a count (a number). The parsed constraint objects ridepredicate— an array of{ field, op, value, unit? }objects. Constraintfieldnames are per-query vocabulary (the same query may parse toingredient/strengthone run andvitamin/dosagethe next) — treat them as data, not an enum.geois an object (region,countryCode, …) when present, and may be absent.merchant(a prose merchant descriptor) may appear when the query pins one.
search-resolve — the merchant fan-out plan ($0)
Captured live (trimmed):
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):
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)
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.
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.
cost — the orchestrator’s own model spend
phase, usd (may be null).
search-done — the run’s terminal (not the turn’s)
stop ∈ exhausted | query-budget | spend-budget | agent-settled | stopped (open
enum). Guaranteed: phase, stop, records.
Two honest details here:
uncheckedlists merchants that were never dispatched — a fact about us, stated, not hidden. Coverage you didn’t get is named.- A record ending at
search-doneis a turn still judging, not a lost answer. Wait forsearch-answerand the terminal envelope.
search-adjudication — the judging window is never silent
phase.
search-answer — the authoritative summary (bounded by construction)
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
phase, plus reason on an abstain and message on an error. A
terminal error additionally carries code — the stable
stream-terminal vocabulary; mid-run fail-soft
error events may omit it. The session status and the ledger settle accordingly:
an abstain is a **0.
session-turn — a turn closed (SSE terminal envelope)
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:
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.
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.
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.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.