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.

173 lines (159 loc) 9.95 kB
/** * # frame/kernel-walk — the ONE cockpit-kernel walk + the shared kernel helpers (ADR-0052 §1) * * The SAFETY / CONTROL cockpit kernel has exactly ONE traversal: {@link walkKernel} visits the * optional mandate/brief prefix and then the 8 fixed-order {@link ./types.ts KERNEL_SECTION_LABELS} * sections, IN ORDER, calling a {@link KernelSink} method for each. Every section method is ALWAYS * called (absent-not-hidden is a structural property of the walk — a section can never be silently * skipped), and the SECTION ORDER lives here once. The two shipped renderers are format ADAPTERS * over this one walk: the canonical text sink ({@link ./render.ts}) materializes the delta-anchored * cell-lines, the token-optimal ARM sink ({@link ./render-arm.ts}) materializes the compact ASCII * lines. A new section, or a reordering, lands in BOTH renderers from a single edit here. * * This module also owns the kernel helpers both adapters share — the engine-log bucket set + * classification, the resting-order state label, and the action id token — parameterized by the * small vocabulary each adapter differs on, so the LOGIC is single-source while the BYTES stay each * adapter's own. PURE: no wall clock, no RNG (the whole render path is deterministic, RUNTIME §0). */ import type { Brief, Claim, EngineAction, EngineActionKind, Kernel, Mandate, OwnerAct, Position, RestingOrder, VehicleHealth, WakeRef, } from "./types.ts"; // ───────────────────────────────────────────────────────────────────────────── // The sink — one method per kernel section (called in fixed order by walkKernel) // ───────────────────────────────────────────────────────────────────────────── /** * A format adapter for the cockpit kernel: {@link walkKernel} calls these in the fixed cockpit order * and each implementation accumulates its own line representation (canonical cell-lines, arm strings). * A section method is called EVEN WHEN its data is empty (absent-not-hidden) — the adapter renders the * explicit `none`/`UNKNOWN` placeholder, never a dropped section. The `mandate`/`brief` methods are * called ONLY when present (they are the optional standing-context prefix, not one of the 8 sections). */ export interface KernelSink { /** The lead block: the kernel sentinel (canonical) / the `#KERNEL <kind>` banner (arm). */ sentinel(frameKind: "OPEN" | "WAKE"): void; /** The `frame=<kind>` line (canonical only; the arm folds the kind into its sentinel). */ frameLine(frameKind: "OPEN" | "WAKE"): void; /** MANDATE — the hard, machine-checkable admission channel (ADR-0026). Present-only. */ mandate(mandate: Mandate): void; /** BRIEF — the soft, directional English channel; NOT an admission input (ADR-0026). Present-only. */ brief(brief: Brief): void; /** §1 WAKE — reason / severity / RELATIVE deadline. */ wake(wake: WakeRef | null | undefined): void; /** §2 DATA-HEALTH per vehicle + the NAMED unavailable capabilities. */ dataHealth(rows: readonly VehicleHealth[], unavailable: readonly string[]): void; /** §3 POSITIONS / INVENTORY-CLAIMS. */ positions(positions: readonly Position[]): void; /** §4 RESTING ORDERS. */ resting(resting: readonly RestingOrder[]): void; /** §5 BUDGET / REMAINING-R + the sizing headroom. */ budget(envelope: BudgetEnvelopeArg): void; /** §6 OWNER ENVELOPE + ACTS (the envelope echo reads the same budget as §5). */ ownerActs(envelope: BudgetEnvelopeArg, acts: readonly OwnerAct[]): void; /** §7 L0/L1 ENGINE LOG — bucketed fired | cancelled | rejected | clamped (+ catch-all `other`). */ engineLog(elog: readonly EngineAction[]): void; /** §8 PREDICTOR / REGIME CLAIMS. */ claims(claims: readonly Claim[]): void; } /** The budget-envelope argument §5 and §6 share (the cockpit risk envelope, or absent). */ export type BudgetEnvelopeArg = Kernel["budgetEnvelope"]; /** * The ONE cockpit-kernel traversal: mandate/brief prefix (present-only) then the 8 fixed-order * sections, each ALWAYS visited. This is the single source of the kernel's SECTION ORDER and its * absent-not-hidden completeness — both format adapters get an identical structure from it. PURE. */ export function walkKernel(kernel: Kernel, frameKind: "OPEN" | "WAKE", sink: KernelSink): void { sink.sentinel(frameKind); sink.frameLine(frameKind); if (kernel.mandate !== undefined && kernel.mandate !== null) sink.mandate(kernel.mandate); if (kernel.brief !== undefined && kernel.brief !== null) sink.brief(kernel.brief); sink.wake(kernel.wake); // §1 sink.dataHealth(kernel.dataHealth ?? [], kernel.unavailable ?? []); // §2 sink.positions(kernel.positions ?? []); // §3 sink.resting(kernel.resting ?? []); // §4 sink.budget(kernel.budgetEnvelope); // §5 sink.ownerActs(kernel.budgetEnvelope, kernel.ownerActs ?? []); // §6 sink.engineLog(kernel.engineLog ?? []); // §7 sink.claims(kernel.claims ?? []); // §8 } // ───────────────────────────────────────────────────────────────────────────── // Shared kernel helpers (single-source logic; per-adapter vocabulary passed in) // ───────────────────────────────────────────────────────────────────────────── /** The four engine-log buckets, in fixed render order — shared by both adapters. */ export const ENGINE_LOG_BUCKETS: readonly EngineActionKind[] = ["fired", "cancelled", "rejected", "clamped"]; /** The engine log classified into the four fixed buckets (in order) + a catch-all `other` for any * kind outside the closed union (arrived across an untyped/JSON/`as` boundary — surfaced, never * dropped: absent-not-hidden). The classification is single-source; each adapter formats it. */ export interface EngineLogBuckets { readonly buckets: readonly { readonly kind: EngineActionKind; readonly items: readonly EngineAction[] }[]; readonly other: readonly EngineAction[]; } /** Classify an engine log into the fixed buckets (order-preserving) + the catch-all `other`. PURE. */ export function bucketEngineLog(elog: readonly EngineAction[]): EngineLogBuckets { const known: ReadonlySet<string> = new Set<string>(ENGINE_LOG_BUCKETS); return { buckets: ENGINE_LOG_BUCKETS.map((kind) => ({ kind, items: elog.filter((e) => e.kind === kind) })), other: elog.filter((e) => !known.has(e.kind)), }; } /** The per-adapter label vocabulary for {@link restingLabel} — `live` and `clamped` are INDEPENDENT * flags, so a live+clamped order surfaces BOTH (a `live` label can never mask a premium-band clamp). */ export interface RestingLabels { readonly live: string; readonly liveClamped: string; readonly clamped: string; readonly off: string; } /** The resting-order state label under an adapter's vocabulary. `live`/`clamped` independent * (fail-closed honesty: a clamped price is never hidden by a LIVE label). PURE. */ export function restingLabel(o: RestingOrder, labels: RestingLabels): string { const live = o.live === true; const clamped = o.clamped === true; if (live) return clamped ? labels.liveClamped : labels.live; return clamped ? labels.clamped : labels.off; } /** The per-adapter shape of an engine action's id token (canonical `id@seqN (reason)`, arm * `id@N(reason)`). `seqPrefix` sits between `@` and the ordinal; `reasonSpace` inserts a space * before the `(reason)` group. Reason-when-present: no reason ⇒ byte-identical to the id@seq alone. */ export interface ActionTokenStyle { readonly seqPrefix: string; readonly reasonSpace: boolean; } /** An engine action's render token `id@<seqPrefix><ordinal>` plus `(reason)` WHEN present (the * engine-authored cause — never-naked / plan-budget / regime-UNKNOWN — that must not be silently * dropped at the render seam, kestrel-75n). PURE. */ export function actionToken(e: EngineAction, style: ActionTokenStyle): string { const reason = e.reason != null && e.reason !== "" ? `${style.reasonSpace ? " " : ""}(${e.reason})` : ""; return `${e.id}@${style.seqPrefix}${e.asofSeq}${reason}`; } /** * A bucket's action tokens with each CONSECUTIVE RUN of items sharing an identical `(id, reason)` * collapsed to `<count>x <exemplar token>` instead of N near-duplicate tokens (kestrel-7bip). A plan * whose STATE trigger holds every tick emits the SAME refusal (`uncovered sell refused: never naked`) * once per tick — 24 byte-identical entries in one frame — flooding the very percept the token-optimal * renderer exists to conserve. The de-dup is a RENDER-time collapse ONLY (the engine still authors and * counts every action; the bucket's `items.length` count is unchanged): a run of one renders as the * bare token (byte-identical to pre-collapse, so no existing frame churns), and the exemplar keeps its * real `@seq` so the collapsed line stays a concrete, addressable receipt. Order-preserving; PURE. */ export function collapseActionTokens(items: readonly EngineAction[], style: ActionTokenStyle): string[] { const out: string[] = []; for (let i = 0; i < items.length; ) { const e = items[i]!; let j = i + 1; while (j < items.length && items[j]!.id === e.id && (items[j]!.reason ?? null) === (e.reason ?? null)) j++; const count = j - i; const token = actionToken(e, style); out.push(count > 1 ? `${count}x ${token}` : token); i = j; } return out; }