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.

252 lines 18.6 kB
/** * # session/harness/prompt — the prompt profile + fail-closed turn parser (provider-free) * * The PURE half of the BYOK live harness (kestrel-rul, ADR-0013): it turns a frozen, date-blind * {@link BriefingInput}/{@link ActingFrame} into the model prompt, and turns the model's raw reply * back into a validated {@link AgentTurn} — with NO provider SDK import, NO wall clock, and NO * network. `{@link liveAgent}` composes these two functions around an injected {@link LlmClient}; * the concrete AI-SDK client is the only file that touches the wire. * * ## Fail-closed by construction (ADR-0013 (a), m9i) * The reply is parsed into an {@link AgentTurn}, never silently repaired. The three m9i outcomes * are kept DISTINCT and never collapsed: an *explicit* `standDown` the model authored, an *invalid * output* (unparseable / malformed), and a *provider failure* (raised by the client). An invalid * output is turned into a **legitimate pass** (`actions: []`) carrying a JOURNAL note naming the * failure — never a fabricated `standDown` (which would conflate "the agent chose to de-arm" with * "the harness could not read the reply"), and never a crash. The standing book keeps riding its * own TP/EXIT and the platform staleness backstop, so a pass is fail-closed-enough while staying * honest about what happened. The `standDown` action described in the prompt copy below is the ONE * agent-path de-arm — its discriminant, predicates, and turn factory live in `src/engine/disarm.ts` * (kestrel-z473.5), the single home for the de-arm rule. * * ## Scrub at capture time (ADR-0013 (e)) * {@link scrubSecrets} is the credential fence the AI-SDK client runs over every byte of wire * evidence BEFORE it hits any artifact — never a credential in a committed record (an m9i * acceptance criterion, met structurally). It lives here (provider-free) so the scrub can be * tested with no network and no provider import. */ import type { AgentTurn, Action, AgentConfig, AuthoringReply } from "../agent.ts"; import type { BriefingInput, Kernel, WakeDeltaInput } from "../../frame/types.ts"; import { type ViewSelection } from "../../frame/pane-catalog.ts"; import type { SeatId } from "../../frame/seat.ts"; /** * The **baseline prompt profile** (ADR-0013 (d)): the system bytes held constant across every model * in the controlled division. It teaches the {@link AgentTurn} contract — the reply is ONE JSON * object `{ journal?, actions[] }`, each action a tagged union member, and the `supersede` document * is a Kestrel surface statement. MUST be byte-stable: {@link liveAgent} hashes it into the config's * `promptHash` (`AuthorPolicy.prompt_sha256`), so any edit mints a new grid column. No wall clock, * no per-run interpolation. */ export declare const BASELINE_SYSTEM_PROMPT: string; /** The stable identity of this profile (a label, not the hash). */ export declare const BASELINE_PROMPT_PROFILE = "baseline-v0"; /** * The worked Kestrel documents the authoring profile teaches — each is CANONICAL, PARSEABLE source. * Exported so a determinism test can run EACH through the real {@link parse} (the prompt can never * teach invalid syntax) and assert the prompt embeds each verbatim. Keep these BYTE-STABLE: they are * folded into {@link AUTHORING_SYSTEM_PROMPT}, whose sha256 is the profile's `prompt_sha256`. */ export declare const AUTHORING_EXAMPLE_DOCUMENTS: { /** Options rider: the multi-line spine — header on line 1, then EACH clause on its OWN indented * line. `WHEN` and `DO` are NEVER on the same line (that collapse is the #1 dry-run-1 failure). * A deliberately generic warm-up shape, not any benchmark cell's setup. */ readonly optionsRider: string; /** Equity-leg plan (ADR-0017): `USING exec <SYM>` + `DO buy N shares` — a spot leg has NO strike * or right. Shown so a model on a chain-less SPOT tape reaches for the equity grammar instead of * standing down. A generic FADE shape (price COMES to you): the buy rests at `@ mid` BELOW the * cross and fills on the pullback — the correct pricing when you fade a cross toward VWAP (NOT the * EQ cell's breakout thesis — teach grammar, not the setup). */ readonly equityReversion: string; /** Equity-leg plan (ADR-0017), the CHASE mirror of {@link equityReversion}: a breakout you must * LIFT THE OFFER to catch. Because a resting BUY fills only when the ask crosses strictly BELOW it, * a continuation UP never fills a `@ mid` buy (the ask climbs away) — so the buy prices `@ ask+2c`, * STRICTLY THROUGH the ask, to cross now. A generic breakout shape (teach the pricing, not the * setup). */ readonly equityBreakout: string; /** Level-break entry with a STATE trigger (ADR-0017 equity leg). The trap this teaches against * (kestrel-4pm): `WHEN spot crosses below X` is an EDGE — it fires ONLY on a fresh crossing (spot * must be ABOVE X at arm, then fall through). Armed when spot is ALREADY below X it can NEVER fire * (no transition to observe), so the plan sits armed forever and never enters. When the level is * already broken, use the STATE trigger `WHEN spot < X` (fires whenever the condition HOLDS, * including already-true at arm). A generic already-through shape — teach the trigger, not the trade. */ readonly levelBreakState: string; /** A VIEW request: a name (+ optional `budget`), then one indented pane line each (a pane is a * catalogued pane id, optionally followed by args the pane understands). Every pane + arg here is one * the catalog actually SERVES against a product-shaped frame (kestrel-wa0j.19 §6 — the CI fence * `harness.prompt-profile` resolves + materializes this View so a taught example the arg gate would * refuse can never ship again): `tape 5m` re-buckets the served 1-minute tape to 5-minute candles * (the ONE window arg the tape pane takes); `chain` + `levels` are plain, arg-less panes. */ readonly view: string; }; /** Every worked document, in the order the prompt presents them — the corpus the profile test parses. */ export declare const AUTHORING_EXAMPLE_DOCUMENT_LIST: readonly string[]; /** * The **authoring-taught prompt profile** (kestrel-rul followup) — the corrected baseline for the * controlled division and the profile every request-loop teaching extends. Same {@link AgentTurn} * contract as `baseline-v0`, but it TEACHES the Kestrel authoring grammar by worked, copyable * example. It is example-driven ON PURPOSE: a live A/B showed a compact grammar-spec-only variant * regressed authoring (opus 0%→15% invalid; equity engagement lost), so the worked JSON turns stay — * but the examples are deliberately GENERIC shapes (a warm-up call, a VWAP reversion), NOT any * benchmark cell's setup, so this teaches SYNTAX, not the trade. MUST be byte-stable: {@link liveAgent} * hashes it into the config's `promptHash`, so any edit mints a new grid column. No wall clock. */ export declare const AUTHORING_SYSTEM_PROMPT: string; /** The stable identity of the authoring-taught profile (a label, not the hash). */ export declare const AUTHORING_PROMPT_PROFILE = "authoring-v1"; /** * The **viewshop-v1 prompt profile** (ADR-0029 §4). Byte-stable GIVEN a fixed catalog; {@link liveAgent} * hashes it into `promptHash`, so it is a distinct grid column from `authoring-v1`. No wall clock, no * per-run interpolation (the pane menu is a pure function of the compile-time catalog). */ export declare const VIEWSHOP_SYSTEM_PROMPT: string; /** The stable identity of the viewshop profile (a label, not the hash). */ export declare const VIEWSHOP_PROMPT_PROFILE = "viewshop-v1"; /** * The **manage-v1 prompt profile** (ADR-0032 §5, owner watcher-v1). EXTENDS the DSL-taught baseline * ({@link AUTHORING_SYSTEM_PROMPT} — so the watcher knows the exact order/price grammar it manages * with) and OVERLAYS the manage-only doctrine: manage WITHIN the strategist's armed Plan and budget * (reload/exit/adjust/size, place/cancel orders inside the envelope), never arm NEW authority * (`supersede`), and ESCALATE — do not guess — when the regime breaks. Byte-stable; {@link liveAgent} * hashes it into `promptHash`, so it is a distinct grid column from `authoring-v1`/`viewshop-v1`. The * escalation sentinel below is the token {@link import("./cascade.ts").classifyEscalation} matches; * the `manage-v1` profile test asserts they agree (they are coupled by design). */ export declare const MANAGE_SYSTEM_PROMPT: string; /** The stable identity of the manage-only watcher profile (a label, not the hash). */ export declare const MANAGE_PROMPT_PROFILE = "manage-v1"; /** * The profile `liveAgent` uses when the caller pins none. Defaults to the corrected * {@link AUTHORING_PROMPT_PROFILE} — the dry-run-1 fix — while `baseline-v0` stays registered for A/B. */ export declare const DEFAULT_PROMPT_PROFILE = "authoring-v1"; /** Resolve a profile label to its byte-stable system prompt. Fails closed on an unknown label — a * named-but-unregistered profile is a caller error (the bytes ARE the config identity), never a * guess. */ export declare function systemPromptForProfile(profile: string): string; /** Render the OPEN briefing into the user message (the date-blind keyframe cockpit). Pure. When a * `view` is supplied (the bounded authoring loop materializing a requested lens, ADR-0029 §2) the * body panes are SELECTED + ORDERED by it against the one catalog; absent ⇒ the OPEN default View * (byte-identical to today). The kernel LEADS non-configurably regardless. An unknown pane / * over-budget View fails closed inside the renderer (the driver catches it → a repair iteration). */ export declare function renderBriefingPrompt(briefing: BriefingInput, config: AgentConfig, view?: ViewSelection, seat?: SeatId): string; /** The repair re-ask message (ADR-0029 §2, the repair half of the bounded loop): surface the * repair-guiding error and ask the model to fix + resubmit. Kept terse so it does not re-explain the * whole contract — the model already has the system profile + its prior attempt in context. Pure. */ export declare function renderRepairPrompt(error: string): string; /** * Render a WAKE delta frame into the user message (the date-blind since-last-vantage cockpit). Pure. * * CACHE-MONOTONE kernel delta-encoding (kestrel-312): when the session runs a STREAMING policy * (`conversation`/`conversation-cached` — the reader provably holds the prior full kernel in cached * context) AND the caller threads the prior frame's kernel (`prevKernel`), the SAFETY/CONTROL kernel * is delta-encoded — only the fields that MOVED are re-emitted, the byte-stable skeleton rides the * cached prefix ({@link renderWakeDeltaStreamed}). Under `stateless-redraw` (no prior context) — or * when no `prevKernel` is available (e.g. the first wake, or a non-harness caller) — the wake frame * is a KEYFRAME carrying the COMPLETE kernel ({@link renderWakeDelta}); self-complete frames stay * self-complete. The composition is fail-closed-verified inside the renderer. * * A supplied `view` (kestrel-wa0j.4 — a scheduled wake's stored View, or a config-pinned forced * View) SELECTS + ORDERS the body panes against the one catalog; absent ⇒ the default WAKE panes, * byte-identical to today. The kernel LEADS non-configurably regardless, and the View's own token * budget is guarded at materialization exactly as on OPEN (fail-closed). */ export declare function renderWakePrompt(frame: WakeDeltaInput, config: AgentConfig, prevKernel?: Kernel, view?: ViewSelection, seat?: SeatId): string; /** The per-action attribution of an ATOMIC turn rejection (kestrel-gxz). Present iff the fail-closed * cause was an invalid ACTION (not a top-level JSON/shape defect). Names WHICH action index failed * and its reason, AND the VALID sibling actions that were NOT applied because the turn is * all-or-nothing — the exact facts the reject-and-reprompt renderer needs so the model can resubmit * a valid sibling alone (or fix the invalid one) rather than conclude "execution is broken". */ export interface TurnReject { /** The index of the FIRST invalid action in the turn. */ readonly failedIndex: number; /** The validator's reason for that action (e.g. `action[1] supersede: document parse escape — …`). */ readonly failedReason: string; /** The individually-VALID sibling actions, dropped by atomicity — index + a legible one-line summary. */ readonly validSiblings: readonly { readonly index: number; readonly summary: string; }[]; } /** The outcome of parsing one model reply. `ok` distinguishes a validated turn from a fail-closed * pass; `reason` (present iff `!ok`) is the human-readable failure the pass carries as its JOURNAL — * the *invalid output* signal, kept distinct from an authored `standDown` and a provider failure. * `reject` (present iff `!ok` AND the cause was an invalid action) carries the per-action attribution * the reject-and-reprompt loop surfaces to the model on its NEXT wake. */ export interface TurnParse { readonly turn: AgentTurn; readonly ok: boolean; readonly reason?: string; readonly reject?: TurnReject; } /** A short, DATE-BLIND-INTENT, legible one-line summary of a validated action — the token the * reject-and-reprompt renderer shows the model so it recognizes its own un-applied valid sibling * (e.g. `placeOrder buy 200 SPY @ @mid`). Pure. Any model-authored token it echoes (an instrument, * a document header) is re-fenced through {@link scanDateLeak} at the harness boundary before it can * reach the model, so a leaking symbol is WITHHELD there, never surfaced. */ export declare function describeAction(a: Action): string; /** * Parse a raw model reply into a validated {@link TurnParse}. Fail-closed and never repaired: * - no JSON object, malformed JSON, or wrong top-level shape ⇒ a pass whose JOURNAL names the defect; * - ANY invalid action ⇒ the WHOLE turn is rejected to that same fail-closed pass (never partially * applied — an invalid action never silently vanishes, a valid neighbour never arms alone). The * fail-closed pass additionally carries a {@link TurnReject} (the failed index + its reason + the * valid siblings that were dropped) so the harness can FEED THE REASON back to the model next wake; * - a `{ "kind": "pass" }` action (or an empty `actions`) ⇒ a legitimate pass, carrying the model's * own `journal` if it supplied one. * Pure: no clock, no RNG, no I/O. */ export declare function parseTurn(reply: string): TurnParse; /** The date-blind WITHHELD fallback the harness substitutes when a rendered reprompt would leak a * calendar token (fail-closed by redaction — the atomic-reject fact is preserved, the leaking * specifics are dropped, never a crash). Byte-stable; carries no per-turn material. */ export declare const REPROMPT_WITHHELD: string; /** * Render the PER-ACTION reject attribution the harness prefixes onto the NEXT wake's user message * after a turn failed closed (kestrel-gxz). Pure and date-agnostic: it names the invalid action index * + its reason, and — the step beyond a bare reason — lists the individually-VALID sibling actions * that atomicity DROPPED, telling the model they were NOT applied and to resubmit them (alone, or with * a corrected sibling). This turns "execution is broken in this kernel" into a self-correcting signal * while PRESERVING the atomic reject (bounded-risk / never-naked: a valid order never arms alone). * * The text is re-fenced through {@link scanDateLeak} at the harness boundary before it reaches the * model — a leaking model-authored token (e.g. an OCC symbol with a compact date) causes the whole * reprompt to be replaced by {@link REPROMPT_WITHHELD}. Pure: no clock, no RNG, no I/O. */ export declare function renderReprompt(reason: string, reject?: TurnReject): string; /** * Parse a raw model reply in the AUTHORING window into a classified {@link AuthoringReply} (ADR-0029 * §1, Option C). This is the ONE new parser branch the bounded loop reads; it composes over * {@link parseTurn} rather than replacing it, so the three m9i outcomes stay distinct. * * - When `allowRequestView` (the **viewshop** arm), a top-level `{ requestView: { view, reason } }` * object is validated: `view` a non-empty string that `parse()`s to EXACTLY ONE `View` statement (a * `Plan`/`Wake`/`Grade` or a multi-statement payload is a defect), `reason` a string, and NO * `actions` present (a mixed reply is a defect). A well-formed one ⇒ the non-terminal `requestView` * variant the driver materializes + re-asks on. * - Under the baseline (`allowRequestView=false`), a top-level `requestView` key is an unknown/illegal * outcome ⇒ the `invalid` variant (the controlled division genuinely lacks the move). * - ANY defect (malformed JSON, wrong shape, a bad requestView, a mixed reply, an invalid action) ⇒ * the `invalid` variant carrying the repair-guiding `reason`. The driver surfaces it for a repair * re-ask and it becomes the terminal fail-closed pass only when the budget is spent. * - A valid normal turn (incl. an authored `standDown` / empty pass) ⇒ the `turn` variant. * * Pure: no clock, no RNG, no I/O. */ export declare function parseAuthoringReply(reply: string, opts: { readonly allowRequestView: boolean; }): AuthoringReply; /** Env var names whose values are credentials the harness must never let into an artifact. */ export declare const CREDENTIAL_ENV_VARS: readonly string[]; /** * Redact credential material from a byte string BEFORE it becomes an artifact (ADR-0013 (e)). Three * belt-and-suspenders passes, so a key is caught by identity OR by shape: * 1. every explicitly-supplied secret value (e.g. the env bearer token, passed at capture time) — * exact-substring redaction, so any occurrence anywhere is scrubbed regardless of framing; * 2. `Authorization: Bearer <token>` / bearer-token shapes, including the AWS Bedrock `ABSK…` and * `bedrock-api-key-…` bearer-token formats; * 3. JSON auth-header VALUES (`authorization`, `x-api-key`, `x-amz-security-token`, `set-cookie`). * Pure — a deterministic function of (text, secrets). Empty/whitespace secrets are ignored (a blank * secret must never redact the whole document to `[REDACTED]`). */ export declare function scrubSecrets(text: string, secrets?: readonly string[]): string; //# sourceMappingURL=prompt.d.ts.map