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.

182 lines (168 loc) 7.96 kB
/** * # cli/backend/local — the DEFAULT backend: in-process engine + light render/lang. * * openSession/openDaySession/grade dynamic-import the heavy barrel through loadHeavy * (chdb/bun:sqlite stay lazy, exit-4 under bun-less node). getPercept/submitPlan use * ONLY the light frame/lang code — no heavy import on those paths. Behavior is * byte-identical to today's grade.ts/frame.ts. */ import { readFileSync, statSync } from "node:fs"; import { type KestrelNode } from "../../lang/index.ts"; import { canonicalPlansText } from "../../canonical/plans.ts"; import { renderBriefing, renderWakeDelta, type RenderOptions } from "../../frame/render.ts"; import { KernelHonestyError } from "../../frame/types.ts"; import type { BriefingInput, WakeDeltaInput } from "../../frame/index.ts"; import type { GradeResult, CertifiedGrade, Blotter, TrialCapability } from "../../protocol/index.ts"; import type { EpisodeReport } from "../heavy.ts"; import { CliError, EXIT } from "../errors.ts"; import { loadHeavy } from "../commands/_heavy.ts"; import type { ExecutionBackend, Gated, PerceptArgs, PerceptFrame, PerceptKind, PlanResult, SessionArgs, DayArgs, LocalRunResult, GradeArgs, SessionScope, } from "./index.ts"; const ok = <T>(value: T): Gated<T> => ({ gated: false, value }); export class LocalBackend implements ExecutionBackend { readonly kind = "local" as const; async bootstrapCapability(): Promise<TrialCapability> { // Local execution is unmetered: a synthetic always-valid trial cap over the full // wallet-signable scope set. No network. Deterministic id so tests are stable. return { kind: "trial", capabilityId: "local-trial", scopes: ["data", "sim", "grade", "paper"], expiry: "9999-12-31T00:00:00Z", subjectCommitment: "local", rateLimit: { limit: Number.MAX_SAFE_INTEGER, windowSeconds: 1 }, }; } async getPercept(args: PerceptArgs): Promise<Gated<PerceptFrame>> { // Identical to today's frameCommand fixture logic, minus the mode-branch render // (render stays in the command handler). const raw = args.raw ?? readInput(args.input); let input: unknown; try { input = JSON.parse(raw); } catch (e) { throw usage(`frame input is not valid JSON: ${msg(e)}`); } if (typeof input !== "object" || input === null) throw usage("frame input must be a JSON object matching BriefingInput or WakeDeltaInput"); const kind: PerceptKind = args.kind ?? ("wakeIndex" in input ? "wake" : "briefing"); // The ONE View-addressing seam (kestrel-wa0j.73.2): thread the caller's {view, seat} into the // renderer's RenderOptions. Absent ⇒ {} ⇒ resolveView's phase default, byte-identical to before. const renderOpts: RenderOptions = { ...(args.view !== undefined ? { view: args.view } : {}), ...(args.seat !== undefined ? { seat: args.seat } : {}), }; let ascii: string; try { ascii = kind === "wake" ? renderWakeDelta(input as WakeDeltaInput, renderOpts) : renderBriefing(input as BriefingInput, renderOpts); } catch (e) { // A View/seat was requested and the renderer REFUSED it (unknown pane, args on an arg-less // pane, over budget, reserved kernel id): surface it as the View refusal it is — never the // misleading "input shape" message, and never a silent fall-through to phase defaults. if ((args.view !== undefined || args.seat !== undefined) && e instanceof KernelHonestyError) { throw usage(`frame View refused: ${msg(e)}`); } throw usage(`frame render failed — input does not match ${kind} shape: ${msg(e)}`); } return ok({ kind, ascii }); } async submitPlan(documents: readonly KestrelNode[]): Promise<Gated<PlanResult>> { // Local parse already succeeded (caller parsed). Canonicalize + return. const canonicalText = canonicalPlansText(documents); return ok({ ok: true, canonicalText, documents, diagnostics: [] }); } async openSession(_scope: SessionScope, args: SessionArgs): Promise<Gated<LocalRunResult>> { assertBusReadable(args.busPath); const sim = await loadHeavy(() => import("../heavy.ts")); const report = sim.runSimSession({ documents: args.documents, fillModel: args.fillModel, rUsd: args.rUsd, ...(args.busPath !== undefined ? { busPath: args.busPath } : {}), ...(args.fairTauYears !== undefined ? { fairTauYears: args.fairTauYears } : {}), ...(args.makerFairParams !== undefined ? { makerFairParams: args.makerFairParams } : {}), ...(args.out !== undefined ? { out: args.out } : {}), }); return ok({ report, plansText: canonicalPlansText(args.documents), wakes: [] }); } async openDaySession(_scope: SessionScope, args: DayArgs): Promise<Gated<LocalRunResult>> { assertBusReadable(args.busPath); const day = await loadHeavy(() => import("../heavy.ts")); // NOTE: runDaySession reads the day's plan document from the handshake dir // (`plans-0.kestrel`), NOT from `args.documents` — the stepped path authors in-band. const { report, plansText, wakes } = await day.runDaySession({ dir: args.dir, fillModel: args.fillModel, rUsd: args.rUsd, wakes: args.wakes ?? [], structural: args.structural ?? false, ...(args.busPath !== undefined ? { busPath: args.busPath } : {}), ...(args.fairTauYears !== undefined ? { fairTauYears: args.fairTauYears } : {}), ...(args.makerFairParams !== undefined ? { makerFairParams: args.makerFairParams } : {}), ...(args.maxWaitSec !== undefined ? { maxWaitSec: args.maxWaitSec } : {}), ...(args.awaitAuthor === true ? { awaitAuthor: true } : {}), ...(args.noAuthor === true ? { noAuthor: true } : {}), ...(args.out !== undefined ? { out: args.out } : {}), }); return ok({ report, plansText, wakes }); } async grade(subject: Blotter | EpisodeReport, _opts: GradeArgs): Promise<Gated<GradeResult | CertifiedGrade>> { // v1: derive a portable GradeResult from a EpisodeReport's totals (self-hosted numbers, // per protocol GradeResult doc). No signed receipt locally. const r = subject as EpisodeReport; const result: GradeResult = { subjectSessionId: r.session.determinism_hash, metrics: { realized_floor_usd: r.totals.realized_floor_usd, expected_usd: r.totals.expected_usd, premium_spent: r.totals.premium_spent, bus_events: r.session.bus_events, }, }; return ok(result); } } function readInput(pathOrDash: string | undefined): string { const src = pathOrDash === undefined || pathOrDash === "-" ? 0 : pathOrDash; try { return readFileSync(src, "utf8"); } catch (e) { throw new CliError({ code: "NOT_FOUND", exit: EXIT.NOT_FOUND, message: `cannot read frame input ${JSON.stringify(pathOrDash ?? "-")}: ${msg(e)}`, }); } } /** * Fail closed on an absent `--bus` file BEFORE the heavy runner reads it. Without this, the ENOENT * from `readBus`'s `readFileSync` bubbles up as a bare `Error` and lands in GENERIC/exit 1 — the * taxonomy's "unexpected/uncaught" bucket — when the CLI's own table reserves NOT_FOUND/exit 3 for * "an input file is absent" (kestrel-96rw). Content buses (RUNTIME's "path or string": a JSONL bus * begins with `{`) are not paths and are left for {@link readBus} to parse. */ function assertBusReadable(busPath: string | undefined): void { if (busPath === undefined || busPath.trimStart().startsWith("{")) return; try { statSync(busPath); } catch (e) { throw new CliError({ code: "NOT_FOUND", exit: EXIT.NOT_FOUND, message: `cannot read bus ${JSON.stringify(busPath)}: ${msg(e)}`, }); } } const usage = (m: string): CliError => new CliError({ code: "USAGE", exit: EXIT.USAGE, message: m }); const msg = (e: unknown): string => (e instanceof Error ? e.message : String(e));