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.

167 lines (145 loc) 8.31 kB
/** * # cli/backend — the ExecutionBackend seam (ADR-0010, ADR-0004). * * ONE interface both a LocalBackend (in-process engine + registry) and a * RemoteBackend (HTTP client vs kestrel.markets/api) implement. The control * plane is typed with the OSS protocol types (src/protocol); the domain payload * each verb carries is opaque to the protocol and rides in the payload T of * Gated<T>. Imports are type-only (erased) → this module and both backends stay * node-light. Local is the DEFAULT; --api/KESTREL_API selects remote. */ import type { TrialCapability, Operation, GradeResult, CertifiedGrade, Blotter, } from "../../protocol/index.ts"; // type-only (erased at build) → no heavy runtime pulled: import type { EpisodeReport, FillModelName, TimeOfDay } from "../heavy.ts"; import type { WakeRecord } from "../../session/day.ts"; import type { EpisodeSigmoidParams } from "../../fill/index.ts"; import type { KestrelNode } from "../../lang/index.ts"; import type { ViewSelection } from "../../frame/pane-catalog.ts"; import type { SeatId } from "../../frame/seat.ts"; import type { WireOfferResponse } from "./wire.ts"; /** A control-plane result that may be gated behind a 402/Offer. A LocalBackend * never gates (`gated:false` always). Callers switch on `.gated`. The gated * payload is the platform's STRUCTURED 402 body ({@link WireOfferResponse}, * snake_case + {@link ../backend/wire.ts Money}) surfaced as DATA (ADR-0004), not * an exception and never a browser redirect. */ export type Gated<T> = | { readonly gated: false; readonly value: T } | { readonly gated: true; readonly payment: WireOfferResponse }; /* ---- domain payloads (opaque to the protocol; what renderers/registry read) ---- */ export type PerceptKind = "briefing" | "wake"; export interface PerceptArgs { /** Fixture path or `-`/undefined = stdin (local face); catalog selector (remote face). */ readonly input?: string; readonly kind?: PerceptKind; /** Raw JSON already read (local face short-circuit); backend may ignore. */ readonly raw?: string; /** Explicit View to render under (kestrel-wa0j.73.2, local face). Resolution is * {@link ../../frame/pane-catalog.ts resolveView}'s strict precedence: explicit View > * seat founder View > phase default — so when present, `seat` is never consulted. * Unknown pane ids / args on arg-less panes REFUSE loudly (never a silent fall-through). */ readonly view?: ViewSelection; /** Acting pod seat (ADR-0041 §2 / A1). When no `view` is given, a seat with a founder * seed for the frame kind reads its founder View; a seat without one falls through to * the phase default (honest absence). Absent ⇒ byte-identical to before this axis. */ readonly seat?: SeatId; } /** The rendered percept frame (protocol has no Frame type; this is the domain body). */ export interface PerceptFrame { readonly kind: PerceptKind; readonly ascii: string; } export interface PlanResult { readonly ok: boolean; /** Byte-stable canonical text the registry hashes (`print`-joined). */ readonly canonicalText: string; readonly documents: readonly KestrelNode[]; readonly diagnostics: readonly string[]; /** Control-plane identity of the submit. Absent on the remote face: `POST /validate` * is pure and returns no Operation (a validated `artifact_id` is the only handle). */ readonly operation?: Operation; } export interface SessionArgs { readonly busPath?: string; readonly documents: readonly KestrelNode[]; readonly fillModel: FillModelName; readonly rUsd: number; readonly fairTauYears?: (now: number) => number | null; readonly makerFairParams?: EpisodeSigmoidParams; readonly out?: string; } export interface DayArgs extends SessionArgs { readonly dir: string; readonly wakes?: readonly TimeOfDay[]; readonly structural?: boolean; readonly maxWaitSec?: number; /** `--await-author`: an external author WILL reply out-of-band — block up to `maxWaitSec` at each * handshake, then abandon loudly (kestrel-4fc9). Supplying `maxWaitSec` implies it. */ readonly awaitAuthor?: boolean; /** `--no-author`: no external author will reply — the handshake documents are pre-staged in `dir` * and a miss is a prompt typed refusal instead of a `maxWaitSec` ride (kestrel-4fc9). Redundant with * the shipped default (which also refuses promptly); kept as an explicit assertion. */ readonly noAuthor?: boolean; } /** What `run`/`day` GUARANTEE back on either face: the EpisodeReport (domain) + the * authored text + the wake records. This is the {@link ExecutionBackend} seam's promise — * it names NO control-plane receipts, so a caller holding the interface can never read a * `blotter`/`operation` that a LocalBackend does not produce. The remote-only certified * fields live on {@link RemoteRunResult}; a caller must hold the concrete RemoteBackend * (or narrow) to reach them (arch-review A9). */ export interface SessionRunResult { readonly report: EpisodeReport; // domain payload → renderer + registry readonly plansText: string; // for recordGraded (day concatenates authored text) readonly wakes: readonly WakeRecord[]; // day only; [] for run } /** The LOCAL face's result: exactly the seam guarantee, nothing certified. The in-process * engine mints no server {@link Operation} and no signed {@link Blotter}, so this is the * bare {@link SessionRunResult} — the type states precisely what local promises. */ export type LocalRunResult = SessionRunResult; /** The REMOTE face's result: the seam guarantee PLUS the control-plane receipts the platform * certifies — the server {@link Operation} identity and the certified execution {@link Blotter}. * BOTH are required (not `?`): the type asserts that a remote run always carries them, and the * RemoteBackend fails closed if the stream omits either. Reachable only through a concrete * RemoteBackend-typed handle (arch-review A9). */ export interface RemoteRunResult extends SessionRunResult { readonly operation: Operation; // server identity of the run readonly blotter: Blotter; // certified execution record } export interface GradeArgs { readonly fillModel: FillModelName; readonly corpus?: readonly Blotter[]; // additional subject blotters readonly certified?: boolean; // request the signed CertifiedGrade form } /** Scopes the CLI opens a session under. `sim` = the free deterministic path (trial-covered); * `paper` is beyond trial → remote returns a 402/Offer. */ export type SessionScope = "sim" | "paper"; export interface ExecutionBackend { readonly kind: "local" | "remote"; /** Anonymous, rate-limited trial capability (ADR-0002). Local returns a synthetic * always-valid trial cap; remote mints one via `POST /capabilities/trial`. * Idempotent (cached after first call). */ bootstrapCapability(): Promise<TrialCapability>; /** The View/briefing frame. Local renders from a fixture. NOTE (kestrel-5rb): * the M1 platform contract has NO percept/View route, so the RemoteBackend FAILS * CLOSED here rather than fabricate one — a remote View projection is an owner * decision (local-only face vs a new platform op). */ getPercept(args: PerceptArgs): Promise<Gated<PerceptFrame>>; /** `POST /validate` — the real deterministic parse/validate, fail-closed. Pure: * the contract returns a ValidateResult, NO Operation (remote leaves * {@link PlanResult.operation} undefined). */ submitPlan(documents: readonly KestrelNode[]): Promise<Gated<PlanResult>>; /** `POST /sim` — create an Operation and run the deterministic Session (the report * arrives over the operation's SSE stream). May `402` (paid dataset boundary). */ openSession(scope: SessionScope, args: SessionArgs): Promise<Gated<SessionRunResult>>; /** `POST /sim` (stepped/wake variant). */ openDaySession(scope: SessionScope, args: DayArgs): Promise<Gated<SessionRunResult>>; /** `POST /grade` — create an Operation grading one or more Blotter artifacts; the * certified GradeResult arrives over the operation's SSE stream. (Local derives a * portable GradeResult from a EpisodeReport.) */ grade(subject: Blotter | EpisodeReport, opts: GradeArgs): Promise<Gated<GradeResult | CertifiedGrade>>; }