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.

486 lines (443 loc) 22.6 kB
/** * # agent language card — a compiled, token-budgeted projection of the capability catalog (kestrel-gsh.4) * * The card (`docs/public/card.md` + `docs/public/card.json`) is the *compact* surface an agent author * loads into context before writing Kestrel. It is **not** a hand-maintained prompt: a hand-written * card would drift from what the runtime actually does, and a public claim a reader cannot verify is a * lie the repo tells (AGENTS.md). So the card is a **pure projection of the typed capability catalog** * (`tests/support/catalog.ts`) — the same four-axis, receipt-backed source of truth the status page * (gsh.2) compiles from. Every capability, surface, and fail-closed doctrine on the card is therefore * exactly what the catalog asserts, no more; a new capability changes the projection and a CI test * (`tests/docs.card.test.ts`) fails until the committed card is regenerated. * * Three properties make the card trustworthy rather than merely pretty: * * 1. **Byte-determinism.** `renderCardMarkdown` / `renderCardJson` are pure functions of the catalog * (+ the package version) with stable ordering and no wall clock: same catalog ⇒ byte-identical * card. The CI test re-renders and compares byte-for-byte. * 2. **Version coupling via a catalog hash.** {@link catalogHash} is a content hash of the catalog * snapshot the card compiled from. A drifted catalog changes the hash, which changes the card, * which fails the equality test — the card can never silently describe a stale catalog. * 3. **A tested token budget.** The card is for agents under token constraints, so its own size is a * first-class property: {@link countTokens} + {@link TOKEN_BUDGET} bound the Markdown card, and a * test asserts it fits. Compactness never drops the fail-closed / never-naked / marks-lie / * evidence-status semantics — those are load-bearing, not decoration. * * The card also carries a handful of **object ⇄ text** pairs (ADR-0004): the typed AST object IS the * language and text is a byte-stable projection of it. Each pair's canonical text is *derived* by * running the real `print(parse(text))`, and the card's test asserts the round-trip is a fixed point — * the examples are executable receipts, not prose. * * This module is pure data + pure rendering over `tests/support/catalog.ts` and `src/lang`; it is NOT * on the runtime path and mutates nothing. */ import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import type { AccessSurface, Capability, EvidenceTier, RuntimeStatus, SyntaxStatus } from "./catalog.ts"; import { CAPABILITIES, CERTIFIED_COHORT, EVIDENCE_SOURCE } from "./catalog.ts"; import { REPO_ROOT } from "./fences.ts"; import { parse, print } from "../lang/index.ts"; // ── Declared, tested economy ──────────────────────────────────────────────────── /** The command the generator writes into the card's "regenerate with" banner. */ export const REGEN_COMMAND = "bun scripts/gen-card.ts"; /** * The card's tokenizer, declared so its budget is reproducible. It is deliberately simple and * self-contained (no model-specific vocabulary, no network): one token per contiguous run of * word characters (`[A-Za-z0-9_]`) and one token per other non-whitespace character (each brace, * pipe, backtick, colon, …). It over-counts relative to a BPE tokenizer, so a card that fits this * budget fits a real model's budget with margin — the honest direction to err for a token guarantee. */ export const TOKENIZER = "kestrel-card/word-punct-v1"; /** * The maximum token budget for the Markdown card under {@link TOKENIZER}. The card is what an agent * pays for on every author turn, so this is a hard, tested ceiling — {@link countTokens} of the * generated `card.md` must not exceed it (see `tests/docs.card.test.ts`). Chosen with headroom over * the current card so incremental catalog growth does not trip CI, while still forcing a deliberate * decision (raise the budget, or trim the card) if the card ever bloats. */ export const TOKEN_BUDGET = 6000; /** Count tokens under {@link TOKENIZER}: word-runs + individual punctuation. Pure, deterministic. */ export function countTokens(text: string): number { const m = text.match(/[A-Za-z0-9_]+|[^\sA-Za-z0-9_]/g); return m === null ? 0 : m.length; } // ── Version coupling: a content hash of the catalog snapshot ───────────────────── /** * A canonical, order-independent serialization of one capability — the exact facts the card * projects. Reordering the catalog array, or reordering a capability's receipts/examples/surfaces, * must NOT change the hash; changing any asserted fact MUST. So every list is sorted and every field * is spelled out. */ function canonicalCapability(c: Capability): string { const surfaces = (Object.entries(c.access.surfaces) as [AccessSurface, string][]) .map(([s, l]) => `${s}:${l}`) .sort(); return [ `id=${c.id}`, `title=${c.title}`, `summary=${c.summary}`, `public=${c.public}`, `syntax=${c.syntax.status}${c.syntax.note ? `|${c.syntax.note}` : ""}`, `runtime=${c.runtime.status}${c.runtime.note ? `|${c.runtime.note}` : ""}`, `evidence=${c.evidence.tier}${c.evidence.note ? `|${c.evidence.note}` : ""}`, `access=${surfaces.join(",")}${c.access.note ? `|${c.access.note}` : ""}`, `source=${[...c.receipts.source].sort().join(",")}`, `test=${[...c.receipts.test].sort().join(",")}`, `examples=${[...c.examples].sort().join(",")}`, ].join("\n"); } /** * The catalog hash the card stamps for version coupling: a SHA-256 over the canonical serialization * of every capability (sorted by id) plus the certified-cohort invariant. Deterministic — no wall * clock, no insertion-order dependence. A drifted catalog ⇒ a different hash ⇒ a stale card ⇒ CI red. */ export function catalogHash(capabilities: readonly Capability[] = CAPABILITIES): string { const sorted = [...capabilities].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); const payload = [ `certified_cohort=${CERTIFIED_COHORT}`, `evidence_source=${EVIDENCE_SOURCE}`, ...sorted.map(canonicalCapability), ].join("\n---\n"); return createHash("sha256").update(payload, "utf8").digest("hex"); } /** Read the language version from `package.json` (deterministic; version-matched, no wall clock). */ export function readLanguageVersion(root: string = REPO_ROOT): string { const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version?: string }; return typeof pkg.version === "string" ? pkg.version : "0.0.0"; } // ── Object ⇄ text examples (ADR-0004 round-trip, derived from the real parser/printer) ── /** One object⇄text pair for the card. `text` is authored canonical; the round-trip is tested. */ export interface ObjectTextExample { readonly title: string; readonly note: string; /** Authored canonical Kestrel text; the card derives its printed form via `print(parse(text))`. */ readonly text: string; /** Show the full parsed AST object on the Markdown card (small statements only, to respect budget). */ readonly showObject: boolean; } /** * The object⇄text pairs the card demonstrates. Each `text` is already byte-canonical, so * `print(parse(text)) === text` (the card's test proves it). Generic tickers only (SPX/SPY) — * never anything that reveals a strategy (ARCHITECTURE §7). */ export const OBJECT_TEXT_EXAMPLES: readonly ObjectTextExample[] = [ { title: "Wake — a standing subscription over the trigger algebra", note: "A Wake spends attention, never risk. Its `when` is the shared trigger algebra as a typed node.", text: "WAKE momentum\n WHEN spot crosses above hod", showObject: true, }, { title: "Grade — an imperative replay with counterfactuals", note: "`VS` counterfactuals and `BY` dimensions are syntax, not config; the object mirrors the text one-for-one.", text: "GRADE plan fade OVER 2024-01-01..2024-03-31 FILL maker-fair-v1\n VS ungated null\n BY vehicle", showObject: true, }, { title: "Plan — bounded-risk contingent program (trigger → action → take-profit)", note: "The printer elides USING-covered qualifiers and normalizes spacing; the object carries the full structure (see card.json).", text: "PLAN fade budget 0.3R ttl +30m\n" + " WHEN spot crosses below vwap\n" + " DO buy 1 -1 P @ lean(bid, fair, 0.5) peg\n" + " TP 2x frac 0.5 @ fair", showObject: false, }, { title: "Plan — an equity ticket (shares, not option strikes)", note: "An equity/spot leg is `buy N shares` — the `shares` marker replaces the strike+right an option leg carries " + "(ADR-0017); the ambient `exec` symbol is the instrument. Omit `shares` and the parser refuses `buy N @ …`, " + "naming both continuations.", text: "PLAN accumulate budget 0.5R ttl +30m\n" + " WHEN spot crosses above vwap\n" + " DO buy 100 shares @ spot", showObject: false, }, ]; /** Derive the round-trip triple for one example: the parsed object + its canonical printed text. */ export function roundTrip(ex: ObjectTextExample): { object: unknown; printed: string } { const object = parse(ex.text); return { object, printed: print(object) }; } // ── The machine card model (what card.json serializes) ────────────────────────── const SURFACE_ORDER: readonly AccessSurface[] = ["text", "sdk", "cli", "mcp", "adapter"]; const SYNTAX_LABEL: Record<SyntaxStatus, string> = { supported: "supported", proposed: "proposed", rejected: "rejected (fail-closed)", }; const RUNTIME_LABEL: Record<RuntimeStatus, string> = { implemented: "implemented", partial: "partial", none: "none", }; const EVIDENCE_LABEL: Record<EvidenceTier, string> = { none: "none", mechanical: "mechanical (practice)", certified: "certified", }; /** The distinct access surfaces the catalog exposes, in canonical order (drives coverage). */ export function exposedSurfaces(capabilities: readonly Capability[] = CAPABILITIES): AccessSurface[] { const seen = new Set<AccessSurface>(); for (const c of capabilities) { if (!c.public) continue; for (const s of Object.keys(c.access.surfaces) as AccessSurface[]) seen.add(s); } return SURFACE_ORDER.filter((s) => seen.has(s)); } function accessString(c: Capability): string { const parts: string[] = []; for (const s of SURFACE_ORDER) { const level = c.access.surfaces[s]; if (level !== undefined) parts.push(`${s}: ${level}`); } return parts.length === 0 ? "—" : parts.join(", "); } /** The sorted, public capabilities the card projects (private capabilities are omitted by design). */ export function cardCapabilities(capabilities: readonly Capability[] = CAPABILITIES): Capability[] { return [...capabilities] .filter((c) => c.public) .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); } /** The fail-closed rejects, extracted from the catalog (syntax=rejected) — never dropped for brevity. */ export function rejects(capabilities: readonly Capability[] = CAPABILITIES): Capability[] { return cardCapabilities(capabilities).filter((c) => c.syntax.status === "rejected"); } // ── Deterministic JSON (stable key order: `kind` first, then alphabetical) ──────── function stableStringify(value: unknown, indent = 2): string { const pad = (n: number): string => " ".repeat(n * indent); const walk = (v: unknown, depth: number): string => { if (v === null || typeof v !== "object") return JSON.stringify(v); if (Array.isArray(v)) { if (v.length === 0) return "[]"; const items = v.map((x) => pad(depth + 1) + walk(x, depth + 1)); return `[\n${items.join(",\n")}\n${pad(depth)}]`; } const obj = v as Record<string, unknown>; const keys = Object.keys(obj).sort((a, b) => { if (a === "kind") return b === "kind" ? 0 : -1; if (b === "kind") return 1; return a < b ? -1 : a > b ? 1 : 0; }); if (keys.length === 0) return "{}"; const entries = keys.map((k) => `${pad(depth + 1)}${JSON.stringify(k)}: ${walk(obj[k], depth + 1)}`); return `{\n${entries.join(",\n")}\n${pad(depth)}}`; }; return walk(value, 0); } /** The non-negotiable doctrines the card must always carry (compactness never drops these). */ const NON_NEGOTIABLES: readonly { readonly rule: string; readonly detail: string }[] = [ { rule: "Fail closed", detail: "a parse escape stands down; an unknown series de-arms with a logged reason — never a silent default." }, { rule: "Never naked / bounded risk", detail: "a budget is a positive risk fraction (a type); a SELL is floored at intrinsic." }, { rule: "Marks lie", detail: "a quote is a health signal, never a value: no active EXIT conditions on the mark — mid/bid/ask/last or the human words mark/premium — and mid is never a price anchor." }, { rule: "Evidence honesty", detail: `the certified honest-tier cohort is ${CERTIFIED_COHORT}; a mechanical (practice) run is never labeled certified.` }, { rule: "Text is a projection", detail: "the typed object model is canonical; `print(parse(text))` is a byte-stable fixed point (ADR-0004)." }, ]; /** The fully-resolved, serializable card model — the single source both renderers consume. */ export interface CardModel { readonly languageVersion: string; readonly catalogHash: string; readonly capabilities: readonly Capability[]; readonly surfaces: readonly AccessSurface[]; readonly rejects: readonly Capability[]; readonly examples: readonly ObjectTextExample[]; } /** Build the resolved card model from the catalog + package version. Pure given its inputs. */ export function buildCardModel( capabilities: readonly Capability[] = CAPABILITIES, languageVersion: string = readLanguageVersion(), ): CardModel { return { languageVersion, catalogHash: catalogHash(capabilities), capabilities: cardCapabilities(capabilities), surfaces: exposedSurfaces(capabilities), rejects: rejects(capabilities), examples: OBJECT_TEXT_EXAMPLES, }; } // ── Renderers (pure functions of the model) ────────────────────────────────────── const cell = (s: string): string => s.replace(/\|/g, "\\|"); /** The four statement kinds (surfaces) of the language — a fixed pedagogical spine (CONTEXT.md). */ const STATEMENT_KINDS: readonly { readonly kind: string; readonly gloss: string }[] = [ { kind: "VIEW", gloss: "what should I see? — panes at a token budget, materialized as a Frame" }, { kind: "WAKE", gloss: "when should I look? — a standing subscription that spends attention, not risk" }, { kind: "PLAN", gloss: "what may execute? — a bounded-risk contingent program the runtime fires" }, { kind: "GRADE", gloss: "did it actually work? — an imperative replay reporting honest EV" }, ]; /** * Render the human Markdown card. Pure: byte-identical for a given model. Uses only tables and inline * / `text` / `json` code fences (never a bare ```kestrel fence) so it adds NO managed fence to the * gsh.1 corpus — its examples are governed by this card's own round-trip test instead. */ export function renderCardMarkdown(model: CardModel): string { const out: string[] = []; const L = (line = ""): void => void out.push(line); L("<!-- GENERATED FILE — do not edit by hand."); L(` Regenerate from the typed capability catalog with: ${REGEN_COMMAND}`); L(" Source of truth: tests/support/catalog.ts · renderer: tests/support/card.ts"); L(" A CI test (tests/docs.card.test.ts) asserts this file equals a fresh regeneration,"); L(" that it fits the declared token budget, and that every object⇄text pair round-trips. -->"); L(); L("# Kestrel agent language card"); L(); L( "A compact, receipt-backed reference for **authoring Kestrel** — one typed, token-efficient " + "language for agentic trading with four statement kinds over one lexical core. This card is a " + "**compiled projection of the capability catalog** (`tests/support/catalog.ts`), the same " + "four-axis source of truth the [status page](./status.md) derives from. It claims nothing the " + "catalog does not: every capability below is graded on syntax · runtime · evidence · access and " + "backed by a source (`src/`) and test (`tests/`) receipt.", ); L(); L(`- **Language version:** \`${model.languageVersion}\``); L(`- **Catalog hash:** \`sha256:${model.catalogHash.slice(0, 16)}\` (full hash in \`card.json\`) — a drifted catalog invalidates this card.`); L(`- **Token budget:** ≤ ${TOKEN_BUDGET} tokens under \`${TOKENIZER}\` (enforced in CI).`); L(`- **Certified honest-tier cohort:** ${CERTIFIED_COHORT} — every \`mechanical (practice)\` row below is a mechanically-graded practice run, never certified.`); L(); L("## Non-negotiables"); L(); L("Compactness never drops these — they are the doctrine the runtime enforces, not style."); L(); for (const n of NON_NEGOTIABLES) L(`- **${n.rule}.** ${n.detail}`); L(); L("## Statement kinds (surfaces)"); L(); L("| Kind | What it answers |"); L("| --- | --- |"); for (const s of STATEMENT_KINDS) L(`| \`${s.kind}\` | ${cell(s.gloss)} |`); L(); L("## Capabilities — four axes"); L(); L( "A feature can *parse* (syntax) yet have no *runtime*, no graded *evidence*, and only partial " + "*access*. These four facts are independent; a single \"supported\" flag would collapse them into " + "one flattering lie.", ); L(); L("| Capability | id | Syntax | Runtime | Evidence | Access |"); L("| --- | --- | --- | --- | --- | --- |"); for (const c of model.capabilities) { L( `| ${cell(c.title)} | \`${c.id}\` | ${SYNTAX_LABEL[c.syntax.status]} | ${RUNTIME_LABEL[c.runtime.status]} | ${EVIDENCE_LABEL[c.evidence.tier]} | ${cell(accessString(c))} |`, ); } L(); L(`**Access surfaces exposed:** ${model.surfaces.map((s) => `\`${s}\``).join(" · ")}.`); L(); L("## What each capability is"); L(); for (const c of model.capabilities) { L(`- **${cell(c.title)}** (\`${c.id}\`) — ${cell(c.summary)}`); } L(); L("## Fail-closed rejects"); L(); L("These are refused at parse time — the illegal statement never reaches the engine."); L(); L("| id | Doctrine |"); L("| --- | --- |"); for (const c of model.rejects) { const note = c.syntax.note ?? c.summary; L(`| \`${c.id}\` | ${cell(note)} |`); } L(); L("## Object ⇄ text (ADR-0004 round-trip)"); L(); L( "The typed AST object **is** the language; text is a byte-stable projection of it. For every pair " + "below `print(parse(text)) === text` (the card's test proves it). Objects are shown as JSON; the " + "full objects for all pairs are in `card.json`.", ); L(); for (const ex of model.examples) { const { object, printed } = roundTrip(ex); L(`### ${cell(ex.title)}`); L(); L(ex.note); L(); L("Canonical text:"); L(); L("```text"); L(printed); L("```"); L(); if (ex.showObject) { L("Object (parsed AST):"); L(); L("```json"); L(stableStringify(object)); L("```"); L(); } } L("## Provenance"); L(); L( "Generated from `tests/support/catalog.ts` by `tests/support/card.ts`. Regenerate with " + `\`${REGEN_COMMAND}\`. The committed card is asserted byte-identical to a fresh regeneration in ` + "`tests/docs.card.test.ts`; if you change the catalog, regenerate or CI fails.", ); L(); return out.join("\n") + "\n"; } /** * Render the machine card (card.json). Pure given `model` + the measured Markdown token count. * `markdownTokens` is passed in (not recomputed here) so the two files are generated from one * measurement and stay mutually consistent. */ export function renderCardJson(model: CardModel, markdownTokens: number): string { const doc = { $generated: { note: "GENERATED FILE — do not edit by hand.", regenerate: REGEN_COMMAND, sourceOfTruth: "tests/support/catalog.ts", renderer: "tests/support/card.ts", checkedBy: "tests/docs.card.test.ts", }, languageVersion: model.languageVersion, catalogHash: `sha256:${model.catalogHash}`, tokenizer: TOKENIZER, tokenBudget: TOKEN_BUDGET, markdownTokens, certifiedCohort: CERTIFIED_COHORT, evidenceSource: EVIDENCE_SOURCE, nonNegotiables: NON_NEGOTIABLES.map((n) => ({ rule: n.rule, detail: n.detail })), statementKinds: STATEMENT_KINDS.map((s) => ({ kind: s.kind, answers: s.gloss })), surfaces: model.surfaces, capabilities: model.capabilities.map((c) => ({ id: c.id, title: c.title, summary: c.summary, syntax: { status: c.syntax.status, ...(c.syntax.note ? { note: c.syntax.note } : {}) }, runtime: { status: c.runtime.status, ...(c.runtime.note ? { note: c.runtime.note } : {}) }, evidence: { tier: c.evidence.tier, ...(c.evidence.note ? { note: c.evidence.note } : {}) }, access: { surfaces: c.access.surfaces, ...(c.access.note ? { note: c.access.note } : {}), }, receipts: { source: [...c.receipts.source].sort(), test: [...c.receipts.test].sort() }, examples: [...c.examples].sort(), })), rejects: model.rejects.map((c) => ({ id: c.id, doctrine: c.syntax.note ?? c.summary })), objectText: model.examples.map((ex) => { const { object, printed } = roundTrip(ex); return { title: ex.title, note: ex.note, text: printed, object }; }), }; return stableStringify(doc) + "\n"; } /** Build both card files from the catalog + package version. The generator and tests share this. */ export function renderCard( capabilities: readonly Capability[] = CAPABILITIES, languageVersion: string = readLanguageVersion(), ): { markdown: string; json: string; markdownTokens: number; model: CardModel } { const model = buildCardModel(capabilities, languageVersion); const markdown = renderCardMarkdown(model); const markdownTokens = countTokens(markdown); const json = renderCardJson(model, markdownTokens); return { markdown, json, markdownTokens, model }; }