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