Repository navigation
refactor(intrinsics): resolve the adapter's output contract from the adapter, not a parallel argument (Epic #929 Phase 2) #1516
Description
Parent epic: #929
Design discussion: #1486
Phase: 2
Blocks: #1144
Problem
An adapter's output contract and the adapter itself travel two separate paths, and nothing
ties them together.
call_intrinsic resolves the adapter and then discards it:
# mellea/stdlib/components/intrinsic/_util.py:185
backend.resolve_adapter(intrinsic_name)The contract it needs arrives instead as a separate parameter (_util.py:150), which each
caller supplies from a module-level Adapter constant — ten call sites, six in rag.py
and four in guardian.py:
# mellea/stdlib/components/intrinsic/rag.py:208
io_contract=_ANSWERABILITY_ADAPTER.io_contract,So the object that describes how to parse the output is chosen by the caller, while the
adapter that actually produced the output is resolved by the backend. Nothing in the type
system requires the two to refer to the same capability. Passing the wrong constant is a
silent mismatch.
The obvious fix — read io_contract off the resolved adapter — does not work today, and
that is the substance of this issue.
The blocker
resolve_adapter returns adapters whose io_contract is a placeholder that raises:
# mellea/backends/adapters/adapter.py:87
class _ShimIOContract(IOContract):
"""Phase 1 placeholder; Phase 2 (issue #1137) implements real I/O."""
def parse(self, raw: str) -> dict[str, object]:
raise NotImplementedError(...)Both shim adapter classes construct with it — IntrinsicAdapter (adapter.py:257) and
EmbeddedIntrinsicAdapter (adapter.py:663) — and those are what resolution hands back.
Reading the contract off the resolved object today therefore raises on every adapter
function call.
The docstring above points at #1137 as the issue that implements real I/O, but #1137 is
closed. It built the real contracts as the module-level constants in rag.py and
guardian.py; the resolution path never got them. That is the gap: real contracts exist,
but only on the far side of the split this issue is about.
Behaviour change to account for
core.py's three helpers (check_certainty, requirement_check,
find_context_attributions) pass no contract at all today, so their output goes through
raw json.loads (_util.py:219). Once resolution supplies a real contract they would
start validating. That is a behaviour change, not a refactor: each needs its expected
output contract declared and verified against current weights, the same way #1137 did for
rag.py.
requirement_check additionally hand-rolls its own validation after the call
(core.py:~72, mirroring requirement_check_to_bool() in requirement.py) with a comment
saying Phase 2 will consolidate it via IOContract. That consolidation belongs here.
Scope
mellea/stdlib/components/intrinsic/_util.py—call_intrinsic- The resolution path in
mellea/backends/adapters/adapter.py core.py's three helpers gain declared output contracts- The ten
io_contract=call sites inrag.pyandguardian.py
Out of scope
- Removing the shim adapter classes — that is refactor(intrinsics): remove deprecation shims; rewrite intrinsics_and_adapters.md; write 3 tutorials (Epic #929 Phase 4) #1144, Phase 4, and needs this first
WeightsBindingverbs — feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1141, feat(backends): EmbeddedBinding implements apply_activation (Granite Switch path); remove render_controls + set_request_adapter (Epic #929 Phase 2) #1142- Output-schema versioning — design: adapter output-schema versioning (deferred from #929) #1111
Acceptance criteria
-
call_intrinsicuses the adapter returned byresolve_adapterrather than discarding it - The output contract is obtained from that resolved adapter, not from a separate argument
-
call_intrinsicno longer takes anio_contractparameter - Resolution yields real contracts; no adapter reachable through
resolve_adaptercarries_ShimIOContract -
core.py's three helpers each have a declared, documented output contract -
requirement_check's hand-rolled post-call validation is replaced by its contract - Supplying a capability name whose contract disagrees with the resolved adapter is no longer expressible
-
ruff format,ruff check,mypyclean
Test plan
- Per helper across
rag.py,guardian.py,core.py: output missing a required field
raisesAdapterSchemaMismatchError; output with an extra optional field does not core.py's three helpers: contracts verified against current weights, with the
verification recorded in the PR description (as refactor(intrinsics): rag.py whole-file migration to new Adapter types (Epic #929 Phase 1) #1137 did)- Existing adapter-function tests pass unchanged (behavioural neutrality on the happy path)
- No test constructs an adapter whose
io_contractis_ShimIOContract
Activity
planetf1 commentedon Aug 11, 2026
Extra scope found while finishing #1454: the ten constants carry placeholder bindings
Discovered by trying the "cross-check only" stopgap from #1486 — having
Adapter.__post_init__reject a binding whoseadapter_typedisagrees with itsidentity's — and having to back it out. Recording it here because this issue is the one
that gives these constants real content.
What's there today. The ten module-level
Adapterconstants inmellea/stdlib/components/intrinsic/rag.py(6) andguardian.py(4) are each built likethis:
So each declares
alorain its identity and pairs it with a bareLocalFileBinding()whose
adapter_typedefaults toAdapterType.LORA, whosenameandrepo_idare emptystrings, and whose
revisionwas"main"until #1454 changed the default toNone(= "use the catalogue's pinned revision").
Why it isn't a bug today. Nothing resolves these constants — that's this issue's whole
premise — so
weightsis never used, and every catalogue entry supports bothloraandalora, soalorais a legitimate choice rather than a contradiction. They areunconfigured placeholders, not inconsistent objects. That distinction is why the
cross-check had to come out: it fired on "not configured yet", which is the wrong signal,
and it broke collection of eight test modules at import time.
Why it matters for this issue. Once resolution hands back these
Adapterobjectsrather than
_AdapterCoreshims, theirweightsstops being decorative, and three thingsbecome live at once:
identity.adapter_typesaysalorawhileweights.adapter_typesayslora, and thetwo feed different lookup paths — registration and every verb key on the binding's
qualified_name, while_find_adapterscans on the identity. Both returnNoneon amiss, so a disagreement surfaces as "adapter not found" a long way from its cause.
name=""makesqualified_name"_lora"for all ten, so they collide with eachother in
_added_adapters. feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1454 added a loudprepare()guard against an emptyname, so this fails clearly rather than silently — but it does fail.
repo_id=""means there's nothing forobtain_lorato download from.Suggested addition to this issue's scope: give the ten constants real bindings —
LocalFileBinding.from_catalog(<capability>)is the obvious candidate, though note itresolves
adapter_types[0](lora) rather than thealorathe identities currentlydeclare, so which of the two is intended needs deciding rather than assuming. Then
enforce the invariant: reinstate the
Adapter.__post_init__cross-check, so this class ofdrift is caught at construction instead of as a silent lookup miss.
_core.pycarries aNOTE(#1516)at the spot with the same reasoning.Related: @AngeloDanducci found the same drift in #1454's own tests, where an identity
saying
alorasat next to a binding that had loaded theloraweights and the testpassed because nothing asserted the identity. Those two call sites are fixed in #1454;
the systemic version is the ten above.
feat(backends): implement LocalFileBinding verbs and from_catalog()
feat(backends): implement LocalFileBinding verbs and from_catalog()
feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) +…
test(intrinsics): add contract-wiring tests and fix docstring accuracy
refactor(intrinsics): drop dead per-helper Adapter constants, consoli…
2 remaining items
refactor(intrinsics): resolve the output contract from the adapter, n…
test(intrinsics): add contract-wiring tests and fix docstring accuracy
refactor(intrinsics): drop dead per-helper Adapter constants, consoli…
docs(intrinsics): fix increment review findings — stale docs, unguard…
fixup: leave io_contract bullet untouched (owned by generative-comput…
chore(adapters): split EmbeddedIntrinsicAdapter's io_contract/weights…
refactor(intrinsics): resolve the output contract from the adapter, n…
test(intrinsics): add contract-wiring tests and fix docstring accuracy
refactor(intrinsics): drop dead per-helper Adapter constants, consoli…
docs(intrinsics): fix increment review findings — stale docs, unguard…
fixup: leave io_contract bullet untouched (owned by generative-comput…
refactor(intrinsics): resolve the adapter's output contract from the …
fix(backends): address code-review findings on composed-Adapter path
feat(backends): accept composed Adapter directly (Epic generative-com…