kestrel.markets
Version:
A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.
250 lines (249 loc) • 15.7 kB
JavaScript
/**
* # bus/types — the typed append-only event bus envelope + payloads (RUNTIME §1)
*
* The bus is the single substrate underneath every mode: it is the **audit trail**, the
* **replay corpus**, and the **grading corpus** at once (RUNTIME §1). Every event is one
* flat JSONL record with the envelope `{ seq, ts, stream, type, ...payload }` and a
* monotonic `seq`. The engine writes everything it does back onto the bus; a Session in
* `sim` mode reads a recorded/synthetic bus *as its clock* — `now` is the current event's
* `ts` (RUNTIME §0), so nothing on the runtime path reads a wall clock.
*
* This file is the **typed object model of the bus**. Other modules import these types;
* they never redefine an envelope or a payload. The model is a discriminated union keyed on
* `stream` (and, within `TICK`, on `type`) — narrow on `event.stream` first, then on
* `event.type`.
*
* ## Streams (RUNTIME §1)
* `META` (session header) · `TICK` (`SPOT` | `BOOK` | `HEARTBEAT`) · `DETECTOR` ·
* `PLAN` (lifecycle) · `ORDER` (place/cancel/fill/reject) · `WAKE` · `CONTROL` (author
* actions) · `REGIME` (tag writes) · `JOURNAL` (author reasoning, full markdown inline) ·
* `TELEMETRY` (engine-emitted per-order observability). Schema-versioned via {@link BUS_SCHEMA},
* carried on the `META` header and validated by the reader.
*
* `JOURNAL` is the one stream that is **author metadata, not an engine input** (a57.11): the
* per-event pass never folds it, so adding JOURNAL records leaves the emitted engine event
* stream — and the determinism hash — byte-identical. It is also the one stream discriminated
* on `kind` rather than an engine `type` (there is no `(stream,type)` pairing for it); the
* reader tolerates a forward/unknown `kind` by a logged skip instead of a throw.
*
* `TELEMETRY` (a57.9) is the mirror-image case: it IS engine output (emitted onto the same
* stream as PLAN/WAKE, so it joins the determinism hash — same input ⇒ same telemetry ⇒ stable
* hash) but it is **observational only** — no engine DECISION reads it back, so the per-event
* pass skips it exactly like JOURNAL and its presence never changes an order, fill, or plan
* outcome. It carries an **open** `type` vocabulary (like DETECTOR/CONTROL) so per-order
* observability can grow without a schema bump; a reader tolerates an unrecognized telemetry
* `type` by simply ignoring it, and a report over a bus that carries no telemetry degrades to a
* documented default (esc stage 0, reprice count 0) rather than crashing (RUNTIME §8).
*
* ## Equity-only sessions
* An equity-only session is representable two ways, both first-class: emit no `BOOK` events
* at all (SPOT-only tape), or emit `BOOK` events whose `legs` array is empty. Options are a
* per-instrument overlay on the underlier tape, never a structural requirement.
*/
// ─────────────────────────────────────────────────────────────────────────────
// Schema + enumerations
// ─────────────────────────────────────────────────────────────────────────────
/** The bus schema version new buses are written at. Bumped on any breaking envelope/payload
* change — including the addition of a new stream tag, since a reader that predates the tag
* hard-throws on it (fail-closed, but mid-stream): v2 adds the {@link JournalEvent JOURNAL} stream,
* v3 adds the {@link TelemetryEvent TELEMETRY} stream. (The TELEMETRY `type` vocabulary is open and
* may grow WITHOUT a bump — that is a within-stream extension a telemetry-aware reader tolerates;
* introducing the STREAM tag itself is the breaking change, exactly as JOURNAL was.)
*
* v4 (kestrel-a57.1 slice 1, ADR-0011) records the **probabilistic accounting on the bus** so the
* Blotter projector re-derives `totals{floor,expected}` + `orders[].support` with no engine scrape:
* a settle-outcome TELEMETRY record ({@link TelemetrySettlePayload}, `type: "settle"`) per order, and
* the **judge self-description** on a GRADED bus's META ({@link MetaPayload.fill_model}/`instance`/
* `fidelity`). Both are strictly additive & OPTIONAL — the settle `type` rides the already-open
* TELEMETRY vocabulary (no reader gate to widen) and the META judge fields are absent on a fill-model-
* agnostic INPUT tape (the two-bus rule, ADR-0011) — so a v1..v3 bus reads clean here and the bump is
* really only about advertising the new self-describing shape. The reader accepts every schema in
* {@link SUPPORTED_BUS_SCHEMAS} and refuses anything outside it loudly (fail-closed, RUNTIME §8).
*
* v5 (kestrel-22j.15) mints a per-Plan-**instance** identity — {@link PlanPayload.plan_instance}, carried
* onto the authored {@link OrderPayload}/{@link WakePayload}/{@link ControlPayload} evidence — so a legal
* same-NAME replacement (a Lineage key recurs after its predecessor is `done`, CONTEXT: Lineage) no longer
* collapses into the prior instance's lifecycle trace at report projection. The id is minted DETERMINISTICALLY
* at registration (the authored name + a registration ordinal; NO wall clock, NO unseeded RNG, RUNTIME §0) —
* same bus + documents ⇒ byte-identical ids. It is additive & OPTIONAL (a v1..v4 bus simply carries none), but
* the bump is DELIBERATE: it advertises that a graded bus now self-identifies its Plan instances, and it draws
* the fail-closed line for the migration. A projector reads the explicit `plan_instance` when present and, for a
* pre-v5 bus that lacks it, MIGRATES EXPLICITLY by sessionizing each name's lifecycle at its structural
* `done`→re-authored boundary ({@link ../blotter/project.ts}) — never inferring identity from timestamps or
* fuzzy name-matching (which the F4 same-name-supersession guard makes exact: a name only recurs after its
* predecessor is `done`). The reader accepts every schema in {@link SUPPORTED_BUS_SCHEMAS} and refuses anything
* outside it loudly (fail-closed, RUNTIME §8).
*
* v6 (kestrel-w7la.1, ADR-0040 — clock-honest wakes) records **deliberation cost on the Bus**: the
* within-stream WAKE type `"deliberation"` ({@link DeliberationEvent}) prices one clocked Seat turn —
* `ts` IS the derived return time `checkpoint.ts + measured_ms + buffer_ms`, `wake_seq` names the WAKE
* checkpoint it belongs to — so replay/certification re-project byte-identically from recorded time
* (the determinism invariant's recorded-time clause, RUNTIME §0). The type is within-stream-ADDITIVE,
* yet the bump is DELIBERATE (exactly the v5 precedent): a v5 build that *ignored* deliberation records
* would re-project a clocked bus into a *different* byte stream — silent wrongness — so stamping v6
* makes every pre-0040 build refuse a clocked bus up front at the META line. Only session-OUTPUT (graded)
* buses stamp v6; synthetic INPUT tapes keep their pinned literal schema (an input tape can never carry a
* deliberation record, and re-stamping the corpus would force a corpus re-mint — ADR-0018). A `deliberation`
* record on a bus whose META stamps `bus_schema < 6` is refused loudly by the reader (a clocked record on a
* pre-clocked bus is a corrupt bus). See docs/design/clock-honest-wakes.md §1. */
export const BUS_SCHEMA = 6;
/** The bus schemas this build can read. JOURNAL landed in v2, TELEMETRY in v3, the settle-outcome
* telemetry + META judge self-description in v4, and the per-Plan-instance identity
* ({@link PlanPayload.plan_instance}) in v5 — every one an append-only / additive change that
* moves no engine semantics, so older buses are forward-compatible: a v1 bus simply has no JOURNAL/
* TELEMETRY records, a v2 bus no TELEMETRY, a v3 bus no settle-outcome record and a bare (judge-less)
* META, a v4 bus no `plan_instance` (a projector migrates it explicitly by lifecycle sessionization,
* never a silent mis-identify). The reader tolerates any schema listed here and rejects the rest
* loudly — a build that predates a stream refuses a bus carrying it up front at the META line rather
* than throwing mid-stream (fail-closed, §8). v6 (clock-honest wakes, ADR-0040) adds the WAKE
* `deliberation` record; a v1..v5 bus simply carries none and replays byte-identically. */
export const SUPPORTED_BUS_SCHEMAS = new Set([1, 2, 3, 4, 5, 6]);
/** The set of legal stream tags, for reader validation (fail-closed on an unknown stream). */
export const STREAMS = new Set([
"META",
"TICK",
"DETECTOR",
"PLAN",
"ORDER",
"WAKE",
"CONTROL",
"REGIME",
"JOURNAL",
"TELEMETRY",
]);
/**
* The interface **face** an author reached the model through (kestrel-m9i.2 owner tweak, ENV·CFG): the
* closed vocabulary the m9i.7 face tournament pivots on — `http` (raw provider HTTP), `sdk` (a
* provider/AI SDK client), `cli` (a terminal harness), `mcp` (a Model Context Protocol server). A CLOSED
* set: the projector fail-closes a declared envelope whose `face` is outside it (a typed vocabulary,
* never an open string that drifts).
*
* CANONICAL in the protocol leaf ({@link import("../protocol/index.ts").Face}, ADR-0004) — the bus
* re-exports it so the a57.14 envelope-identity axis validates against the SAME closed vocabulary the
* surfaces project, one source of truth across the protocol/bus seam (arch-review A6). Protocol does
* not import bus, so this cannot cycle.
*/
export { FACES } from "../protocol/index.js";
/** The closed {@link CorpusTier} vocabulary in a FIXED order (deterministic validation reasons; mirrors
* {@link FACES}). */
export const CORPUS_TIERS = ["public", "semi-private", "private"];
/**
* The practice-vs-holdback **benchmark-tiering wall** (kestrel-m9i.26; benchmark-matrix-design §3; ADR-0018
* season) mapped explicitly onto the {@link CorpusTier} vocabulary. A pure, TOTAL classifier over the closed
* set: `public` is the freely-trainable, non-ranking PRACTICE tier; `semi-private`/`private` are the sealed
* forward-window HOLDBACK tiers (event-date > training_cutoff, ranking-eligible). No wall clock/RNG.
*/
export function corpusTierWall(tier) {
return tier === "public" ? "practice" : "holdback";
}
/** The known JOURNAL kinds. A JOURNAL whose `kind` is outside this set is well-formed but
* unknown — the reader drops it with a logged reason (crash-tolerant forward-compat, RUNTIME §8),
* never widening it onto the union. */
export const JOURNAL_KINDS = new Set(["author", "debrief", "note"]);
/** The telemetry `type`s this build emits/recognizes. Not a closed reader gate (telemetry is
* open-vocabulary) — it is the set a consumer folds; an unrecognized `type` is skipped, not an
* error. */
export const TELEMETRY_TYPES = new Set(["order", "settle", "guard", "hazard", "settle_mark", "theta_bleed"]);
/**
* Fold a {@link BookEvent} into a {@link BookState} (latest-wins, per instrument, per leg). Pure.
*
* A BOOK event may carry the **full chain** (a snapshot) or a **single-leg delta** (the recorded
* OPRA tapes emit one `(strike,right)` leg per event). When `prior` is supplied, each incoming
* leg **upserts** into the prior book keyed by `(strike, right)` — legs the event does not mention
* keep their last-known top-of-book. Without `prior` (or a different instrument) the event's legs
* ARE the book (a snapshot). This is what makes a per-leg delta stream accumulate a full book
* instead of collapsing to just the most-recently-quoted leg (RUNTIME §2).
*/
export function foldBook(ev, prior) {
let legs;
if (prior === undefined || prior.instrument !== ev.instrument || prior.legs.length === 0) {
legs = ev.legs;
}
else {
const merged = new Map();
for (const l of prior.legs)
merged.set(`${l.strike}:${l.right}`, l);
for (const l of ev.legs)
merged.set(`${l.strike}:${l.right}`, l); // incoming leg wins
legs = [...merged.values()];
}
return {
instrument: ev.instrument,
underlier_px: ev.underlier_px,
...(ev.expiry !== undefined ? { expiry: ev.expiry } : {}),
legs,
asof_seq: ev.seq,
asof_ts: ev.ts,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Guards
// ─────────────────────────────────────────────────────────────────────────────
/** Is `s` a legal stream tag? */
export function isStream(s) {
return typeof s === "string" && STREAMS.has(s);
}
/** The closed `type` vocabulary of the `TICK` stream (RUNTIME §1). */
export const TICK_TYPES = new Set([
"SPOT",
"BOOK",
"HEARTBEAT",
]);
/** The closed `type` vocabulary of the `ORDER` stream. */
export const ORDER_ACTIONS = new Set([
"place",
"cancel",
"fill",
"reject",
]);
/** The closed `type` vocabulary of the `WAKE` stream's *delivery* records ({@link WakeKind}).
* The v6 `deliberation` record is a WAKE `type` too, but NOT a {@link WakeKind} — it carries the
* {@link DeliberationPayload}, not a `wake` — so it is paired separately in {@link isValidStreamType}. */
export const WAKE_KINDS = new Set([
"wake",
"coalesced",
"downgraded",
]);
/** The v6 WAKE `type` of the {@link DeliberationEvent} (clock-honest wakes §1). */
export const DELIBERATION_TYPE = "deliberation";
/**
* Is `type` a legal event `type` for `stream`? Enforces the pairing the discriminated union
* encodes (RUNTIME §1): a `TICK` must be `SPOT | BOOK | HEARTBEAT`, an `ORDER` a `place |
* cancel | fill | reject`, a `PLAN` a `lifecycle`, a `WAKE` a `wake | coalesced | downgraded`,
* a `REGIME` a `tag`, a `META` a `session`. `DETECTOR` and `CONTROL` carry **open**
* vocabularies (the detector's own event names; the L2→L1 author-action channel) — any
* non-empty string. A `(stream, type)` pair that does not inhabit the union is not a thing to
* skip; it is corruption (the reader raises loudly, §8).
*
* `JOURNAL` is deliberately absent: it has no engine `type` (its discriminant is `kind`), so it
* is never routed through this pairing check — the reader validates it on `kind`/`body` directly
* and returning `false` here would misreport a well-formed journal as corruption.
*
* `TELEMETRY` (a57.9) carries an **open** vocabulary like DETECTOR/CONTROL — observational
* per-order records may grow without a schema bump, so any non-empty `type` is well-formed and a
* consumer ignores the ones it does not recognize (crash-tolerant, §8).
*/
export function isValidStreamType(stream, type) {
switch (stream) {
case "META":
return type === "session";
case "TICK":
return TICK_TYPES.has(type);
case "DETECTOR":
return type.length > 0;
case "PLAN":
return type === "lifecycle";
case "ORDER":
return ORDER_ACTIONS.has(type);
case "WAKE":
return WAKE_KINDS.has(type) || type === DELIBERATION_TYPE;
case "CONTROL":
return type.length > 0;
case "REGIME":
return type === "tag";
case "TELEMETRY":
return type.length > 0;
default:
return false;
}
}