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.
162 lines • 12 kB
TypeScript
/**
* # sdk/op-schema — the ONE self-description of the agent JSONL protocol (kestrel-yuw4)
*
* The `kestrel agent` JSONL face lists its ops (`kestrel agent --help`, and the `unknown-op` refusal, #115)
* but historically documented NONE of the per-op ARGUMENT OBJECT shapes — the `{ kind: … }` unions a
* `subject` / `advance.response` must carry, and the exact accepted field NAMES (`document`, not
* `text`/`bytes`/`source`/`content`). A cold integrator had to brute-force ~14 trial-and-error guesses to
* discover them. This module is the SINGLE, machine-readable catalogue of those shapes:
*
* - {@link AGENT_OP_SCHEMAS} — one {@link AgentOpSchema} per op (its args + response shape), the source the
* `describe` op emits VERBATIM on the wire, the source `kestrel agent --help` renders, and the source the
* MCP face's argument descriptions read. ONE definition; the cross-face drift guards
* (tests/cli.agent-describe.test.ts, tests/mcp.projection.test.ts) pin that nothing forks from it.
* - The shape VOCABULARY constants ({@link SUBJECT_SHAPE} et al.) — referenced by BOTH the catalogue entries
* AND the argument VALIDATORS below, so a refusal that names the expected shape can never drift from the
* shape the catalogue advertises (they read the same string).
* - The argument VALIDATORS ({@link asSessionRef} / {@link asAuthoredResponse} / {@link asBoundResponse} /
* {@link asEventCursor}) — a malformed argument object is a fail-closed {@link ArgShapeError} whose message
* NAMES the expected `{ kind: … }` vocabulary. STRUCTURE only: a well-formed-but-unresolvable VALUE (an
* unknown catalog id, an unsupported subject kind) is deliberately passed through to the SDK, whose typed
* DOMAIN refusal answers it — the transport/argument-error class must never absorb a Kestrel refusal.
*
* ZERO-WEIGHT: pure data + pure functions over type-only imports (erased at build). No runtime-semantics
* import, so the light agent/SDK projection stays light (pinned by tests/package.boundary.test.ts).
*/
import type { AuthoredResponse, BoundResponse, SessionRef } from "./types.ts";
import type { EventCursor } from "../protocol/index.ts";
/** The `openSession.subject` shape — a discriminated {@link SessionRef} union (a STRING is rejected). */
export declare const SUBJECT_SHAPE = "{ \"kind\": \"catalog\", \"id\": string } | { \"kind\": \"dataset\", \"source\": string, \"dataset\": string }";
/**
* The `advance.response` / `revise.response` shape — an {@link AuthoredResponse} union. `document` is the
* ONLY accepted authored-body field name (NOT `text` / `bytes` / `source` / `content`), the exact fact a cold
* integrator could not discover from any published surface (kestrel-yuw4).
*/
export declare const AUTHORED_RESPONSE_SHAPE = "{ \"kind\": \"authored\", \"document\": string } | { \"kind\": \"pass\" } | { \"kind\": \"stand-down\", \"reason\": string }";
/**
* The CLOSED `AuthoredResponse.kind` union (kestrel-lzz7). UNLIKE `subject.kind` — an open discovery union
* whose unknown values the SDK answers with an `unsupported-subject` DOMAIN refusal — the authored-response
* kind is a fixed protocol enum with NO domain answer for an unknown value: a `frobnicate` kind reaches the
* controller's exhaustive `responseToBody` switch, matches no case, and dereferences `body.kind` on the
* resulting `undefined` (a raw `TypeError` surfaced as `code:"error"`). An unknown value is therefore a
* malformed ARGUMENT, caught at the boundary as `bad-arg`, not passed through.
*/
export declare const AUTHORED_RESPONSE_KINDS: readonly ["authored", "pass", "stand-down"];
/** The `submit.response` shape — a fully-bound {@link BoundResponse} (the full pentad header + a TurnBody). */
export declare const BOUND_RESPONSE_SHAPE = "{ \"sessionId\": string, \"ordinal\": number, \"parentHash\": string, \"frameRoot\": string, \"body\": TurnBody }";
/**
* The CLOSED {@link import("../protocol/session.ts").TurnBody}`.kind` union (kestrel-lzz7). A `submit`
* response whose `body` is absent or carries an unknown `kind` reaches the controller's exhaustive
* `reconcileTurn` switch, matches no case, and dereferences `body.kind` on `undefined` — the same raw
* `code:"error"` leak as an unknown authored kind. A missing/unknown body kind is a malformed argument.
*/
export declare const TURN_BODY_KINDS: readonly ["authored", "stand-down", "failure"];
/** The optional `resume.after` / `resumeOperation.after` resume-cursor shape — an {@link EventCursor}. */
export declare const EVENT_CURSOR_SHAPE = "{ \"sequence\": number, \"token\": string }";
/** The `validate.document` shape — the Kestrel plan document source text (REQUIRED). */
export declare const VALIDATE_DOCUMENT_SHAPE = "the Kestrel plan document (source text)";
/** The `grade.blotters` shape — a REQUIRED non-empty array of Blotter artifact ids. */
export declare const BLOTTERS_SHAPE = "string[] \u2014 Blotter artifact ids (a Blotter's sessionId is its id)";
/** The `artifact.ref` shape — a REQUIRED content-addressed artifact reference. */
export declare const ARTIFACT_REF_SHAPE = "a content-addressed artifact ref";
/** The `resumeOperation.operationId` shape — the REQUIRED durable Operation id. */
export declare const OPERATION_ID_SHAPE = "the durable Operation id";
/**
* The `catalog` op's response ROW shape — a {@link import("../protocol/catalog.ts").CatalogListing} (kestrel-dnxq).
* The catalog op UNDER-specified its response before dnxq (`"CatalogEntry[] — each carries the id"`, no field
* schema), so two divergent shapes both satisfied it; this pins the exact canonical fields BOTH transports emit
* byte-identically. The reproducibility pins (content roots / engine versions / View identities) are NOT part of
* discovery — they resolve at `openSession`/grade.
*/
export declare const CATALOG_LISTING_SHAPE = "{ \"id\": string, \"title\": string, \"description\": string, \"instrument\": string, \"period\": { \"start\": string, \"end\": string }, \"frameCount\": number, \"free\": boolean }";
/** One argument of an agent op: its wire field name, whether it is required, its JSON type, and — the point
* of this whole module — the exact `{ kind: … }` object SHAPE it must carry. */
export interface AgentOpArg {
readonly name: string;
readonly required: boolean;
readonly type: "string" | "object" | "array";
/** The exact accepted shape, in the `{ kind: … }` vocabulary — what a cold integrator needs to stop guessing. */
readonly shape: string;
}
/** The self-description of one agent op: its args and its response shape, plus whether it needs a live Session. */
export interface AgentOpSchema {
/** The `op` field a request line carries (`{ "op": <name>, …args }`). */
readonly op: string;
/** `true` ⇒ the op requires a prior `openSession` in the same stream (else it refuses `no-session`). */
readonly session: boolean;
/** A one-line human summary of what the op does. */
readonly summary: string;
/** The op's argument fields (empty for the session-free discovery ops). */
readonly args: readonly AgentOpArg[];
/** A one-line description of the protocol payload the op emits on the `value` channel. */
readonly response: string;
}
/**
* THE single ordered catalogue of the agent protocol's ops — the source `describe` emits, `--help` renders,
* and the MCP argument descriptions read. `describe` leads (the self-description entry point); the order is
* the handler/dispatch order and is the order {@link agentOpNames} (and thus `AGENT_OPS`) preserves.
*/
export declare const AGENT_OP_SCHEMAS: readonly AgentOpSchema[];
/** The ordered op names — derived from {@link AGENT_OP_SCHEMAS} so the advertised set can never fork from the
* described set. `AGENT_OPS` (src/cli/commands/agent.ts) is this list; the switch dispatches exactly it. */
export declare const agentOpNames: readonly string[];
/** The `describe` op's on-the-wire payload — the whole catalogue, tagged with the protocol version. */
export interface AgentDescribe {
readonly protocol: string;
readonly ops: readonly AgentOpSchema[];
}
/** Build the `describe` payload for a given protocol version tag (the agent face passes its own version). */
export declare function agentDescribe(protocolVersion: string): AgentDescribe;
/**
* A fail-closed ARGUMENT-SHAPE refusal: a malformed argument OBJECT whose message NAMES the expected shape.
* Carries a stable `code` (`bad-arg`) an agent matches on, and the offending `field`. DISTINCT from a Kestrel
* DOMAIN refusal (an unresolvable but well-formed value) — validators throw this only for STRUCTURE errors.
*/
export declare class ArgShapeError extends Error {
readonly name = "ArgShapeError";
readonly code = "bad-arg";
readonly field: string;
constructor(field: string, expected: string, got: unknown);
}
/**
* VALIDATE an `openSession.subject` — an OBJECT with a string `kind` and the fields its known kind requires
* (`catalog` → `id`; `dataset` → `source` + `dataset`). An UNKNOWN kind value is passed through UNCHANGED to
* the SDK (its `unsupported-subject` domain refusal answers it) — only STRUCTURE is enforced here.
*/
export declare function asSessionRef(v: unknown, field?: string): SessionRef;
/**
* VALIDATE an `advance.response` / `revise.response` — an {@link AuthoredResponse} union. Enforces the exact
* accepted authored-body field name (`document`, a string) — the fact a cold integrator could not discover.
* `pass` needs no field; `stand-down` needs a string `reason`. An UNKNOWN kind is passed through to the SDK.
*/
export declare function asAuthoredResponse(v: unknown, field?: string): AuthoredResponse;
/**
* VALIDATE a `submit.response` — a fully-bound {@link BoundResponse} OBJECT carrying a well-formed
* {@link import("../protocol/session.ts").TurnBody} `body`. Field-level RECONCILIATION (a sessionId / ordinal
* / parentHash mismatch against the recorded slot) stays the Session's DOMAIN job; only the STRUCTURE is
* enforced here — including that `body` is present with a known `kind` (kestrel-lzz7), because a missing or
* unknown body kind reaches the exhaustive `reconcileTurn` switch and dereferences `body.kind` on `undefined`.
*/
export declare function asBoundResponse(v: unknown, field?: string): BoundResponse;
/**
* VALIDATE a REQUIRED string arg — `artifact.ref`, `resumeOperation.operationId`, `validate.document`
* (kestrel-8h9f). An ABSENT value (`undefined`, or an empty `""` string) is a malformed ARGUMENT: the
* `describe` catalogue marks the field `required: true`, and forwarding the absent value as `""` made the
* server answer a MISDIRECTING `not-found` (404) / `422` instead of naming the omitted field. Refuse it
* client-side as `bad-arg` naming the described shape, matching `openSession.subject` / `resume.after`.
*/
export declare function asRequiredString(v: unknown, field: string, shape: string): string;
/**
* VALIDATE `grade.blotters` — a REQUIRED non-empty array of Blotter id strings (kestrel-8h9f). An absent /
* empty / non-string-element array is a malformed argument refused `bad-arg` naming the shape, rather than the
* server-side `empty-grade` domain refusal (inconsistent validation locus with the client-side arg checks).
*/
export declare function asBlotters(v: unknown, field?: string): readonly string[];
/**
* VALIDATE an optional resume `after` cursor. ABSENT (`undefined`) is fine (resume from the start); a PRESENT
* one must be a well-formed {@link EventCursor} `{ sequence:number, token:string }`. A malformed present
* cursor is a bad ARGUMENT SHAPE (never silently ignored). STRICT: `null` is a present, wrong-typed value and
* fails closed (deliberately NOT coalesced to undefined).
*/
export declare function asEventCursor(v: unknown, field?: string): EventCursor | undefined;
//# sourceMappingURL=op-schema.d.ts.map