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.

87 lines (75 loc) 5.29 kB
/** * # engine/disarm — one home for the de-arm RULE (kestrel-z473.5) * * The disarm EVENT (engine → Bus CONTROL `disarm` / WAKE inform → Blotter {@link * ../blotter/types.ts DisarmRecord}) is **intrinsic** and lives on the Bus — it stays exactly where it * is. This module owns only the RULE around it: **what COUNTS as a disarm**, and **how a de-arm reason * is WORDED**. Both were previously restated across `engine/orgfacts.ts`, `engine/plans.ts`, * `session/agent.ts`, `session/authoring-loop.ts`, `session/harness/cascade.ts`, and * `blotter/project.ts`; here they are stated ONCE so every surface reads the same source of truth. * * ## Fail-closed doctrine * A de-arm is never silent (AGENTS: "de-arm with a logged reason, never a silent default"). Every reason * a de-arm carries is a **byte-stable** string minted through the builders below — a re-homing, not a * re-wording, so goldens and e2e fixtures that pin these substrings pass unchanged. * * ## Layering * `engine` sits BELOW `session`, so this module NEVER imports a session type. The stand-down * predicates/factory are typed **structurally** (the action discriminant alone), which every * `Action`/`AgentTurn` from `session/agent.ts` satisfies. Session imports FROM here — the sanctioned * `session → engine` direction. */ // ── the disarm EVENT discriminant (Bus CONTROL vocabulary) ────────────────── /** The CONTROL-stream `type` a de-arm event carries (`src/bus/types.ts`, open control vocabulary). The * Blotter projects a {@link ../blotter/types.ts DisarmRecord} from exactly this discriminant, so the * projector and the driver's emit sites agree by construction. */ export const DISARM_CONTROL_TYPE = "disarm"; // ── what COUNTS as a disarm (the agent-path predicate) ────────────────────── /** The action-kind discriminant of the fail-closed de-arm turn — the ONLY de-arm an agent can author * (CONTEXT / `session/agent.ts`: "the agent-path stand-down is the only de-arm"). A `standDown` de-arms * clean; inventory rides its own TP/EXIT (it never liquidates). */ export const STAND_DOWN = "standDown" as const; /** Structural shape of an authored action — its discriminant alone. Kept minimal so this engine module * never imports the `Action` union from `session/agent.ts`; every concrete `Action` satisfies it. */ interface ActionKind { readonly kind: string; } /** Is this action the fail-closed de-arm (`standDown`)? */ export function isStandDown(action: ActionKind): boolean { return action.kind === STAND_DOWN; } /** Does this terminal turn DISARM the standing book? A `standDown` de-arms the armed Plan; an empty PASS * (or a manage/order action) leaves it armed. The cascade frozen-fan uses this to detect a frozen * strategist's fail-closed re-invoke and HOLD the plan armed instead of tearing it down * (`session/harness/cascade.ts`). */ export function disarmsPlan(turn: { readonly actions: readonly ActionKind[] }): boolean { return turn.actions.some(isStandDown); } /** The fail-closed de-arm turn: stand down clean with a logged reason (inventory rides its TP/EXIT — * never liquidates; RUNTIME §8). The recorded/fixed adapters and the bounded authoring loop return this * rather than crash. Byte-stable — the only per-turn material is the `reason`. Typed structurally (the * returned shape IS a `StandDownAction` turn) so `session` can assign it straight to an `AgentTurn`. */ export function standDownTurn( reason: string, ): { readonly actions: readonly [{ readonly kind: typeof STAND_DOWN; readonly reason: string }] } { return { actions: [{ kind: STAND_DOWN, reason }] }; } // ── de-arm reason WORDING (the engine mints through these) ─────────────────── /** The loud fail-closed reason an ARMED plan whose WHEN (entry OR manage) names a genuinely unresolvable * ORG path de-arms with. `engine/orgfacts.ts` mints it (the org read view names the path); `engine/plans.ts` * relays it to a WAKE inform, edge-latched per plan (RUNTIME §3/§8, CONTEXT: "de-arms cleanly with a * logged reason"). `pathKey` is the already-formatted org path (`children(any).drawdown`, `fills.count`, …). */ export function orgPathUnresolvableReason(pathKey: string): string { return `org path "${pathKey}" is unresolvable — no such org fact in this session; de-arm (fail-closed, RUNTIME §8)`; } /** The loud fail-closed reason a `WHEN <event> leg <n>` per-leg fill qualifier de-arms with: no * fill-telemetry provider can scope a single leg today, so honouring the qualifier would fire on ANY * session fill (cross-plan bleed). `engine/plans.ts` `ComposedProvider.fillEvent` mints it; the real * capability (per-plan/per-leg scoping) lands with kestrel-qbwo.2 (kestrel-s3h8). */ export function perLegFillQualifierReason(event: string, leg: number): string { return ( `fill trigger \`${event} leg ${leg}\` names a per-leg qualifier no fill-telemetry provider can scope today — ` + `the available telemetry is session-scoped, so honouring it would fire on ANY session fill (cross-plan bleed). ` + `UNKNOWN, de-armed (fail closed; per-plan/per-leg scoping is kestrel-qbwo.2)` ); }