Wire encoding for the HTTP gateway.
One dumb, deterministic boundary: engine EDN -> JSON. Keyword/symbol
keys become snake_case strings (namespace dropped), keyword VALUES keep
their full ns/name (a badge role like :tool-color/search must survive
the hop — dropping the namespace made the remote TUI see :search while
the in-process web saw the full keyword), non-JSON leaves fall back to
str. The walker makes ZERO semantic decisions - no flattening, no
rendering. Canonical IR vectors pass through structurally
([:ir {...} ...] -> ["ir", {...}, ...]), which is exactly the
ALWAYS-IR contract: the client walks IR; the gateway never renders.
canonical is the SAME shape on the Clojure side: by definition what
parse-json ∘ json-str yields — serve it from a facade and in-process
readers see exactly what a remote client sees.
Wire encoding for the HTTP gateway.
One dumb, deterministic boundary: engine EDN -> JSON. Keyword/symbol
keys become snake_case strings (namespace dropped), keyword VALUES keep
their full `ns/name` (a badge role like `:tool-color/search` must survive
the hop — dropping the namespace made the remote TUI see `:search` while
the in-process web saw the full keyword), non-JSON leaves fall back to
`str`. The walker makes ZERO semantic decisions - no flattening, no
rendering. Canonical IR vectors pass through structurally
(`[:ir {...} ...]` -> `["ir", {...}, ...]`), which is exactly the
ALWAYS-IR contract: the client walks IR; the gateway never renders.
`canonical` is the SAME shape on the Clojure side: by definition what
`parse-json` ∘ `json-str` yields — serve it from a facade and in-process
readers see exactly what a remote client sees.(->wire x)Recursively convert an engine value into JSON-encodable data.
Recursively convert an engine value into JSON-encodable data.
(bounded-pr x limit)Bounded pr-str for tool results / errors riding events. Protects the
event log and SSE frames from multi-megabyte values.
Bounded `pr-str` for tool results / errors riding events. Protects the event log and SSE frames from multi-megabyte values.
(bounded-str s limit)Bounded plain-string clamp for an ALREADY-rendered value (e.g. the
model-facing render-form-value string) — same megabyte protection as
bounded-pr but WITHOUT re-pr-str'ing, so the string rides the wire
verbatim instead of quoted/escaped.
Bounded plain-string clamp for an ALREADY-rendered value (e.g. the model-facing `render-form-value` string) — same megabyte protection as `bounded-pr` but WITHOUT re-`pr-str`'ing, so the string rides the wire verbatim instead of quoted/escaped.
(canonical x)THE canonical gateway value shape — BY CONSTRUCTION identical to what a
remote client holds after parse-json ∘ json-str: snake_case KEYWORD
map keys (namespaces dropped), keyword/symbol values stringified, dates
as epoch millis. Serve (canonical x) from a facade and the HTTP hop
becomes an IDENTITY — in-process and remote consumers read the SAME
shape, so a channel written against one transport can never break on
the other. Invariant (guarded by the wire round-trip test):
(canonical x) == (parse-json (json-str x)) for every engine value.
THE canonical gateway value shape — BY CONSTRUCTION identical to what a remote client holds after `parse-json` ∘ `json-str`: snake_case KEYWORD map keys (namespaces dropped), keyword/symbol values stringified, dates as epoch millis. Serve `(canonical x)` from a facade and the HTTP hop becomes an IDENTITY — in-process and remote consumers read the SAME shape, so a channel written against one transport can never break on the other. Invariant (guarded by the wire round-trip test): `(canonical x)` == `(parse-json (json-str x))` for every engine value.
(json-str x)Encode any engine value as a JSON string via ->wire.
Encode any engine value as a JSON string via [[->wire]].
(json-str-pretty x)Pretty-printed (2-space indent) JSON via ->wire — for
HUMAN-facing surfaces (the web ctx rail's trailer view), never the
wire itself.
Pretty-printed (2-space indent) JSON via [[->wire]] — for HUMAN-facing surfaces (the web ctx rail's trailer view), never the wire itself.
(kebab-keys x)Recursively rewrite every MAP KEY snake_case -> kebab-case — the structural
INVERSE of the -->_ munge [[wire-key]]/->wire apply on the way out.
Values pass through UNTOUCHED (paths, labels, ids never mutate).
Deliberately OPT-IN, applied only at a TYPED boundary whose keys are known to
be engine keywords (e.g. the gateway workspace record) — NOT folded into
parse-json: once JSON has flattened every key to a string the encoder's
keyword-vs-string distinction is gone, so a legit STRING key that genuinely
carries an underscore (a fact key, a scope like turn_5, a file path) is
indistinguishable from a munged keyword and a blanket rewrite would corrupt it.
Where the payload has no such string keys, this restores the engine's kebab
shape LOSSLESSLY and — unlike a hand-maintained rename map — can never silently
miss a newly-added key. Idempotent on already-kebab input.
Recursively rewrite every MAP KEY snake_case -> kebab-case — the structural INVERSE of the `-`->`_` munge [[wire-key]]/[[->wire]] apply on the way out. Values pass through UNTOUCHED (paths, labels, ids never mutate). Deliberately OPT-IN, applied only at a TYPED boundary whose keys are known to be engine keywords (e.g. the gateway workspace record) — NOT folded into [[parse-json]]: once JSON has flattened every key to a string the encoder's keyword-vs-string distinction is gone, so a legit STRING key that genuinely carries an underscore (a fact key, a scope like `turn_5`, a file path) is indistinguishable from a munged keyword and a blanket rewrite would corrupt it. Where the payload has no such string keys, this restores the engine's kebab shape LOSSLESSLY and — unlike a hand-maintained rename map — can never silently miss a newly-added key. Idempotent on already-kebab input.
(parse-json s)Parse a JSON request body into keyword-keyed Clojure data. Returns nil on blank or malformed input (callers map that to 400).
Parse a JSON request body into keyword-keyed Clojure data. Returns nil on blank or malformed input (callers map that to 400).
Queue lifecycle event types every attached channel mirrors LIVE even when
they belong to a DIFFERENT (queued) turn of the same session — the ONE set
both transports forward (the in-process gateway.state subscriptions AND
the SSE loop in gateway.client), so a message queued/edited/deleted in
one channel shows up in every sibling. turn.queued.drained marks the
queue head leaving the queue because the gateway auto-STARTED it, so
mirrors drop the entry and a replayed history nets to zero
(turn.queued … turn.queued.drained).
Queue lifecycle event types every attached channel mirrors LIVE even when they belong to a DIFFERENT (queued) turn of the same session — the ONE set both transports forward (the in-process `gateway.state` subscriptions AND the SSE loop in `gateway.client`), so a message queued/edited/deleted in one channel shows up in every sibling. `turn.queued.drained` marks the queue head leaving the queue because the gateway auto-STARTED it, so mirrors drop the entry and a replayed history nets to zero (`turn.queued` … `turn.queued.drained`).
(sse-frame {:keys [seq type] :as event})Render one event map as an SSE frame. The event's :seq doubles as
the SSE id: so Last-Event-ID reconnects resume losslessly.
Render one event map as an SSE frame. The event's `:seq` doubles as the SSE `id:` so `Last-Event-ID` reconnects resume losslessly.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |