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.

790 lines (758 loc) 147 kB
/** * # session/simulate — the frozen-at-wake Simulate driver (the Agent seam's companion to `./day.ts`) * * The **wake-driven, agent-in-loop** driver (RUNTIME §7). Where {@link import("./day.ts").runDaySession} * pauses at each wake and hands an external author a Frame through a FILE handshake, this drives the SAME * {@link SessionCore} — the same bus-stepping, the same {@link import("../engine").Gate}, the same * fills-before-sweep ordering — but hands the frozen vantage to the {@link Agent} seam IN-PROCESS and * consumes the {@link AgentTurn} it returns. Between wakes the standing Plans fire autonomously * (fire-then-inform); at a wake the driver freezes market time, assembles a date-blind {@link ActingFrame}, * `await`s `agent.decide(frame)`, and serializes every returned {@link Action} onto the graded Bus. * * ## Where the determinism boundary sits (RUNTIME §0, the whole point) * The single value that crosses the boundary is the returned {@link AgentTurn}; BELOW it everything is a * pure function of Bus bytes (ADR-0011). Every Action is stamped at the **injected wake ts** (no wall * clock, no unseeded RNG on the record path), so `project(gradedBus)` re-derives a byte-identical Blotter * and a captured run re-runs as a Backtest through {@link recordedAgent}. The delivered {@link ActingFrame} * (Rendering, model call, sampling) sits ABOVE the line — the recorded/fixed adapters ignore it entirely. * * ## Action → Bus routing (fail-closed, bounded-risk) * - `supersede` → parse the Kestrel document; the FIRST is the initial arm (no CONTROL, `day.ts` plans-0 * parity), a later one emits a CONTROL `supersede` + de-arm + arm. A parse escape de-arms * clean with a logged reason (never a crash — RUNTIME §8). * - `standDown` → a CONTROL `disarm` + de-arm clean (inventory rides its TP/EXIT — never liquidates). * - `journal` → a **pre-hoc** JOURNAL `author` record, emitted BEFORE the turn's other actions (provably * pre-hoc by `seq`). * - `scheduleWake` → recorded as a WAKE `scheduled` record AND unioned onto the agent-driven wake * frontier (kestrel-5zl.4): the agent's own cadence drives, floored (never replaced) by * the staleness backstop + optional structural cadence, coalesced/downgraded past the * attention budget (the squelch stays visible — never a silent drop). * - `placeOrder` / `cancelOrder` → routed through the **SAME Gate** a fired Plan uses (`core.gate`): an * order is expressed as a one-shot Plan and armed, so it crosses the identical * engine→Gate→fill path — SELL≥intrinsic, the budget clamp, and never-naked are enforced by * the engine, never re-implemented here (bounded risk by construction). * * Scope: this is the tracer-bullet slice — the thinnest deterministic Simulate path exercised by * {@link fixedPlanAgent} + {@link recordedAgent}. The live adapter body, the config matrix, and * wire-capture are later slices; the full Wake-grammar authoring (subscriptions/Views, the budget * algebra, per-session structural re-anchoring) is the b3l Wake-router epic — see `armStaleness`/ * `resolveWakeAt` for the conservative defaults taken here. */ import type { BusEvent, MetaEvent } from "../bus/index.ts"; import { readBus } from "../bus/index.ts"; import type { Right } from "../bus/types.ts"; import { intrinsic } from "../fair/index.ts"; import { assertNever, parse, print, type KestrelNode, type Module, type PlanStatement } from "../lang/index.ts"; import { armedPlanTermsOf, ENFORCED_PLAN_STATES } from "./armed-plan-terms.ts"; import { decisionControlFields, driverMeasurementVersions, type DecisionEmit } from "../grade/index.ts"; import type { InstrumentSpec } from "../engine/index.ts"; import type { EpisodeSigmoidParams } from "../fill/index.ts"; import { heldPositionAsOfGradedBus } from "../fill/index.ts"; import type { Blotter } from "../blotter/index.ts"; import { project } from "../blotter/index.ts"; import type { BriefingInput, EngineAction, FillRecord, FramePhase, FrameRight, InstrumentSpec as FrameInstrumentSpec, Kernel, Mandate, Brief, PlanStateEntry, Position, PriorSessionTape, PriorVantage, SizingHeadroom, RestingOrder, VehicleHealth, WakeRef, } from "../frame/types.ts"; import { expiryTauYears, etMinuteOfDay, etWallClockMs, type FairTauProvider } from "./clock.ts"; import { SessionCore, busSha256Of, fidelityOf, requireMeta, resolveSpecs, type FillModelName, type SessionReport } from "./sim.ts"; import { authorCutoffTs, briefingSnapshot, computeWakes, projectAuthorFrame, scanDateLeak, snapshotOf, type AuthorFrame, type TimeOfDay, } from "./day.ts"; import { OPEN_WAKE_KEY, assertHandlerResponseTimeless, wakeKeyOf, type Action, type ActingFrame, type Agent, type AgentConfig, type AgentTurn, type CapturedTurn, type CapturedTurns, type PlaceOrderAction, type WakeAt, type WakeKey, type WakeSource, } from "./agent.ts"; import { resolveView, windowServableBy, paneById, atmStraddleExtrinsic, type ViewSelection } from "../frame/pane-catalog.ts"; import type { OptionsAnalytics } from "../frame/options-analytics.ts"; import { canonicalizeConfig, cellConfigId as cellConfigIdOf, deriveConfigId, envelopeForConfig, fillSeedOf, sampledQualificationOf } from "./config.ts"; import { carryFrom, emptyCarry, type SessionCarry } from "./carry.ts"; // The stowaway concepts extracted from this driver into sibling modules (kestrel-z473.1) — this file // keeps ONLY the wake loop. Each is imported back for the loop's own use; the ones this file used to // export are re-exported below so the public surface is byte-identical. import { vehicleHealthOf, vehicleBookFromSpot, type VehicleBook } from "./vehicle-health.ts"; import { sizingHeadroomOf, budgetEnvelopeOf } from "./sizing.ts"; import { marketPaneOf, buildFrameOptions, SERVED_TAPE_BUCKET_MIN, type ChainFairContext, type OptionsOverlayConfig, } from "./market-pane.ts"; import { runOpenAuthoringLoop, viewSelectionOfDocument, DEFAULT_VIEW_REQUEST_CAP, DEFAULT_AUTHORING_TOKEN_BUDGET, type EmergenceRecord, } from "./authoring-loop.ts"; import { wakeFloorForBand, DAY_STALENESS_BACKSTOP_MIN, type FrontierWake, type WakeCtx } from "./wake-frontier.ts"; // Re-export the moved public symbols so `import { … } from ".../session/simulate.ts"` (and the // `src/session/index.ts` barrel) resolve exactly as before — the extraction is API-transparent. export { marketPaneOf, SERVED_TAPE_BUCKET_MIN, type ChainFairContext, type OptionsOverlayConfig } from "./market-pane.ts"; export { vehicleHealthOf, vehicleBookFromSpot, type VehicleBook } from "./vehicle-health.ts"; export { sizingHeadroomOf } from "./sizing.ts"; export { viewSelectionOfDocument, DEFAULT_VIEW_REQUEST_CAP, DEFAULT_AUTHORING_TOKEN_BUDGET, type EmergenceRecord, } from "./authoring-loop.ts"; export { type WakeCtx } from "./wake-frontier.ts"; // ───────────────────────────────────────────────────────────────────────────── // Public surface // ───────────────────────────────────────────────────────────────────────────── /** * The DAY-COMPAT side-channel (kestrel-5zl.3) — the narrow bridge the {@link import("./day.ts").runDaySession} * fileHandshakeAgent and the driver share so the day agent can render its inspectable artifacts + validate * revisions against the LIVE core WITHOUT a forked progression path. It carries NOTHING onto the graded * stream (it sits entirely ABOVE the determinism line): the driver sets `vantage` at each vantage BEFORE * calling `agent.open`/`agent.decide`, and binds `previewSupersede` once to the live engine's pure collision * preview. Present ONLY on the actual day file agent; a plain {@link dayCompat} run (e.g. a recordedAgent * replay) passes no bridge and the driver never touches it. */ export interface DayCompatBridge { /** The raw date-blind {@link AuthorFrame} for THIS vantage (OPEN briefing / each wake), set by the driver * before the corresponding `agent.open`/`agent.decide`. The day agent names each wake artifact from it * (`frame-<n>-<HHMM>.txt`) and serializes it as the `state-<n>.json` machine channel; the TEXT frames * render through the canonical `src/frame` renderer over the driver's typed frames (kestrel-wa0j.2). */ vantage?: AuthorFrame; /** The engine's PURE supersede-then-arm collision preview ({@link import("../engine/index.ts").PlanEngine} * `supersedeArmRejection`), bound once by the driver to the live core: a rejection reason or `null`. The * day agent's reject-retry consults it so the standing book is NEVER torn down for a doomed revision * (fail-closed blast-radius, RUNTIME §8) — the collision preview guarantees the driver's subsequent * supersede+arm cannot reject. */ previewSupersede?: (mod: Module) => string | null; } /** Options for {@link runSimulateSession}. Exactly one of `busPath`/`events` supplies the bus; the * {@link Agent} seam supplies the decisions. The fill/tau/budget knobs mirror {@link import("./sim.ts").RunSimOptions}. */ export interface RunSimulateOptions { readonly busPath?: string; readonly events?: readonly BusEvent[]; /** The agent under test — live, recorded, fixed-plan, or file-handshake (the ONE seam). */ readonly agent: Agent; /** Authored wake vantages (ET times-of-day), strictly inside the driven window. */ readonly wakes?: readonly TimeOfDay[]; /** The author acting cutoff (an ET wall-clock time-of-day). The OPEN briefing is built ONLY from * observations watermarked at or before this instant; defaults to the canonical benchmark's T-5 * (09:25 ET). Injected + explicit so no post-cutoff print leaks to the agent (kestrel-m9i.11). */ readonly authorCutoff?: TimeOfDay; /** Present the OPEN authoring vantage as an INTRADAY keyframe carrying the observed range * (kestrel-1vw): when `true`, the OPEN briefing folds every exec observation at/before {@link * authorCutoff} into the range (`hod`/`lod`/`tape` + the current spot) instead of the pre-open * keyframe — so an ORB cell authoring at its opening-range close sees the range it must break out of. * The causal cutoff fence is unchanged (no post-cutoff print leaks); this ONLY changes whether the * admitted prefix is collapsed to the opening keyframe or presented as the range. Absent / `false` ⇒ * today's pre-open briefing, byte-identical. Set together with an intraday {@link authorCutoff}. */ readonly authorFromRange?: boolean; /** Add the structural cadence into the close (T-2h…T-5m) — see {@link computeWakes}. */ readonly structural?: boolean; readonly fillModel: FillModelName; readonly rUsd: number; /** The cell-sourced hard {@link Mandate} (ADR-0026) — the objective + R-definition + success * criterion + bounded-risk rule the agent is graded against. Rendered in the kernel; the ONLY * admission input. A GRADED run must declare one ({@link import("../frame/types.ts").assertGradedMandate}); * an ungraded/dev run may omit it (the frame renders unchanged). NOT hardcoded — the cell owns it. */ readonly mandate?: Mandate; /** The cell-sourced soft {@link Brief} (ADR-0026) — directional English (goal/approach/persona), * rendered as standing context, hashed + attributed, but NEVER an admission input (the hard guard). */ readonly brief?: Brief; readonly instruments?: readonly InstrumentSpec[]; readonly fairTauYears?: FairTauProvider; readonly makerFairParams?: EpisodeSigmoidParams; /** The prior session's {@link SessionCarry} (the multi-session horizon design, kestrel-5zl.16). Its `positions` seed this * session's opening book and its `document` is re-armed fresh (new plan-instance ids) BEFORE the OPEN * turn. Absent — or an `emptyCarry()` — is today's flat open, byte-identical. */ readonly openingCarry?: SessionCarry; /** `true` for a NON-final session of a multi-session Instance: settle carry-marks held inventory (it * survives into the returned {@link SimulateResult.carry}) instead of forcing it to intrinsic. Absent / * `false` — a standalone or the final session — is today's terminal intrinsic settle exactly. */ readonly carryClose?: boolean; /** This session's ordinal `k` in its Instance (the multi-session horizon design, kestrel-5zl.16.4) — the relative-day * coordinate `d{k}`. Absent for a standalone run ⇒ today's coordinate (no `d{k}` prefix), byte-identical. */ readonly sessionOrdinal?: number; /** The bounded OPEN authoring loop's dual budget (ADR-0029 §2). Consulted ONLY when the agent * exposes {@link Agent.authoringOpen} (the viewshop arm); a baseline/recorded/fixed run ignores it * entirely (single-shot open, byte-identical). Both fields are config axes — a different budget is a * different grid column. Absent ⇒ the conservative defaults ({@link DEFAULT_VIEW_REQUEST_CAP} / * {@link DEFAULT_AUTHORING_TOKEN_BUDGET}). */ readonly authoring?: { /** Max non-terminal iterations (view-requests + repairs, shared) before fail-closed standDown. */ readonly viewRequestCap?: number; /** Cumulative model-token ceiling (input + output + thinking, summed across iterations). */ readonly authoringTokenBudget?: number; }; /** The Percept v5 EMERGENCE LOG sink (ADR-0029 §5) — OFF the graded path. When supplied, the driver * appends one {@link EmergenceRecord} per non-terminal authoring iteration (BOTH view-requests AND * parse-error/repair events), keyed by `(OPEN_ORDINAL, iteration)`. Private application data (the raw * data for the emergence study AND the grammar-evolution signal, ADR-0030); never a graded input. */ readonly emergenceLog?: EmergenceRecord[]; /** * DAY-COMPAT MODE (kestrel-5zl.3 re-run) — the CALLER-OWNS-CADENCE path the {@link import("./day.ts").runDaySession} * fileHandshakeAgent refactor drives through. When the caller owns the wake cadence (the day's structural * offsets), the driver reproduces the STEPPED runner's cadence + byte stream EXACTLY: * - NO injected platform-default staleness/backstop wakes for this path — the day's own structural wakes * provide the coverage, so the delivered wake set is exactly `computeWakes(...)` (no 30-min DAY backstop * churn), reproducing the frozen day determinism_hash 44cf2cbf; * - CONTROL supersede carries the day's `{target:"wake-N", note:"revision-N"}` byte shape (not the bare * `{note?}` the agent-driven path emits), so `serializeEvent` bytes match the stepped runner. * * This is NARROW: it is NOT "silence the backstop". It never weakens the band fail-closed logic (16.7) for a * normal band run — {@link wakeFloorForBand}/{@link armStaleness} still floor every agent-driven session and * {@link tests/simulate.wake-band.test.ts} stays green. Absent / `false` ⇒ today's full agent-driven frontier, * byte-identical. (Threaded by the green step; declared here as the contract the TDD-red test targets.) */ readonly dayCompat?: boolean; /** The DAY-COMPAT {@link DayCompatBridge} (kestrel-5zl.3) — present ONLY when the caller is the * {@link import("./day.ts").runDaySession} file agent (it renders artifacts from `vantage` + validates * revisions through `previewSupersede`). Off the graded line: the driver sets `vantage` before each * `open`/`decide` and binds `previewSupersede` to the live core, but emits NOTHING from it. A plain * `dayCompat` run (a recordedAgent replay) omits it and the driver never touches it. */ readonly dayBridge?: DayCompatBridge; /** The options-analytics OVERLAY (kestrel-4gl.13.13) — the perception arm's OPRA surface, folded * causally into `market.options` at every frame cutoff so the `gex`/`iv-skew`/`implied-realized` * panes render in a LIVE run. **Config-gated: supplied ONLY for the perception arm** (a View that * selects the options-analytics panes). Absent — the minimal arm — leaves `market.options` * undefined and every frame byte-identical to before (the panes are off-default; the fold is never * paid). Folded per-cutoff with the T-5m guard (only events at/before the cutoff, no look-ahead) and * relabeled date-blind before it reaches the agent frame ({@link buildFrameOptions}). */ readonly optionsOverlay?: OptionsOverlayConfig; /** * THE MULTIDAY TAPE SEAM (kestrel-wa0j.20 — the DATA half of Train 1B's `tape d-N` address). The * PRIOR sessions' tapes (date-blind: relative ordinals + HH:MM clocks, NEVER dates), threaded onto * every frame this session serves so a View naming `tape d-N` (N ≥ 1) renders ordinal N's OWN * single-session block instead of the Train 1B absence line. Each ordinal renders SEPARATELY — no * cross-session folding (that is wa0j.20b, behind PR #194's render unification). * * INJECTABLE seam: the v1 single-day sim carries no multi-session tape state, so this is typically * ABSENT (honestly — `tape d-N` then renders absent-with-reason, byte-identical). A multi-day harness * (or a future day/backtest path that has the prior sessions' rows cheaply available) INJECTS them * here; the driver threads them, unchanged, into both the OPEN briefing and every wake frame. Absent * ⇒ no `priorSessions` field on any frame, byte-identical to today. */ readonly priorSessions?: readonly PriorSessionTape[]; /** * THE THETA GATE (theta-cell seam b, {@link import("../fill/held-position.ts").heldPositionAsOfGradedBus}). * When `true`, after settle the driver asks the held-position owner for the position held as of the * graded bus — it reconstructs the net HELD option legs + their blended basis, re-marks them against the * tape's own option book at every DELIVERED wake (theta decay between wakes), and returns ONE graded * `TELEMETRY theta_bleed` record carrying the stand-down floor (the mark at the LAST wake), which the * driver emits verbatim. The Blotter folds that into `totals.floor` (ADR-0011), so a watcher that STANDS PAT on a * bleeding straddle realizes a graded NEGATIVE floor instead of paying no running cost for doing nothing * — the whole point of the theta cell (it dissolves the doing-nothing-wins degenerate reward). * * Threaded from the FOMC-OPTIONS {@link import("./harness/plan-fixture.ts").FanFrame} `markToModel`. * Absent / `false` (every other cell) ⇒ no reconstruction, no record, no fold — every existing pinned * floor is byte-identical. Only binds when a position is actually HELD across ≥1 wake (a flat book, or a * watcher that closed out, emits nothing). CLEAN BOUNDARY: the bleed rides the graded Bus / Blotter as a * mark-to-model GRADING penalty; it is deliberately NOT folded into the engine-native SessionReport * `realized_floor_usd` or the multi-session carry cash (that report/ledger integration is separate work). */ readonly markToModel?: boolean; /** * The injectable wall clock the driver measures a clock-honest Seat turn with (ADR-0040 / * clock-honest wakes §7 — the ONE minting seam, the `opts.now ?? Date.now` idiom of `liveAgent`). * Consulted ONLY in a LIVE-shaped clock-honest run: a latency-blind session never measures (its * captures stay costless-deterministic), and a REPLAY (an agent exposing `recordedCostMs`) reads * recorded costs and never touches this — a replay path that reads a clock is a defect by * definition (the poisoned-clock fixture). Defaults to `Date.now`. */ readonly now?: () => number; /** * The session's DECLARED data rung (ADR-0040 data floor / clock-honest wakes §4) — the tape's * observability provenance (`"tick" | "second" | "minute" | "session"`, the `GradingFidelity` * lattice), declared on the run options / session catalog and carried with the tape like * `fill_model`. Typed loosely (injected provenance bytes) and VERIFIED fail-closed via the isRung * discipline: `tick`/`second` ⇒ clock-honest eligible; `minute`/`session` ⇒ latency-blind; an * off-lattice or ABSENT rung ⇒ latency-blind (an untrusted rung never compares as eligible). * Requesting `clockHonest: true` over an ineligible/undeclared rung REFUSES the run loudly at open — * never a silent downgrade to latency-blind. Irrelevant to a latency-blind run (may be omitted). */ readonly dataRung?: string; } /** The result of a Simulate session: the graded {@link Blotter} (a PURE projection of the graded Bus, * ADR-0011), the {@link CapturedTurns} the run consumed (the deterministic replay input for * {@link recordedAgent}), and the agent's {@link AgentConfig} (the comparison axis). */ export interface SimulateResult { readonly blotter: Blotter; /** * The native graded {@link SessionReport} of this run (kestrel-m9i.7.4) — the SAME `core.buildReport` * projection {@link import("./day.ts").runDaySession} returns, computed here from the finalized core so a * live/handshake Simulate run lands in the run {@link import("../ledger").ledger} WITHOUT a Blotter→report * bridge (the tournament runner records this verbatim; `session.settle_ts` is the injected `recorded_at`). * A PURE read of the finalized session — no wall clock, no RNG, and byte-identical to a `runDaySession` * report of the same graded bus (RUNTIME §0). */ readonly report: SessionReport; readonly captured: CapturedTurns; readonly config: AgentConfig; /** The canonical GRADED bus of this run (`core.gradedBus()` — the judge-stamped META + every emitted * engine reaction, re-stamped gap-free): the record itself, surfaced so a caller (and the w7la.1 * clocked fixtures) can assert bus-level facts — deliberation records, checkpoints, refusal * stamping — without any engine-state scrape (ADR-0010: the Bus is the truth). A PURE read of the * finalized core; `project(gradedBus)` is byte-identical to {@link SimulateResult.blotter}. */ readonly gradedBus: readonly BusEvent[]; /** The {@link SessionCarry} this session hands to the next (the multi-session horizon design, kestrel-5zl.16) — a PURE read of * the finalised session. On a final / standalone session (`carryClose` absent) its `positions` are * empty (inventory settled at intrinsic). The runner ({@link import("./instance.ts").runInstance}) * threads it into the next session's `openingCarry`. */ readonly carry: SessionCarry; } // ───────────────────────────────────────────────────────────────────────────── // Small helpers (pure, deterministic — no wall clock, no RNG) // ───────────────────────────────────────────────────────────────────────────── /** Normalize a parsed node into a module (a bare statement → a one-statement module) so it can be armed. */ function toModule(node: KestrelNode): Module { return node.kind === "module" ? node : { kind: "module", imports: [], statements: [node] }; } function msgOf(e: unknown): string { return e instanceof Error ? e.message : String(e); } // ───────────────────────────────────────────────────────────────────────────── // Clock-honest eligibility (ADR-0040 data floor / clock-honest wakes §4) — declared, verified, fail-closed // ───────────────────────────────────────────────────────────────────────────── /** The rungs of the observability lattice fine enough to ground a clock-honest session (ADR-0040: * sub-minute ground truth — 1s bars / event streams). The isRung discipline (`src/series/registry.ts`) * applied to THIS question: anything outside the closed lattice is untrusted and never compares as * eligible. */ const CLOCK_HONEST_ELIGIBLE_RUNGS: ReadonlySet<string> = new Set(["tick", "second"]); const OBSERVABILITY_RUNGS: ReadonlySet<string> = new Set(["tick", "second", "minute", "session"]); /** * Why a declared data rung CANNOT ground a clock-honest session, or `null` when it is eligible * (clock-honest wakes §4). PURE and fail-closed — declared-then-verified is not trust: * - `"tick"` / `"second"` → eligible (`null`); * - `"minute"` / `"session"` → too coarse (the ADR-0040 data floor) — such a session stays VALID but * latency-blind, and may never claim clock-honesty; * - absent → undeclared provenance — refuse (never a silent default); * - off-lattice → an untrusted rung — refuse (never compared as coarser/finer than anything). * The caller (the driver, at open) REFUSES a `clockHonest: true` run whose rung returns non-null — * never a silent downgrade to latency-blind (a producer must never *believe* it bought a * latency-honest result it didn't get). */ export function clockHonestIneligibility(declaredRung: string | undefined): string | null { if (declaredRung === undefined) { return "the session declares NO data rung (`dataRung`) — clock-honest requires sub-minute ground truth declared with the tape (ADR-0040 data floor); refused at open, never a silent default"; } if (!OBSERVABILITY_RUNGS.has(declaredRung)) { return `declared data rung ${JSON.stringify(declaredRung)} is not on the observability lattice (tick, second, minute, session) — an untrusted rung never compares as clock-honest eligible (fail-closed)`; } if (!CLOCK_HONEST_ELIGIBLE_RUNGS.has(declaredRung)) { return `declared data rung "${declaredRung}" is too coarse for clock-honest semantics (ADR-0040 data floor: tick/second only) — run it latency-blind or declare finer ground truth; refused at open, never a silent downgrade`; } return null; } // ───────────────────────────────────────────────────────────────────────────── // Schedule-time View validation (kestrel-wa0j.19 §1b) — the driver-side guard the wake loop applies // when a `scheduleWake` is accepted. (The bounded OPEN authoring loop itself + the View-document → // ViewSelection mapper live in `./authoring-loop.ts`; the wake floors + frontier shapes in // `./wake-frontier.ts`; extracted from this driver in kestrel-z473.1.) // ───────────────────────────────────────────────────────────────────────────── /** * SCHEDULE-TIME View value/budget validation (kestrel-wa0j.19 §1b): the checks that are KNOWABLE when a * `scheduleWake` is accepted — BEFORE the View ever reaches an (otherwise unguarded) delivery render. * Returns a refusal reason when the resolved View carries a defect the constant session context can prove * NOW, else `null`. Two checks, both mirroring the exact guards `materializePanes` applies at delivery so * the two can never drift: * • **window-arg value** — the served tape bucket is a session constant ({@link SERVED_TAPE_BUCKET_MIN}), * so a `tape <window>` that is sub-resolution or a non-multiple of it is refusable now via the SHARED * {@link windowServableBy} predicate (the same one the arged tape builder enforces at materialization). * • **budget sanity** — if the View declares a token budget already below the SUM of its selected panes' * catalog `tokenCostEstimate`s, it can never fit; refuse now. (The exact per-tokenizer measurement at * materialization stays the precise guard — the delivery seam §1a is the fail-closed backstop for a * View that fits the estimate yet overruns the real count.) * PURE. Distinct from the STRUCTURAL/date-leak refusals (unknown pane / reserved id / calendar leak), which * stay whole-wake refusals: a structurally-broken or unsafe View is nonsense to honor, whereas a sound View * that this session's resolution simply cannot serve drops to the default panes while the wake still fires. */ export function scheduleTimeViewDefect(sel: ViewSelection): string | null { for (const p of sel.panes) { for (const a of p.args ?? []) { if (a.kind === "arg-window" && p.name === "tape" && !windowServableBy(SERVED_TAPE_BUCKET_MIN, a.window)) { return ( `pane "tape" window "${a.window.value}${a.window.unit}" is not servable — the tape rows are served ` + `at ${SERVED_TAPE_BUCKET_MIN}m, so a renderable window must be a positive multiple of ${SERVED_TAPE_BUCKET_MIN}m` ); } } } if (sel.budget !== undefined) { let est = 0; for (const p of sel.panes) { const entry = paneById(p.name); if (entry !== undefined) est += entry.tokenCostEstimate; } if (est > sel.budget) { return `View "${sel.name}" is over its own token budget: the selected panes' estimated cost (~${est} tokens) exceeds the declared budget of ${sel.budget}`; } } return null; } /** `HH:MM` ET clock for a ts (dateless — a clock pins no calendar date). */ function clockOf(ts: number): string { const m = ((etMinuteOfDay(ts) % 1440) + 1440) % 1440; return `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`; } const FRAME_PHASES = new Set<string>(["pre", "open", "regular", "close", "post"]); /** Map the snapshot's phase string to a {@link FramePhase}, or `null` when unresolved (never a guess). */ function mapPhase(phase: string): FramePhase | null { return FRAME_PHASES.has(phase) ? (phase as FramePhase) : null; } // ───────────────────────────────────────────────────────────────────────────── // AuthorFrame → frame/types mappers (the acting Frame is assembled from the SAME date-blind projection // the stepped day emits — relative time + relative dte only, so no channel can re-leak the calendar) // ───────────────────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────────────────── // Options-analytics overlay fold (kestrel-4gl.13.13) — the perception arm's live `market.options` // // The driver (the impure orchestrator) folds the OPRA overlay tape causally to each frame cutoff and // hands the pure frame builders the finished, date-blind {@link OptionsAnalytics}. Kept OUT of the // pure `projectWakeFrame`/`briefingInputOf` seams (they receive the built projection, never the tape), // so those stay pure functions of their inputs. // ───────────────────────────────────────────────────────────────────────────── /** Stream `events` up to and including `tsMax` — the causal T-5m cutoff (no look-ahead). A lazy * generator over a lazy source reads only the prefix (the fold stops at the first later event). */ function* untilTs(events: Iterable<BusEvent>, tsMax: number): Generator<BusEvent> { for (const e of events) { if (e.ts > tsMax) return; yield e; } } /** The plan-lifecycle HALF of the {@link Kernel} superset (positions / resting / fills / budget / * plan states), scraped from the date-blind snapshot. The WAKE frame layers the enriched 4gl.3 * cockpit lead block on top of this base ({@link enrichedKernelOf}); the OPEN briefing carries the * base alone (the enriched cockpit at OPEN is out of scope for this slice — m9i.11 briefing). */ /** The cell-sourced STANDING context (ADR-0026) layered onto every frame's kernel: the hard * {@link Mandate} (the only admission input) + the soft, directional {@link Brief}. Sourced from * the SESSION/CELL ({@link RunSimulateOptions}), NOT hardcoded — different cells declare different * objectives. Frame content sits ABOVE the determinism line, so this never perturbs the graded bus. */ export interface KernelStanding { readonly mandate?: Mandate; readonly brief?: Brief; } /** Roll same-(instrument, strike, right) fill legs into ONE net position line (kestrel-dd8): the fill * engine SETTLES these netted, so the pane must too — a GROSS per-leg render shows huge OFFSETTING marks * that misread as a phantom winner (`+$18,000` on a net-zero book) or invite a churn to "flatten" an * already-flat book. Net qty is the signed sum (a net-0 pair renders a single clearly-flat `0 …` line — * already flat, nothing to churn); the blended basis is the ABS-qty-weighted average of the leg bases * (always well-defined, even for a net-zero pair); the net `unrealUsd` is the SUM of the per-leg unreal * (which nets to the TRUE figure). If ANY grouped leg's mark is UNKNOWN (`null`) the net is `null` * (fail-closed — never sum a fabricated 0 into a real figure); if EVERY leg omits it the net omits it. * Grouping preserves each group's FIRST-APPEARANCE order (deterministic given the deterministic fills), * so a book with NO same-(strike,right) duplicates is BYTE-IDENTICAL to the un-netted projection. Pure. */ function netPositionLegs(legs: readonly Position[]): Position[] { const groups = new Map<string, Position[]>(); for (const p of legs) { const key = `${p.instrument}${p.strike}${p.right}`; const grp = groups.get(key); if (grp === undefined) groups.set(key, [p]); else grp.push(p); } const out: Position[] = []; for (const grp of groups.values()) { if (grp.length === 1) { out.push(grp[0]!); // the common case — passed through untouched (byte-identical) continue; } const first = grp[0]!; const qty = grp.reduce((n, p) => n + p.qty, 0); const absQty = grp.reduce((n, p) => n + Math.abs(p.qty), 0); const basis = absQty > 0 ? grp.reduce((s, p) => s + Math.abs(p.qty) * p.basis, 0) / absQty : first.basis; const anyUnknown = grp.some((p) => p.unrealUsd === null); const allAbsent = grp.every((p) => p.unrealUsd === undefined); const unrealUsd: number | null | undefined = allAbsent ? undefined : anyUnknown ? null : grp.reduce((s, p) => s + (p.unrealUsd ?? 0), 0); // Provenance survives only when every leg agrees; a mixed group has no single opener (drop it). const plan = grp.every((p) => p.plan === first.plan) ? first.plan : undefined; out.push({ instrument: first.instrument, // ADR-0017: an equity/spot leg carries NEITHER strike NOR right — preserve their absence through the // netting (a spot group nets under the `NVDA undefined undefined` key), so the frame renders it as // `<instrument> shares`, never a phantom strike. An option group carries both (byte-identical). ...(first.strike !== undefined ? { strike: first.strike } : {}), ...(first.right !== undefined ? { right: first.right } : {}), qty, basis, ...(plan !== undefined ? { plan } : {}), ...(unrealUsd !== undefined ? { unrealUsd } : {}), }); } return out; } export function kernelOf( af: AuthorFrame, instrument: string, standing: KernelStanding = {}, // kestrel-c11: the exec contract multiplier (equity/spot 1, option 100) — the SAME scaling the fill // engine's `pnl` applies (src/engine/orgfacts.ts), threaded so the surfaced P&L is in DOLLARS, never // per-contract points. Defaults to 1 (equity/spot); the OPEN briefing carries no fills so it is moot. multiplier: number = 1, ): Kernel { const spot = af.spot; const markKnown = spot !== null && Number.isFinite(spot); // Per-fill legs (kestrel-c11 running $ P&L each), then NETTED per (instrument, strike, right) so the // pane matches the engine's netted settle (kestrel-dd8) — a gross render misreads offsetting marks. const legs: Position[] = af.fills.map((f) => { const qty = f.side === "buy" ? f.qty : -f.qty; // kestrel-c11 — the position's running unrealized P&L in DOLLARS: `qty × (mark − basis) × multiplier`, // marked to spot (equity/spot, ADR-0017) or intrinsic (option), with the SAME dollar scaling the fill // engine's `pnl` uses. So the agent READS `unreal=-$25.97` instead of deriving it and mis-scaling // cents-for-dollars (the 100× abandon bug). `null` when the mark is UNKNOWN (no spot) — fail-closed to // `—`, never a fabricated 0. An option leg carries a finite strike + a real C/P right; equity does not. const isOption = Number.isFinite(f.strike) && (f.right === "C" || f.right === "P"); const mark = !markKnown ? null : isOption ? intrinsic(spot as number, f.strike, f.right as Right) : (spot as number); // single exec instrument per session (ADR-0017); a mixed-multiplier book would need a per-instrument multiplierOf like orgfacts. const unrealUsd = mark === null ? null : qty * (mark - f.px) * multiplier; return { instrument, strike: f.strike, right: f.right as FrameRight, qty, basis: f.px, plan: f.plan, unrealUsd, }; }); // kestrel-dd8: net same-(instrument, strike, right) legs into ONE line so the pane matches the engine's // netted settle — a book with no duplicates is byte-identical to the un-netted per-fill projection. const positions: Position[] = netPositionLegs(legs); const resting: RestingOrder[] = af.resting.map((r) => ({ // The AUTHORITATIVE Gate ref the fill engine rests this order under (kestrel-5zl.5): a `cancelOrder` // carrying this ref resolves to the real resting order through the SAME Gate a fired Plan cancels // through. (It was a synthetic `plan:role:strikeRight` handle that no Gate ref ever matched, so // cancel-by-ref silently no-op'd.) The kernel PRINTS it (`ref=o1`) — the ref is what a cancel names. ref: r.ref, side: r.side as "buy" | "sell", instrument, strike: r.strike, right: r.right as FrameRight, qty: r.qty, px: r.px, plan: r.plan, })); const plans: PlanStateEntry[] = af.plans.map((p) => ({ name: p.name, state: p.state as PlanStateEntry["state"], // kestrel-50w: carry the arm-time gate-block reason into the acting Frame so a plan stuck `authored` // on an unsatisfiable regime gate renders `authored (blocked: <reason>)`, never a bare (live-looking) // `authored`. Frame content, above the determinism line — the graded bus is untouched. ...(p.blockedReason !== undefined ? { blockedReason: p.blockedReason } : {}), })); return { // Cell-sourced standing context (ADR-0026) — present only when the cell declared it (absent ⇒ // the frame renders unchanged, byte-identical to pre-mandate). Never a fabricated default. ...(standing.mandate !== undefined ? { mandate: standing.mandate } : {}), ...(standing.brief !== undefined ? { brief: standing.brief } : {}), positions, resting, // Per-vantage fill deltas are a later refinement; the authoritative fill record is the graded Bus. fillsSinceLast: [] as FillRecord[], budget: { used: af.committedUsd, remaining: af.rUsd - af.committedUsd, total: af.rUsd }, plans, }; } /** The OPEN briefing input, mapped from the date-blind briefing {@link AuthorFrame}. */ function briefingInputOf( af: AuthorFrame, meta: MetaEvent, instrument: string, firstTs: number, lastTs: number, standing: KernelStanding = {}, options?: OptionsAnalytics, // kestrel-121: the chain-fair context evaluated at the OPEN author cutoff. Absent ⇒ the briefing // chain `fair` stays `—` (byte-identical to a pre-fair briefing). fairCtx?: ChainFairContext, // kestrel-wa0j.20: the injected prior-session tapes, threaded onto the OPEN briefing so `tape d-N` // serves ordinal N's block. Absent ⇒ no `priorSessions` field (byte-identical, Train 1B absence). priorSessions?: readonly PriorSessionTape[], ): BriefingInput { const instruments: FrameInstrumentSpec[] = meta.instruments.map((si) => ({ symbol: si.symbol, assetClass: si.assetClass, ...(si.role !== undefined ? { role: si.role } : {}), })); // The PRESENTED current-time is the frame's OWN vantage, reconstructed from its relative `sinceOpenMs` // (projected against this same `firstTs` origin) — NOT a re-assumption that the author acts at the // open. For the default (pre-open) briefing `sinceOpenMs === 0`, so `vantageTs === firstTs` and both // fields are byte-identical to before. For an intraday `authorFromRange` briefing the vantage is the // acting cutoff (kestrel far-cutoff clock), so the clock reads the cutoff and time-to-close shrinks // accordingly — the honest "it's 13:45, ~2h15m to close" a live FOMC author must reason over. const vantageTs = firstTs + af.sinceOpenMs; return { timeToCloseMin: Math.round((lastTs - vantageTs) / 60_000), phase: mapPhase(af.phase), clockET: clockOf(vantageTs), instruments, market: marketPaneOf(af, instrument, options, fairCtx), kernel: kernelOf(af, instrument, standing), ...(priorSessions !== undefined ? { priorSessions } : {}), }; } // ───────────────────────────────────────────────────────────────────────────── // The agent-driven wake frontier (kestrel-5zl.4) — constants + shape // // After the T-5 OPEN baseline (m9i.11) the AGENT drives its own monitoring cadence via `scheduleWake`; // the platform FLOORS it (never replaces it) with a staleness backstop + the optional structural close // cadence, and coalesces/downgrades past the attention budget. These are author-tunable config knobs, // NOT removable by the agent — safety can never be silenced (CLAUDE fail-closed). The budget algebra // proper and per-session structural re-anchoring are scoped to the b3l Wake-router epic. // ───────────────────────────────────────────────────────────────────────────── /** Total, deterministic frontier tiebreak when two wakes share a ts: the agent first, then the author * seed, then an own-fill management wake, then the structural cadence, then the staleness backstop (safety * sorts last, never masking an earlier authored intent). Same bus + same agent turns ⇒ identical frontier * order ⇒ identical Blotter. The `own-fill` wake exists only on a seeded (sampled-channel) run — a * fire-then-inform management prompt raised when the agent's order fills, ADR-0016 / kestrel-9gu.8.3. * `latency-fold` (ADR-0040 / clock-honest wakes §2.3) is a determinism-bearing tie-break PIN: no tie is * currently reachable — a frontier wake at exactly `returnTs` folds INTO the catch-up rather than sorting * beside it — but the rank is pinned anyway so the sort stays total (RUNTIME §0). */ const WAKE_SOURCE_RANK: Record<WakeSource, number> = { agent: 0, seed: 1, "own-fill": 2, structural: 3, staleness: 4, "latency-fold": 5 }; // ───────────────────────────────────────────────────────────────────────────── // The SeqWindow + engine-actions-since-last-wake (kestrel-5zl.11 — the 4gl.3 cockpit engine log) // // The frame's L0/L1 engine log is the ACTIONS-SINCE-THE-LAST-WAKE, not the whole session: the // `[prevWakeSeq, thisWakeSeq)` slice of the graded Bus in **seq space** (every event carries a // monotonic `seq`, the determinism key — bus/types.ts EnvelopeBase). `prevWakeSeq`/`thisWakeSeq` // are the two adjacent WAKE-checkpoint seqs the driver stamps at each vantage. Reading only events // with `seq < thisWakeSeq` is frozen-at-wake by construction (no look-ahead), and an `asofSeq` is an // ORDINAL (date-blind), so the window is deterministic and replay-stable. // ───────────────────────────────────────────────────────────────────────────── /** The `[prevWakeSeq, thisWakeSeq)` bus-seq window the engine-actions-since-last-wake ride. Both are * WAKE-checkpoint `seq`s (an ordinal, never a wall-clock/date); the half-open window is the gap * between the previous vantage and this one. */ export interface SeqWindow { readonly prevWakeSeq: number; readonly thisWakeSeq: number; } /** The pure inputs {@link projectWakeFrame} consumes beyond the graded bus + the seq window: the * date-blind vantage projection (market pane + plan-lifecycle base), the exec instrument, the * per-vehicle books for the cockpit data-health, and the wake ctx (ordinal / reason / deadlines / * attention). Everything here is a pure derivation of the injected bus (no wall clock, no RNG). */ export interface WakeFrameConfig { readonly af: AuthorFrame; readonly instrument: string; /** The exec instrument's contract multiplier (kestrel-m9i.32) — `1` for equity/spot, `100` for a * listed option. Drives the cost-basis sizing headroom (equity basis = spot × mult; option basis = * premium × mult, ADR-0017). Absent ⇒ treated as `1` (equity/spot). */ readonly execMultiplier?: number; readonly vehicles: readonly VehicleBook[]; readonly ctx: WakeCtx; /** The cell-sourced standing context (ADR-0026 mandate + brief) layered onto the wake kernel. */ readonly standing?: KernelStanding; /** The options-analytics projection for THIS wake's cutoff (kestrel-4gl.13.13) — already folded * causally + date-blind by the driver ({@link buildFrameOptions}). Present only for the perception * arm; absent leaves `market.options` undefined (the minimal arm, byte-identical). */ readonly options?: OptionsAnalytics; /** The injected time-to-expiry resolver (`fairTauYears`) the chain's `fair` column is priced through * (kestrel-121) — the SAME source the fill engine prices `@fair` with, evaluated at THIS wake's cutoff * (`ctx.wakeTs`). Absent ⇒ the chain `fair` stays `—` (byte-identical to a pre-fair frame). */ readonly fairTauYears?: FairTauProvider; /** kestrel-wcnd: the exec chain's OWN expiry token at this wake (from the RAW snapshot — the * date-blind `af` cannot carry it). Feeds `fairTauYears`'s 2nd argument ONLY; never rendered. */ readonly fairExpiry?: string; /** The prior author vantage's SERVED values (kestrel-wa0j.48) — the typed snapshot the driver served * at the PREVIOUS vantage, so the `delta` pane can state what MOVED under stateless-redraw. Present * only on a wake AFTER the first (the driver captures it from the prior frame's served levels/book); * absent ⇒ the frame carries no `priorVantage` (byte-identical, and the delta pane fails closed). */ readonly priorVantage?: PriorVantage; /** The PRIOR sessions' tapes (kestrel-wa0j.20) — the injected multiday tape (`opts.priorSessions`), * threaded so `tape d-N` serves ordinal N's rows as its own block. Absent in the v1 single-day sim ⇒ * the frame carries no `priorSessions` (byte-identical, and `tape d-N` renders the Train 1B absence). */ readonly priorSessions?: readonly PriorSessionTape[]; /** Every armed plan STATEMENT the driver holds, in arm order (kestrel-wa0j.29) — the typed ASTs the * `armed-plan` pane's terms are captured from. The seam filters these to the plans in an ENFORCED * lifecycle state (per this wake's kernel plan set) and prints each clause through the canonical * printer. Absent (or no enforced plan) ⇒ the frame carries no `armedPlan` and the pane fails closed * to absent-with-reason, byte-identical to a frame without the field. */ readonly armedPlanAsts?: readonly PlanStatement[]; /** The tape's `session_date` (kestrel-wa0j.74) — the reference an ABSOLUTE per-leg `exp 2026-08-21` * is date-blinded to a relative `<n>dte` against when the `armed-plan` pane captures the plan's terms. * Required so an authored iso-date tenor cannot leak into the acting Frame and trip {@link * assertFrameDateBlind}; unused when no plan carries an absolute per-leg expiry (byte-identical). */ readonly sessionDate: string; } /** Map ONE graded-bus event to its {@link EngineAction} bucket, or `null` when it is not an L0/L1 * engine action (RUNTIME §5–6): an `ORDER place` FIRED, an `ORDER cancel` (reason `cancelled`) * CANCELLED, an `ORDER reject` REJECTED, a `TELEMETRY guard` whose directional cap was `applied` * CLAMPED. An `ORDER fill` and an esc-reprice cancel (a non-`cancelled` reason) are not engine * actions on this SAFETY/CONTROL surface. `id` is the order ref; `asofSeq` is the event's ordinal. */ /** Refusal-reason markers (engine-authored constant strings) that promote a PLAN lifecycle record to a * `rejected` engine action so the acting agent sees WHY an order was refused (kestrel-7kt). A normal * lifecycle reason (`fired`, `superseded`, `ttl`, `invalidate`, `cancel-if`, `transferred`) is NOT a * refusal and stays out of the engine log. * * The list must cover EVERY reason `PlanEngine.#emitReject` authors (src/engine/plans.ts — the sole * producer of a PLAN refusal notice; a plain `#transition` reason is open-vocabulary lifecycle, which is * why this seam matches rather than inverts). Matching is the only discriminator the bus schema affords: * a reject notice and a transition are the same `PLAN lifecycle` shape, so an unlisted refusal is dropped * SILENTLY at the agent's only feedback loop (kestrel-x4j8). Adding an `#emitReject` reason means adding * its marker here — tests/simulate.engine-log-refusals.test.ts pins the vocabulary. */ const REFUSAL_MARKERS: readonly string[] = [ "never naked", "uncovered sell", "unfillable", "unresolvable", "exceeds plan budget", // kestrel-x4j8 — the fire-time refusals the 7kt list missed, each silently invisible until now: // the fail-closed far-OTM cap on unresolved moneyness (plans.ts, kestrel-9gu.6 — its call site asks // for it to be surfaced LOUDLY; `unresolvable` above never matched the engine's `unresolved`), the // budget-exhausted peg that can no longer chase, and the reprice dropped onto an already-filled intent. "unresolved", "fill-guard", "peg capped", "reprice dropped", // kestrel-ocyf — the arm-time regime activation-gate notices (`armDocument`): the definitive // dead-on-arrival verdict on a pre-scanned regime-less tape, and the conditional not-yet-written // block when the future is unknown. Both are #emitReject-authored and must reach the wake frame's // engine log, or the acting agent re-learns the phantom-position trap one wake late. "never written", "not yet written", ]; function isRefusalReason(reason: string | undefined): boolean { return reason !== undefined && REFUSAL_MARKERS.some((m) => reason.includes(m)); } function engineActionOf(e: BusEvent): EngineAction | null { if (e.stream === "ORDER") { switch (e.type) { case "place": return { id: e.order_id, kind: "fired", asofSeq: e.seq }; case "cancel": return e.reason === "cancelled" ? { id: e.order_id, kind: "cancelled", asofSeq: e.seq } : null; case "reject": return { id: e.order_id, kind: "rejected", asofSeq: e.seq, ...(e.reason !== undefined ? { reason: e.reason } : {}) }; default: return null; // fill — not an engine action bucket } } if (e.stream === "TELEMETRY" && e.t