Skip to content

feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1141

Description

@planetf1

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 with AdapterMixin.add_adapter, then loads with AdapterMixin.load_peft_adapter.
  • activate() — switches adapter on (PEFT layer enabled) via the backend's PEFT activation primitive.
  • deactivate() — switches off. Auto-called by adapter_scope even on exception.
  • release() — removes PEFT adapter via AdapterMixin.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 "prepare once per session (or
explicit release()+prepare() cycle)". That contradicts the merged WeightsBinding
ABC from #1134, which states: "release() — terminal; releases all resources. The
binding is not reusable after release()." 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_name stays 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, is adapter_function for the parent and
adapter_function.<phase> for the children, with mellea.adapter_function.*
span attributes. Likewise the content-capture gate is MELLEA_TRACES_CONTENT
(plural TRACES), not MELLEA_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): emit adapter-function spans from a tracing plugin (Epic #929 Phase 2) #1466 with the
    spans themselves.
  • docs/docs/advanced/intrinsics.md — add new Adapter(weights=LocalFileBinding.from_catalog(...)) construction pattern; old IntrinsicAdapter(...) shown as deprecated with migration note
  • docs/examples/intrinsics/ — at least one example shows new construction; existing examples gain model_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

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_revision
  • test_prepare_allows_main_override
  • test_release_is_idempotent
  • test_from_catalog_returns_binding_with_correct_revision
  • test_activate_deactivate_call_correct_mixin_verbs
  • Span tests via synthetic OTel exporter: test_call_span_emitted, test_prepare_span_has_revision_attribute, test_content_events_absent_by_default, test_content_events_present_with_gate_set — moved to feat(telemetry): emit adapter-function spans from a tracing plugin (Epic #929 Phase 2) #1466 with the span criteria
  • test_metrics_invocation_counter_increments
  • test_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):

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

Metadata

Metadata

Assignees

Labels

area/adapter-functionsGranite adapter functions: framework and adaptiers including RAG, Guardian, Corearea/aloraLoRA/aLora adapter trainingarea/backendsProvider-specific work: Ollama, HF, LiteLLM, OpenAI, Bedrock, vLLMarea/telemetryOTel spans, metrics, tracing, semconvenhancementNew feature or requestp1High: important bugs (workaround exists) or high-value core features. Do soon, not on fire.refactor

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions