Blocks
A block is a small immutable record with apayload wrapped in an envelope. The envelope is what
block_id is computed over, and it has exactly five keys:
Content a block names but does not carry
A payload is JSON, canonically serialized and hashed on every access, so a datum large enough to matter does not belong inside one. A block may instead name its content by digest and leave the bytes in the store — which is what a canonical block has always done with the original it describes.put_content stores the bytes and returns a ContentRef for a payload to name. It writes no block and
publishes no snapshot: until a committed block names them, the bytes are unreachable and a prune
reclaims them.
The reference it returns is a fact, not a claim: size is measured from the bytes, and media_type
must be a bare, lowercase type/subtype — no parameters, no trailing punctuation. Media types are
parsed with the standard library’s RFC 2045 parser and then required to round-trip exactly, because
image/png, IMAGE/PNG and image/png; charset=utf-8 are the same type to compare and three
different strings to hash. Both fields are hashed into the resulting block_id and are what a consumer
reads to decide whether to fetch content it does not hold, so a wrong value cannot be corrected later —
only superseded by a different block. A payload assembled by a proposer rather than by put_content is
checked in the validation gate instead, which also compares the declared size against the stored bytes
when the brain holds them.
Both checks sit on the write path and never on decode. NormalizedView extends ContentRef and takes
its media type from a third-party normalization pipeline, so a malformed one may already be inside a
published block_id; refusing it at decode would make this SDK unable to read a brain an older one
wrote.
Canonical has named its original since the beginning. Semantic, episodic and procedural blocks gained an
optional content field in schema version 2, so an interpretation whose subject is an image, a
recording, or any other file can state what it claims about that file without inlining it. The text stays
required — statement, summary and goal are what a natural-language query can reach, and when the
content is binary they are the interpretation.
A block is written under the oldest schema its payload satisfies. A payload naming no content is
still built as v1, so adding a version does not re-version knowledge that does not use it. Since
schema_version is inside the hashed envelope, that choice is a choice of block_id: a brain only
becomes unreadable to an older client at the point where it genuinely uses something that client has
no schema for.Content is not evidence. Evidence is canonical — it lives in the canonical composition, other blocks
cite it, and dropping it cascades to everything derived from it. Content is the block’s own datum, so
nothing cites it and nothing needs to; it lives and dies with its block. A source other blocks will cite
is a canonical block, through
register.content_digests rather than testing its type. So a
schema that starts naming content is handled correctly by all of them at once, and an
index is handed a reader for exactly this reason.
Publishing across SDK versions
An artifact declares which schema versions each of its modules holds, in theai.gaussia.boltzmann.schema-versions manifest annotation. A pull checks it against the schemas the
client implements, scoped to the modules being installed, and refuses before fetching any blob:
Compositions
AComposition is an immutable set of block identities committed by a single Merkle root. Deriving one
returns a new one:
composition.json). Without it a snapshot would identify a version it could not reopen.
Modules
AModule is one memory module at one version: a composition, the store its bytes live in, and its
indices.
with_blocks and without_blocks derive new versions. Module exposes no write method at all —
extending a brain goes through commit, which is the only write path.
Snapshots
ASnapshot is the state of a brain: one ModuleRef per installed module, plus the snapshots it succeeds,
which form an auditable history.
parents is a list because history is a DAG rather than a chain: a linear history carries one entry, a root
snapshot none, and a snapshot that joined two histories two or more. Order matters in exactly one way — the
first parent is the one every rule meaning the parent refers to. See
Reconciliation.
Each ModuleRef carries the module’s root, the composition digest, the block_count, the Merkle
layout identifier, the identities whose bytes were deliberately destroyed in tombstones, and the
embedding_model behind a travelling vector index when one ships with it. Tombstones remain composition
members: they preserve membership while making the loss explicit and signed. Snapshots written before the
field existed remain readable; newly written snapshots always emit the list, including when it is empty.
A brain may hold a subset of modules. Selective installation is the point of packaging each module
separately, so “not installed” is a legitimate state — and therefore an error rather than an empty
module when you ask for one you do not have.
ancestry and reachable_history answer different questions. The first is the line an audit follows. The
second is what a containment check asks — a history merged in is genuinely contained without appearing on
the first-parent chain, which is why a push compares against that one.The store
Blocks live in aBlockStore. Two ship with the SDK:
A store must not normalize: bytes that are not canonical do not decode, and bytes that do not hash to
the digest they are filed under are refused. A redacted block is tombstoned, never silently missing —
a removed block must never look like a corrupted one.