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
text/typescript
/**
* # 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)`
);
}