Raxol. AgentClientProtocol. Transport. Envelope
(Raxol Agent Client Protocol v0.1.0)
Copy Markdown
View Source
A pure, process-free sequencing envelope for unordered-carrier
transports (no shipped transport uses it; see
Raxol.AgentClientProtocol.Transport.Reassembly for the receive-side
counterpart).
Every shipped transport (Transport.Stdio, Transport.Paired) is
already an ordered carrier: frames arrive at the owner in exactly the
order the peer sent them, so no sequencing is needed. A carrier
that reorders frames in flight (e.g. frames fanned over several ordered
streams, or datagrams with app-level retransmit below this layer) can
satisfy the same ordered-delivery contract by wrapping each frame with a
sender-assigned, per-connection, per-direction monotone counter (tseq)
before handing it to the carrier, and unwrapping + reassembling
(Reassembly) on receipt — restoring order before the frame is ever
handed to the owner. The Connection, the Session, the Client, and the
ACP wire never observe tseq; it lives and dies entirely inside the
transport.
Wire shape
The envelope wraps a frame map with two extra top-level fields:
%{"acpenv" => 1, "s" => tseq, "f" => frame}"acpenv"— envelope format version (currently always1)."s"— the sender-assignedtseq, a positive integer, monotone per connection per direction. This is transport-internal framing, never ACP protocol content: it is stripped byunwrap/1before the frame reaches anything above the transport, and it must never be threaded through_metaor any other ACP-visible location (a peer-visible sequence number is not trusted for ordering, which is why the Connection's delivery stamp one layer up is receiver-assigned)."f"— the original JSON-RPC frame (request, response, or notification map), untouched.
A concrete carrier may re-encode this shape however its wire format
requires (e.g. as a binary header instead of JSON keys); the three
fields — version, sequence, frame — are what's normative, and this
module's wrap/2 / unwrap/1 give the reference (map-shaped)
implementation used by pure tests and by any carrier happy to use JSON
maps directly.
Purity
Both functions are total, deterministic, and side-effect-free: no
process, no I/O, no shared state. The caller (a transport's
single-writer send path, mirroring the Framer's threading pattern) is
responsible for minting successive tseq values and for threading
Reassembly state across push/3 calls on receipt.
Summary
Types
The wire-shaped envelope map produced by wrap/2.
One JSON-RPC frame (a decoded map), unmodified by the envelope.
Sender-assigned, per-connection, per-direction monotone sequence number (starts at 1).
Functions
Unwrap an enveloped map, returning its tseq and inner frame.
Wrap frame with sequencing metadata for tseq.
Types
The wire-shaped envelope map produced by wrap/2.
@type frame() :: map()
One JSON-RPC frame (a decoded map), unmodified by the envelope.
@type tseq() :: pos_integer()
Sender-assigned, per-connection, per-direction monotone sequence number (starts at 1).
Functions
Unwrap an enveloped map, returning its tseq and inner frame.
Succeeds only for a map with the exact envelope shape — the current
"acpenv" version, an "s" that is a positive integer, and an "f"
that is a map. Anything else (a bare un-enveloped frame, a future/past
envelope version, a malformed "s"/"f") yields
{:error, :not_enveloped} rather than raising: the caller (the
receiving carrier) decides whether that is a protocol violation or
simply "not this envelope version" — this module only recognizes its
own shape.
Round-trips with wrap/2: unwrap(wrap(frame, tseq)) == {:ok, tseq, frame}
for every valid frame/tseq pair. The returned frame is
byte-for-byte (term-for-term) identical to the wrapped input — the
envelope carries no rider that survives into it.
Wrap frame with sequencing metadata for tseq.
frame must be a map (a decoded JSON-RPC frame); tseq must be a
positive integer. Returns the envelope map
%{"acpenv" => 1, "s" => tseq, "f" => frame}.