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.

101 lines (90 loc) 6.17 kB
# OSS-ADR-0052The Rendering seam: one kernel/tape walk, format adapters, and the human chart Status: accepted (2026-07-18, owner grill — 2026-07-17 depth review, G1) ## Context The Frame has two renderers kept in sync by hand: `src/frame/render.ts` (canonical) and `src/frame/render-arm.ts` (the token-optimal ARM) each independently walk the 8-section SAFETY/CONTROL kernel (`KERNEL_SECTION_LABELS`) and re-materialize the 40-column tape geometry, with near-verbatim copies of `restingState`, `sizingLine`, `ENGINE_LOG_BUCKETS`, and the action-id helpers. The only bridge is the after-the-fact information-parity gate, and the renderer epoch (`renderer-revision.ts`) is a hand-bumped constant whose own docstring has already drifted from the value (prose `/5`, constant `/8`) — the behaviourepoch link is prose, not a check. Meanwhile `RequestedFormat` admits `json` and `html` that nothing produces: the format seam has exactly one adapter (text). The cost surfaced concretely — the generated stock/options education videos rendered the *agent's* vertical tape, not a human candlestick chart, because the agent tape is the only Rendering that exists. CONTEXT.md's Rendering entry already promises the other half ("the agent's ASCII screen and the human's HTML chart are two Renderings of one Frame"); Caller doctrine says a human gets the human Rendering. There was no human Rendering to serve. ## Decision 1. **One walk.** The kernel/tape model is materialized once, in one deep module. Canonical text, the per-tokenizer ARM texts (the `TOKEN_OPTIMAL_ARMS` registry), and `json` are **format adapters** at the Rendering seam. The information-parity gate becomes a property of the seam, not a bridge between hand-kept copies. **Proof obligation: the canonical adapter reproduces today's canonical bytes exactly** (golden + parity fixtures) — canonical is the frozen leaderboard control; the refactor lands byte-identical or not at all. 2. **The `json` Rendering is materialized.** Frame typed JSON carrying Fields whole (`value` + Attribution + SourceWatermark). A Rendering never invents or changes a value; `null` Fields cross the wire as explicit UNKNOWN. 3. **The human visualization layer is a Rendering adapter package, shipped from this repo** as a subpath export of the published package (`kestrel.markets/react`). **Isolation is a requirement, not a hope**: the subpath is its own `exports` entry (the CLI/agent entry never resolves it), and `react` is a peer dependency marked optional (`peerDependenciesMeta.react.optional: true`), so the `npx` funnel installs nothing new and loads nothing new — a consumer needs React only if they import the subpath. Two component families: - the **honest chart** — standard human candlesticks consuming the Frame `json` Rendering: UNKNOWN renders as a gap (never an interpolated candle), `MODEL` Fields are badged with their receipt, provenance is inspectable. No generic-OHLC entry point: a chart that can't prove it invented nothing defeats the point. - the **terminal view** — displays an *existing text Rendering* (canonical or an ARM chosen by a tokenizer/model prop). It renders bytes the engine already emits; it is a shell, never a fourth renderer. 4. **React hosts Renderings; it never produces them.** This is the invariant uniting the web components above with the already-decided Ink oversight TUI (human-oversight PRD 2026-07-14, build decision 11; kestrel-jvr4.4): both are shells that *display* engine output — the web terminal-view shows text Renderings the engine already emitted; the Ink TUI is a strict view over `OversightFrame` that invents no value, while every one-shot verb keeps its byte-stable `text`/`json`/`ascii` Rendering untouched ("strip the SGR wrappers and the exact text bytes remain"). No React reconciler — DOM or Ink — ever sits inside a Rendering: renderers stay pure `Frame bytes` functions on the deterministic path. The earlier spike recommendation against Ink (kestrel-5kt, 2026-07-12: @clack over Ink — install weight, raw-mode-on-pipes) is superseded on the *choice* by decision 11, but its concerns survive as constraints: the TUI rides the same isolation discipline as `/react` — its own entrypoint, lazily loaded, never a hard dependency the `npx` funnel pays for, and never attached to a piped/ non-interactive stream (raw-mode fails open on pipes; the Caller check already routes agents to machine Renderings). 5. **The renderer epoch is derived, not hand-typed.** The epoch that stamps session identity is computed from the renderer's observable behaviour ( mechanism chosen at implementation: e.g. a hash over the golden-fixture render corpus), so a byte-changing edit cannot ship without the epoch moving. ## Considered and rejected - **Formalize a `FrameRenderer` interface but keep both implementations** — the seam gets a type, the copy-paste cost stays forever. - **Freeze canonical untouched; dedupe only ARM + json** — zero byte-risk, but two walks remain and every kernel-section change still lands twice. - **Platform-side visualizer** — the dashboard gets components but OSS docs/blog/videos cannot import them, and outsiders cannot render Frames the way we do; the trust substrate should ship its own legible visualization. - **Generic OHLC props on the chart** — maximum reuse, but the chart loses Field provenance and with it the honest-chart claim. ## Consequences - Caller doctrine becomes servable at the content layer: human-facing content (dashboard, docs, blog, videos) defaults to the human Rendering; the agent screen appears only when the subject *is* the agent's perception. - Kernel-section and tape-geometry bugs concentrate in one module; the duplicated helpers are deleted. - `TOKEN_OPTIMAL_ARMS` becomes the seam's adapter registry — the terminal view's model-selector prop reads it directly. - The parity gate survives as the seam's regression harness, and doubles as the byte-identity check for the refactor itself.