Repository navigation
feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1141
Description
Parent epic: #929
Design proposal: PR #1080 §8.1, §9.2, §9.3
Phase: 2 (Wave 4 — start after 2.1 merges)
Depends on: #1134, #1135, #1140
Blocks: #1144, #385, #1465
Problem
LocalFileBinding is a stub from issue 0.1, raising NotImplementedError on each verb. Reality A (today's IntrinsicAdapter — local PEFT/aLoRA file) needs the four verbs working before the binding is usable.
Agreed design
Four verbs
prepare()— resolves configured HF revision (from catalogue pinned SHA; defaults to the pinned SHA if unspecified), downloads PEFT weights via the existing HF path, registers withAdapterMixin.add_adapter, then loads withAdapterMixin.load_peft_adapter.activate()— switches adapter on (PEFT layer enabled) via the backend's PEFT activation primitive.deactivate()— switches off. Auto-called byadapter_scopeeven on exception.release()— removes PEFT adapter viaAdapterMixin.unload_peft_adapter. Second call is a no-op.
Session-scoped: prepare once per session. Call-scoped: activate/deactivate.
Corrected 2026-08-11. This line previously read "
prepareonce per session (or
explicitrelease()+prepare()cycle)". That contradicts the mergedWeightsBinding
ABC from #1134, which states: "release()— terminal; releases all resources. The
binding is not reusable afterrelease()." The implementation follows the ABC, so the
cycle this issue promised is not available. Which of the two is right is an open
question, tracked in #1528 — that issue also covers the related defect that
release()leaves the adapter registered in the backend's_added_adapters, so its
qualified_namestays claimed for the backend's lifetime.
from_catalog() classmethod
LocalFileBinding.from_catalog(name: str) -> LocalFileBinding — looks up the catalogue entry by name (post-0.2, with pinned revision), returns a fully configured binding. This is the user-facing standard path:
Adapter(name="answerability", weights=LocalFileBinding.from_catalog("answerability"))OTel spans — moved to #1466 (2026-07-29)
Superseded. Do not implement from this section. The span work moved to #1466;
see "Criteria moved out" below for why. The names originally written here
(intrinsic.call,intrinsic.prepare,intrinsic.activate,intrinsic.deactivate,
intrinsic.parse) were never the convention this repo uses and must not be
copied. The merged naming, established by #1140 / PR #1422 and documented in
docs/dev/adapter_observability.md, isadapter_functionfor the parent and
adapter_function.<phase>for the children, withmellea.adapter_function.*
span attributes. Likewise the content-capture gate isMELLEA_TRACES_CONTENT
(pluralTRACES), notMELLEA_TRACE_CONTENT— the latter never existed; see
docs/dev/adapter_observability.md:133, which already records this same mistake
against #1140's criteria.
Metric hooks (ship with this PR)
ADAPTER_FUNCTION_PHASE_COMPLETE and ADAPTER_FUNCTION_INVOCATION_COMPLETE are fired
from the lifecycle, and AdapterFunctionMetricsPlugin (already merged, at
mellea/telemetry/metrics_plugins.py) turns them into metrics. Firing the hooks is
correct and stays in scope here; only span production moved to #1466.
Docs updates
docs/dev/adapter_observability.md(from 2.1) — cover the LocalFile lifecycle and
which metric hooks it fires. Not span attributes: those moved to feat(telemetry): add complete adapter-function lifecycle observability #1466 with the
spans themselves.docs/docs/advanced/intrinsics.md— add newAdapter(weights=LocalFileBinding.from_catalog(...))construction pattern; oldIntrinsicAdapter(...)shown as deprecated with migration notedocs/examples/intrinsics/— at least one example shows new construction; existing examples gainmodel_options=
Both user-facing docs items deferred, 2026-08-11. Rationale, recorded here rather
than left as a silently unmet criterion: this PR makes LocalFileBinding and
adapter_scope real, but nothing in production calls them — _generate_from_intrinsic
still generates outside the scope, and resolve_adapter still returns adapters whose
io_contract raises. Documenting Adapter(weights=LocalFileBinding.from_catalog(...))
as a construction pattern now would be documenting a path a user cannot yet use, and
deprecating IntrinsicAdapter(...) in favour of it would be actively wrong. Both belong
with the cutover that makes the path real: #1465 for the generation path, #1144 for the
shim removal and tutorial rewrite.
Out of scope
EmbeddedBinding (2.3), ServerMediatedBinding (3.1), long-running session refresh policy (deferred — PR #1080 §17 Q5), full rewrite of docs/dev/intrinsics_and_adapters.md (4.1).
Also out of scope, added 2026-07-29: routing _generate_from_intrinsic through adapter_scope (#1465) and emitting the adapter-function spans from a tracing plugin (#1466). See "Criteria moved out" below.
Acceptance criteria
- All four verbs implemented for
LocalFileBinding -
prepare()uses pinned revision from catalogue;revision="main"opts into tracking-latest -
activate/deactivatetoggle PEFT layer correctly -
release()unloads the weights and is idempotent on a second call -
— not met, deferred to refactor(backends): release() leaves the adapter registered; is a binding reusable after release? (Epic #929) #1528.release()cleanly unregistersrelease()
callsunload_peft_adapter, which removes the entry from the backend's loaded
set but not from_added_adapters, the registered set. There is no inverse verb
foradd_adapterto call. PR feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1454 correctsrelease()'s docstring to state what
it does and does not release, and makes the resulting "Backend refused to register"
error name both possible causes; whether a released name should become re-claimable
at all is the open question on refactor(backends): release() leaves the adapter registered; is a binding reusable after release? (Epic #929) #1528. -
LocalFileBinding.from_catalog("answerability")returns correct binding with pinned revision -
AdapterFunctionMetricsPlugin:mellea.adapter_function.invocationscounter increments;mellea.adapter_function.parse_failuresonAdapterSchemaMismatchError;mellea.adapter_function.phase_durationrecords prepare + activate durations
(theparse_failureshalf needed a fix:adapter_scopedeclared
outcome: Literal["success", "error"]and so could never emit"schema_error",
the only value that increments that counter. It now classifies
AdapterSchemaMismatchErrorseparately.) -
— deferred to refactor(backends): route intrinsic generation through adapter_scope (Epic #929 Phase 2) #1465 / refactor(intrinsics): remove deprecation shims; rewrite intrinsics_and_adapters.md; write 3 tutorials (Epic #929 Phase 4) #1144, see "Docs updates"docs/docs/advanced/intrinsics.mdupdated: new construction pattern present, deprecated old pattern noted -
— deferred to refactor(backends): route intrinsic generation through adapter_scope (Epic #929 Phase 2) #1465 / refactor(intrinsics): remove deprecation shims; rewrite intrinsics_and_adapters.md; write 3 tutorials (Epic #929 Phase 4) #1144, see "Docs updates"docs/examples/intrinsics/updated; all examples passuv run pytest docs/examples/intrinsics/ -
docs/dev/adapter_observability.mdupdated for the LocalFile lifecycle and metric hooks (span-attribute sections moved to feat(telemetry): add complete adapter-function lifecycle observability #1466) -
ruff format,ruff check,mypyclean
Criteria moved out (2026-07-29)
PR #1454 removes the inline span implementation, because spans must be produced
by a plugin rather than opened inside mellea/backends/ — see the
PR discussion
for the reasoning. These were acceptance criteria here and are now tracked where
the work will actually happen, rather than left unticked on this issue:
Criterion text below is restated in the merged naming convention
(adapter_function / MELLEA_TRACES_CONTENT), not in the intrinsic.* /
MELLEA_TRACE_CONTENT spellings this issue was originally written with — neither of
those ever existed in the codebase.
| Was a criterion here | Now tracked in |
|---|---|
adapter_function parent span emitted; child spans adapter_function.prepare, adapter_function.activate, adapter_function.deactivate, adapter_function.parse with required attributes |
#1466 |
adapter_function.prepare records resolved HF SHA (not "main") as mellea.adapter_function.revision |
#1466 |
MELLEA_TRACES_CONTENT=1: content events present; absent otherwise |
#1466 |
| E2E adapter call (prepare → activate → generate → deactivate → release) passes | #1465 |
The metrics criterion is unaffected and still applies — firing the hooks is
correct, and prepare() is reachable through from_catalog(). The last of the
four moved because generation does not yet run inside adapter_scope; that
cutover is #1465, and it is not "4.1" (#1144, which is shim removal plus a
docs rewrite).
Test plan
Unit tests (test/backends/test_adapters/test_local_file_binding.py) with mocked HF download + mocked backend:
test_prepare_uses_pinned_revisiontest_prepare_allows_main_overridetest_release_is_idempotenttest_from_catalog_returns_binding_with_correct_revisiontest_activate_deactivate_call_correct_mixin_verbsSpan tests via synthetic OTel exporter:— moved to feat(telemetry): add complete adapter-function lifecycle observability #1466 with the span criteriatest_call_span_emitted,test_prepare_span_has_revision_attribute,test_content_events_absent_by_default,test_content_events_present_with_gate_settest_metrics_invocation_counter_incrementstest_metrics_parse_failures_increments— inject synthetic schema-mismatch
Integration tests (test/backends/test_adapters/test_local_file_integration.py, @pytest.mark.integration, @pytest.mark.hf, @pytest.mark.slow):
LocalHFBackend × LocalFileBinding × {lora, alora} × {check_answerability, requirement_check}— full lifecycle- Regression: pre-fix: move test_huggingface.py to granite4.1; and small rag intrinsic … #1008 shape
{"requirement_likelihood": 0.9}raisesAdapterSchemaMismatchError
Qualitative (optional, @pytest.mark.qualitative): test_check_answerability_quality
Breaking changes
None for end users. Replaces internal stubs; behaviour matches existing IntrinsicAdapter runtime path post-1.A.
References
- PR docs: design proposal for adapter lifecycle (#929) — review channel, do not merge #1080 §8.1, §9.2, §9.3
- OTel omissions/gaps: chat content events, gen_ai.conversation.id, prompt templates, error status #1035 / PR feat(telemetry): close five OTel GenAI semantic convention emission gaps (#1035) #1036 —
MELLEA_TRACES_CONTENTcontent-capture gate convention - refactor(backends): AdapterMixin verb rename/narrow + resolve_model_options + IntrinsicMetricsPlugin (Epic #929 Phase 2) #1140 / PR refactor(backends)!: narrow AdapterMixin verbs, centralize option resolution, add AdapterFunctionMetricsPlugin skeleton #1422 —
adapter_functionspan and metric naming convention;AdapterFunctionMetricsPlugin docs/dev/adapter_observability.md— the merged naming of record
Activity
planetf1 commentedon May 28, 2026
Worth noting: there are two TODO comments in
mellea/backends/adapters/adapter.py(inIntrinsicAdapter.__init__andIntrinsicAdapter.download_and_get_path) that read:These will be resolved when this issue is implemented —
LocalFileBinding.prepare()replaces both call sites and carries the pinned revision through toobtain_lora()/obtain_io_yaml(). The TODO comments were corrected to reference #1141 in PR #1157.fix(intrinsics): correct phase-2.2 TODO references to point at genera…
fix(intrinsics): tighten revision validator + clean up stale comments (…
fix(intrinsics): pin catalogue entries to HF revision SHAs + deduplic…
planetf1 commentedon Jun 11, 2026
This issue uses vocabulary that has been superseded by the agreed Epic #929 glossary. Please use the following terms in any new work related to this issue:
The canonical definitions are now in the project glossary and were established as part of #1192. Python symbol names (
IntrinsicAdapter,fetch_intrinsic_metadata, etc.) keep their current names until Phase 1 (#1136) and Phase 4 (#1144).feat(intrinsics): shim old adapters onto Adapter dataclass; rewrite c…
feat(intrinsics): shim old adapters onto Adapter dataclass; rewrite c…
feat(intrinsics): shim old adapters onto Adapter dataclass; rewrite c…
30 remaining items
feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) +…
test(intrinsics): add contract-wiring tests and fix docstring accuracy
docs(backends): document shim scaffolding as current behaviour, no tr…
refactor(intrinsics): drop dead per-helper Adapter constants, consoli…
test(intrinsics): add contract-wiring tests and fix docstring accuracy
refactor(intrinsics): drop dead per-helper Adapter constants, consoli…
docs(backends): document shim scaffolding as current behaviour, no tr…
test(intrinsics): add contract-wiring tests and fix docstring accuracy
refactor(intrinsics): drop dead per-helper Adapter constants, consoli…
docs(backends): document shim scaffolding as current behaviour, no tr…
refactor(intrinsics): resolve the adapter's output contract from the …
docs(backends): document shim scaffolding as current behaviour, no tr…
fix(backends): route intrinsic generation through adapter_scope, fix …
feat(backends): EmbeddedBinding implements apply_activation (Granite …