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.

735 lines 128 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 { readBus } from "../bus/index.js"; import { intrinsic } from "../fair/index.js"; import { assertNever, parse, print } from "../lang/index.js"; import { armedPlanTermsOf, ENFORCED_PLAN_STATES } from "./armed-plan-terms.js"; import { decisionControlFields, driverMeasurementVersions } from "../grade/index.js"; import { heldPositionAsOfGradedBus } from "../fill/index.js"; import { project } from "../blotter/index.js"; import { expiryTauYears, etMinuteOfDay, etWallClockMs } from "./clock.js"; import { SessionCore, busSha256Of, fidelityOf, requireMeta, resolveSpecs } from "./sim.js"; import { authorCutoffTs, briefingSnapshot, computeWakes, projectAuthorFrame, scanDateLeak, snapshotOf, } from "./day.js"; import { OPEN_WAKE_KEY, assertHandlerResponseTimeless, wakeKeyOf, } from "./agent.js"; import { resolveView, windowServableBy, paneById, atmStraddleExtrinsic } from "../frame/pane-catalog.js"; import { canonicalizeConfig, cellConfigId as cellConfigIdOf, deriveConfigId, envelopeForConfig, fillSeedOf, sampledQualificationOf } from "./config.js"; import { carryFrom, emptyCarry } from "./carry.js"; // 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 } from "./vehicle-health.js"; import { sizingHeadroomOf, budgetEnvelopeOf } from "./sizing.js"; import { marketPaneOf, buildFrameOptions, SERVED_TAPE_BUCKET_MIN, } from "./market-pane.js"; import { runOpenAuthoringLoop, viewSelectionOfDocument, DEFAULT_VIEW_REQUEST_CAP, DEFAULT_AUTHORING_TOKEN_BUDGET, } from "./authoring-loop.js"; import { wakeFloorForBand, DAY_STALENESS_BACKSTOP_MIN } from "./wake-frontier.js"; // 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 } from "./market-pane.js"; export { vehicleHealthOf, vehicleBookFromSpot } from "./vehicle-health.js"; export { sizingHeadroomOf } from "./sizing.js"; export { viewSelectionOfDocument, DEFAULT_VIEW_REQUEST_CAP, DEFAULT_AUTHORING_TOKEN_BUDGET, } from "./authoring-loop.js"; export {} from "./wake-frontier.js"; // ───────────────────────────────────────────────────────────────────────────── // 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) { return node.kind === "module" ? node : { kind: "module", imports: [], statements: [node] }; } function msgOf(e) { 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 = new Set(["tick", "second"]); const OBSERVABILITY_RUNGS = 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) { 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) { 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) { 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(["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) { return FRAME_PHASES.has(phase) ? phase : 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, tsMax) { for (const e of events) { if (e.ts > tsMax) return; yield e; } } /** 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) { const groups = new Map(); 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 = []; 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 = 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, instrument, standing = {}, // 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 = 1) { 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 = 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, f.strike, f.right) : spot; // 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, 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 = netPositionLegs(legs); const resting = 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, instrument, strike: r.strike, right: r.right, qty: r.qty, px: r.px, plan: r.plan, })); const plans = af.plans.map((p) => ({ name: p.name, state: p.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: [], 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, meta, instrument, firstTs, lastTs, standing = {}, options, // 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, // 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) { const instruments = 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 = { agent: 0, seed: 1, "own-fill": 2, structural: 3, staleness: 4, "latency-fold": 5 }; /** 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 = [ "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) { return reason !== undefined && REFUSAL_MARKERS.some((m) => reason.includes(m)); } function engineActionOf(e) { 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.type === "guard" && e.directional_cap === "applied") { return { id: e.order_id, kind: "clamped", asofSeq: e.seq }; } // A never-naked SELL refusal (or a synthesized-order de-arm) is a PLAN lifecycle record, not an ORDER // reject — no order was ever submitted. Surface the REFUSAL ones (only) as a `rejected` engine action // carrying the reason, so the agent can see why its order never rested (kestrel-7kt). if (e.stream === "PLAN" && e.type === "lifecycle" && isRefusalReason(e.reason)) { return { id: e.plan, kind: "rejected", asofSeq: e.seq, reason: e.reason }; } // An agent order-action refuse (cancelOrder no-such-ref, scheduleWake unresolvable/past/beyond) is a // CONTROL record — surface it too, so a refused agent action is never invisible to the agent. if (e.stream === "CONTROL" && e.type === "refuse") { const note = e.note ?? "refused"; return { id: note.split(":")[0].trim(), kind: "rejected", asofSeq: e.seq, reason: note }; } return null; } /** The FILLS-SINCE-LAST-WAKE: the `[prevWakeSeq, thisWakeSeq)` slice of the graded stream's ORDER * `fill` events, mapped to {@link FillRecord}. Frozen-at-wake (only `seq < thisWakeSeq`) and scoped to * the gap (not cumulative-since-open), so an order that filled between vantages reaches the acting agent * (kestrel-fud — this was hard-coded `[]`). Pure + deterministic, a projection of the graded bus. */ function fillsInWindow(emitted, window) { const out = []; for (const e of emitted) { if (e.seq < window.prevWakeSeq || e.seq >= window.thisWakeSeq) continue; // outside [prev, this) if (e.stream !== "ORDER" || e.type !== "fill") continue; out.push({ side: e.side, instrument: e.instrument, // ADR-0017 (kestrel-1kp): an equity/spot fill carries NEITHER strike NOR right — PRESERVE their // absence, never substitute a phantom `0`/`"C"` zero-strike CALL. A concrete `0`/`"C"` defeats // {@link isSpotLeg} (which keys on presence), so the since-last fill line MISRENDERED the share leg // as `0C` instead of `<instrument> shares` (the same phantom-chrome class kestrel-orx fixed on the // kernel/positions surfaces). An OPTION fill carries BOTH — byte-identical to before. ...(e.strike !== undefined ? { strike: e.strike } : {}), ...(e.right !== undefined ? { right: e.right } : {}), qty: e.qty, px: e.px ?? 0, clock: clockOf(e.ts), ...(e.plan !== undefined ? { plan: e.plan } : {}), }); } return out; } /** The engine-actions-since-the-last-wake: the `[prevWakeSeq, thisWakeSeq)` slice of the graded * `emitted` stream, bucketed by {@link engineActionOf}. Frozen-at-wake (only events with * `seq < thisWakeSeq`) and scoped to the gap (not cumulative-since-open). Pure + deterministic. */ function engineActionsInWindow(emitted, window) { const out = []; for (const e of emitted) { if (e.seq < window.prevWakeSeq || e.seq >= window.thisWakeSeq) continue; // outside [prev, this) const a = engineActionOf(e); if (a !== null) out.push(a); } return out; } /** Layer the enriched 4gl.3 cockpit lead block onto the plan-lifecycle base {@link Kernel}: the wake * ref (RELATIVE `T-Nm` deadline), per-vehicle data-health, the (empty) unavailable set, the cockpit * budget envelope, owner acts, the engine-actions-since-last-wake, and predictor/regime claims. Every * section is always present (absent renders as an explicit `none`/`UNKNOWN`, never dropped). This is * the REAL kernel {@link renderKernel} consumes — not a partial stub. Pure + deterministic. */ function enrichedKernelOf(base, dataHealth, engineLog, ctx, sizing) { const minutesToClose = Math.round((ctx.lastTs - ctx.wakeTs) / 60_000); const wake = { reason: ctx.wakeReason, severity: "routine", deadline: Number.isFinite(minutesToClose) ? minutesToClose : null, }; return { ...base, // The extended cockpit lead block (4gl.3) — always present (absent-not-hidden). wake, dataHealth, // kestrel-wa0j.47: the v1 harness serves no macro/calendar capability, and the kernel's // unavailable-capabilities line is now that absence's ONE rendering (the `macro` pane left the // default OPEN set) — an empty list here would hide the absence entirely (absent-not-hidden). unavailable: ["macro calendar"], budgetEnvelope: budgetEnvelopeOf(base.budget, sizing), ownerActs: [], engineLog, claims: [], }; } /** * **The pure WAKE-frame builder** (kestrel-5zl.11): `(graded bus, seq window, config) → ActingFrame`. * Assembles the date-blind {@link ActingFrame} handed to the agent at one wake, whose `kernel` is the * REAL enriched 4gl.3 cockpit lead block — positions / resting / budget / data-health / owner-acts / * the engine-actions-since-last-wake / predictor-regime claims — that {@link renderKernel} consumes, * NOT the partial stub. The engine log is the `[prevWakeSeq, thisWakeSeq)` bus-window slice * ({@link engineActionsInWindow}), so it is the since-LAST-wake delta, frozen-at-wake (only * `seq < thisWakeSeq`) with no look-ahead. * * PURE: no wall clock, no unseeded RNG — the output is a byte-identical function of its inputs, so the * same bus + same seq window + same config projects an identical frame (the determinism the tracer's * replay rides on). Frame content sits ABOVE the determinism line (recorded/fixed adapters ignore it), * so it never perturbs the graded bus. DATE-BLIND: only relative time, the `HHMM` ET clock, and * ordinals reach the frame; {@link assertFrameDateBlind} is the fail-closed backstop at the seam. */ export function projectWakeFrame(emitted, window, config) { const { af, instrument, vehicles, ctx, standing, options } = config; const minutesToClose = Math.round((ctx.lastTs - ctx.wakeTs) / 60_000); const dataHealth = vehicles.map((v) => vehicleHealthOf(v, ctx.wakeTs)); const engineLog = engineActionsInWindow(emitted, window); // The fills that happened since the last vantage — a graded-bus projection over the same seq window, // so an order that filled between wakes reaches the agent (kestrel-fud; the base `kernelOf` carries the // OPEN-briefing `[]`, which is correct pre-open). const fillsSinceLast = fillsInWindow(emitted, window); const base = kernelOf(af, instrument, standing ?? {}, config.execMultiplier ?? 1); // The sizing headroom (kestrel-m9i.32): the max fillable size the remaining-R budget admits for the // exec instrument, so a model sizes WITHIN the bounded-risk cost basis (ADR-0017) rather than into a // silent fire-time clamp. PURE — derived from the plan-lifecycle budget (`remaining $`) + the frame's // exec spot + the exec multiplier (equity 1 / option 100). An option's per-leg premium is not sourced // at this seam, so the option cue surfaces the remaining-$ ceiling + the basis rule (never a guess). const sizing = sizingHeadroomOf({ instrument, remainingUsd: base.budget?.remaining ?? null, spot: af.spot, multiplier: config.execMultiplier ?? 1, }); const kernel = { ...enrichedKernelOf(base, dataHealth, engineLog, ctx, sizing), fillsSinceLast }; // The ARMED plans' enforced terms for the `armed-plan` pane (kestrel-wa0j.29) — captured from the armed // ASTs the driver holds (`config.armedPlanAsts`), filtered to the plans this wake's kernel reports in an // ENFORCED lifecycle state (armed/fired/managing). The watcher manages plans whose document text it // never otherwise sees; the terms enter the frame THROUGH this typed input (the renderer invents none). // Absent config, or no enforced plan ⇒ `undefined` (absent-not-hidden: the field is unserialized and the // pane fails closed to absent-with-reason, byte-identical to a frame without it). const enforcedPlanNames = new Set(base.plans.filter((p) => ENFORCED_PLAN_STATES.has(p.state)).map((p) => p.name)); const armedPlan = config.armedPlanAsts !== undefined ? armedPlanTermsOf(config.armedPlanAsts, enforcedPlanNames, config.sessionDate) : undefined; return { // WakeDeltaInput wakeIndex: af.n, minutesSinceLast: Math.round((ctx.wakeTs - ctx.prevVantageTs) / 60_000), timeToCloseMin: minutesToClose, phase: mapPhase(af.phase), clockET: clockOf(ctx.wakeTs), wakeReason: ctx.wakeReason, // kestrel-121: price the chain `fair` column through the injected `fairTauYears` at this wake's cutoff // (`ctx.wakeTs`) — the SAME source the fill engine prices `@fair` with. Absent ⇒ `fair` stays `—`. market: marketPaneOf(af, instrument, options, config.fairTauYears !== undefined ? { tauYears: config.fairTauYears, nowTs: ctx.wakeTs, ...(config.fairExpiry !== undefined ? { expiry: config.fairExpiry } : {}) } : undefined), kernel, // The prior author vantage's served values (kestrel-wa0j.48) — threaded from the driver so the // `delta` pane states what MOVED under stateless-redraw (the renderer invents no value; the prior // data must enter through the frame input). Present only on a wake AFTER the first; absent ⇒ the // field is unserialized and the frame is byte-identical to today (the delta pane fails closed). ...(config.priorVantage !== undefined ? { priorVantage: config.priorVantage } : {}), // The prior sessions' tapes (kestrel-wa0j.20) — threaded from the driver so `tape d-N` serves // ordinal N's rows as its own single-session block. Present only when the run injects a multiday // tape; absent ⇒ the field is unserialized and the frame is byte-identical (Train 1B absence). ...(config.priorSessions !== undefined ? { priorSessions: config.priorSessions } : {}), // The armed plans' enforced terms (kestrel-wa0j.29) — threaded so the `armed-plan` pane renders the // WHEN/entries/exits/invalidation/size the watcher enforces. Present only on a wake with at least one // plan in an enforced state; absent ⇒ the field is unserialized and the frame is byte-identical. ...(armedPlan !== undefined ? { armedPlan } : {}), // acting-kernel strip wakeOrdinal: ctx.wakeOrdinal, // The stable replay identity (kestrel-5zl.19) — the source that raised this wake + its monotone // per-source ordinal, the two halves `wakeKeyOf` joins the captured turns on. wakeSource: ctx.wakeSource, wakeSourceOrdinal: ctx.wakeSourceOrdinal, // The relative-day coordinate (kestrel-5zl.16.4) — present ONLY for a multi-session run (it rides the // date-blind author projection), absent + unserialized for a standalone session (goldens unchanged). ...(af.sessionOrdinal !== undefined ? { sessionOrdinal: af.sessionOrdinal } : {}), severity: "routine", deadline: { minutesToClose, minutesToNextWake: ctx.nextWakeTs === null ? null : Math.round((ctx.nextWakeTs - ctx.wakeTs) / 60_000), }, dataHealth: { asOfClockET: clockOf(ctx.wakeTs), stale: [], unavailable: [] }, // The scheduled wake's stored View rides the delivery (kestrel-wa0j.4) — the adapter renders THIS // wake's panes under it; absent ⇒ the field is unserialized and the frame is byte-identical to today. ...(ctx.view !== undefined ? { view: ctx.view } : {}), attention: { wakesRemaining: ctx.wakesRemaining, coalesced: ctx.coalesced }, }; } /** Fail-closed date-blind fence over the assembled acting Frame (the agent boundary): serialize with the * SAME 1e-6 rounding the machine summary uses (so a raw float tail can't trip the epoch grep as a false * calendar signal), then run the SHARED {@link scanDateLeak}. A leak is REFUSED, never delivered. */ function assertFrameDateBlind(frame) { const body = JSON.stringify(frame, (_k, v) => typeof v === "number" && Number.isFinite(v) ? Math.round(v * 1e6) / 1e6 : v); const hit = scanDateLeak(body); if (hit !== null) { throw new Error(`simulate: acting Frame leaks a ${hit.name} (${JSON.stringify(hit.match)}) at the agent boundary ` + "(fail-closed, EVALUATION contamination fence)"); } } // ───────────────────────────────────────────────────────────────────────────── // runSimulateSession // ───────────────────────────────────────────────────────────────────────────── /** * Drive one frozen-at-wake Simulate session through the {@link Agent} seam and return its graded * {@link Blotter} + the {@link CapturedTurns} it consumed. Reuses the SAME {@link SessionCore} the stepped * day drives (bus-stepping, Gate, `projectAuthorFrame`); the ONE behavioral change is that the wake set is * answered by `agent.decide` rather than a file handshake. `async` because the live loop is deliberately * non-deterministic; the recorded/fixed adapters resolve synchronously. */ export async function runSimulateSession(opts) { if (opts.busPath === undefined && opts.events === undefined) { throw new Error("simulate: runSimulateSession needs either `busPath` or `events`"); } const events = opts.events !== undefined ? [...opts.events] : [...readBus(opts.busPath)]; const meta = requireMeta(events); const specs = resolveSpecs(meta, opts.instruments); const fairTauYears = opts.fairTauYears ?? expiryTauYears(meta.session_date); const firstTs = events[0]?.ts ?? 0; const lastTs = events[events.length - 1]?.ts ?? firstTs; const sessionDate = meta.session_date; // ── Clock-honest session semantics (ADR-0040 / kestrel-w7la.1, clock-honest wakes §§2, 4, 7) ───── // The switch is the agent config's `clockHonest` (a ConfigId axis — a clocked run never shares a grid // column with an unclocked one). EVERY precondition refuses LOUDLY at open — never a silent default, // never a silent downgrade to latency-blind (§4). const clockHonest = opts.agent.config.clockHonest === true; if (clockHonest) { const buf = opts.agent.config.latencyBufferMs; if (typeof buf !== "number" || !Number.isInteger(buf) || buf < 0) { throw new Error(`simulate: clock-honest run refused at open — \`latencyBufferMs\` must be a non-negative integer (got ${JSON.stringify(buf)}); there is NO default (never a silent default — a designed default would be a judge-designed weighting, ADR-0030 spirit). \`0\` is legal: it declares a colocated seat.`); } // Eligibility: the ADR-0040 data floor, declared with the tape and verified fail-closed (§4). const ineligible = clockHonestIneligibility(opts.dataRung); if (ineligible !== null) { throw new Error(`simulate: clock-honest run refused at open — ${ineligible}`); } // A costless (pre-v6 / latency-blind) capture replays ONLY under latency-blind semantics (§7): // replaying it clocked would silently change what the run claims to be. Probed at the OPEN key — // the one entry every driver-captured recording holds. if (opts.agent.recordedCostMs !== undefined && opts.agent.recordedCostMs(OPEN_WAKE_KEY) === undefined) { throw new Error("simulate: clock-honest run refused at open — the recorded capture is COSTLESS (it carries turns but no measured deliberation costs); a costless capture replays only latency-blind (ADR-0040 §7, never a silent downgrade)"); } } /** The configured latency buffer, applied to every Seat-answered vantage (0 when latency-blind — * never read on that path). */ const bufferMs = clockHonest ? opts.agent.config.latencyBufferMs : 0; /** The injectable wall clock — consulted ONLY on the live clocked path (§7's one minting seam). */ const nowFn = opts.now ?? Date.now; /** * Measure — or, in replay, READ — one Seat turn's deliberation cost (§7: mint once, live-side, at * THIS seam; the only minting site). Replay mode (an agent exposing `recordedCostMs`) never touches * the clock — a replay path that re-times is a defect by definition (the poisoned-clock fixture). */ const clockedTurn = async (key, call) => { const costSource = opts.agent.recordedCostMs?.bind(opts.agent); if (costSource !== undefined) { const turn = assertHandlerResponseTimeless(await call()); const c = costSource(key); // `null` = the capture never held this key — the replay machinery SYNTHESIZED the turn // (divergence stand-down / inserted-vantage pass). Engine-minted, not Seat deliberation: // cost 0, no buffer, no record (§7 — charging tape for a turn no Seat took fabricates cost). if (c === null) return { turn, measuredMs: 0, seatAnswered: false }; if (c === undefined || !Number.isInteger(c) || c < 0) { throw new Error(`simulate: clock-honest replay hit a costless/malformed capture entry at wake ${key} — a costless capture replays only latency-blind (fail-closed, ADR-0040 §7)`); } return { turn, measuredMs: c, seatAnswered: true }; } const t0 = nowFn(); const turn = assertHandlerResponseTimeless(await call()); const t1 = nowFn(); return { turn, measuredMs: Math.max(0, Math.round(t1 - t0)), seatAnswered: true }; }; // Capture the a57.14 experimental envelope from the agent's config (kestrel-5zl.6) and stamp it on the // graded META at open — the CAPTURE side of a57.14. Only an authoring config declares one; a NO-LLM // Backtest (fixedPlan/recorded with BACKTEST_CONFIG) declares none ⇒ no stamp ⇒ the Blotter stays // certified (fail-closed by omission — never a fabricated identity). A declared-but-incomplete identity // is stamped so the projector fails the Blotter closed to PROVISIONAL (a57.14). The FULL ConfigId // rides alongside under the SAME condition (m9i.2 owner tweak): the grid CellKey keys on it, so // temperature-only config variants land in DISTINCT cells. const envelope = envelopeForConfig(opts.agent.config); const configId = envelope !== undefined ? deriveConfigId(canonicalizeConfig(opts.agent.config)) : undefined; // The cell config axis (fillSeed stripped — ADR-0016 §3): stamped ONLY when it DIVERGES from configId (the // config carried a fillSeed) — a seedless run's cell axis equals configId, so no stamp and the graded bus // stays byte-identical. Seed-only variants pool as replicates in ONE cell; configId keeps the seeded RUN id. const cellConfigId = envelope !== undefined ? cellConfigIdOf(opts.agent.config) : undefined; const cellConfigStamp = cellConfigId !== undefined && cellConfigId !== configId ? cellConfigId : undefined; // Arm the seeded sampled fill channel when the agent's config declares a fillSeed (kestrel-9gu.8.3, // ADR-0016); a Backtest config (or any omitting the seed) leaves the sampler OFF (fail-closed). const fillSeed = fillSeedOf(opts.agent.config); // The sampled-channel qualification claim (kestrel-9gu.12), read fail-closed off the config: absent — // every config until the 9gu.8.6 study passes — the headline gate resolves to the strict-cross floor. const sampledQualification = sampledQualificationOf(opts.agent.config); const openingCarry = opts.openingCarry; // The wake-frontier floor is a FUNCTION of the cell's timescale band (kestrel-5zl.16.7). Read the band // once from the opening carry (an absent carry — a standalone / flat open — is `emptyCarry().band` = // `day`, so this run is byte-identical to today), resolve the pure floor, and use its three controls // wherever the frontier gates a wake (`popNext` spacing/ceiling, `armStaleness` backstop, the own-fill // spacing). The DAY floor reproduces today's exact constants — no churn. const floor = wakeFloorForBand((openingCarry ?? emptyCarry()).band); const core = new SessionCore({ meta, specs, fillModel: opts.fillModel, rUsd: opts.rUsd, fairTauYears, firstTs, // NO `tapeRegimeScopes` pre-scan here, DELIBERATELY (kestrel-ocyf round 3): this is the // agent-interactive path (day.ts drives through it), and a whole-bus pre-scan is a LOOKAHEAD // ORACLE — a mid-session supersede→arm would receive a notice grade that is a function of // post-vantage events (definitive notice = "never written all day"; silence = "a REGIME write is // coming"), and the REFUSAL_MARKERS seam pipes that onto the acting agent's frame, letting it // read tomorrow's regime vocabulary through throwaway supersedes. Violates this file's own // charter (no post-cutoff leak / no look-ahead) and the eval contamination firewall. Interactive // sessions take the CONDITIONAL wording — derived only from `#tags` (events ingested so far), // causally honest at any vantage, still loud at arm. The batch-replay `runSimSession` (a post-hoc // report with no author to leak to) keeps the definitive pre-scan. ...(opts.makerFairParams !== undefined ? { makerFairParams: opts.makerFairParams } : {}), ...(envelope !== undefined ? { envelope } : {}), ...(configId !== undefined ? { configId } : {}), ...(cellConfigStamp !== undefined ? { cellConfigId: cellConfigStamp } : {}), ...(openingCarry !== undefined ? { openingCarry } : {}), ...(fillSeed !== undefined ? { fillSeed } : {}), ...(sampledQualification !== undefined ? { sampledQualification } : {}), // The clock-honest attestation input (ADR-0040 §5): every §4 precondition already held above (the // run refuses at open otherwise), so a constructed clocked core is an attested one. `true` or // absent — the driver NEVER stamps `false`. ...(clockHonest ? { clockHonest: true } : {}), }); const instrument = core.execSpec.symbol; const instruments = meta.instruments.map((i) => i.symbol); // DAY-COMPAT MODE (kestrel-5zl.3): the caller-owns-cadence path `runDaySession` drives through. When set, // the driver reproduces the stepped runner's cadence + byte stream EXACTLY — the day's own structural wakes // are the coverage, so NO platform staleness/backstop wake is injected (Divergence #1), and the CONTROL // supersede carries