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
TypeScript
/**
* # 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