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.
107 lines (106 loc) • 6.4 kB
JavaScript
/**
* # session/harness/bounded-numerics — the BOUNDED-NUMERICS emission guard (fail-closed, never scored)
*
* A strict schema / constrained-decoding grammar can force a syntactically-VALID but DEGENERATE number: a
* `json_schema` `qty:integer` probe forced digits and the model looped to an **8,173-digit integer** —
* valid JSON, garbage. Such an emission is a GENERATION FAILURE, not a decision. It classifies INVALID
* (fail-closed) and is NEVER scored — the same "reject unknown input, never default it" rule the
* empty→INVALID guard already enforces in {@link file://./live-agent.ts} (empty/zero-token → `invalid`).
*
* This is the LOCAL port of the HOSTED adapter's `invalid-degenerate` class
* (`scripts/bench/hosted/hosted-adapter.ts` `checkBoundedNumerics` / `classifyEmission`), lifted here so the
* local live harness and the hosted eval adapter share ONE convention: a degenerate numeric is rejected
* identically on both lanes. Pure: no clock, no RNG, no I/O.
*
* Two cheap, load-bearing checks:
* 1. **pre-parse raw-string digit cap** — any run of >{@link MAX_NUMERIC_DIGITS} consecutive digits in the
* RAW emission is a decoding loop (the 8,173-digit integer), caught BEFORE `JSON.parse` silently
* coerces it to `Infinity` (which `Number.isFinite` would then reject anyway) OR — the sneakier case —
* to a FINITE-but-absurd float (e.g. a 15-digit run → ~1e15) that a bare finiteness check would SCORE
* as a real order. The raw cap catches BOTH before the coercion hides them.
* 2. **per-field range sanity** — every action's numeric field (`order.qty` / `order.strike` /
* `scheduleWake.at.minutes`) must be finite and inside {@link NUMERIC_FIELD_BOUNDS}. A `NaN`/`Infinity`/
* absurd value is degenerate. Bounds are generous — they catch garbage, not judgment.
*/
/** Max run of consecutive digits tolerated in a RAW emission. Any longer run is a constrained-decoding
* digit loop, not a number (the 8,173-digit integer). Matches the hosted adapter's cap. */
export const MAX_NUMERIC_DIGITS = 12;
/** Per-field sanity ranges for the action-schema numeric fields (finite + in-range; a NaN/Inf/absurd value
* is a degenerate emission, never scored). Generous — they catch garbage, not judgment. Mirrors the hosted
* adapter's `NUMERIC_FIELD_BOUNDS` and maps onto the Kestrel action schema (`src/session/agent.ts`):
* `placeOrder.order.{qty,strike}` and `scheduleWake.at.minutes` (the `inMinutes` variant). */
const NUMERIC_FIELD_BOUNDS = {
/** contracts (option) or shares (equity) — a positive, finite, non-absurd count. */
qty: { min: 0, max: 1e7, exclusiveMin: true },
/** an option strike — positive, finite, bounded well above any real underlying. */
strike: { min: 0, max: 1e7, exclusiveMin: true },
/** scheduleWake `inMinutes` — non-negative minutes within a sane horizon. */
minutes: { min: 0, max: 1e7, exclusiveMin: false },
};
function inBounds(v, b) {
if (typeof v !== "number" || !Number.isFinite(v))
return false;
if (b.exclusiveMin ? v <= b.min : v < b.min)
return false;
return v <= b.max;
}
/** Extract + JSON.parse the single top-level object from a reply. Mirrors `prompt.ts:extractJsonObject`
* (not exported) and the hosted adapter's `extractObject`: tolerate a ```json fence and surrounding prose,
* take the first `{` to the last `}`. Returns `null` when there is no parseable object. */
function extractObject(reply) {
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(reply);
const body = (fenced !== null ? fenced[1] : reply).trim();
const start = body.indexOf("{");
const end = body.lastIndexOf("}");
if (start === -1 || end === -1 || end < start)
return null;
try {
const v = JSON.parse(body.slice(start, end + 1));
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
}
catch {
return null;
}
}
/**
* The bounded-numerics guard. Returns `{ ok: true }` when the emission carries no degenerate number;
* otherwise `{ ok: false, reason }` naming the defect. A parse failure here is NOT this guard's concern
* (the turn parser owns unparseable input) — an unparseable body simply passes the field check and is
* rejected by the parser downstream. See the module header for the two checks.
*/
export function checkBoundedNumerics(text) {
// 1. Pre-parse raw digit-run cap — catches the digit loop BEFORE JSON.parse coerces it away.
const longRun = /\d{13,}/.exec(text);
if (longRun !== null) {
return {
ok: false,
reason: `degenerate numeric: a ${longRun[0].length}-digit run exceeds the ${MAX_NUMERIC_DIGITS}-digit cap (constrained-decoding digit loop, not a number)`,
};
}
// 2. Per-field range sanity on the parsed turn (finite + inside NUMERIC_FIELD_BOUNDS).
const obj = extractObject(text);
const actions = obj !== null && Array.isArray(obj.actions) ? obj.actions : null;
if (actions === null)
return { ok: true }; // no turn shape to range-check; the parser owns that.
for (const a of actions) {
if (typeof a !== "object" || a === null)
continue;
const act = a;
const order = act.order;
if (order !== undefined && order !== null && typeof order === "object") {
if ("qty" in order && !inBounds(order.qty, NUMERIC_FIELD_BOUNDS.qty)) {
return { ok: false, reason: `degenerate numeric: order.qty=${JSON.stringify(order.qty)} out of range (0,${NUMERIC_FIELD_BOUNDS.qty.max}]` };
}
if ("strike" in order && order.strike !== undefined && !inBounds(order.strike, NUMERIC_FIELD_BOUNDS.strike)) {
return { ok: false, reason: `degenerate numeric: order.strike=${JSON.stringify(order.strike)} out of range (0,${NUMERIC_FIELD_BOUNDS.strike.max}]` };
}
}
const at = act.at;
if (at !== undefined && at !== null && typeof at === "object" && at.kind === "inMinutes" && "minutes" in at) {
if (!inBounds(at.minutes, NUMERIC_FIELD_BOUNDS.minutes)) {
return { ok: false, reason: `degenerate numeric: scheduleWake.at.minutes=${JSON.stringify(at.minutes)} out of range [0,${NUMERIC_FIELD_BOUNDS.minutes.max}]` };
}
}
}
return { ok: true };
}