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
text/typescript
/**
* # 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;
}