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.

141 lines (140 loc) 6.84 kB
/** * 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" }; }