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.
132 lines • 10.5 kB
TypeScript
/**
* # session/harness/live-agent — the BYOK live {@link Agent} adapter (provider-free, kestrel-rul)
*
* The concrete body of `liveAgent` (declared, provider-free, in `../agent.ts`). It satisfies the
* ONE {@link Agent} seam — `open(briefing) / decide(frame)` — by composing the pure prompt profile
* and the fail-closed turn parser (`./prompt.ts`) around an INJECTED {@link LlmClient}. It imports
* NO provider SDK: the wire lives only inside whatever `LlmClient` the caller supplies (the AI-SDK
* client in `./ai-sdk-client.ts`, or a mock in tests), so the deterministic core — which
* `../simulate.ts` imports — stays provider-free (ADR-0013 (c), the invariant across both drivers).
*
* ## Determinism boundary (ADR-0013, RUNTIME §0)
* The live model call is the sole non-deterministic act, ABOVE the determinism line. Below it, only
* the returned {@link AgentTurn} crosses into the graded path — and because the driver captures each
* returned turn into {@link CapturedTurns}, {@link recordedAgent} replays the SAME turns and
* re-derives a byte-identical graded Bus (dual-harness fidelity). The per-turn wire evidence
* (model id, usage, reasoning, latency, scrubbed bytes) is captured OFF that path into an optional
* {@link TurnCapture} sink — free audit evidence for the leaderboard's attention axis, never a graded input.
*
* ## Session conversation (KV) reuse (kestrel-rul, the cache-monotone doctrine)
* Under the conversation policies ({@link CachePolicy}, default `conversation-cached`) the session is a
* GROWING message list — byte-stable `system` + the OPEN briefing + each wake's appended delta frame + the
* model's prior replies — passed whole to the {@link LlmClient} each wake, so a provider can prompt-cache the
* stable prefix (Anthropic `cache_control` / Bedrock cache-point via the AI SDK) and bill the re-read prefix
* at ~0.1× instead of full price on every wake. This all sits ABOVE the determinism line: only the returned
* {@link AgentTurn} crosses into the graded path, so {@link recordedAgent} replays byte-identically no matter
* the policy. `stateless-redraw` keeps the v0 arm (each wake one fresh message, no memory) for the m9i.5 A/B.
*
* ## Config envelope (ADR-0013 (d))
* `liveAgent` derives `AuthorPolicy.prompt_sha256` from the EXACT system-prompt bytes and stamps it
* onto the config it advertises, so the run lands in the grid under a `ConfigId` that folds the
* prompt profile — any prompt edit mints a new column. The active {@link CachePolicy} is stamped too (a
* ConfigId axis only, never an envelope field). Credentials are NEVER part of the config.
*/
import { type Agent, type AgentConfig, type CachePolicy, type LlmClient, type LlmUsage } from "../agent.ts";
import type { ViewSelection } from "../../frame/pane-catalog.ts";
/** The classification of one turn's outcome — the m9i three-way distinction, kept explicit on the
* capture record so a leaderboard never conflates an authored stand-down, an invalid reply, and a
* provider failure (ADR-0013 (a)). `authored` covers any validated turn (incl. an explicit standDown). */
export type TurnOutcome = "authored" | "invalid" | "provider-error";
/** Per-turn wire evidence captured ABOVE the determinism line (never a graded input): the model
* identity, provider-native usage, the raw reasoning trace (free evidence), the authored journal,
* measured latency, and the SCRUBBED wire bytes. Recorded so a run's cost/attention axis and audit
* trail survive without re-touching the wire; the graded replay rides {@link CapturedTurns} instead. */
export interface TurnCapture {
/** {@link OPEN_ORDINAL} for the open turn, else the wake ordinal. */
readonly ordinal: number;
readonly model: string;
readonly promptProfile: string;
readonly usage: LlmUsage;
readonly reasoning?: string;
readonly journal?: string;
/** Wall-clock latency of the model call in ms — audit evidence only (OFF the record path). */
readonly latencyMs: number;
/** The scrubbed wire evidence the client captured (`undefined` for a mock without wire). */
readonly wire?: string;
readonly outcome: TurnOutcome;
/** The KINDS of actions this turn EMITTED (`action.kind[]`, e.g. `["placeOrder"]`; `[]` for a PASS or a
* fail-closed/provider-error turn). Recorded so a leaderboard can tell a genuine PASS (empty actions)
* apart from an admitted-but-unfilled order or a held supersede WITHOUT inferring participation from P&L
* alone (kestrel harness-observability, Bug 2). Above the determinism line — audit evidence, never graded. */
readonly actionKinds: readonly string[];
/** The count of emitted actions (`= actionKinds.length`) — a convenience aggregate for run summaries. */
readonly actionCount: number;
/** Present when the reply was invalid — the fail-closed reason journalled onto the pass. */
readonly invalidReason?: string;
/** The EXACT user-message the model saw this turn (the effective wake/briefing message, incl. any
* reject-reprompt prefix). Populated on the decide path so a distillation harvest can reconstruct the
* (prompt → gold action) SFT pair faithfully, without re-deriving the frame. OFF the graded path — audit
* evidence only, never a graded input (the graded replay rides {@link CapturedTurns}). */
readonly prompt?: string;
/** The RAW model reply text (`completion.text`) this turn — the frontier watcher's verbatim emission, the
* distillation TARGET for SFT. OFF the graded path — audit evidence only. */
readonly reply?: string;
/** The {@link CachePolicy} this turn ran under — the axis m9i.5's prompt-cache economics are indexed by
* (so a sweep can compare `cacheReadTokens`/`cacheWriteTokens` across policies on identical Frames). */
readonly cachePolicy: CachePolicy;
/** CACHE-LIVENESS LOST (kestrel-wa0j.1): under `conversation-cached` with a reused prefix, the provider
* reported `cacheReadTokens === 0` on TWO OR MORE consecutive reused turns ending at this one — the stable
* prefix is provably NOT being served from cache (a TTL expiry between wakes, or a silent prefix
* invalidator), so every wake is re-writing the whole prefix at the cache-write premium. Recorded loudly
* on the capture (and warned once per session) — never a silent economics regression. Absent on healthy
* turns, on the first (cold) miss, and when the client reports no cache counters (no evidence ≠ a miss). */
readonly cacheLivenessLost?: true;
}
/** Options for {@link liveAgent}. `capture` is a sink array the adapter APPENDS one record per turn
* to (open + each decide); `promptProfile` labels the active variant set (baseline vs a model
* variant) for the leaderboard, and `now` is an injectable clock for latency capture (kept OFF the
* graded path — defaults to `Date.now`, which is legitimate here because latency is wire evidence,
* never a graded input). */
export interface LiveAgentOptions {
readonly capture?: TurnCapture[];
readonly promptProfile?: string;
readonly now?: () => number;
/** A FORCED View (kestrel-4gl.13.13) — SELECTS + ORDERS the body panes rendered to the model at
* every OPEN + WAKE frame, overriding the frame-kind default. Supplied for the **perception arm** of
* a View A/B (e.g. `{ name:"perception", panes:[…,{name:"gex"},{name:"iv-skew"}] }`) so the agent is
* SHOWN the options-analytics panes without having to author a View itself. Absent ⇒ the frame-kind
* default panes (the minimal arm), byte-identical to before. Distinct from the viewshop authoring
* loop (where the MODEL requests the View); this pins it by config. */
readonly forcedView?: ViewSelection;
}
/** The adapter/harness identity `liveAgent` advertises when the caller pins none (m9i.2 owner tweak —
* adapter identity is mandatory-for-certified before the m9i.7 harness tournament). This is the harness
* naming ITSELF — the one identity it genuinely knows — never a fabricated third-party harness. */
export declare const LIVE_AGENT_ADAPTER = "kestrel-live-agent";
/** The adapter/harness revision token paired with {@link LIVE_AGENT_ADAPTER}. Bump on any behavioral
* change to this adapter (prompt loop, parsing, capture) — it folds into the ConfigId, so a bump mints a
* new grid column rather than contaminating an old one (ADR-0013 (d)). `v2` (kestrel-wa0j.19 §5): the
* percept-v5 merge changed BOTH the prompt loop (a scheduled/forced View is now delivered on the WAKE
* frame, not only OPEN) AND the capture schema (the `cacheLivenessLost` flag), and the delivery render is
* now fail-closed to the default WAKE panes on a materialization refusal — behavioral changes to what the
* model perceives, so they must mint a NEW grid column rather than contaminate the v1 column (the
* file-handshake precedent in the same merge bumped v1→v2 for exactly this class of change). */
export declare const LIVE_AGENT_ADAPTER_VERSION = "v2";
/** The default {@link CachePolicy} the live harness runs under when the caller pins none: reuse the
* session conversation AND request provider prompt-cache control on the byte-stable prefix (kestrel-rul —
* the cost lever for benchmark sweeps). A caller can pin `stateless-redraw` (the v0 arm) or `conversation`
* (reuse, no provider cache) to sweep the economics (m9i.5). */
export declare const DEFAULT_CACHE_POLICY: CachePolicy;
/**
* Build the BYOK live {@link Agent} over an injected {@link LlmClient}. Provider-free: the wire is
* entirely the client's concern. The returned agent's `config` carries the derived
* `promptHash = sha256(system bytes)` (unless the caller pinned one) and this harness's own
* adapter identity ({@link LIVE_AGENT_ADAPTER}/{@link LIVE_AGENT_ADAPTER_VERSION}, unless the caller
* pinned one — m9i.2), so the Simulate driver stamps a complete-enough a57.14 envelope and the run lands
* in the grid under a prompt-profile- and adapter-aware `ConfigId`. The interface `face` is deliberately
* NOT defaulted here: only the caller knows the transport its injected client rides (`sdk` for the
* AI-SDK client), and a fabricated face would be a fabricated identity — an undeclared face fails the
* envelope closed to PROVISIONAL (a57.14).
*/
export declare function liveAgent(config: AgentConfig, llm: LlmClient, opts?: LiveAgentOptions): Agent;
//# sourceMappingURL=live-agent.d.ts.map