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