symfonic.kernel.contracts.resolved¶
resolved ¶
The resolved-input snapshot — what an effectful stage hands a pure one.
STG-7 as reformulated splits prompt-assembly into two kinds of stage:
resolution, which may perform I/O once per invocation under a grant, and
compilation, which is a pure function of (plan, request, snapshot). This
module is the snapshot, and the whole reason it can sit between them.
Neutral. The kernel never interprets an entry's value. A memory recall,
a retrieved document set and a fetched policy are all one shape to it: a
capability's name, a payload, and a provenance string. That is what lets two
capabilities compose without importing each other — the seam
capabilities/memory/contribution.py already describes as kept "by
vocabulary, not by an import". A kernel that understood the payload would be a
third party to that agreement.
Typed. Not dict[str, Any] and not a bare tuple. An entry knows which
capability produced it and where the value came from, because the question a
reader asks of a compiled prompt is "why is this line in here?", and a payload
alone cannot answer it.
Immutable, and that has to reach the payload. A compilation stage that could
mutate the snapshot would make the compile depend on which compilation stage ran
first, which is precisely the reproducibility STG-7 exists to keep.
:meth:ResolvedInputs.extend returns a new snapshot rather than growing this
one — but review of PR #94 found that freezing only the outer mapping is not
enough, because value kept mutable payloads by reference. Reproduced: a
capability contributing ({"contribution_id": ...},) had that dict rewritten
by one compiler and the next compiler read the rewritten version.
So containers are frozen on the way in — dict to a read-only mapping,
list to tuple, set to frozenset, recursively — and the entries
mapping is copied defensively rather than adopted, since a caller holding the
original dict could otherwise inject entries after construction and bypass the
one-entry-per-capability rule as well.
Fail-closed on what cannot be represented. The first version of this fix
froze containers and left unrecognised objects by reference, with the limit
written in a docstring. Review was right that a note is not a boundary: a
resolver handing over a nested mutable object still let one compilation stage
rewrite what the next one compiled. Construction now refuses any payload it
cannot vouch for — scalars, mappings, sequences, sets and frozen dataclasses
are admitted and traversed; anything else names itself and its path in the
refusal. Mapping keys are checked on the same terms as values: deep_freeze
rebuilds a mapping keeping its keys by reference, so a hashable-but-mutable key
would otherwise slip through the one place nobody looks.
This is deliberately stricter than :func:is_deeply_frozen, which returns
True for an unrecognised object because IPL-4 exempts plan group G6's live
ports. A payload is not a port: it is data a pure compilation stage reads, so an
object nobody can vouch for is a hole rather than an exemption.
ResolvedInput
dataclass
¶
One capability's resolved contribution to this turn.
value is opaque to the kernel and meaningful only to the capability that
compiles it. provenance is not decoration: it is how a reader of a
compiled prompt gets from a line back to the retrieval that produced it,
and how a digest of the snapshot means something.
ResolvedInputs
dataclass
¶
The frozen set of resolved inputs a compilation stage may read.
Keyed by capability because that is the unit a compiler asks for — "did memory resolve anything this turn?"
One entry per capability is a constraint of this ledger, not a claim about stages. A capability may declare as many resolution stages as it needs; what it may not do is leave two entries under one name, because then "what did memory resolve?" would depend on which stage ran last and one of the two would vanish silently. A capability that reads from several places composes its own result and hands over one entry. If a real case ever needs several entries under one name, the fix is to widen the ledger deliberately — a list per capability, or namespaced keys — not to let the last writer win.
extend ¶
Return a new snapshot with entry added. Never mutates.
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
if the capability already left an entry this turn. Two under one name would make "what did memory resolve?" depend on which stage ran last, and silently drop one. This bounds the ledger, not how many resolution stages a capability may declare. |
Source code in src/symfonic/kernel/contracts/resolved.py
of ¶
What capability resolved, or None if it resolved nothing.
None rather than a raise: a compiler that works with memory and
without it is the normal case, and "this capability is not installed"
is not an error condition for the stage that reads it.