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.

181 lines (166 loc) 8.85 kB
/** * # cli/commands/frame — `frame`/`percept` (LIGHT-ish: src/frame only, no bun/chdb) * * Routes the percept through the selected {@link ExecutionBackend}: the LocalBackend renders a Frame * from a **fixture/snapshot JSON** matching `BriefingInput` or `WakeDeltaInput` (`src/frame/types.ts`) * — a struct file (a path, or `-`/omitted = stdin), NOT derived from bus state — while the * RemoteBackend fetches `GET /percepts`. Either way the mode-branch rendering below is unchanged, so * the local path stays byte-identical to before and node-runnable with no bun/chdb. `--kind` overrides * the briefing/wake inference (presence of `wakeIndex` ⇒ wake). * * ## View addressing (kestrel-wa0j.73.2) * `--view <name[:panes]>` selects the View the Frame renders under: a bare name resolves against the * shipped named-View registry ({@link FOUNDER_VIEWS}); `name:pane[ arg…][,pane…]` defines an inline * View in the language's own pane syntax (e.g. `--view "custom:delta,tape 5m"`). `--seat * <pm|strategist|watcher>` binds the acting pod seat, so a seat with a founder seed for the frame * kind reads its founder View; `--seat-views founder` is the explicit opt-in assertion of that same * behaviour (it requires `--seat`). Resolution precedence is the renderer's own (strict): explicit * `--view` > seat founder View > phase default. FAIL-CLOSED: an unknown View name, an unknown seat, a * malformed pane/arg token, or a pane the catalog refuses is a loud typed USAGE error (exit 2) — * never a silent fall-through to the phase defaults. * * Only `text`/`json`/`human` are backed in v1 — a request for any other format (e.g. `--format=html`) * is rejected loudly by the resolver/router, never silently downgraded. A gated remote percept * (402/Offer) is rendered as structured data and exits `5` (PAYMENT_REQUIRED). */ import type { OutputCtx } from "../context.ts"; import { parseArgs } from "../args.ts"; import { CliError, EXIT } from "../errors.ts"; import { colorizeFrame } from "../render/frame.ts"; import { renderOffer } from "../render/offer.ts"; import { FOUNDER_VIEWS } from "../../frame/founder-views.generated.ts"; import { SEAT_IDS, isSeatId, type SeatId } from "../../frame/seat.ts"; import type { PaneSelectionArg, ViewSelection } from "../../frame/pane-catalog.ts"; import type { ExecutionBackend, PerceptArgs, PerceptKind } from "../backend/index.ts"; function usageErr(message: string): CliError { return new CliError({ code: "USAGE", exit: EXIT.USAGE, message }); } /** View + pane names on the CLI surface: an identifier with `-`/`_` (the golden Views' shape). */ const NAME_RE = /^[A-Za-z][A-Za-z0-9_-]*$/; /** The language's time units (`src/lang/ast.ts` TimeUnit) — the window supersort's unit set. */ const WINDOW_RE = /^(\d+)(mo|[smhdwqy])$/; /** * One pane-arg token → its **syntactic supersort** (ADR-0041 §1: the five closed surface classes the * parser assigns with zero catalog knowledge): a window (`5m`), a bare numeral count (`12`), a * session ordinal (`d-1`), an expiry ordinal (`e-0`), or a bare ident (`vwap`). Anything else is a * MALFORMED token → loud typed refusal (never dropped, never coerced). Ordinal/expiry are matched * before ident on purpose — `d-1` is also ident-shaped. */ function parsePaneArg(token: string, whole: string): PaneSelectionArg { const win = WINDOW_RE.exec(token); if (win !== null) return { kind: "arg-window", window: { value: Number(win[1]), unit: win[2]! } }; if (/^\d+$/.test(token)) return { kind: "arg-count", count: Number(token) }; const ord = /^d-(\d+)$/.exec(token); if (ord !== null) return { kind: "arg-ordinal", ordinal: Number(ord[1]) }; const exp = /^e-(\d+)$/.exec(token); if (exp !== null) return { kind: "arg-expiry", expiry: Number(exp[1]) }; if (NAME_RE.test(token)) return { kind: "arg-ident", name: token }; throw usageErr( `malformed --view ${JSON.stringify(whole)}: pane arg ${JSON.stringify(token)} is not a window (5m), ` + `count (12), session ordinal (d-1), expiry ordinal (e-0), or ident (vwap)`, ); } /** One `pane[ arg…]` entry of an inline View — the language's own pane syntax, comma-separated. */ function parsePaneRef(entry: string, whole: string): ViewSelection["panes"][number] { const words = entry.trim().split(/\s+/).filter((w) => w !== ""); const name = words[0]; if (name === undefined) { throw usageErr(`malformed --view ${JSON.stringify(whole)}: empty pane entry (a trailing/doubled comma?)`); } if (!NAME_RE.test(name)) { throw usageErr(`malformed --view ${JSON.stringify(whole)}: bad pane name ${JSON.stringify(name)}`); } const args = words.slice(1).map((t) => parsePaneArg(t, whole)); return args.length > 0 ? { name, args } : { name }; } /** * `--view <name[:panes]>` → a {@link ViewSelection}, FAIL-CLOSED. A bare `name` must exist in the * shipped named-View registry ({@link FOUNDER_VIEWS}) or it REFUSES loudly, naming the known Views — * never a silent fall-through to the phase default. `name:pane[ arg…][,pane…]` builds an inline View * (pane VALIDITY against the catalog is the renderer's fail-closed job — resolveView refuses unknown * panes and args on arg-less panes; this parser refuses malformed surface syntax). */ function parseViewFlag(value: string): ViewSelection { const idx = value.indexOf(":"); if (idx === -1) { const named = FOUNDER_VIEWS[value]; if (named === undefined) { throw usageErr( `unknown View ${JSON.stringify(value)} — known named Views: ${Object.keys(FOUNDER_VIEWS).join(", ")}. ` + `An inline View is --view "<name>:<pane[ arg…][,pane…]>" (e.g. --view "custom:delta,tape 5m")`, ); } return named; } const name = value.slice(0, idx); const paneList = value.slice(idx + 1); if (!NAME_RE.test(name)) { throw usageErr(`malformed --view ${JSON.stringify(value)}: bad View name ${JSON.stringify(name)}`); } if (paneList.trim() === "") { throw usageErr(`malformed --view ${JSON.stringify(value)}: empty pane list after ":"`); } const panes = paneList.split(",").map((entry) => parsePaneRef(entry, value)); return { name, panes }; } /** `frame <input.json>` / `percept <input.json>` — render a Frame via the backend. */ export async function frameCommand( argv: readonly string[], ctx: OutputCtx, backend: ExecutionBackend, ): Promise<number> { const { positionals, flags } = parseArgs(argv, new Set(), new Set(["input", "kind", "view", "seat", "seat-views"])); const path = flags.get("input") ?? positionals[0]; const kindFlag = flags.get("kind"); if (kindFlag !== undefined && kindFlag !== "briefing" && kindFlag !== "wake") { throw usageErr(`--kind must be briefing|wake, got ${JSON.stringify(kindFlag)}`); } // --seat: the closed pod-seat roster (ADR-0041 §2 / A1) — an unknown seat REFUSES, never coerces. const seatFlag = flags.get("seat"); let seat: SeatId | undefined; if (seatFlag !== undefined) { if (!isSeatId(seatFlag)) { throw usageErr( `unknown seat ${JSON.stringify(seatFlag)} — the pod-seat roster is closed: ${SEAT_IDS.join("|")}`, ); } seat = seatFlag; } // --seat-views founder: the explicit founder-View opt-in assertion (mirrors AgentConfig.seatViews). // On the CLI, supplying --seat IS the opt-in; this flag only asserts it, so it requires --seat and // accepts only "founder" (any other value is a loud refusal, never a silent policy). const seatViewsFlag = flags.get("seat-views"); if (seatViewsFlag !== undefined) { if (seatViewsFlag !== "founder") { throw usageErr( `--seat-views must be "founder" (the explicit founder-View opt-in), got ${JSON.stringify(seatViewsFlag)}`, ); } if (seat === undefined) { throw usageErr(`--seat-views founder requires --seat <${SEAT_IDS.join("|")}> — a founder View is keyed by seat`); } } const viewFlag = flags.get("view"); const view = viewFlag !== undefined ? parseViewFlag(viewFlag) : undefined; const perceptArgs: PerceptArgs = { ...(path !== undefined ? { input: path } : {}), ...(kindFlag !== undefined ? { kind: kindFlag as PerceptKind } : {}), ...(view !== undefined ? { view } : {}), ...(seat !== undefined ? { seat } : {}), }; const g = await backend.getPercept(perceptArgs); if (g.gated) { renderOffer(g.payment, ctx); return EXIT.PAYMENT_REQUIRED; } const { kind, ascii } = g.value; if (ctx.mode === "json") { process.stdout.write(JSON.stringify({ schema: "kestrel.frame/v1", kind, ascii }) + "\n"); } else if (ctx.mode === "human") { process.stdout.write(colorizeFrame(ascii, ctx) + "\n"); } else { // text / agent / piped — the canonical agent Frame, verbatim. process.stdout.write(ascii + "\n"); } return 0; }