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.
141 lines (140 loc) • 6.84 kB
JavaScript
/**
* kestrel.markets/protocol — the incremental SESSION contract (protocol v0.2).
*
* This module is the L0 CONTRACT of the transport-neutral Session interaction
* (djm.2 AC3/AC4; the schema the djm.4 controller and every face encode over).
* It is the hash-chained transcript vocabulary of ONE agent-day: a genesis seals
* the pure SessionSpec (SessionId = genesis hash); turn entries carry the binding
* pentad `(sessionId, ordinal, parentHash, frameRoot)` plus the exact authored
* bytes; a finalize seal closes the chain into a receipt root a CertifiedGrade
* can pin. Idempotency IS content-addressing; a changed-bytes-same-slot turn
* FAILS CLOSED. No face may reimplement Session progression — the wire verbs
* deliberately omit an `advance`.
*
* HARD CONSTRAINTS (identical to src/protocol/index.ts — see its header):
* - ZERO RUNTIME dependencies. The ONLY import is intra-protocol
* (`./index.ts`, itself dependency-free); this module pulls in NO engine /
* session / blotter / fill / lang / grade / frame / render / ledger / cli
* code and must typecheck with `chdb` uninstalled. The import-graph boundary
* is an automated CI invariant (tests/protocol.boundary.test.ts).
* - DETERMINISM. Pure types + own constants; no wall clock, no RNG.
* - FAIL CLOSED. Unknown / changed-bytes-same-identity ⇒ a typed diagnostic,
* never a silent accept.
* - GENERIC ONLY. No founding-app tickers or strategy names in types, comments,
* or examples. Product identities ride INSIDE these fields at runtime.
*/
/* ─────────────────────────── the reserved OPEN ordinal ─────────────────────── */
/**
* Turn ordinal of the date-blind OPEN Frame — the author's first answer, before
* any Wake. Reserved as `-1` so real Wake ordinals stay `0,1,2,…` and the OPEN
* turn is never confused with a Wake turn.
*/
export const OPEN_ORDINAL = -1;
/** Runtime tuple of every {@link FailureClass}. Same both-ways guard as the index rdu vocabularies. */
export const FAILURE_CLASSES = ["provider", "timeout", "malformed"];
/**
* Runtime tuple of every {@link SessionDiagnostic}, in the L0 order. Same
* both-ways closed-vocabulary guard as the index rdu vocabularies: a diagnostic
* added to (or removed from) the union without editing this tuple breaks the
* build, so the runtime vocabulary can never silently drift from the type.
*/
export const SESSION_DIAGNOSTICS = [
"wrong-session",
"broken-chain",
"turn-conflict",
"stale-frame",
"not-pending",
"sealed",
"malformed-entry",
];
/**
* Every Session wire verb as a runtime tuple — the closed `kind` vocabulary of
* {@link SessionMessage}. Driven by the union itself (`satisfies readonly
* SessionMessage["kind"][]`) so adding a message forces this tuple to grow in
* lockstep. Same both-ways closed-vocabulary guard as `RECEIPT_KINDS`: a `kind`
* added to (or removed from) the union without editing this tuple breaks the
* build. Fail-closed, no drift — and no `advance`.
*/
export const SESSION_MESSAGE_KINDS = [
"session/open",
"session/turn",
"session/frame",
"session/events",
"session/describe",
"session/finalize",
];
/** Runtime tuple of every {@link SessionInteraction}, in lifecycle order. Same both-ways guard. */
export const SESSION_INTERACTIONS = [
"start",
"open-delivery",
"authored-or-stand-down",
"wake-delta",
"revision",
"resume",
"finalization",
"artifacts",
];
/**
* The TOTAL map proving every required interaction is served by a real wire verb
* — `Record<SessionInteraction, SessionMessageKind>`, so a missing interaction or
* an unknown verb fails the build. Several interactions share a verb (the wire
* vocabulary is intentionally smaller than the interaction set): authored /
* revision both ride `session/turn`; OPEN delivery / Wake delta both ride
* `session/frame`; finalization / artifacts both ride the `session/finalize`
* seal. There is no bespoke verb per interaction — and still no `advance`.
*/
export const INTERACTION_MESSAGE = {
start: "session/open",
"open-delivery": "session/frame",
"authored-or-stand-down": "session/turn",
"wake-delta": "session/frame",
revision: "session/turn",
resume: "session/events",
finalization: "session/finalize",
artifacts: "session/finalize",
};
/**
* Reconcile an `incoming` turn against the `prior` entry recorded at the same
* intended slot — the idempotency + fail-closed primitive (djm.2 AC4; djm.4
* "idempotency IS content-addressing"). Pure and synchronous; no crypto, no I/O.
*
* Ladder (fail-closed, most-specific first):
* 1. structurally unreadable incoming → `malformed-entry`
* 2. names a different Session → `wrong-session`
* 3. does not continue the chain (parent/ord) → `broken-chain`
* 4. answers a superseded Frame → `stale-frame`
* 5. SAME slot, SAME entry address → idempotent duplicate (`ok`)
* 6. SAME slot, CHANGED bytes → `turn-conflict` (the headline
* rule: a different-bytes-same-identity turn is NEVER silently accepted)
*
* (`not-pending` / `sealed` are phase diagnostics raised by the djm.4 L1 gate
* that knows the Session's phase — not by this same-slot reconciler.)
*/
export function reconcileTurn(prior, incoming) {
// 1. Structural readability — fail closed on an unreadable envelope.
if (incoming.entryHash.length === 0 ||
(incoming.body.kind === "authored" && incoming.authoredSha256.length === 0)) {
return { ok: false, diagnostic: "malformed-entry" };
}
// 2. Same Session.
if (incoming.sessionId !== prior.sessionId) {
return { ok: false, diagnostic: "wrong-session" };
}
// 3. Continues the chain (same ordinal AND same parent link).
if (incoming.ordinal !== prior.ordinal || incoming.parentHash !== prior.parentHash) {
return { ok: false, diagnostic: "broken-chain" };
}
// 4. Answers the current Frame (not a superseded one).
if (incoming.frameRoot !== prior.frameRoot) {
return { ok: false, diagnostic: "stale-frame" };
}
// 5. Same slot, same content address ⇒ idempotent duplicate. Content-addressing
// IS idempotency: an identical entry re-sent is a no-op that returns the
// settled hash. (Equal entryHash implies equal authored bytes.)
if (incoming.entryHash === prior.entryHash) {
return { ok: true, duplicate: true, entryHash: prior.entryHash };
}
// 6. Same slot, CHANGED bytes ⇒ fail closed. Never silently accept a
// different-bytes-same-semantic-identity turn.
return { ok: false, diagnostic: "turn-conflict" };
}