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.

399 lines (398 loc) 21.6 kB
/** * # cli/backend/mock — the TRUTHFUL in-process contract server (kestrel-5rb). * * A `fetch(url, init) → Response` that serves the EXACT platform contract * (kestrel.markets): the M1 routes, **snake_case** wire bodies, the structured * 402/OfferResponse for the paid-dataset boundary, a canonical resumable SSE stream * whose events are the `operation.*` {@link ./wire.ts OperationEvent.type} vocabulary * with **opaque-STRING** `id:` cursors, and `Idempotency-Key` effect-once replay. * Inject into RemoteBackend (`{ baseUrl, fetch: mockFetch }`) → the whole CLI runs * in CI with no real API. * * It shares its wire shapes and SSE event names with {@link ./remote.ts} through * `wire.ts` — one definition of the dialect for client AND server — so this mock can * never again hand-roll a fabricated dialect that hides a divergence (the failure * mode kestrel-5rb was filed to kill). The route/shape/vocabulary conformance is * independently pinned to the platform OpenAPI fixture by * `tests/cli.remote-conformance.test.ts`. */ import { PROTOCOL_VERSION } from "../../protocol/index.js"; // kestrel-djm.5: the BAKED catalog-conformant records this server ALSO serves so the SDK's HTTP transport // returns the SAME protocol Blotter / transcript / grade / root the LOCAL projection yields (light: pure // data, no engine — the server stays on the remote graph, tests/package.boundary.test.ts). import { CATALOG_ENTRIES, CATALOG_RECORDS, SESSION_TO_ENTRY } from "./catalog-records.js"; // kestrel-dnxq: the light contract server serves `GET /catalog` in the DEPLOYED snake_case listing dialect // (`{ entries: HostedCatalogEntry[], version }`) — NOT the old bare camelCase CatalogEntry[] that hid the // remote-vs-local divergence (kestrel-y9v1/uasi). Built through the SAME shared listing constructor the LOCAL // transport uses, so `remoteTransport`'s decode reconstructs the byte-identical canonical CatalogListing. import { buildCatalogListing, toCatalogListingWire } from "../../catalog/catalog-listing.js"; const TRIAL = { // `capability` IS the bearer token (openapi TrialCapability). Scopes exclude // paper/broker/live — the trial is a subset of the wallet-signable set (ADR-0002). capability: "cap_trial_mock_0001", subject_commitment: "sc_ephemeral_mock", expiry: "2099-01-01T00:00:00Z", scopes: ["data", "sim", "grade"], rate_limit: { requests_per_minute: 100, compute_quota: "trial" }, }; /** The `GET /catalog` body the light server serves — the deployed `{ entries, version }` snake_case listing * wire, one row per baked catalog entry, built once for byte-stability through the SAME shared listing * constructor the LOCAL transport uses (kestrel-dnxq). The `version` is a fixed content tag (the fixture is * static); a self-hosted mirror may serve any opaque version — the client never interprets it. */ const CATALOG_LISTING_BODY = { entries: buildCatalogListing(CATALOG_ENTRIES.map((e) => e.id)).map(toCatalogListingWire), version: "cat_mock_1", }; const SIM_OP_ID = "op_sim_1"; const GRADE_OP_ID = "op_grade_1"; function simOperation() { return { operation_id: SIM_OP_ID, intent_hash: "ih_" + SIM_OP_ID, status: "running", cursor: "c0", artifacts: [], receipts: [] }; } function gradeOperation() { return { operation_id: GRADE_OP_ID, intent_hash: "ih_" + GRADE_OP_ID, status: "running", cursor: "c0", artifacts: [], receipts: [] }; } /** A running Operation whose id ENCODES a catalog entry (kestrel-djm.5) — its events serve the baked record. */ function catalogOperation(opId) { return { operation_id: opId, intent_hash: "ih_" + opId, status: "running", cursor: "c0", artifacts: [], receipts: [] }; } /** The paid boundary is a property of the DATASET (§4 Sim): a free-catalog artifact * runs free; anything outside it triggers the 402. (The client sends no `scope`.) */ function isPaidDataset(artifactId) { return typeof artifactId === "string" && /premium|paid|out-of-catalog/.test(artifactId); } // kestrel-djm.5 — catalog-conformant serving. Operation ids ENCODE the catalog entry so the events endpoint // serves the right baked record on resume (the op id is the durable handle, ADR-0004). const SIM_CAT_PREFIX = "op_sim_cat_"; const GRADE_CAT_PREFIX = "op_grade_cat_"; /** The baked catalog entry id a `POST /sim` dataset addresses, or `null` when the dataset is not a * catalog-Session subject (a non-baked id / a raw dataset — those fall through to the generic mock behavior, * unchanged). Accepts the BARE `<id>` handle — the dialect the deployed server speaks and the one the SDK's * `remoteTransport` now sends after kestrel-p6hz dropped the client-invented `catalog:` prefix — AND the * legacy `catalog:<id>` form, so both the new bare path and any prefix-passing caller resolve identically. */ function catalogSimEntry(artifactId) { if (typeof artifactId !== "string") return null; const id = artifactId.startsWith("catalog:") ? artifactId.slice("catalog:".length) : artifactId; return Object.prototype.hasOwnProperty.call(CATALOG_RECORDS, id) ? id : null; } /** Resolve a baked Session record by its graded-Session id — the SDK's durable Operation/artifact handle * (src/sdk/local.ts models an Operation id as the finalized Session id). `undefined` when unknown. */ function recordBySession(sessionId) { const entryId = SESSION_TO_ENTRY[sessionId]; return entryId !== undefined ? CATALOG_RECORDS[entryId] : undefined; } /** Resolve a baked Session record by its conformance root (the graded-bus sha256). `undefined` when unknown. */ function recordByRoot(root) { return Object.values(CATALOG_RECORDS).find((r) => r.conformanceRoot === root); } function offerResponse() { return { operation_id: SIM_OP_ID, offer: { offer_id: "of_mock_paper_01", operation_id: SIM_OP_ID, intent_hash: "ih_" + SIM_OP_ID, // the canonical intent this Offer prices scope: ["paper"], // out-of-trial scope → must convert ceiling: 500, amount: { value: "25.00", asset: "USDC" }, // Money = { value, asset } terms_digest: "sha256:termsdigest", expiry: "2099-01-01T00:00:00Z", nonce: "nonce_off_01", requires_human: false, }, proof: { proof_id: "pf_" + SIM_OP_ID, operation_id: SIM_OP_ID, intent_hash: "ih_" + SIM_OP_ID, artifacts: [], url: "https://kestrel.markets/proof/" + SIM_OP_ID, }, settlement_methods: [ { method: "stripe-mpp", network: "tempo", accepts: ["USDC"] }, // MPP first (DESIGN §5) { method: "x402", network: "base", accepts: ["USDC"] }, ], human_action: { kind: "claim-and-fund", url: "https://kestrel.markets/claim/" + SIM_OP_ID, required: false, label: "Send to your human to claim & fund", }, }; } const REPORT = { session: { date: "2024-05-07", instruments: ["SPY"], mode: "sim", fill_model: "maker-fair-v1", bus_events: 42, determinism_hash: "dh_mock", bus_sha256: "bs_mock", reprice_count: 0, settle_ts: 1715112000000, }, plans: [], orders: [], totals: { realized_floor_usd: 27, expected_usd: 30, premium_spent: 12 }, emitted: { PLAN: 1, ORDER: 2 }, }; const SAMPLE_BLOTTER = { sessionId: "bl_mock_1", orders: [], fills: [], positions: [], settlement: { realized: 0, asset: "usd", settledAt: "2024-05-07T20:00:00Z" }, }; const GRADE = { subjectSessionId: "dh_mock", metrics: { bankable_ev: 27.3, fill_support: 0.61 } }; /** client-facing idempotency ledger: Idempotency-Key → whether it has been seen. */ const seenIdempotencyKeys = new Set(); /** The contract fetch handler. Pass to RemoteBackend as `fetch`. */ export function mockFetch(url, init) { const u = new URL(url); const p = u.pathname; const method = (init?.method ?? "GET").toUpperCase(); const body = init?.body ? JSON.parse(String(init.body)) : {}; const headers = lowerHeaders(init?.headers); const json = (o, status = 200, extra = {}) => new Response(JSON.stringify(o), { status, headers: { "content-type": "application/json", ...extra } }); // effect-once: a replayed Idempotency-Key returns the prior result marked replayed. const replay = () => { const key = headers["idempotency-key"]; if (key === undefined) return {}; if (seenIdempotencyKeys.has(key)) return { "idempotent-replay": "true" }; seenIdempotencyKeys.add(key); return {}; }; // ── capability-discovery: POST /capabilities/trial ── if (p === "/capabilities/trial" && method === "POST") return Promise.resolve(json(TRIAL, 201, replay())); // ── validate: POST /validate (pure; no Operation, no idempotency) ── if (p === "/validate" && method === "POST") { const r = { valid: true, artifact_id: "art_plan_1", diagnostics: [] }; return Promise.resolve(json(r, 200)); } // ── catalog: GET /catalog → the deployed snake_case listing wire (kestrel-dnxq; public, no auth) ── if (p === "/catalog" && method === "GET") return Promise.resolve(json(CATALOG_LISTING_BODY, 200)); // ── sim: POST /sim → 201 Operation (direct) | 402 OfferResponse (paid dataset) ── if (p === "/sim" && method === "POST") { const dataset = body["dataset"]; // kestrel-djm.5: a real catalog-Session subject → an Operation whose id encodes the entry (its events // serve the baked catalog record). Non-catalog datasets are unchanged (paid → 402; else generic sim). const catEntry = catalogSimEntry(dataset?.artifact_id); if (catEntry !== null) return Promise.resolve(json(catalogOperation(SIM_CAT_PREFIX + catEntry), 201, replay())); if (isPaidDataset(dataset?.artifact_id)) return Promise.resolve(json(offerResponse(), 402)); return Promise.resolve(json(simOperation(), 201, replay())); } // ── grade: POST /grade → 201 Operation ── if (p === "/grade" && method === "POST") { // kestrel-djm.5: grading a baked catalog Blotter (its sessionId) → an Operation whose id encodes the // entry (its events serve the baked grade). A generic Blotter id → the generic grade, unchanged. const blotters = body["blotters"]; const b0 = blotters?.[0]; const catId = typeof b0 === "string" ? SESSION_TO_ENTRY[b0] : undefined; if (catId !== undefined) return Promise.resolve(json(catalogOperation(GRADE_CAT_PREFIX + catId), 201, replay())); return Promise.resolve(json(gradeOperation(), 201, replay())); } // ── artifacts: GET /artifacts/{ref} (kestrel-djm.5) ── // Resolve a content-addressed artifact ref to the SAME { ref, kind, value } the LOCAL projection returns // (src/sdk/local.ts artifact()): `blotter:<sessionId>` → the protocol Blotter; `sha256:<root>` → the // conformance root. So the SDK's HTTP transport returns a byte-identical ArtifactResult to LOCAL. An // unresolvable ref falls through to the hard 404 below (fail-closed, never a fabricated payload). const artMatch = p.match(/^\/artifacts\/([^/]+)$/); if (artMatch && method === "GET") { const ref = decodeURIComponent(artMatch[1]); if (ref.startsWith("blotter:")) { const rec = recordBySession(ref.slice("blotter:".length)); if (rec !== undefined) return Promise.resolve(json({ ref, kind: "blotter", value: rec.blotter }, 200)); } else if (ref.startsWith("sha256:")) { const rec = recordByRoot(ref.slice("sha256:".length)); if (rec !== undefined) return Promise.resolve(json({ ref, kind: "conformance-root", value: rec.conformanceRoot }, 200)); } // unresolvable artifact ref → fall through to the hard 404 below } // ── operations: GET /operations/{id} — the STATUS envelope ONLY ── // kestrel-o3bi: the plain operation resource carries the operation's control-plane STATUS, NOT its domain // OUTCOME. A COMPLETED operation returns { operation_id, status, cursor } with NO `payload` — the outcome // (blotter / report / receipt + artifact refs) is carried on the RESUMABLE EVENT STREAM below // (`GET /operations/{id}/events`), the SAME stream sim/grade consume. This reflects production: the earlier // mock FABRICATED a `payload:{ blotter, grade, artifacts }` here that the real endpoint never serves, which // masked the empty-payload resume defect a wire-first integrator hit. `resumeOperation` now reads the event // stream, so this endpoint is deliberately payload-free — a client that reads it for the outcome gets {} // (the decisive delete-the-call-site probe: revert the transport to this endpoint → empty payload → RED). const opMatch = p.match(/^\/operations\/([^/]+)$/); if (opMatch && method === "GET") { const id = decodeURIComponent(opMatch[1]); const rec = recordBySession(id); if (rec !== undefined) { // A finalized durable operation: COMPLETED, but the outcome lives on /events (no payload here). return Promise.resolve(json({ operation_id: id, status: "completed", cursor: null }, 200)); } const op = id === GRADE_OP_ID ? gradeOperation() : simOperation(); return Promise.resolve(json({ ...op, status: "completed", cursor: "c3" }, 200)); } // ── events: GET /operations/{id}/events (resumable SSE) ── const evMatch = p.match(/^\/operations\/([^/]+)\/events$/); if (evMatch && method === "GET") { const id = decodeURIComponent(evMatch[1]); const cursor = u.searchParams.get("cursor") ?? headers["last-event-id"] ?? null; // kestrel-o3bi: RESUME a finalized durable operation by its Session-id handle — replay the OUTCOME // ({ blotter, grade, artifacts }) on the resumable stream, byte-identical to the LOCAL projection. This is // the stream `resumeOperation` reads; a COMPLETED operation returns its full outcome here (the plain // status endpoint above is payload-free), closing the wire read-back the batch redirect points at. const finalizedRec = recordBySession(id); if (finalizedRec !== undefined) return Promise.resolve(resumptionSse(id, finalizedRec, cursor)); // kestrel-djm.5: a catalog Operation streams the baked record (sim) or the baked grade (grade). if (id.startsWith(SIM_CAT_PREFIX)) { const rec = CATALOG_RECORDS[id.slice(SIM_CAT_PREFIX.length)]; if (rec !== undefined) return Promise.resolve(catalogSse(id, rec, "sim", cursor)); } if (id.startsWith(GRADE_CAT_PREFIX)) { const rec = CATALOG_RECORDS[id.slice(GRADE_CAT_PREFIX.length)]; if (rec !== undefined) return Promise.resolve(catalogSse(id, rec, "grade", cursor)); } return Promise.resolve(sseResponse(id, cursor)); } // fail closed: an off-contract route is a hard 404 (problem+json), never a lie. return Promise.resolve(new Response(JSON.stringify({ type: "about:blank", title: "not_found", status: 404, code: "not_found" }), { status: 404, headers: { "content-type": "application/problem+json" }, })); } /** A canonical SSE stream (§9 Events + §"Cursor-based SSE resume"): `id:` is an * OPAQUE STRING; each `event:` is an OperationEvent.type; the domain payload rides * opaquely in the OperationEvent `data`; terminal is `operation.completed`. A client * re-opening at cursor `c2` gets only the tail (resume). */ function sseResponse(opId, resumeFrom) { const isGrade = opId === GRADE_OP_ID; const artifactType = isGrade ? "operation.receipt" : "operation.artifact"; const artifactData = isGrade ? { artifact: { artifact_id: "gr_mock_1", kind: "grade" }, grade: GRADE } : { artifact: { artifact_id: SAMPLE_BLOTTER.sessionId, kind: "blotter" }, report: REPORT, blotter: SAMPLE_BLOTTER, wakes: [] }; const all = [ { id: "c1", type: "operation.started", data: {} }, { id: "c2", type: artifactType, data: artifactData }, { id: "c3", type: "operation.completed", data: {} }, ]; // resume: replay only events strictly AFTER the given opaque cursor. const start = resumeFrom ? all.findIndex((e) => e.id === resumeFrom) + 1 : 0; const emit = all.slice(Math.max(0, start)); const frames = emit.map((e) => { const env = { cursor: e.id, type: e.type, operation_id: opId, data: e.data }; return `id: ${e.id}\nevent: ${e.type}\ndata: ${JSON.stringify(env)}\n\n`; }); const stream = new ReadableStream({ start(c) { const enc = new TextEncoder(); for (const f of frames) c.enqueue(enc.encode(f)); c.close(); }, }); return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } }); } /** * The catalog-conformant SSE stream (kestrel-djm.5): the SAME resumable framing as {@link sseResponse}, but * the artifact/receipt frame carries the BAKED record — for `sim`, the protocol Blotter PLUS the full * server-computed Session record (`session`: id + pentad-chained entries + delivered frames + tip + root + * artifacts) the HTTP transport replays; for `grade`, the baked portable GradeResult. So the HTTP transport * returns byte-identical protocol objects to the LOCAL projection (the djm.5 headline). */ function catalogSse(opId, record, mode, resumeFrom) { const artifactType = mode === "grade" ? "operation.receipt" : "operation.artifact"; const artifactData = mode === "grade" ? { artifact: { artifact_id: `gr_cat_${record.entryId}`, kind: "grade" }, grade: record.grade } : { artifact: { artifact_id: record.blotter.sessionId, kind: "blotter" }, blotter: record.blotter, session: { sessionId: record.sessionId, blotter: record.blotter, entries: record.entries, frames: record.frames, tipHash: record.tipHash, conformanceRoot: record.conformanceRoot, artifacts: record.artifacts, }, }; const all = [ { id: "c1", type: "operation.started", data: {} }, { id: "c2", type: artifactType, data: artifactData }, { id: "c3", type: "operation.completed", data: {} }, ]; const start = resumeFrom ? all.findIndex((e) => e.id === resumeFrom) + 1 : 0; const emit = all.slice(Math.max(0, start)); const frames = emit.map((e) => { const env = { cursor: e.id, type: e.type, operation_id: opId, data: e.data }; return `id: ${e.id}\nevent: ${e.type}\ndata: ${JSON.stringify(env)}\n\n`; }); const stream = new ReadableStream({ start(c) { const enc = new TextEncoder(); for (const f of frames) c.enqueue(enc.encode(f)); c.close(); }, }); return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } }); } /** * The RESUME stream for a FINALIZED durable operation (kestrel-o3bi): the SAME resumable framing as * {@link sseResponse}, whose single `operation.artifact` frame carries the operation's OUTCOME — * `{ blotter, grade, artifacts }` — so a client that replays this stream (`resumeOperation`) accumulates * exactly the payload the LOCAL projection returns (src/sdk/local.ts resumeOperation). This is the stream the * outcome is served on; the plain `GET /operations/{id}` status endpoint is payload-free. */ function resumptionSse(opId, record, resumeFrom) { const all = [ { id: "c1", type: "operation.started", data: {} }, { id: "c2", type: "operation.artifact", data: { blotter: record.blotter, grade: record.grade, artifacts: record.artifacts } }, { id: "c3", type: "operation.completed", data: {} }, ]; const start = resumeFrom ? all.findIndex((e) => e.id === resumeFrom) + 1 : 0; const emit = all.slice(Math.max(0, start)); const frames = emit.map((e) => { const env = { cursor: e.id, type: e.type, operation_id: opId, data: e.data }; return `id: ${e.id}\nevent: ${e.type}\ndata: ${JSON.stringify(env)}\n\n`; }); const stream = new ReadableStream({ start(c) { const enc = new TextEncoder(); for (const f of frames) c.enqueue(enc.encode(f)); c.close(); }, }); return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } }); } function lowerHeaders(h) { const out = {}; if (!h) return out; if (h instanceof Headers) { h.forEach((v, k) => (out[k.toLowerCase()] = v)); } else if (Array.isArray(h)) { for (const pair of h) { const k = pair[0]; if (k !== undefined) out[k.toLowerCase()] = String(pair[1] ?? ""); } } else { for (const [k, v] of Object.entries(h)) out[k.toLowerCase()] = String(v); } return out; } export const CONTRACT_PROTOCOL_VERSION = PROTOCOL_VERSION;