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