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.

132 lines 10.5 kB
/** * # 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