UNPKG

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.

896 lines (846 loc) 50.9 kB
/** * # frame/types — the plain, typed inputs a Frame renderer consumes (ADR-0008/0009) * * The renderer is a **pure function of these objects** — it invents no value (ADR-0008 * lineage): every number it prints is a field on the input, and an absent/UNKNOWN field * renders as an explicit `—` (never a guessed or defaulted number). The stepped day runner * populates these from the canonical state (`spot`/`hod`/`lod`/`vwap`/opening range, RUNTIME * §2), the book reduction ({@link ../bus/types.ts BookState}), and the fill/plan engines * (positions, resting orders, fills, budget, plan lifecycle — RUNTIME §5–6). * * ## Date-blind by construction (EVALUATION.md) * There is **no date field anywhere** in these inputs. Time is carried three ways, all * relative or clock-only: minutes-to-close (`T-92m to close`), a wake ordinal + minutes since * the last vantage (`wake 3, 41m since last`), and an **HH:MM ET clock** string on tape rows * and headers (`13:24`). `session_date`, day-of-week, and epoch-millisecond values are * **forbidden** — the runner must not put them here, and the renderer never emits them. A CI * grep over rendered output enforces this. * * ## Unknowns are modelled explicitly * A value that is genuinely unavailable is `null` (or an omitted optional) — the renderer * turns it into `—`. This is the fail-closed discipline of RUNTIME §8 carried into the screen: * an unknown is shown as unknown, never filled in. * * ## The Frame of Fields + the cockpit lead block (this milestone) * Two additions land here on top of the plain inputs above: * - {@link Field} (4gl.1) — the one **watermarked, attributed** value type: every number the * author reads is a `Field { value, attribution, source, modelVer, confidence, asOfSeq }`, so a * number carries how it was derived (`OBS`/`CALC`/`MODEL`) and, for a `MODEL` value, its receipt * (source + modelVer) and confidence. {@link makeField} is the construct-time honesty guard: a * `MODEL` field that omits its receipt or confidence is REFUSED (fail-closed). * - The extended {@link Kernel} — the acting **cockpit lead block** (4gl.3): wake, per-vehicle * data-health + named unavailable capabilities, positions/inventory-claims, resting orders, * the risk-envelope budget, owner acts, the engine log, and predictor/regime claims — the * superset of the plan-lifecycle section. Every section is always present (absent renders as an * explicit `none`/`UNKNOWN`, never dropped), the block LEADS every frame, and it is * non-configurable (built in code and prepended outside any pane selection — see * {@link KERNEL_SECTION_LABELS} / {@link RESERVED_PANE_IDS}). */ import type { OptionsAnalytics } from "./options-analytics.ts"; import type { SessionScheme } from "./session-scheme.ts"; /** Session-calendar phase (mirrors {@link ../bus/types.ts SessionPhase}); a label, never a date. */ export type FramePhase = "pre" | "open" | "regular" | "close" | "post"; /** Option right — the two contract kinds (shared vocabulary with the language `Leg`). */ export type FrameRight = "C" | "P"; /** One instrument the session carries — its spec, as the briefing lists it. */ export interface InstrumentSpec { readonly symbol: string; readonly assetClass: "equity" | "index" | "future" | "option-underlier"; /** Signal-space vs execution-space role, when the session pins one (ARCHITECTURE §2). */ readonly role?: "signal" | "exec"; /** Contract multiplier (per-contract $ per point), when known. `null`/absent ⇒ `—`. */ readonly multiplier?: number | null; /** Minimum price increment, when known. `null`/absent ⇒ `—`. */ readonly tick?: number | null; /** * The instrument's SessionScheme (ADR-0041 §1/§3, kestrel-wa0j.44) — a series-registry attribute * declaring its monotone boundary stream (which `open`/`close`/session-split boundaries exist), the * stream `SessionOrdinal`'s `d-N` indexes. OPTIONAL: absent ⇒ {@link ../frame/session-scheme.ts * DEFAULT_SESSION_SCHEME} (`equity-rth`, which carries a close), so every existing frame input is * byte-identical (no-churn). A scheme with NO close boundary (a perp's rolling UTC day) makes the * `prior-context` pane's close address DEFECTIVE — the pane renders the declared rolling extreme, * never an invented midnight close ({@link ../frame/paradigm-ledger.ts}, ADR-0041 §3). One of the * closed {@link ../frame/session-scheme.ts SESSION_SCHEMES} roster. */ readonly sessionScheme?: SessionScheme; } /** The canonical levels of one signal instrument (RUNTIME §2). Any level may be `null` (UNKNOWN * before warm — e.g. `priorClose` is a cross-session inject that may be absent; the opening * range reads UNKNOWN until the first tick). `null`/absent ⇒ `—`. */ export interface LevelSet { readonly spot: number | null; readonly priorClose?: number | null; readonly hod?: number | null; readonly lod?: number | null; readonly vwap?: number | null; readonly orHigh?: number | null; readonly orLow?: number | null; /** * The declared ROLLING EXTREME (ADR-0041 §3, kestrel-wa0j.44) — the anchor a no-close * {@link ../frame/session-scheme.ts SessionScheme} (a perp's rolling UTC day) uses IN PLACE of a * prior close: the rolling-window high/low the frame carries (e.g. the rolling 24h extreme). The * `prior-context` pane renders THESE for a no-close scheme (the sanctioned periphrasis), never an * invented midnight close. OPTIONAL: absent on every equity frame (byte-identical — a close-bearing * scheme never reads them); `null`/absent under a no-close scheme ⇒ the pane fails closed to the * DEFECTIVE refusal (no honest anchor to render), never a fabricated value. */ readonly rollingHigh?: number | null; /** The rolling-window LOW companion to {@link rollingHigh} (the declared rolling extreme's low). */ readonly rollingLow?: number | null; /** * Whole seconds since the SPOT tick that established {@link spot} last printed, at this frame's * cutoff (kestrel-rs4). A dead feed and a frozen market produce the SAME number for `spot`; only * this age tells them apart, so once it passes the staleness backstop * ({@link ../frame/options-analytics.ts SPOT_STALE_AFTER_MS}) the pane tags the rendered value * `[STALE <age>]` rather than present a price the feed stopped supporting with the confidence of * a fresh tick. Mirrors {@link ../frame/options-analytics.ts OptionsAnalytics.spotStaleSeconds} * (the same age, the same backstop — the two surfaces can never call one spot fresh and dead at * once). `null`/absent ⇒ the caller stated no age; the value renders bare (byte-identical to a * pre-rs4 frame) — an unaged spot is NOT a claim of freshness. */ readonly spotStaleSeconds?: number | null; } /** * One tape bucket, in the incumbent rotated-candlestick geometry (rendering-variants.md * candidate 2). Carries the bucket's OHLC and the HH:MM **ET clock** it closed at — the clock * is the only time token on a row (date-blind). The renderer maps price → column position; the * numbers here are the truth, the column is layout. */ export interface TapeRow { /** HH:MM ET clock label for this bucket (e.g. `13:24`). Date-blind: no date component. */ readonly clock: string; readonly open: number; readonly high: number; readonly low: number; readonly close: number; /** Bucket volume, when the tape carries it (v1 SPOT tape does not). `null`/absent ⇒ omitted. */ readonly volume?: number | null; } /** One near-money chain leg summary for the chain pane. A `null` bid/ask side is **dark** (the * MM-pull fingerprint, ARCHITECTURE §4) — rendered as a flag, never as a price. `fair` carries * its receipt annotation (`fairNote`, e.g. `b76 nLiq=5` or `fallback(mid)`). */ export interface ChainRow { readonly strike: number; readonly right: FrameRight; /** Top-of-book bid; `null` ⇒ bid dark. */ readonly bid: number | null; /** Top-of-book ask; `null` ⇒ ask dark. */ readonly ask: number | null; /** ExecutionFair for the leg; `null`/absent ⇒ unbuildable this moment (`—`). */ readonly fair?: number | null; /** The fair receipt annotation carried onto the value (RUNTIME §4). */ readonly fairNote?: string; /** Last printed trade for the leg, when one has printed. */ readonly last?: number | null; } /** The market pane for one instrument: its levels, its recent tape, its near-money chain. For a * briefing the tape is the long-horizon orientation window; for a wake delta it is only the * buckets **since the last vantage**. */ export interface MarketPane { readonly instrument: string; readonly levels: LevelSet; /** Recent tape buckets, oldest first, newest last (append-only, ADR-0008). Keep ≤ ~20. */ readonly tape: readonly TapeRow[]; /** Bucket width in minutes (default 5; finer near the close). */ readonly tapeBucketMin: number; /** Near-money chain legs (bid/ask/fair + receipt + dark flags). Empty for equity-only. */ readonly chain: readonly ChainRow[]; /** * RELATIVE days-to-expiry of the near-money chain (`0` for a 0dte) — the day driver's machine * channel already carries it (`AuthorFrame.chain.dte`); threading it here restores the dte the * chain-pane header lost in the one-renderer swap (kestrel-wa0j.19 §4). Date-blind by * construction: a relative offset, never a date. `null`/absent ⇒ omitted from the header * (the {@link TapeRow.volume} idiom — the header is byte-identical to today). */ readonly chainDte?: number | null; /** * The **options-analytics projection** (pane-library-spec §5) — the full-chain per-strike surface * (NBBO/mid OBS, IV/greeks MODEL, OI OBS) the GEX/IV panes read. Optional: absent on the * SPOT-only / near-money-chain-only constructions (the pane fails closed to UNKNOWN when it is * absent), populated only for a View that opts into the options-analytics panes. See * {@link ./options-analytics.ts}. */ readonly options?: OptionsAnalytics; } /** A held position in the acting book — basis + current fair (RUNTIME §6). `qty` is signed * (long positive). `fair` `null`/absent ⇒ `—`. */ export interface Position { readonly instrument: string; /** Strike — an OPTION leg carries a finite strike; an EQUITY/SPOT leg carries NEITHER strike NOR * right (ADR-0017: a spot instrument has neither, and a fictional strike is never written). Both * absent ⇒ the percept renders `<instrument> shares` with no option chrome ({@link isSpotLeg}). */ readonly strike?: number; /** Right — present (with {@link strike}) for an OPTION leg; absent for an EQUITY/SPOT leg (ADR-0017). */ readonly right?: FrameRight; readonly qty: number; /** Average cost per contract (the position's basis). */ readonly basis: number; /** Current ExecutionFair per contract, when buildable. `null`/absent ⇒ `—`. */ readonly fair?: number | null; /** * The position's running **unrealized P&L in DOLLARS** (kestrel-c11) — `qty × (mark − basis) × * multiplier`, marked to the current spot (equity/spot) or intrinsic (option), with the SAME * dollar scaling the fill engine's `pnl` applies (`src/engine/orgfacts.ts`). Computed UPSTREAM in * the org-facts layer ({@link ../session/simulate.ts kernelOf}) and PRINTED here, so the agent * READS its P&L (`-$25.97`) rather than deriving it and mis-scaling cents-for-dollars (the 100× * abandon bug). `null` ⇒ the mark is UNKNOWN (no spot) ⇒ renders `—` (fail-closed, never a * fabricated 0). Absent ⇒ the field is not rendered at all (purely additive; pre-c11 constructions * are byte-identical). */ readonly unrealUsd?: number | null; /** The plan that opened the position (provenance), when it came from one. */ readonly plan?: string; /** * The plan that **owns this inventory claim** — the cockpit's inventory-claim ownership: exactly * one plan may claim a given inventory line, and the owner and the engine see the same claim * (mutual visibility). Distinct from {@link plan}: `plan` records which plan *opened* the leg; * `claimOwner` records the live ownership the cockpit renders. Absent on the pre-cockpit, * summary-only construction. */ readonly claimOwner?: string; /** The structure this leg belongs to (e.g. a vertical, a barbell) — the cockpit position label. */ readonly structure?: string; } /** A resting order working in the book — its resolved price + the price-resolution annotation * it carries (RUNTIME §4: a silent mid is forbidden, so the annotation rides here). */ export interface RestingOrder { readonly ref: string; readonly side: "buy" | "sell"; readonly instrument: string; /** Strike/right — BOTH present for an OPTION leg, BOTH absent for an EQUITY/SPOT leg (ADR-0017; a * spot order has neither). Both absent ⇒ the percept renders `<instrument> shares` ({@link isSpotLeg}). */ readonly strike?: number; readonly right?: FrameRight; readonly qty: number; readonly px: number; /** Price-resolution annotation (e.g. `fair=fallback(mid)`, `cap fair,0.73`). */ readonly note?: string; readonly plan?: string; /** * Working in the book right now. An **independent** flag from {@link clamped}: the cockpit * surfaces both, so a `live` label can never mask a clamped price (fail-closed honesty, * RUNTIME §4 — a silent price is forbidden). Absent on the pre-cockpit construction. */ readonly live?: boolean; /** * Price was clamped to the premium band. **Independent** of {@link live}: a working order whose * price was clamped is `live` AND `clamped` — both render, never collapsed into one label. */ readonly clamped?: boolean; } /** A fill that happened since the last vantage (RUNTIME §6). `clock` is the HH:MM ET it filled * at, when known (date-blind). */ export interface FillRecord { readonly side: "buy" | "sell"; readonly instrument: string; /** Strike/right — BOTH present for an OPTION fill, BOTH absent for an EQUITY/SPOT fill (ADR-0017; a * spot fill has neither). Both absent ⇒ the percept renders `<instrument> shares` ({@link isSpotLeg}). */ readonly strike?: number; readonly right?: FrameRight; readonly qty: number; readonly px: number; readonly clock?: string; readonly plan?: string; } /** The book's risk envelope usage (RUNTIME §5). Any of these may be `null`/absent ⇒ `—`. */ export interface Budget { readonly used: number | null; readonly remaining: number | null; readonly total?: number | null; /** Max concurrent R the envelope permits, when the book pins one. */ readonly maxConcurrentR?: number | null; } /** One plan's lifecycle state (RUNTIME §5). */ export interface PlanStateEntry { readonly name: string; readonly state: "authored" | "armed" | "fired" | "managing" | "done"; readonly outcome?: "filled" | "expired" | "invalidated"; /** A logged reason (a de-arm/invalidation reason, a wake note). */ readonly note?: string; /** kestrel-50w: the ARM-TIME gate-block reason when this plan is stuck `authored` on an unsatisfiable * regime gate (the gated tag is UNKNOWN — no feed — or holds a different value). Present ⇒ the plan is * rendered `authored (blocked: <reason>)` so a bare `authored` (a live plan awaiting its WHEN) is * DISTINGUISHABLE from one that can never arm (the phantom-position trap: an agent misread `authored` * as armed-and-live and traded a position that never existed). Reuses the engine's own reason string. */ readonly blockedReason?: string; } // ───────────────────────────────────────────────────────────────────────────── // The Frame of Fields (4gl.1) — a watermarked, attributed value // ───────────────────────────────────────────────────────────────────────────── /** * Provenance class of a rendered number (CONTEXT: Field). How the value was derived: * - `OBS` — an observed market datum (a quote, a trade). * - `CALC` — a deterministic transform of `OBS` (VWAP, range, %chg) — carries no receipt. * - `MODEL` — a model output (a fair, a predictor/regime claim) — MUST carry a receipt * (`source` + `modelVer`) and a `confidence`; an unattributed model claim is dishonest. */ export type Attribution = "OBS" | "CALC" | "MODEL"; /** * The one value type: **every rendered number is a Field**, carried with its provenance so the * renderer invents nothing. A `MODEL` value is honest only if it carries its receipt (`source` * carrying the model, `modelVer`) and its `confidence`; `OBS`/`CALC` carry neither. `asOfSeq` is * the engine's monotonic sequence at derivation — an **ordinal, never a wall-clock time** (the * determinism / replay-stability key; there is no date anywhere on a Field, RUNTIME §8). * * Construct a Field through {@link makeField}, which refuses a dishonest `MODEL` at construct time. */ export interface Field<T = number> { /** The value itself — the number (or label) that reaches the screen. */ readonly value: T; /** How the value was derived (OBS / CALC / MODEL). */ readonly attribution: Attribution; /** The source watermark carrying the model — required for `MODEL`; absent for `OBS`/`CALC`. */ readonly source?: string; /** The model version receipt — required for `MODEL`; absent for `OBS`/`CALC`. */ readonly modelVer?: string; /** Model confidence in `[0,1]` — required for `MODEL`; absent for `OBS`/`CALC`. */ readonly confidence?: number; /** Engine monotonic sequence at derivation (an ordinal, NEVER a wall-clock/date). */ readonly asOfSeq?: number; } /** * Raised by the honesty guards when a claim/field would misrepresent its provenance. Throwing is * the fail-closed response: a dishonest `MODEL` claim is REFUSED, never rendered as if honest. */ export class KernelHonestyError extends Error { constructor(message: string) { super(message); this.name = "KernelHonestyError"; } } /** * The `[0,1]` range contract {@link Field.confidence} pins, as an executable predicate (4gl.19). * Merely FINITE is not honest: a `confidence` of `1.7` or `-0.5` is a number the contract cannot * express, so admitting it would let a guard report "honest MODEL claim" while the frame renders * `conf=1.70` — a silent-false. Bounds are CLOSED (`0` and `1` are honest: no confidence, and * certainty). `NaN`/`±Infinity` fail both comparisons, so they are refused here too. */ function isHonestConfidence(confidence: number): boolean { return confidence >= 0 && confidence <= 1; } /** * Construct a {@link Field}, refusing a dishonest one at construct time (the 4gl.1 honesty guard): * - a `MODEL` field MUST carry `source` + `modelVer` + a `confidence` in `[0,1]` — a `MODEL` claim * missing any of them, or carrying an out-of-range confidence, is REFUSED (fail-closed); * - an `OBS`/`CALC` field MUST carry neither a `modelVer` receipt nor a `confidence` — a * mis-attributed field that smuggles a model receipt in as `OBS`/`CALC` is REFUSED. * Never returns a Field that violates the attribution contract. */ export function makeField<T>(spec: { readonly value: T; readonly attribution: Attribution; readonly source?: string; readonly modelVer?: string; readonly confidence?: number; readonly asOfSeq?: number; }): Field<T> { const { attribution } = spec; if (attribution === "MODEL") { if (spec.source === undefined || spec.source === "") { throw new KernelHonestyError( "MODEL Field carries no source watermark — a MODEL value MUST carry source(modelVer) + confidence", ); } if (spec.modelVer === undefined || spec.modelVer === "") { throw new KernelHonestyError( "MODEL Field carries no modelVer receipt — a MODEL value MUST carry source(modelVer) + confidence", ); } if (spec.confidence === undefined) { throw new KernelHonestyError( "MODEL Field carries no confidence — a MODEL value MUST carry source(modelVer) + confidence", ); } if (!isHonestConfidence(spec.confidence)) { throw new KernelHonestyError( `MODEL Field carries an out-of-range confidence ${spec.confidence} — MODEL confidence MUST be in [0,1]`, ); } } else { if (spec.confidence !== undefined) { throw new KernelHonestyError(`${attribution} Field must carry no confidence`); } if (spec.modelVer !== undefined) { throw new KernelHonestyError(`${attribution} Field must carry no modelVer receipt`); } } // Build without ever assigning `undefined` to an optional key (exactOptionalPropertyTypes). const out: { value: T; attribution: Attribution; source?: string; modelVer?: string; confidence?: number; asOfSeq?: number; } = { value: spec.value, attribution }; if (spec.source !== undefined) out.source = spec.source; if (spec.modelVer !== undefined) out.modelVer = spec.modelVer; if (spec.confidence !== undefined) out.confidence = spec.confidence; if (spec.asOfSeq !== undefined) out.asOfSeq = spec.asOfSeq; return out; } // ───────────────────────────────────────────────────────────────────────────── // The extended cockpit lead block (4gl.3) — the sections the kernel always leads with // ───────────────────────────────────────────────────────────────────────────── /** Wake urgency — a named severity (not a raw magnitude). */ export type WakeSeverity = "routine" | "elevated" | "urgent"; /** * Why the author is looking now, and the deadline it must act within. `deadline` is * **minutes-to-close — a RELATIVE duration** (rendered `T-Nm`), `null` ⇒ `UNKNOWN`; it is NEVER a * wall-clock time or a date (date-blind by construction, EVALUATION.md). */ export interface WakeRef { readonly reason: string; readonly severity: WakeSeverity; /** Minutes to close (relative duration), or `null` when unknown. Never an absolute time/date. */ readonly deadline: number | null; } /** * Per-vehicle book health (the routing gate). `bidPresentRate` (0..1) is the market-maker-pull * fingerprint; `dark` means the book pulled / went one-sided. A dark vehicle is NAMED, never * hidden or blank (absent-not-hidden), and taints its dependents (they render UNKNOWN, RUNTIME §8). */ export interface VehicleHealth { readonly instrument: string; readonly bidPresentRate: number; readonly twoSided: boolean; readonly staleS: number; readonly dark: boolean; } /** The four engine-log buckets (fixed order in the rendered section). */ export type EngineActionKind = "fired" | "cancelled" | "rejected" | "clamped"; /** One L0/L1 engine action since the last vantage. `asofSeq` is an ordinal, never a wall-clock. */ export interface EngineAction { readonly id: string; readonly kind: EngineActionKind; readonly asofSeq: number; /** The reason a `rejected` action was refused (e.g. `"uncovered sell refused: never naked"`, a * cancelOrder no-such-ref refuse) or a `clamped` action was reduced (e.g. `"exceeds plan budget"`). * Carried on the kernel data so the acting agent can see WHY its order was refused/clamped * (kestrel-7kt / kestrel-75n); the text renderer surfaces it after the `id@seq` as `(<reason>)` when * present, byte-stable when absent. Absent for non-refusal/non-clamp actions. */ readonly reason?: string; } /** One owner act (a standing-directive change the owner made). `asofSeq` is an ordinal. */ export interface OwnerAct { readonly id: string; readonly kind: string; readonly asofSeq: number; } /** The two claim kinds the kernel carries. */ export type ClaimType = "predictor" | "regime"; /** * A predictor/regime claim — an **honest `MODEL` {@link Field}** carrying its receipt and * confidence. Validate with {@link assertClaimHonest}: an `OBS`/`CALC` claim, or a `MODEL` claim * missing its receipt/confidence, is REFUSED (fail-closed). */ export interface Claim { readonly field: Field; readonly claimType: ClaimType; } /** * The **sizing headroom** (kestrel-m9i.32) — the MAX fillable size the remaining-R budget admits for * the exec instrument, so a model can size WITHIN the bounded-risk envelope instead of discovering it * by a SILENT fire-time clamp. Bounded risk is the FULL COST BASIS (ADR-0017): a long equity lot * charges `qty × entry_px × mult` (mult 1), a long option `qty × premium × mult` — so the admissible * size is `floor(remainingUsd / basisPerUnit)`. PURE: a derivation of the injected budget + price, no * wall clock / RNG. Absent-not-hidden: an unbuildable basis (dark/absent price, or an option whose * per-leg premium isn't cleanly surfaced) reads `maxUnits: null` and carries a {@link note}, never a * fabricated cap. */ export interface SizingHeadroom { /** The exec instrument the headroom is computed for. */ readonly instrument: string; /** Order unit — an equity/spot leg sizes in `shares`, an option leg in `contracts` (ADR-0017). */ readonly unit: "shares" | "contracts"; /** The cost basis the bounded-risk clamp charges PER UNIT (ADR-0017): equity = `spot × mult` * (mult 1), option = `premium × mult`. `null` when the basis price is unbuildable this moment (a * dark/absent spot, or an option premium not cleanly surfaced) — never a guessed value. */ readonly basisPerUnit: number | null; /** MAX fillable size the remaining-R budget admits: `floor(remainingUsd / basisPerUnit)`. `null` * when {@link basisPerUnit} or {@link remainingUsd} is unbuildable (shown as UNKNOWN, not faked). */ readonly maxUnits: number | null; /** The remaining-R budget in $ (the numerator; `null` ⇒ no budget state). */ readonly remainingUsd: number | null; /** A basis-rule note when the per-unit basis can't be surfaced cleanly (e.g. an option whose * per-leg premium isn't available — the model still gets the remaining $ + the cost-basis rule). */ readonly note?: string; } /** * The cockpit's risk-envelope budget — the remaining risk plus the three nested envelopes the * fire-time router lives inside (plan ⊆ book ⊆ owner). This is the cockpit "budget / remaining-R" * + "owner envelope" view; the plan-lifecycle usage view is the separate {@link Budget}. */ export interface BudgetEnvelope { readonly remainingR: number; readonly planEnvelope: number; readonly bookEnvelope: number; readonly ownerEnvelope: number; /** The MAX fillable size the remaining-R budget admits for the exec instrument (kestrel-m9i.32) — * so a model sizes WITHIN the bounded-risk envelope, not into a SILENT fire-time clamp. Absent on * the pre-cockpit / degenerate construction (renders as `sizing: UNKNOWN`, absent-not-hidden). */ readonly sizing?: SizingHeadroom | null; } /** * The honesty guard, as a validator (4gl.3): a predictor/regime claim MUST be a `MODEL` {@link * Field} with a source watermark, a `modelVer` receipt, and a `confidence` in `[0,1]`. An * `OBS`/`CALC` "claim", or a `MODEL` claim missing its receipt/confidence or carrying an * out-of-range confidence (4gl.19), is REFUSED — throwing a {@link KernelHonestyError} * (fail-closed; a dishonest claim never renders as if honest). */ export function assertClaimHonest(claim: Claim): void { const label = claim.claimType; const f: Field | undefined = claim.field; if (f === undefined || f === null) { throw new KernelHonestyError(`claim ${label} carries no Field`); } if (f.attribution !== "MODEL") { throw new KernelHonestyError( `claim ${label} is attributed ${f.attribution}, not MODEL — a predictor/regime claim MUST be ` + "a MODEL Field with source + confidence (honesty guard)", ); } if (f.source === undefined || f.source === "") { throw new KernelHonestyError(`MODEL claim ${label} carries no source watermark`); } if (f.modelVer === undefined || f.modelVer === "") { throw new KernelHonestyError(`MODEL claim ${label} carries no modelVer receipt`); } if (f.confidence === undefined) { throw new KernelHonestyError(`MODEL claim ${label} carries no confidence`); } if (!isHonestConfidence(f.confidence)) { throw new KernelHonestyError( `MODEL claim ${label} carries an out-of-range confidence ${f.confidence} — MODEL confidence MUST be in [0,1]`, ); } } // ── The non-configurable / fail-closed contract of the cockpit lead block ──────── // The kernel LEADS every frame and is built in code, prepended OUTSIDE any pane selection; it // cannot be dropped or reordered by configuration. These constants are the canonical contract a // renderer consumes: the lead sentinel, the 8 fixed-order section labels (every section always // present — absent-not-hidden), and the reserved pane ids a configured pane may never claim. /** The line the cockpit lead block begins with — the kernel-leads sentinel a frame checks for. */ export const KERNEL_LEAD_SENTINEL = "==== SAFETY / CONTROL KERNEL (non-configurable; leads every frame) ===="; /** * The line a **delta-encoded** kernel block begins with (kestrel-312, the KV-cache-efficiency half * of the v5 percept). In a cache-monotone stream (the `conversation` / `conversation-cached` * policies, where the reader provably holds the prior full kernel in cached context) a WAKE frame's * kernel is transmitted as ONLY the fields that MOVED since the prior frame — the byte-stable * skeleton (the {@link KERNEL_LEAD_SENTINEL} banner, the 8 section labels, and every unchanged field * value) lives in the reader's cached prefix and is NOT re-emitted. This sentinel LEADS such a block * so the lead guard ({@link ./render.ts assertKernelLeads}) still fires, and it tells the reader the * complete current kernel is `cached skeleton + these moved fields`. * * ## The reinterpreted lead invariant (fail-closed, honesty-preserving) * Every frame makes the COMPLETE current kernel available to the reader — as a full block in a * KEYFRAME, or as `cached-skeleton + delta` in a cache-monotone stream — and a fail-closed * composed-completeness guard ({@link ./render.ts composeKernelDelta}) verifies the composition * reconstructs the full kernel BYTE-IDENTICALLY (a delta that would compose to an INCOMPLETE kernel * throws, exactly as the lead guard does). Absent-not-hidden is preserved: a field that MOVED to * UNKNOWN is present-and-marked in the delta, never silently omitted — omitted-because-unchanged * (not in the moved-set, held in cache) is never confused with omitted-because-unknown. A KEYFRAME * (the OPEN briefing, and every `stateless-redraw` frame where the reader holds NO prior context) * ALWAYS carries the COMPLETE {@link KERNEL_LEAD_SENTINEL} block — self-complete frames stay * self-complete. */ export const KERNEL_DELTA_SENTINEL = "==== KERNEL DELTA (unlisted fields unchanged) ===="; /** The 8 fixed, always-present section labels — in fixed order; none may be dropped or reordered. */ export const KERNEL_SECTION_LABELS = [ "-- WAKE --", "-- DATA-HEALTH --", "-- POSITIONS / INVENTORY-CLAIMS --", "-- RESTING ORDERS --", "-- BUDGET / REMAINING-R --", "-- OWNER ENVELOPE + ACTS --", "-- L0/L1 ENGINE LOG --", "-- PREDICTOR / REGIME CLAIMS --", ] as const; /** Pane ids reserved for the non-configurable kernel — a configured pane may never claim them. */ export const RESERVED_PANE_IDS = ["kernel", "cockpit"] as const; /** * Fail-closed guard for pane configuration: a configured pane may not claim a reserved id (the * kernel is built in code and cannot be impersonated or displaced by a pane). Throws a * {@link KernelHonestyError} on a reserved id. */ export function assertPaneIdAllowed(paneId: string): void { const id = paneId.trim().toLowerCase(); if ((RESERVED_PANE_IDS as readonly string[]).includes(id)) { throw new KernelHonestyError( `pane id "${paneId}" is reserved for the non-configurable cockpit kernel and cannot be ` + "claimed by a configured pane (fail-closed)", ); } } /** * The **MANDATE** (ADR-0026) — the HARD, machine-checkable, narrowing-ONLY channel: what the agent * may NOT do + what it is graded against. It is the ONLY thing that feeds admission/narrowing (its * risk envelope is the {@link BudgetEnvelope} the kernel already carries; this makes the objective + * R-definition + success criterion EXPLICIT and labeled). Sourced from the SESSION/CELL — different * cells declare different objectives. A GRADED run MUST declare one ({@link assertGradedMandate}). */ export interface Mandate { /** The objective in one line, e.g. `session day-trade, flat-by-close`. */ readonly objective: string; /** The dollar value of one risk unit, `1R = $X` (the R-definition the grade uses). */ readonly rUsd: number; /** The success criterion, e.g. `maximize risk-adjusted return vs the null baseline`. */ readonly successCriterion: string; /** The bounded-risk rule in one line, e.g. `bounded-risk; never exceed your envelope`. */ readonly riskRule: string; } /** * The **BRIEF** (ADR-0026) — the SOFT, directional English channel: what the agent SHOULD go look * for (goal + philosophy/approach + persona). Versioned + content-hashed so language becomes * attributable ("this performance came from this thesis"); the hash binds into grade provenance * (`brief_hash`, sibling to `promptHash`). **HARD GUARD**: the Brief is unvalidated free text that * can NEVER enter admission/narrowing/bounded-risk — only the {@link Mandate}/envelope do. The OSS * side merely RENDERS a Brief supplied by the cell/config and binds its hash to provenance; the * full Brief/Journal/Thesis lifecycle is a platform (kestrel.markets) artifact. */ export interface Brief { /** The directional English (goal + approach + persona). Unvalidated — never an admission input. */ readonly text: string; /** The content hash of {@link text} (`sha256:<hex>` or a bare hex), bound into grade provenance. */ readonly hash: string; /** An optional version label for the Brief (e.g. `contrarian-v2`). */ readonly version?: string; } /** * Fail-closed guard for a GRADED run: a benchmark cell MUST state what it is optimizing. Throws a * {@link KernelHonestyError} when there is no {@link Mandate} (or a malformed one — a blank objective, * a non-positive/absent `rUsd`, a blank success/risk rule). Takes the {@link Mandate} DIRECTLY (not a * Kernel) so it wires at the point a graded run COMMITS its mandate — the plan-fixture freeze, where * the mandate binds into `plan_fixture_sha` grade provenance (ADR-0032 §7, * {@link ../session/harness/plan-fixture.ts capturePlanFixture}). The deterministic grade driver never * renders, so the honesty is enforced at that commit, not behind a render flag no production path sets. * The renderer itself stays tolerant — an ungraded/degenerate frame renders without a mandate section. */ export function assertGradedMandate(m: Mandate | null | undefined): void { if (m === undefined || m === null) { throw new KernelHonestyError( "graded run declares no MANDATE — a benchmark cell MUST state what it is optimizing (objective, " + "1R = $X, success criterion, bounded-risk rule). Fail-closed: the cell must supply a mandate.", ); } const bad: string[] = []; if (typeof m.objective !== "string" || m.objective.trim() === "") bad.push("objective"); if (typeof m.rUsd !== "number" || !Number.isFinite(m.rUsd) || m.rUsd <= 0) bad.push("rUsd (1R = $X, > 0)"); if (typeof m.successCriterion !== "string" || m.successCriterion.trim() === "") bad.push("successCriterion"); if (typeof m.riskRule !== "string" || m.riskRule.trim() === "") bad.push("riskRule"); if (bad.length > 0) { throw new KernelHonestyError( `graded run declares an INCOMPLETE mandate — missing/blank: ${bad.join(", ")} (fail-closed; a graded ` + "run must state a complete objective + R-definition + success criterion + bounded-risk rule).", ); } } /** * The acting **kernel** (CONTEXT: Kernel) — bound to the session, not the View, and always present * in an acting Frame. It is the **cockpit lead block** (4gl.3): a superset that carries both the * extended cockpit sections and the original plan-lifecycle section. * * ## Absent-not-hidden, and non-configurable * Every cockpit section is OPTIONAL on this type so the pre-cockpit acting-kernel construction * stays valid (a degenerate kernel is constructible) — but a renderer treats **every** section as * always present: an absent/empty section renders as an explicit `none`/`UNKNOWN`, never dropped. * The block LEADS every frame and is built in code and prepended outside any pane selection; it * cannot be dropped or reordered by configuration ({@link KERNEL_SECTION_LABELS} / * {@link RESERVED_PANE_IDS}). A spectator Frame would carry a provenance kernel instead. */ export interface Kernel { // ── The MANDATE + BRIEF standing context (ADR-0026 — kept as two DISTINCT channels) ────────── /** The **MANDATE** (hard, machine-checkable, narrowing-ONLY): the objective + R-definition + * success criterion + bounded-risk rule the agent is graded against. It is the ONLY channel that * feeds admission/narrowing (the risk envelope below is its machine half). Sourced from the * SESSION/CELL. Optional on this type so a degenerate kernel stays constructible; a GRADED cell * MUST declare one ({@link assertGradedMandate}). */ readonly mandate?: Mandate | null; /** The **BRIEF** (soft, directional English — ADR-0026): what the agent SHOULD go look for (goal * + philosophy/approach + persona). Versioned + content-hashed; the hash binds into grade * provenance (`brief_hash`). HARD GUARD: unvalidated free text that can NEVER enter * admission/narrowing — only the {@link mandate}/envelope do. Sourced from the SESSION/CELL. */ readonly brief?: Brief | null; // ── The extended cockpit lead block (4gl.3) ────────────────────────────────── /** Why the author is looking now + the RELATIVE deadline (`T-Nm`). Absent ⇒ `UNKNOWN`. */ readonly wake?: WakeRef | null; /** Per-vehicle book health (routing gate). Empty ⇒ `UNKNOWN`; a dark vehicle is named. */ readonly dataHealth?: readonly VehicleHealth[]; /** Named unavailable capabilities — listed explicitly, never hidden. Empty ⇒ `none`. */ readonly unavailable?: readonly string[]; /** The cockpit risk-envelope budget (remaining-R + plan/book/owner envelopes). */ readonly budgetEnvelope?: BudgetEnvelope | null; /** Owner acts this session (standing-directive changes). Empty ⇒ `none`. */ readonly ownerActs?: readonly OwnerAct[]; /** The L0/L1 engine log (fired / cancelled / rejected / clamped). Empty ⇒ `none`. */ readonly engineLog?: readonly EngineAction[]; /** Predictor/regime claims — each an honest `MODEL` Field. The render seam refuses a dishonest one * per-claim as it materializes the section ({@link assertClaimHonest}, wired in {@link ./render.ts}). */ readonly claims?: readonly Claim[]; // ── The existing plan-lifecycle / acting section (kestrel superset) ─────────── readonly positions: readonly Position[]; readonly resting: readonly RestingOrder[]; readonly fillsSinceLast: readonly FillRecord[]; /** The plan-lifecycle risk-usage view (used / remaining). The cockpit view is {@link budgetEnvelope}. */ readonly budget?: Budget | null; readonly plans: readonly PlanStateEntry[]; } /** * The OPEN keyframe (full briefing) input. Macro/overnight context is intentionally **absent** * in v1 — the renderer emits the pane header with `macro: unavailable (v1 harness)` * (absent-with-reason, per EVALUATION.md), never an invented value. */ export interface BriefingInput { /** Minutes to the session close; `null` ⇒ `T-— to close`. */ readonly timeToCloseMin: number | null; readonly phase?: FramePhase | null; /** Current HH:MM ET clock, when the runner supplies one (date-blind). */ readonly clockET?: string; readonly instruments: readonly InstrumentSpec[]; readonly market: MarketPane; readonly kernel: Kernel; /** The PRIOR sessions' tapes (kestrel-wa0j.20) — each ordinal's rows serve its `tape d-N` address as * its own single-session block. Typically ABSENT in the v1 single-day sim (honestly so); absent ⇒ * `tape d-N` renders the Train 1B absence line, byte-identical. Additive + optional-honest. */ readonly priorSessions?: readonly PriorSessionTape[]; /** The ARMED plans' enforced terms (kestrel-wa0j.29) — threaded so the `armed-plan` pane renders the * WHEN/entries/exits/invalidation/size-envelope the watcher enforces. Typically ABSENT at OPEN (no * plan has armed yet — authoring happens AT the open); absent ⇒ the field is unserialized and the * pane fails closed to absent-with-reason. Additive + optional-honest (byte-identical without it). */ readonly armedPlan?: ArmedPlanTerms; } /** * The prior author vantage's **served values** (kestrel-wa0j.48) — the typed snapshot the driver * SERVED at the author's PREVIOUS vantage, threaded into THIS wake's frame input so the `delta` * pane can state WHAT MOVED under `stateless-redraw` (where the prior percept is NOT in context and * the change information exists nowhere else in the frame). This is the frame-carried prior the * renderer reads: a pane invents no value (ADR-0041 §1), so the prior-vantage data must enter * THROUGH THE FRAME INPUT, never be reconstructed by the renderer. * * Every field is **optional-honest**: it carries ONLY a value the prior frame actually served — an * absent field is a value the prior vantage did not carry (never a fabricated 0). The driver * captures the TYPED values it served (spot/hod/lod/vwap off the served levels, the ATM straddle * extrinsic when computable), NEVER the rendered bytes. DATE-BLIND: `baseSeq` is a relative ordinal * and `baseClockET` is the HH:MM ET clock — no date component. */ export interface PriorVantage { /** The prior vantage's percept ordinal / base seq the kernel already knows (relative, date-blind). * `null`/absent ⇒ the base is named by its clock alone. */ readonly baseSeq?: number | null; /** The prior vantage's HH:MM ET clock (date-blind) — the `delta since <baseClockET>` anchor. */ readonly baseClockET?: string; /** Spot the prior vantage served; `null`/absent ⇒ the prior vantage did not carry a spot. */ readonly spot?: number | null; readonly hod?: number | null; readonly lod?: number | null; readonly vwap?: number | null; /** The prior vantage's ATM straddle EXTRINSIC (time value remaining, `gross book-mid − intrinsic`), * when it was computable at that vantage; `null`/absent otherwise (a dark/one-sided/absent book). */ readonly atmExtrinsic?: number | null; } /** * One PRIOR session's tape (kestrel-wa0j.20 — the DATA half of Train 1B's `tape d-N` ADDRESS). A * SessionOrdinal address `tape d-N` (N ≥ 1) names a prior session; Train 1B (kestrel-wa0j.24) landed * the address (it binds at `mat`, renders honest absence at `renderable`), and THIS is the typed data * that turns that absence into the prior session's OWN bytes. Rendered as its OWN complete * single-session tape block through the EXISTING single-session renderer (own axis from its own * extremes) — sessions are NEVER folded/aggregated across the overnight gap (that is wa0j.20b, behind * the render-unification PR #194). * * DATE-BLIND (ADR-0041 §1): `ordinal` is a RELATIVE session index (1 = the immediately prior session, * 2 = the one before, …) — the same date-blind class as wake ordinals / `T-Nm`, NEVER a date; each * row's `clock` is HH:MM-only; `sessionLabel` (when present) is a relative descriptor, never a date. */ export interface PriorSessionTape { /** Relative session ordinal — `1` = the immediately prior session, `2` = the one before, … Matches * the `d-N` address the author spells. NEVER a date (date-blind, ADR-0041 §1). */ readonly ordinal: number; /** That session's OHLC tape rows, in its own time order (each row's `clock` is HH:MM-only). */ readonly rows: readonly TapeRow[]; /** An optional date-blind label for the session (a relative descriptor, e.g. `prior RTH`) — NEVER a * date. Absent ⇒ the block header names the ordinal alone. */ readonly sessionLabel?: string; } /** * ONE armed plan's ENFORCED terms — the typed snapshot the WATCHER reads (kestrel-wa0j.29 / ADR-0041). * * The watcher is asked to judge a plan's PREMISE ("the plan armed on a thesis — is it still true?") * but the armed document's WHEN/DO/TP/EXIT/INVALIDATE clauses live in the supersede action on the bus * and are NEVER rendered into any frame: the kernel carries only the plan's lifecycle NAME + STATE. The * `armed-plan` pane closes that gap — and, exactly as the `delta`/`prior-session` panes do, its data * enters THROUGH THE FRAME INPUT (a pane invents no value, ADR-0041 §1): the sim driver captures the * plan's TYPED clauses from the armed document it already holds and prints each through the canonical * clause printer ({@link ../lang/print.ts planClause}) — a typed value → its byte-stable text, NEVER the * rendered pane bytes, never a hand-rolled restatement. * * Every field is **optional-honest**: it carries ONLY a term the plan actually authored — an absent * field is a clause the plan does not have (never a fabricated one). The clause texts are the canonical * printed forms grouped by role (a variadic run when the plan authored several of that kind). */ export interface ArmedPlanTermsEntry { /** The plan's lineage NAME (the same name the kernel's PlanStateEntry carries). */ readonly name: string; /** The arming PREMISE — the plan's top-level `WHEN` trigger, canonical text. Absent ⇒ the plan armed * with no top-level entry gate (a pure inventory adopter / an unconditional arm). */ readonly when?: string; /** The ENTRY tickets (`DO` / `ALSO` / `RELOAD`), each as canonical clause text — what the plan * acquires and reloads. Absent/empty ⇒ the plan authored none (a pure management adopter). */ readonly entries?: readonly string[]; /** The EXIT surface (`TP` take-profits + `EXIT` thesis-break stops), each as canonical clause text — * the terms the watcher enforces to get OUT. Absent/empty ⇒ the plan authored no exit surface. */ readonly exits?: readonly string[]; /** The INVALIDATION surface (`INVALIDATE` thesis-dead + `CANCEL-IF` resting cancels), each as * canonical clause text — what kills the thesis. Absent/empty ⇒ the plan authored none. */ readonly invalidations?: readonly string[]; /** The SIZE ENVELOPE — the plan's authored risk `budget` (e.g. `0.5R`), canonical text. Absent ⇒ the * plan declared no budget (it nests under an ancestor Book/Pod envelope). */ readonly sizeEnvelope?: string; } /** * The ARMED plans' enforced terms threaded onto a frame (kestrel-wa0j.29) — the DATA the `armed-plan` * pane renders. Present only on a frame the sim driver threaded armed-plan terms into (a wake with at * least one plan in an enforced lifecycle state — `armed`/`fired`/`managing`); ABSENT when nothing is * armed (the pane then fails closed to exactly ONE absent-with-reason line). Additive + optional-honest: * a frame without it is byte-identical to today. `plans` is non-empty by construction (an empty armed * set is threaded as ABSENT, not an empty list — absent-not-hidden). */ export interface ArmedPlanTerms { readonly plans: readonly ArmedPlanTermsEntry[]; } /** * The WAKE delta-frame input — everything **since the last vantage** (EVALUATION.md §Part 1.3): * the tape buckets since last, the current levels/kernel, and the wake's own identity (ordinal * + minutes since last + reason). Append-only by construction (ADR-0008): a delta frame is a * suffix stamped with its own tape anchor. */ export interface WakeDeltaInput { /** The wake ordinal (`wake 3`). */ readonly wakeIndex: number; /** Minutes since the author's last vantage; `null` ⇒ `—`. */ readonly minutesSinceLast: number | null; readonly timeToCloseMin: number | null; readonly phase?: FramePhase | null; readonly clockET?: string; /** Why this wake fired (the trigger idiom or the structural-cadence reason). */ readonly wakeReason?: string; /** The market pane; its `tape` carries only the buckets since the last vantage. */ readonly market: MarketPane; /** The acting kernel; its `fillsSinceLast` are the fills since the last vantage. */ readonly kernel: Kernel; /** The prior author vantage's served values (kestrel-wa0j.48) — present on a WAKE frame the * driver threaded a prior vantage into (every wake AFTER the first). Absent on the first wake and * on any frame with no prior vantage; the `delta` pane fails closed to absent-with-reason then. * Additive: a frame without it is byte-identical (the delta pane is catalog-only, never a default). */ readonly priorVantage?: PriorVantage; /** The PRIOR sessions' tapes (kestrel-wa0j.20) — each ordinal's rows serve its `tape d-N` address as * its own single-session block. Present only on a frame the driver threaded a multiday tape into * (typically ABSENT in the v1 single-day sim — honestly so); absent ⇒ `tape d-N` renders the Train 1B * absence line, byte-identical. Additive + optional-honest (a frame without it is byte-identical). */ readonly priorSessions?: readonly PriorSessionTape[]; /** The ARMED plans' enforced terms (kestrel-wa0j.29) — threaded so the `armed-plan` pane renders the * WHEN/entries/exits/invalidation/size-envelope the watcher enforces (the watcher's role-keyed * percept: it manages plans whose document text it otherwise never sees). Present only on a wake with * at least one plan in an enforced state (`armed`/`fired`/`managing`); absent ⇒ the field is * unserialized and the pane fails closed to absent-with-reason. Additive + optional-honest. */ readonly armedPlan?: ArmedPlanTerms; }