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.
260 lines (244 loc) • 16.6 kB
text/typescript
/**
* # session/harness/cascade — the two-tier strategist+watcher cascade driver (ADR-0032 tracer, kestrel-aus)
*
* The thinnest real driver that runs ADR-0032's **two-tier cascade** end-to-end through the EXISTING
* Simulate runner ({@link import("../simulate.ts").runSimulateSession}) with **zero determinism-core
* change**. It composes TWO ADR-0012/0013 {@link Agent}s over ONE Session at two cadences:
*
* - **STRATEGIST** — a frontier model (Fable/opus). Invoked at the OPEN keyframe (authors the day's
* Plan + Brief + Mandate + View) and on a watcher **escalation** (re-brief: supersede a new
* Plan/Brief). The few expensive judgments that set the frame.
* - **WATCHER** — a small/fast model (haiku). Invoked at every in-loop Wake. It **manages** the
* strategist's armed Plan within the Mandate (reload/exit/adjust/size-within-budget) — MANAGE-ONLY
* (owner v1 default): it may NOT arm new authority. When it reaches the edge of its authority it
* **escalates via the driver** to the strategist.
*
* ## The load-bearing trick: the cascade IS one {@link Agent} (no new determinism-core plumbing)
* `runSimulateSession` already keys {@link import("../agent.ts").CapturedTurns} on each wake's stable
* identity ({@link import("../agent.ts").wakeKeyOf}, kestrel-5zl.19) and captures the ONE terminal turn
* each wake returns. So the cascade is expressed as a single
* {@link Agent} whose `open` dispatches to the strategist and whose `decide` dispatches to the watcher
* — and, on an escalation, re-invokes the strategist and returns ITS turn instead. Exactly one terminal
* turn crosses to the Bus per wake, so:
* - the driver captures the whole cascade into `CapturedTurns` for free, and
* - {@link import("../agent.ts").recordedAgent} replays the cascade **byte-identically** (it replays
* the captured terminal turn per wake — it never learns which tier authored it).
* This is the ADR §6 claim ("both tiers are above-the-line Agents; recordedAgent replays the whole
* cascade byte-identically") realized without touching the determinism line.
*
* ## The safety keystone rides for free (ADR-0032 §3)
* Every terminal turn — watcher OR strategist — crosses into the SAME fail-closed admission Gate
* (`core.gate`) inside `runSimulateSession`: SELL floored at intrinsic, budget clamp, never-naked, the
* bounded-risk envelope. The watcher buys judgment; the runtime keeps authority. A watcher action that
* would exceed the Mandate/budget is refused fail-closed by that Gate exactly as any over-reaching
* frontier action is; a watcher attempt to **arm new authority** (a `supersede`) is refused at the tier
* boundary here ({@link classifyEscalation}) and **escalated** to the strategist rather than admitted.
* Neither model tier is trusted; both are bounded.
*
* ## Attribution across tiers (ADR-0032 §6, the three-level ladder)
* {@link attributeCascade} computes the frozen-Plan counterfactual over the SAME recorded tape:
* `strategist_alpha = EV(frozen strategist Plan, pure-algo) − EV(benchmark)` and
* `watcher_alpha = EV(cascade) − EV(frozen strategist Plan)`, so we can read which tier earned the P&L.
*/
import {
OPEN_ORDINAL,
OPEN_WAKE_KEY,
wakeKeyOf,
type ActingFrame,
type Agent,
type AgentConfig,
type AgentTurn,
type WakeKey,
} from "../agent.ts";
import type { BriefingInput } from "../../frame/types.ts";
import { disarmsPlan } from "../../engine/disarm.ts";
// ─────────────────────────────────────────────────────────────────────────────
// Escalation — the watcher wakes the strategist (ADR-0032 §4, owner v1 "minimal")
// ─────────────────────────────────────────────────────────────────────────────
/** Why the watcher escalated to the strategist (the two minimal owner-v1 triggers):
* - `arm-new-authority` — the watcher authored a `supersede` (arm/re-author standing authority),
* which is BEYOND its MANAGE-ONLY tactical authority (§5); the driver refuses it at the tier
* boundary and re-invokes the strategist (the right actor for a mandate/thesis change).
* - `defer-and-flag` — the watcher explicitly asked for a re-brief (a `standDown` reason or a journal
* opening with {@link ESCALATION_SENTINEL}), the "I am out of my depth; call the PM" move. */
export type EscalationReason = "arm-new-authority" | "defer-and-flag";
/** The token a prompted watcher opens its `standDown` reason / journal with to explicitly request a
* re-brief (the defer-and-flag escalation). A legible, greppable signal — matched case-insensitively at
* the start of the trimmed text so a fast model's `RE-BRIEF: regime broke` reads as an escalation. */
export const ESCALATION_SENTINEL = "RE-BRIEF";
/**
* Pure classification of a watcher turn: does it escalate to the strategist, and why? Returns the
* {@link EscalationReason}, or `null` when the turn is WITHIN the watcher's manage-only authority (it
* crosses to the Bus and the admission Gate as-is). Above the determinism line — a total function of
* the turn, so the driver's routing is deterministic and unit-testable.
*/
export function classifyEscalation(turn: AgentTurn): EscalationReason | null {
// MANAGE-ONLY: a `supersede` is an attempt to arm/re-author standing authority — the watcher may not.
// Refuse it at the tier boundary and escalate (the strategist is the right actor).
if (turn.actions.some((a) => a.kind === "supersede")) return "arm-new-authority";
// Explicit defer-and-flag: a standDown reason or the journal opening with the sentinel.
const flags: string[] = [];
if (turn.journal !== undefined) flags.push(turn.journal);
for (const a of turn.actions) if (a.kind === "standDown") flags.push(a.reason);
if (flags.some((t) => t.trimStart().toUpperCase().startsWith(ESCALATION_SENTINEL))) return "defer-and-flag";
return null;
}
// ─────────────────────────────────────────────────────────────────────────────
// The cascade Agent
// ─────────────────────────────────────────────────────────────────────────────
/** One routing record the cascade appends per invoked ordinal (audit evidence, ABOVE the determinism
* line — never a graded input, never a replay key). The `escalation-rebrief` records BOTH the watcher's
* escalation (why) and that the strategist's re-brief turn is what crossed to the Bus. */
export interface CascadeStep {
/** {@link OPEN_ORDINAL} for the open turn, else the wake ordinal — a DISPLAY/log coordinate only (it
* shifts when a source inserts a vantage). Join a step to its captured turn on {@link key}. */
readonly ordinal: number;
/** The step's stable wake identity (kestrel-5zl.19) — {@link OPEN_WAKE_KEY} for the open turn, else
* {@link wakeKeyOf} of the delivered frame: the SAME key the driver captured the turn under. */
readonly key: WakeKey;
/** The tier whose turn CROSSED to the Bus at this ordinal (on an escalation, the strategist). On an
* `escalation-hold-armed` step NEITHER tier's turn crosses — the driver holds the frozen plan armed
* (an empty PASS) — so the tier records the strategist (the tier the escalation was routed to). */
readonly tier: "strategist" | "watcher";
/** `escalation-hold-armed` (frozen-fan only): the watcher escalated but the frozen/recorded strategist
* carried NO re-brief; instead of letting its fail-closed stand-down DISARM the armed plan, the driver
* REFUSED the watcher's over-reach and HELD the frozen plan armed (fail-closed to an empty PASS). */
readonly kind: "open" | "wake" | "escalation-rebrief" | "escalation-hold-armed";
readonly escalated: boolean;
readonly escalationReason?: EscalationReason;
}
/** Options for {@link cascadeAgent}. `strategist`/`watcher` are two ordinary {@link Agent}s (live,
* scripted, or recorded); `log`, when supplied, receives one {@link CascadeStep} per invoked ordinal. */
export interface CascadeOptions {
readonly strategist: Agent;
readonly watcher: Agent;
/** Override the advertised cascade {@link AgentConfig}; defaults to {@link cascadeConfigOf}. */
readonly config?: AgentConfig;
/** The above-the-line routing audit (one record per invoked ordinal). */
readonly log?: CascadeStep[];
/**
* FROZEN/RECORDED-strategist FAN MODE ONLY (set by {@link import("./plan-fixture.ts").fanWatchers},
* whose strategist tier is a {@link import("./plan-fixture.ts").recordedStrategistOf} recording). When
* `true`, a watcher escalation whose strategist re-invoke fails closed to a stand-down does NOT DISARM
* the armed Plan: a frozen/recorded strategist carries no captured re-brief, so its `decide` stands the
* book down — and letting that cross would tear down the armed CONTROL plan and collapse the whole
* frozen-plan fan to $0 (docs/results/author-and-fan/, PR #8). Instead the watcher's over-reach is
* REFUSED and the frozen plan is HELD ARMED (fail-closed to an empty PASS — the existing armed authority
* persists), so the fan can measure watcher management against a LIVE armed plan.
*
* SAFETY SCOPING: this is confined to the recorded/frozen fan. A LIVE cascade leaves this UNSET — a live
* watcher that exceeds its mandate is still refused and its live strategist re-briefed or stood down
* exactly as before. It is NOT a global flag: only the recorded-fan builder sets it, and it never
* weakens the admission Gate (every crossing turn, held or not, still passes the same fail-closed Gate).
*/
readonly frozenStrategist?: boolean;
}
/** The fail-closed HOLD turn the frozen fan returns instead of a strategist stand-down: an empty PASS
* (the standing armed Plan keeps managing itself) carrying a legible pre-hoc JOURNAL naming the refusal.
* Byte-stable — no per-turn material, no clock. */
const HELD_ARMED_TURN: AgentTurn = {
actions: [],
journal:
"cascade(frozen-fan): watcher over-reach REFUSED at the tier boundary; frozen plan HELD ARMED " +
"(no captured strategist re-brief — the armed authority persists rather than being torn down)",
};
/** Derive the cascade's advertised {@link AgentConfig} from the two tiers: the strategist's identity
* (the frame-setting brain) with a cascade `label` and the cascade `harness` folded in, so a cascade
* run mints its OWN ConfigId / grid column and never averages with a single-tier column (ADR-0032 §7,
* the ADR-0013 controlled-division discipline). The watcher's model rides the label. */
export function cascadeConfigOf(strategist: AgentConfig, watcher: AgentConfig): AgentConfig {
return {
...strategist,
label: `cascade(${strategist.label}»${watcher.label})`,
harness: "strategist-watcher-cascade",
harnessVersion: "v1",
};
}
/**
* Build the cascade as ONE {@link Agent} over two tiers. `open` → the strategist authors the frame
* (Plan/Brief/Mandate/View). `decide` → the watcher manages in-loop; if the watcher escalates
* ({@link classifyEscalation}), the strategist is re-invoked on the SAME frozen frame (which carries
* the book/mandate/plan state + the engine log since last wake — the acting Frame IS the state) and
* ITS re-brief turn crosses to the Bus instead. Exactly one terminal turn per ordinal → the driver
* captures the whole cascade and {@link recordedAgent} replays it byte-identically.
*/
export function cascadeAgent(opts: CascadeOptions): Agent {
const { strategist, watcher, log } = opts;
const config = opts.config ?? cascadeConfigOf(strategist.config, watcher.config);
return {
config,
async open(briefing: BriefingInput): Promise<AgentTurn> {
const turn = await strategist.open(briefing);
log?.push({ ordinal: OPEN_ORDINAL, key: OPEN_WAKE_KEY, tier: "strategist", kind: "open", escalated: false });
return turn;
},
async decide(frame: ActingFrame): Promise<AgentTurn> {
const watcherTurn = await watcher.decide(frame);
const reason = classifyEscalation(watcherTurn);
if (reason === null) {
// Within manage-only authority — the watcher's turn crosses to the Bus + the admission Gate.
log?.push({ ordinal: frame.wakeOrdinal, key: wakeKeyOf(frame), tier: "watcher", kind: "wake", escalated: false });
return watcherTurn;
}
// Escalate: the watcher wakes the strategist (attention, not risk — ADR-0001's Wake invariant).
// Re-invoke the strategist on the same frozen frame; its re-brief supersede is the terminal turn.
const strategistTurn = await strategist.decide(frame);
// FROZEN/RECORDED-STRATEGIST FAN MODE ONLY (opts.frozenStrategist): a recorded strategist has no
// captured re-brief, so its `decide` fails closed to a stand-down. Letting that DISARM the armed
// plan would collapse the whole frozen-plan fan to $0. Instead REFUSE the watcher's over-reach and
// HOLD the frozen plan armed (fail-closed to an empty PASS — the armed authority persists). A frozen
// strategist that DID capture a genuine re-brief (a non-disarming turn) is still honored. This is
// scoped to the recorded fan: a LIVE cascade leaves frozenStrategist unset and disarms exactly as
// before, and the admission Gate is untouched (the held PASS still crosses the same Gate).
if (opts.frozenStrategist === true && disarmsPlan(strategistTurn)) {
log?.push({
ordinal: frame.wakeOrdinal,
key: wakeKeyOf(frame),
tier: "strategist",
kind: "escalation-hold-armed",
escalated: true,
escalationReason: reason,
});
return HELD_ARMED_TURN;
}
log?.push({
ordinal: frame.wakeOrdinal,
key: wakeKeyOf(frame),
tier: "strategist",
kind: "escalation-rebrief",
escalated: true,
escalationReason: reason,
});
return strategistTurn;
},
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Three-level attribution ladder (ADR-0032 §6) — the frozen-Plan counterfactual
// ─────────────────────────────────────────────────────────────────────────────
/** The three EVs the attribution ladder reads, each a graded run over the SAME recorded tape + fill
* model (so the only variable is the tier under test — the controlled-comparison discipline). Use the
* conservative floor P&L (`blotter.totals.floor`) — a deterministic, definite-fill number under the
* strict-cross family, comparable across the three arms. */
export interface AttributionInput {
/** `EV(benchmark)` — buy-and-hold / the null baseline over the tape. */
readonly benchmark: number;
/** `EV(frozen strategist Plan, replayed pure-algo, no watcher)` — the armed Plan alone. */
readonly frozenPlan: number;
/** `EV(cascade)` — the watcher managing the strategist's armed Plan in the loop. */
readonly cascade: number;
}
/** The three-level `management_alpha` decomposition (ADR-0032 §6): `strategist_alpha` is the frame
* (plan/brief) alpha vs the benchmark; `watcher_alpha` is the in-loop management alpha the watcher adds
* ON TOP of the frozen plan; `cascadeTotal` is their sum (= `EV(cascade) − benchmark`). Pure. */
export interface Attribution {
readonly strategistAlpha: number;
readonly watcherAlpha: number;
readonly cascadeTotal: number;
}
/** Compute the three-level ladder from the three frozen counterfactual EVs (ADR-0032 §6). Pure. */
export function attributeCascade(x: AttributionInput): Attribution {
const strategistAlpha = x.frozenPlan - x.benchmark;
const watcherAlpha = x.cascade - x.frozenPlan;
return { strategistAlpha, watcherAlpha, cascadeTotal: strategistAlpha + watcherAlpha };
}