feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) - #1454
Conversation
AngeloDanducci
left a comment
There was a problem hiding this comment.
A few non-blocking notes on top of @jakelorocco's threads, which already cover the main design questions plus the adapter.py:373 guard and double-backticks nits. I've replied on the 373 thread with a related second site, and left two inline notes below: a contradictory adapter_type between identity and binding in the two new tests, and a prepare() idempotency edge. For context, the diff looks correct relative to #1141, the scope boundary (production hot path deferred to 4.1) is documented honestly, and the revision-forwarding fix has good regression coverage.
ajbozarth
left a comment
There was a problem hiding this comment.
Clean scoping, the 4.1 boundary is well-documented, and the revision-forwarding fix is a good catch with regression coverage. One blocking item on the span telemetry — two equally-fine ways to resolve it below — plus two small independent notes.
Blocker: adapter-function spans are emitted inline instead of through the tracing-plugin pattern
Spans here are opened/closed by direct start_*_span/finish_*_span calls in adapter_scope/_run_adapter_phase/prepare/release. Everywhere else in Mellea, spans are emitted by a *TracingPlugin in mellea/telemetry/tracing_plugins.py subscribing to lifecycle hooks, and core code never imports the span helpers. Inline emission is only used where code is synchronous and can't fire paired hooks (the session lifecycle spans); adapter_scope is async-capable (the e2e test awaits generate_from_context inside it), so it falls under the plugin pattern.
The underlying reason it had to be inline: the ADAPTER_FUNCTION_*_COMPLETE hooks are completion-only. This is the one hook family in HookType without a pre/start sibling — every other family (generation, component, tool, streaming, sampling, validation, session) has paired members. A span needs an opener to anchor on, so completion-only hooks can feed a metric but can't drive span open/close, which forced the inline workaround. (The metrics half of this PR is fine — it consumes those hooks correctly, the same as the other *MetricsPlugins.)
Two ways to resolve, both fine by me:
Option A — do it here, properly. Add pre/post (or start/end) hook points to the adapter-function contract and move span emission into an AdapterFunctionTracingPlugin. Metrics then read the post payload (which already carries duration/outcome) instead of a separate *_COMPLETE event. Two gaps to close while you're in there:
- The phase enum advertises
generateandparse, but nothing fires them (no span, no hook, no call site). Their real production sites are the model call in_generate_from_intrinsic(huggingface.py:575) andaction.parse(result)(huggingface.py:1776) — place the hook sites there so the contract doesn't ship dead phases. adapter_scope's phase and invocation hooks are currently unasserted (onlyprepare's is tested) — the new plugin + paired hooks should get coverage like the existing pairs have.
Option B — pull span telemetry out, follow up separately. Keep the LocalFileBinding lifecycle, from_catalog, verb extraction, and the revision fix (all self-standing), drop the inline spans, and open a follow-up issue to add adapter-function tracing through the plugin pattern. Reasonable if the hook-contract redesign is more than you want in this PR — and it lines up naturally with the 4.1 cutover, which is what would fire generate/parse anyway. Whether the metrics half stays or goes with it is your call.
Small note (file not in the diff, so can't inline it)
mellea/telemetry/metrics_plugins.py — the AdapterFunctionMetricsPlugin docstring still says "No production call site fires these hooks yet." This PR adds them; please update or remove it, depending on which option above you take.
|
@ajbozarth thanks — you're right, and the mechanism is more wrong than the diff makes it look. Summary The spans in this PR are produced the wrong way — inline in How we got here Until #1181 in June, calling the span helpers directly from library code was What it didn't do was write the new rule down. The bit I'd most like fixed: even knowing the plugin rule, I couldn't have In this PR
Follow-ups under #929
One correction before anyone builds on it: the Shout if you'd sequence any of this differently. |
|
Thanks for the comments so far. Will wait to see if the proposed approach is in the right direction on otel (@ajbozarth ) - i read this first, and more generally on the abstraction (@jakelorocco ) - thread - (since this is more than a in-pr review comment to be handled) before addressing the various other detailed comments which need changes & closing off conversation threads etc. |
|
Moved to draft pending the outcome of some design discussions — see #1486. |
|
Progress here stalled on the API-shape question in #1486 — I've posted a summary there of @ajbozarth — none of your review depends on that outcome, so I'll work through your points |
1a51900 to
a3e0d36
Compare
|
Unblocked and ready for review. #1486 is closed as resolved, which was what this was What changed since you last looked
One thing I tried and backed out, flagged rather than buried: the Verification — all run on the rebased head: Four threads left open on purpose, all @jakelorocco's: the lock-duplication question Sequencing note: #1465 is the follow-up that routes generation through |
|
@ajbozarth — apologies, I answered your inline threads and never replied to this review Your blocker — I took Option B. Inline spans are gone: Your diagnosis of why it had to be inline was the useful part, and I've written it into Your small note — done. Two of your Option A points landed anyway, since they weren't really Option-specific:
While tracing the same "contract ships something dead" pattern I found a third instance and One correction to the review, for the record: the Verification on the current head ( Pushed as a separate commit on top rather than a force-push, so you can diff exactly |
…ve_adapter()
LocalFileBinding and IntrinsicAdapter share the same qualified-name key
space (f"{name}_{type}") in a backend's _added_adapters registry. If a
LocalFileBinding is registered for a capability, a later
resolve_adapter() call for the same name silently fails: add_adapter's
duplicate-key guard refuses the new IntrinsicAdapter, and _find_adapter
can't see the LocalFileBinding either (not an _AdapterCore), so the
caller got a bare "Adapter 'x' not found after registration" with no
hint of the cause.
Name the occupying type in both failure points: add_adapter's warning
log, and a new check in resolve_adapter's KeyError path. Doesn't change
the underlying collision -- nothing in the codebase mixes the two
registration paths for the same name today -- just makes the failure
self-diagnosing instead of opaque. Verified the new resolve_adapter test
against the pre-fix code (fails with the old bare message) before
confirming it passes on the fix.
Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
adapter_scope() moved from an unconditional no-op (Phase 1) to calling adapter.weights.activate(), which raises NotImplementedError for the _ShimWeightsBinding every IntrinsicAdapter/LocalHFAdapter returned by resolve_adapter() still carries. Nothing in the codebase calls adapter_scope() with a resolved adapter today, so nothing breaks in practice, but this was an undocumented, unpinned behaviour change on a public method. Add a test asserting the new raise and a note in docs/dev/adapter_observability.md pointing at generative-computing#1465, the tracked cutover that has to reconcile shim/unprepared bindings with real activation before production wires generation through this path. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…lure, guard the invocation-complete hook, and report resolved revision Three fixes from independently verifying two external reviews (codex, deepseek) of this PR: - event_loop_helper.py: closing `co` on a scheduling failure wasn't enough -- `_wrapped()`'s own coroutine object is created before `run_coroutine_threadsafe` is called, so a scheduling failure left it unstarted and unclosed, leaking a second "coroutine was never awaited" warning distinct from `co`'s. Reproduced empirically before fixing. - adapter_scope: `_fire_invocation_complete` ran unguarded in the outer `finally`, so a hook-dispatch failure there could turn a clean `with` block into a thrown error, or replace a genuine body exception with a telemetry one. Wrap it in try/except that logs and swallows -- the activate-phase hook already got this treatment, the invocation hook hadn't. - adapter_scope: revision was read from the raw `.revision` attribute, which is `None` for a lazily-resolved LocalFileBinding even though it downloaded and ran against a concrete catalogue pin. Use `resolved_revision()` when available so telemetry doesn't mislabel an effectively-pinned invocation as unpinned. Every regression guard was run against the pre-fix code and confirmed to fail there before confirming it passes on the fix. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
… release() as terminal Two fixes from independently verifying two external reviews of this PR: - LocalFileBinding.prepare(): add_adapter() sets .backend before load_peft_adapter() runs. If the load raised (e.g. a transient download/load failure), .backend was already non-None, so the idempotency guard (`if self.backend is not None: return`) made every retry a silent no-op -- the caller got no error and no working adapter, forever. Track the load's own success separately (`_loaded`) so a retry redoes only the failed step instead of re-registering (which would hit the backend's own duplicate-registration guard) or silently doing nothing. activate()/deactivate() now also check `_loaded`, since "registered but not loaded" is a newly-reachable, distinct state. - LocalFileBinding.release(): the docstring already said "terminal ... bind_backend() + prepare() will not revive it", but the code let release() clear .backend back to None, so a subsequent bind_backend() + prepare() on a fresh backend silently succeeded anyway. Add a `_released` flag; bind_backend() and prepare() now raise RuntimeError if called after release(), matching the contract the docstring already claimed. Every regression guard was run against the pre-fix code and confirmed to fail there before confirming it passes on the fix. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
… adapter_scope's non-atomicity prepare()/release() call load_peft_adapter()/unload_peft_adapter(), which mutate the same shared PEFT model state activate_peft_adapter/ deactivate_peft_adapter document "must be called while holding _generation_lock" for -- but neither prepare() nor release() took any lock. Wrap both in _adapter_activation_lock(), matching the other two verb pairs. Also attempted, then reverted, widening _adapter_activation_lock to span adapter_scope's whole activate/body/deactivate duration (to fix concurrent-scope interleaving, a real gap independently found in two external reviews) using a reentrant lock. That deadlocks the moment the body does real async generation: the actual generation work runs on mellea's shared event-loop thread, not the calling thread, so a same-thread RLock doesn't provide the needed exclusivity across threads -- confirmed by running test_local_file_e2e.py, which hung. Reverted that change; documented the non-atomicity as a known, tracked limitation on adapter_scope's docstring instead, with a test pinning today's actual (non-atomic) behavior so it isn't silently "fixed" back to interleaving or silently made worse. generative-computing#1465 (wiring real generation through this scope) has to solve the atomicity and the threading interaction together. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…puting#1464's telemetry-doc gap (generative-computing#1483) * docs: migrate unique content from docs/dev/ before deletion Three pieces of useful content from docs/dev/ notes that have no equivalent in the published docs or docstrings: - mellea/core/backend.py: replace stale docs/dev/ reference in generate_from_context and _generate_from_context docstrings with the actual rationale — action is passed separately from ctx so shared context is referentially equal across calls, avoiding deep-copies in rejection sampling and parallel requirement checks. - docs/docs/advanced/lora-and-alora-adapters.md: add 'How automatic routing works' section documenting the three exceptions to aLoRA routing (flag, LLMaJRequirement subtype, adapter exception) and the ALoraRequirement escape hatch. Previously only the flag was mentioned with no explanation of when or why it applies. - docs/docs/concepts/plugins.mdx: add note after component_post_error explaining why component_pre_create/component_post_create are not implemented (Component is a Protocol, not an ABC) and pointing to component_pre_execute as the alternative. These changes are preparation for removing docs/dev/ entirely in a follow-up PR (ref generative-computing#1482, generative-computing#1483). Assisted-by: IBM Bob Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * docs: close gaps left by the docs/dev/ migration prep ec304499 migrated three pieces of confirmed-current content out of docs/dev/ ahead of its planned deletion (generative-computing#1482, generative-computing#1483), but missed others and left dangling references that the deletion would break: - mellea/core/backend.py, mellea/plugins/hooks/component.py: add two more pieces of still-current rationale that had no equivalent in code — the open architectural risk in the action/ctx split once span-based backends land, and why component_post_success/_error are separate hooks rather than one success/failure union. - mellea/backends/huggingface.py, mellea/telemetry/metrics.py, mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py: repoint or drop three in-code comments that literally said "See docs/dev/....md". One pointed at a filename that never existed (generate_signature_decisions.md vs the real generate_ctx_signature.md); one cited a doc for content the doc never actually contained. Both predate this change but would have become silently broken once docs/dev/ is gone. - docs/examples/*/README.md: drop seven "Related Documentation" bullets linking to docs/dev/ files that are about to disappear. All seven point at files the prior stale-marking audit (82704ae) had already flagged, so there's no live replacement to link to instead. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * docs: delete docs/dev/ now that its current content is migrated Ref generative-computing#1482. A maintainer questioned keeping design notes outside the published docs at all — this replaces the mark-as-stale approach (generative-computing#1483) with removing the directory entirely, now that the two prior commits have folded its still-current content into code docstrings, the published lora-and-alora-adapters/plugins docs, and (for the bare-label-vs-mellea.*-attribute convention) the mellea-telemetry skill. The other six files (constrained_decoding.md, intrinsics_and_adapters.md, mellea_library.md, mify.md, spans.md, tool_calling.md) were already confirmed stale, unfinished, or unverified against current code by the generative-computing#1483 audit, so nothing further needed extracting from them before deletion. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * docs(telemetry): document the session.py tracing exception at its import site generative-computing#1464 item 2: the exception was already documented at each call site (the "Called directly, not via hook" comments), but not where a reader skimming imports for "how do I emit a span" would look first. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * test(telemetry): enforce the backends-never-import-tracing invariant generative-computing#1464 item 4. Nothing currently violates this — mellea/backends/ has zero direct telemetry.tracing imports today — but nothing was enforcing it, which is exactly how generative-computing#1454 happened: docs/dev's stale guidance led an implementer to add direct start_*_span calls in mellea/backends/, caught only by review. Resolves relative imports via AST rather than string-matching so it doesn't false-positive on tracing_plugins.py (a different, legitimately imported module) and doesn't miss a violation written with different whitespace or aliasing. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * docs: explain how spans are produced in tracing.md generative-computing#1464 item 1. tracing.md documented which spans exist and what attributes they carry, but never mentioned that library code fires hooks and mellea/telemetry/tracing_plugins.py opens the spans — the same gap that let docs/dev/adapter_observability.md's stale direct-span guidance go unnoticed until it caused a real revert (PR generative-computing#1454). Also names test_tracing_import_boundary.py as the CI enforcement for this rule. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * docs+fix: address ajbozarth review on generative-computing#1483 - component.py: drop observability-flavored examples from the ComponentPostSuccessPayload docstring so the hook contract stays telemetry-agnostic. - plugins.mdx: remove the component_pre_create/post_create deferral note; it's contributor-facing design rationale, not something a plugin author needs. Kept as a one-line comment at the HookType definition instead. - lora-and-alora-adapters.md: fix exception #3, stale since generative-computing#1320 changed schema-mismatch handling to propagate rather than fall back to LLM-as-a-judge. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * refactor: split generative-computing#1464 telemetry work into its own PR ajbozarth's review on generative-computing#1483 asked for the generative-computing#1464 work (import-boundary test, tracing.md section, session.py comment) to move to its own PR — generative-computing#1482 (delete docs/dev/, migrate current content) is self-contained and shouldn't wait on generative-computing#1464's open design questions (test scope, exception extensibility). Removes the three additions from this branch; they continue on telemetry-1464-import-boundary with the requested fixes applied. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * fix: address Codex/Deepseek review findings on generative-computing#1483 - lora-and-alora-adapters.md: the "Use the adapter in Mellea" example registered its CustomIntrinsicAdapter under capability "my-adapter" (derived from model_id) but plain req() routing hardcodes lookup by "requirement-check" (huggingface.py:506) -- the mismatch meant the example silently fell back to regular generation, never using the trained adapter. Pass intrinsic_name="requirement-check" explicitly. Also found CustomIntrinsicAdapter is a deprecated shim (generative-computing#1144) with no working replacement yet -- Adapter's WeightsBinding subclasses are all Phase 2 stubs (NotImplementedError) -- so kept the example on the deprecated class with a note, rather than a broken rewrite. Flagged this page as additional scope on generative-computing#1144 (comment added). Also fixed "guaranteed" phrasing for ALoraRequirement routing: a matching adapter still has to be registered, or it logs and falls back rather than erroring. - docusaurus.config.ts: add /dev/adapter-observability and /dev/hook-system redirects, missed when the other 8 /dev/ pages got redirects. Both are live URLs this PR's deletion turns into 404s. - backend.py: trim generate_from_context's action docstring entry to match its one-line siblings; move the design rationale (referential equality, span-based-backend caveat) to a code comment instead of a public docstring, restoring a caveat about context mutability that was dropped during migration and could be over-read as a guarantee. All verified against source before applying; see PR discussion. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * docs: correct migrated adapter guidance Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * fix: drop orphaned comment in HookType Alex flagged this comment as unclear why it's here. Agreed - it's a leftover footnote from a deleted doc, not something the code needs. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> --------- Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
c6747dc to
94b2b7c
Compare
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
|
Hi @jakelorocco and @ajbozarth — the earlier approvals predate a small set of lifecycle hardening changes. These isolate phase-hook failures, clarify retry/error behaviour, reject invalid binding transitions, and serialise binding prepare/release to prevent a race. The changes are covered by focused regression tests and the full non-qualitative suite. Would you mind taking a quick fresh look at the updated lifecycle code before merge? |
ajbozarth
left a comment
There was a problem hiding this comment.
Some feedback from Claude
LGTM. Verified locally: 276 adapter/event-loop/hf unit tests pass, ruff + mypy clean, and every behavioral claim in the description has a matching test (terminal release, retryable prepare, the non-atomic-scope limitation via a real two-thread test). Lock ordering (_lifecycle_lock → _generation_lock) is consistent with no reverse acquisition, and the except Exception narrowing in event_loop_helper is correctly reasoned.
One optional nit inline. Non-blocking.
5844232
…date build_prompt message Continues the multi-party review follow-ups on this PR (issue generative-computing#1516): - rag.py / guardian.py: drop the ten module-level Adapter constants. Their only purpose (feeding io_contract= to call_intrinsic) was removed by the earlier commits, nothing in production references them, and the generative-computing#1141/generative-computing#1142 weights work builds on the binding classes (PR generative-computing#1454 merged, PR generative-computing#1559 open) rather than on these constants — whose underscore capability axis would never match _find_adapter's name-keyed scan anyway. test_rag_contracts.py / test_guardian_io_contract.py now look contracts up via get_io_contract(), exercising the registry directly. - _core.py NOTE(generative-computing#1516): re-pointed at the remaining placeholder constructions (the core.py _REQUIREMENT_CHECK_ADAPTER test stub and the shims' _ShimWeightsBinding). - core.py: comment no longer assumes the rag/guardian sibling constants are kept. - build_prompt NotImplementedError message consolidated into the _BUILD_PROMPT_NOT_IMPLEMENTED constant (was five copies). - _util.py: call_intrinsic's Raises ValueError entry now covers well-formed JSON with a contract-rejected top-level shape. Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…date build_prompt message Continues the multi-party review follow-ups on this PR (issue generative-computing#1516): - rag.py / guardian.py: drop the ten module-level Adapter constants. Their only purpose (feeding io_contract= to call_intrinsic) was removed by the earlier commits, nothing in production references them, and the generative-computing#1141/generative-computing#1142 weights work builds on the binding classes (PR generative-computing#1454 merged, PR generative-computing#1559 open) rather than on these constants — whose underscore capability axis would never match _find_adapter's name-keyed scan anyway. test_rag_contracts.py / test_guardian_io_contract.py now look contracts up via get_io_contract(), exercising the registry directly. - _core.py NOTE(generative-computing#1516): re-pointed at the remaining placeholder constructions (the core.py _REQUIREMENT_CHECK_ADAPTER test stub and the shims' _ShimWeightsBinding). - core.py: comment no longer assumes the rag/guardian sibling constants are kept. - build_prompt NotImplementedError message consolidated into the _BUILD_PROMPT_NOT_IMPLEMENTED constant (was five copies). - _util.py: call_intrinsic's Raises ValueError entry now covers well-formed JSON with a contract-rejected top-level shape. Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…date build_prompt message Continues the multi-party review follow-ups on this PR (issue generative-computing#1516): - rag.py / guardian.py: drop the ten module-level Adapter constants. Their only purpose (feeding io_contract= to call_intrinsic) was removed by the earlier commits, nothing in production references them, and the generative-computing#1141/generative-computing#1142 weights work builds on the binding classes (PR generative-computing#1454 merged, PR generative-computing#1559 open) rather than on these constants — whose underscore capability axis would never match _find_adapter's name-keyed scan anyway. test_rag_contracts.py / test_guardian_io_contract.py now look contracts up via get_io_contract(), exercising the registry directly. - _core.py NOTE(generative-computing#1516): re-pointed at the remaining placeholder constructions (the core.py _REQUIREMENT_CHECK_ADAPTER test stub and the shims' _ShimWeightsBinding). - core.py: comment no longer assumes the rag/guardian sibling constants are kept. - build_prompt NotImplementedError message consolidated into the _BUILD_PROMPT_NOT_IMPLEMENTED constant (was five copies). - _util.py: call_intrinsic's Raises ValueError entry now covers well-formed JSON with a contract-rejected top-level shape. Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…adapter, not a parallel argument (generative-computing#1556) * chore(adapters): split EmbeddedIntrinsicAdapter's io_contract/weights construction onto separate lines Prep step for Epic generative-computing#929 Phase 2: generative-computing#1516 and generative-computing#1142 both need to change one of io_contract/weights on the same call and the same docstring paragraph. This splits each into its own local variable/bullet, separated by blank lines, so the two PRs no longer touch adjacent lines and can merge in either order. No behavior change. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * test(adapters): add canonical per-adapter-function output-contract registry Introduces mellea/backends/adapters/io_contracts.py: a capability-keyed registry of IOContract instances, keyed by the same catalog name passed to call_intrinsic() and resolve_adapter(). Moves _ListContract (from rag.py) next to the existing _DictContract in _core.py, adds the guardian-specific contracts, and adds _RequirementCheckContract to consolidate the score-range validation core.requirement_check() previously hand-rolled after each call. This is the single source of truth a later commit wires resolve_adapter() and the intrinsic helpers to consume, instead of each declaring its own IOContract instance that could silently drift from the other's. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * refactor(intrinsics): resolve the output contract from the adapter, not a parallel argument call_intrinsic() resolved the adapter and discarded it, taking the output contract instead as a separate io_contract= argument that each caller supplied from its own module-level Adapter constant. Nothing tied the two together, so a caller could pass a contract that didn't match the adapter resolve_adapter() actually returned. call_intrinsic() now keeps the adapter resolve_adapter() returns and calls its own io_contract.parse() on the raw output; the io_contract= parameter is gone, so a mismatched pair is no longer expressible. IntrinsicAdapter and EmbeddedIntrinsicAdapter (the shims resolve_adapter() constructs) now look up their contract in the io_contracts registry instead of the _ShimIOContract placeholder, which is now unreachable and removed. The ten module-level Adapter constants in rag.py/guardian.py keep their identity and weights as before, and read io_contract from that same registry rather than declaring their own instance. core.py's three helpers (check_certainty, requirement_check, find_context_attributions) previously had no declared contract at all — the first two skipped validation (raw json.loads), and requirement_check hand-rolled its own score-range check after the call. All three now have a declared contract in the registry; requirement_check's hand-rolled validation is replaced by _RequirementCheckContract, and find_context_attributions reads its now-list-wrapped result via ["items"]. Weights binding is untouched — this stays entirely on the io_contract axis (the design discussion in generative-computing#1486 that split the two). The ten constants' placeholder LocalFileBinding() and the backed-out Adapter.__post_init__ cross-check remain future work, as noted in _core.py. Verified against real granite-4.1-3b weights: test/stdlib/components/ intrinsic/test_core.py, test_rag.py, and test_guardian.py (qualitative, GPU-gated) all pass on this run — 3 passed + 2 pre-existing xfails (non-deterministic attribution count, tracked separately) for core.py, 14/14 for rag.py, 6/6 for guardian.py. No test constructs a resolve_adapter() result carrying the old placeholder contract; test_io_contracts.py's registry-completeness test guards that going forward. Fixes generative-computing#1516 Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * refactor(intrinsics): address code review — export registry, drop dead constants, fix real duplicate Three independent reviewers found the same gap from different angles: the registry this PR introduces to close the parallel-declaration problem still had one. mellea/stdlib/requirements/requirement.py's requirement_check_to_bool() hand-rolled the exact score-range validation just consolidated into _RequirementCheckContract, with a comment pointing at code this PR deleted from core.py. It now delegates to get_io_contract("requirement-check").parse(), which is a strict improvement on its undocumented AttributeError-on-non-dict failure mode (now the documented ValueError the contract raises). Also, per review: - Export get_io_contract from mellea.backends.adapters.__init__ (and __all__), matching the sibling adapter-package imports. Unexported, it was invisible to the docs pipeline (io_contracts.mdx was pruned as "not imported by __init__.py") and to the AGENTS.md-mandated docstring quality gate, despite being the function the module's own docstring designates as the mandatory entry point. - Delete _UNCERTAINTY_ADAPTER and _CONTEXT_ATTRIBUTION_ADAPTER from core.py: nothing referenced them — their only purpose (supplying io_contract= to call_intrinsic) was exactly what the prior commit removed. Keep _REQUIREMENT_CHECK_ADAPTER, which test_core_schema.py uses as a resolve_adapter() stub. - Add a regression test per shim class (test_shims.py) asserting IntrinsicAdapter/EmbeddedIntrinsicAdapter carry the real registry contract, not a placeholder. Without it, reverting get_io_contract(intrinsic_name) back to a stub would have passed every existing test in the file. Verified by temporarily reintroducing a stub object in place of get_io_contract(): both new tests failed as expected, then passed again once reverted. - Enforce the registry's exhaustiveness over known_intrinsic_names() at import time in io_contracts.py, mirroring the existing duplicate-effective_capability check in catalog.py, rather than relying solely on a test. - Correct test_core_schema.py's module docstring, which claimed resolve_adapter itself runs; only its stubbed return value does. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * refactor(intrinsics): apply remaining review suggestions and nits Follow-up to the review response commit — the lower-severity items all three reviewers raised, applied where they were cheap and genuinely useful: - adapter.py: convert the remaining `.. deprecated::` RST directives on IntrinsicAdapter/EmbeddedIntrinsicAdapter to Google-style `Deprecated:` sections, matching the `Note:` conversion already done on the same docstrings. (CustomIntrinsicAdapter's directive is untouched by this diff and left alone.) - _core.py / io_contracts.py: replace the stale "not used in Phase 1; implemented in Phase 2" build_prompt placeholder message — self-contradictory now that this module *is* the Phase 2 work — with an accurate description of the current state. Updated the one test asserting on the old wording. - _core.py: module docstring now names _ListContract and explains the generic-vs-capability-specific split with io_contracts.py. - io_contracts.py: get_io_contract's docstring now states its keys are catalog `name`s, not `effective_capability` tokens, and corrects "permissive" to mean permissive about which keys are present, not about the JSON shape. Added the matching inline comment on the fallback return. - io_contracts.py: comment distinguishing the two AdapterSchemaMismatchError raise sites in _PolicyGuardrailsContract (neither key present vs. both). _RequirementCheckContract's docstring now names both production consumers it consolidates (core.py and requirement.py) with full paths. - test_io_contracts.py: new non-GPU test feeding the recorded context-attribution model output (test/stdlib/components/intrinsic/testdata) through the real contract. The GPU-gated equivalent is xfail(strict=False) for unrelated non-determinism, so it gives no CI signal on schema drift; this closes that gap without a GPU. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * test(intrinsics): add contract-wiring tests and fix docstring accuracy Applies the multi-party review findings on this PR (issue generative-computing#1516): - test_core_contracts.py: CI-runnable wiring tests for check_certainty and find_context_attributions — the two helpers that moved to registry-contract validation (plus the items unwrap) with only GPU-gated qualitative/xfail coverage before - test_requirement.py: cover the newly-typed ValueError for non-object JSON in requirement_check_to_bool (was an undocumented AttributeError) - test_io_contracts.py: guard the reverse direction of the registry exhaustiveness invariant (orphan keys) - core.py: the new Raises: ValueError entries were narrower than the contracts' actual raise paths (wrong top-level shape is also ValueError) - io_contracts.py: the string literal after the registry assignment was a dead expression — dicts have no docstring; make it a comment - test_rag_contracts.py / test_guardian_io_contract.py: the contracts no longer live in rag.py/guardian.py (Phase 1 -> io_contracts.py, generative-computing#1516) - rag.py / guardian.py / core.py: state consistently that the per-helper Adapter constants are weights scaffolding for generative-computing#1141/generative-computing#1142, not a second contract source Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * refactor(intrinsics): drop dead per-helper Adapter constants, consolidate build_prompt message Continues the multi-party review follow-ups on this PR (issue generative-computing#1516): - rag.py / guardian.py: drop the ten module-level Adapter constants. Their only purpose (feeding io_contract= to call_intrinsic) was removed by the earlier commits, nothing in production references them, and the generative-computing#1141/generative-computing#1142 weights work builds on the binding classes (PR generative-computing#1454 merged, PR generative-computing#1559 open) rather than on these constants — whose underscore capability axis would never match _find_adapter's name-keyed scan anyway. test_rag_contracts.py / test_guardian_io_contract.py now look contracts up via get_io_contract(), exercising the registry directly. - _core.py NOTE(generative-computing#1516): re-pointed at the remaining placeholder constructions (the core.py _REQUIREMENT_CHECK_ADAPTER test stub and the shims' _ShimWeightsBinding). - core.py: comment no longer assumes the rag/guardian sibling constants are kept. - build_prompt NotImplementedError message consolidated into the _BUILD_PROMPT_NOT_IMPLEMENTED constant (was five copies). - _util.py: call_intrinsic's Raises ValueError entry now covers well-formed JSON with a contract-rejected top-level shape. Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * docs(intrinsics): document registered-adapter contract precedence call_intrinsic now parses via the resolved adapter's io_contract, so a user-registered Adapter for a catalog name takes precedence over the io_contracts registry for parsing. State that in the registry's module docstring and in call_intrinsic, where the single-source-of-truth claim otherwise overreaches for the registered-adapter path. Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * docs(intrinsics): fix increment review findings — stale docs, unguarded stub, missing negative test Closes out the 3-party review of the follow-up increment (b88b17d..50ff6a5): - io_contracts.py: the module docstring still described the constant carrying model the increment deleted (and contradicted the precedence paragraph three lines below); re-point at the shims and the core.py test stub as the construction-time readers - _core.py: NOTE(generative-computing#1516) claimed the shims would make the backed-out type-agreement check fire; _ShimWeightsBinding has no adapter_type to compare and shim identities track the configured type — split the two cases - test_core_schema.py: pin _REQUIREMENT_CHECK_ADAPTER to the registry instance (the is get_io_contract(...) guard test_shims.py established was not extended to the one non-shim construction that survived) - test_io_contracts.py: negative test for the context-attribution contract's required item keys (every sibling contract has one; shrinking the frozenset used to pass the whole suite) - rag.py / guardian.py: complete the Raises ValueError accuracy pass the increment started on core.py/_util.py — dict contracts are not a JSON object, list contracts are not a JSON array / non-object element - _util.py: the new Raises clause now covers the array element case, and says 'same capability' (the field _find_adapter actually matches) - small wording fixes: 'only other tests' (docs/examples e2e runs also exist), missing relative pronoun in two docstrings, and the issue reference form in the two rewritten test headers Assisted-by: opencode Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * fix(intrinsics): address remaining contract review feedback Move the requirement-check adapter stub into its test, scope registry invariants to built-in catalogue entries, and distinguish policy-guardrails exclusivity errors.\n\nAssisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> --------- Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…lock reentrancy (generative-computing#1555) * test(backends): add regression coverage for generative-computing#1465 adapter_scope routing Add tests for the intrinsic-generation lock-reentrancy deadlock, atomicity against concurrent callers, and activated-adapter assertions (rather than a generation-succeeded smoke test) ahead of routing _generate_from_intrinsic through adapter_scope. Also drop test_local_file_e2e.py's deferral note and have it generate directly against the real model while the adapter is active, instead of through the standard path, which always deactivates adapters first. These tests target a _generate_intrinsic_with_adapter_scope() helper that does not exist yet; the implementation follows in the next commit. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * test(backends): surface thread exceptions in the concurrent-callers test test_concurrent_intrinsic_calls_cannot_observe_each_others_adapter spawned worker threads without capturing what they raised. threading.Thread swallows exceptions from its target by default, so a totally broken _generate_intrinsic_with_adapter_scope (e.g. missing before this issue's fix) left `mismatches` empty and the test passed vacuously — confirmed by running it against the pre-fix code, where it passed despite every worker thread raising AttributeError. Capture and assert on thread exceptions so the test actually fails when the call raises. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * fix(backends): route intrinsic generation through adapter_scope (generative-computing#1465) _generate_from_intrinsic called _generate_with_adapter_lock directly, bypassing AdapterMixin.adapter_scope entirely -- the model call ran outside the scope whose activate()/deactivate() lifecycle generative-computing#1141 built for this. Add _generate_intrinsic_with_adapter_scope, which builds an ephemeral Adapter around a new _IntrinsicPeftBinding (driving this backend's own load_peft_adapter/activate_peft_adapter/deactivate_peft_adapter -- the same verbs the old code called directly) and runs the generate call inside adapter_scope(), so activation and deactivation now fire the ADAPTER_FUNCTION_PHASE_COMPLETE/ADAPTER_FUNCTION_INVOCATION_COMPLETE hooks, and deactivation is guaranteed even if generation raises. This reactivates the lock-reentrancy deadlock generative-computing#1454's review flagged and _adapter_activation_lock()'s docstring tracked as a TODO(generative-computing#1465): adapter_scope()'s activate()/deactivate() re-acquire _generation_lock (via _adapter_activation_lock()) from inside the same critical section _generate_intrinsic_with_adapter_scope holds it for. Fix by making _generation_lock a threading.RLock -- reentrant acquisition on the same thread succeeds instead of deadlocking, since this method only ever runs inside a single asyncio.to_thread worker thread. Holding the lock for the whole activate -> generate -> deactivate section (not just around activation) also closes the atomicity gap adapter_scope()'s own docstring flagged as latent: a concurrent caller's activate() can no longer interleave with this call's generation. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * test(backends): assert on the adapter-function hook payload, not just that hooks fire _generate_intrinsic_with_adapter_scope's own docstring claims activation now fires ADAPTER_FUNCTION_PHASE_COMPLETE/ADAPTER_FUNCTION_INVOCATION_COMPLETE, but nothing asserted on the payload -- only on lock/activation state. Add a test using the existing capture_adapter_hooks()/hook_payloads() helpers (already used by test_local_file_integration.py for the sibling binding) to pin phases, outcome, name, adapter_type, binding_type, and revision. This fails against the current code: binding_type is the invented "intrinsic_legacy" instead of the "local_file" reality this binding actually is, and revision is unresolved to None (mislabelled "unpinned") despite the adapter being pinned. Both fixed in the next commit. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * fix(backends): fix telemetry mislabelling in the intrinsic adapter_scope path Address code-review findings on the generative-computing#1465 adapter_scope routing: - Thread the catalogue revision through _IntrinsicPeftBinding instead of leaving it unset. adapter_scope() fell back to getattr(..., None) for revision, so every intrinsic invocation was reported as revision=None ("unpinned") despite IntrinsicAdapter.intrinsic_metadata.revision always being a pinned catalogue SHA. - Use "local_file" for binding_type instead of inventing "intrinsic_legacy". This binding loads LoRA/aLoRA weights from local disk via PEFT -- the same reality LocalFileBinding reports -- so it must share that value rather than fragment the weights-reality metric dimension into two series for one reality. - Reuse adapter.identity instead of rebuilding an Identity per call. Rebuilding it re-ran Identity.__post_init__'s KNOWN_CAPABILITIES check on every generate call, firing a UserWarning on every call for hyphenated catalogue names (including "requirement-check", the auto-routed ALoraRequirement adapter) instead of once at registration. - Split load_peft_adapter into prepare() and leave only activate_peft_adapter in activate(), mirroring LocalFileBinding's phase boundary. Previously the weights load (can take seconds on first use) was folded into the "activate" phase metric, making its duration bimodal and incomparable across the two bindings that feed the same metric. Also reconciles adapter_scope()'s docstring, which still claimed "no caller combines concurrent adapter_scope() calls today" and that generative-computing#1465 (this PR) "has to solve the atomicity and threading interaction" -- both false once _generate_intrinsic_with_adapter_scope exists. Documents that ADAPTER_FUNCTION_* hook dispatch is synchronous and runs under _generation_lock, and that phase="generate"/"parse" are not emitted (deferred to generative-computing#1466). Drops the docstring's imprecise "safe because of a single asyncio.to_thread worker thread" framing (to_thread uses a pool with many threads; the safety property is that each invocation is wholly synchronous on whichever thread it lands on). Renames _NoIntrinsicIOContract to _UnusedIOContract and drops embedded issue numbers from runtime exception text (kept in docstrings instead). Types _generate_intrinsic_with_adapter_scope's generate_func/return via a TypeVar. The new test from the previous commit (asserting on the actual hook payload) now passes. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * test(backends): address remaining code-review nits and suggestions - test_local_file_e2e.py: restore a generate_from_context() call after adapter_scope exits as a composition smoke test (the direct model.generate() edit proved activation but dropped the file's only coverage of mellea's own generation path composing with a scoped-and-released adapter). Also drops the now-tautological isinstance(value, str) assertion (value now always comes from tokenizer.decode(), which is always str) in favour of asserting non-empty content. - test_huggingface_unit.py: except BaseException -> except Exception in the concurrency test, so a real KeyboardInterrupt/SystemExit during the test isn't captured as a spurious "error". Tighten the two deactivation assertions to also check the actual set_adapter([]) call, not just the mock's derived active_adapters() state, so a change to the deactivation verb's call shape would break the test loudly instead of the mock silently going stale. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * docs(backends): note that adapter_scope() still can't activate the standard generation path _generate_with_adapter_lock (the standard, non-intrinsic path) always deactivates any adapter before generating, so a caller composing generate_from_context() with adapter_scope() silently generates against the base model regardless. Pre-existing gap, not introduced by the intrinsic routing this PR adds -- documented here since the original note recording it (in test_local_file_e2e.py's module docstring) was removed when that file's own coverage moved to a direct model.generate() call. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * docs(backends): correct hook-dispatch and lock-ownership docstrings Hook dispatch runs the hook coroutines on the shared _EventLoopHandler event-loop thread while the calling thread blocks; the old wording ("runs synchronously on the calling thread") hid that, along with the hazard it creates: a subscriber that re-enters a _generation_lock-holding path deadlocks the backend, which an RLock cannot prevent because reentrance only helps the owning thread. Also name _IntrinsicPeftBinding as the intrinsic-path verb acquirer in the _adapter_activation_lock() caller list (it is not LocalFileBinding's path), and qualify the to_thread claim to production invocations. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> Assisted-by: opencode * test(backends): pin intrinsic error-path and prepare-failure telemetry The generic adapter_scope failure contract is pinned in test_adapter_scope.py, but the intrinsic payload on an error outcome (name, adapter_type, binding_type, revision) and the prepare-failure semantics (no hooks fire, activation state unchanged, next call still succeeds) were untested despite this PR establishing them. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> Assisted-by: opencode * test(backends): drop tautological decode assertion from the GPU e2e model.generate returns the prompt plus completion, so the decode always contained the prompt text and the strip() assertion could never fail for the reason it purported to guard. The real proof is the active-adapter assertion straddling the raw generate call. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> Assisted-by: opencode * docs(backends): align the rewritten hook-dispatch docstrings with the surrounding text Second-pass fixes after re-review: the new hook-dispatch paragraph contradicted the untouched reference-example sentence ("never re-enters the event loop") two paragraphs away, so qualify it to the scope body and name the hook dispatches as the only loop traffic; state the hook-dispatch mechanism once in adapter_scope() and cross-reference it from the HF method; say the nesting fact once in the _adapter_activation_lock() docstring; and include prepare in the section name everywhere, matching the code (the lock wraps weights.prepare()). Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> Assisted-by: opencode * test(backends): name the new intrinsic telemetry tests verb-first Match the family scheme (test_generate_intrinsic_with_adapter_scope_<behaviour>): _error_hook_payload and _prepare_failure_no_hooks name observations, not behaviours. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> Assisted-by: opencode * docs(backends): address final re-review nits across the docstrings Third-round fixes: fold the unearned Attributes: section out of _IntrinsicPeftBinding (AGENTS.md allows it only for transformed/computed storage); let the HF method docstring cross-reference the consequences instead of restating them; correct the over-claim that the unit tests are 'also single-threaded' (the two-thread test runs one invocation per thread); add prepare to the lock-section naming in the two test docstrings that predate the 1dfe7fa alignment; reword the concurrent test away from the now-merged 'latent until generative-computing#1465' framing; and state precisely which attributes _make_fake_intrinsic_adapter exposes for the method versus for realism. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> Assisted-by: opencode * refactor(backends): shrink _generate_with_adapter_lock to its standard-path contract With intrinsic generation routed through adapter_scope, no production caller still passed an adapter name to this method (all three standard call sites passed ""); the load/activate branch survived only as test-covered residue advertising a capability the standard path does not have. The method now takes no adapter name: deactivate-any-adapter, assert base model, generate, assert. The two unit tests that pinned the removed branch collapse into one pinning the remaining contract. Granite Switch's embedded activation is a separate mechanism (EmbeddedBinding.apply_activation, generative-computing#1018) and does not use this method; the orphan was orphaned by this PR, so the cleanup lands with it. Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> Assisted-by: opencode * docs(backends): frame apply_activation as generative-computing#1018/generative-computing#1142 future work; pin the shrunken method's forwarding Delta-review fixes: the new docstring cited apply_activation as a present route but the verb does not exist in-tree (AGENTS.md: verify transcribed identifiers against source), so it is now framed as the designed generative-computing#1018 mechanism landing via generative-computing#1142; the method gains the Args/Returns blocks its sibling lock-holding helper documents; and the shrunken-contract test now captures and asserts the generate_func return so its name matches what it pins (the deactivate-then-generate ordering stays a body fact, stated as such in the docstring). Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> Assisted-by: opencode * test(backends): cover real adapter generation under lock Assisted-by: Codex Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> --------- Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Pull Request
Issue
Fixes #1141
Summary
LocalFileBinding— the weights binding for locally-downloaded LoRA/aLoRA adapters — was fourNotImplementedErrorstubs. This PR makes it real: workingprepare/activate/deactivate/releaseverbs, afrom_catalog()constructor, andAdapterMixin.adapter_scope()wired to actually activate/deactivate weights and fire metric hooks, instead of being a Phase 1 no-op.Two smaller, independent bugs came along for the ride because they blocked writing correct tests for the above:
IntrinsicAdapterwas silently resolvingrevision="main"instead of forwarding the catalogue's pinned SHA, andLocalFileBinding's own revision default did the same thing.Spans and the production generation cutover are explicitly not in this PR — see "Where this fits" below for exactly where they land instead.
Where this fits — Epic #929, Phase 2
This is #1141. Two things are deliberately not here, each with its own issue and acceptance criteria — don't file findings against their absence:
adapter_scope(). Three problems converge on this one issue:adapter_scope()now activates real weights, sowith backend.adapter_scope(backend.resolve_adapter(name)):goes from a no-op toNotImplementedErrorfor every adapterresolve_adapter()currently returns (they carry a Phase-1 shim binding). Deliberate, pinned bytest_adapter_scope_raises_for_a_shim_backed_adapter, not a silent regression — nothing today callsadapter_scope()outside tests.adapter_scope()is not atomic:_adapter_activation_lock()is held only inside eachactivate()/deactivate()verb call, not across thewithbody, so two concurrentadapter_scope()calls on one backend can interleave. Confirmed with a real two-thread test (test_adapter_scope_is_not_atomic_across_concurrent_calls) rather than left as a theoretical concern. Latent today — nothing calls it concurrently — but real._generation_lockreentrant and holding it across the whole scope deadlocks the instant the body does real async generation, because that work runs on mellea's shared event-loop thread, not the calling thread — same-thread reentrancy doesn't help across threads. Confirmed by runningtest_local_file_e2e.py, which hung. refactor(backends): route intrinsic generation through adapter_scope (Epic #929 Phase 2) #1465's own acceptance criteria already require solving the reentrancy and the atomicity together with the threading model, so that's where this belongs rather than a partial fix here.Also referenced, narrower in scope:
Adapterconstants inrag.py/guardian.pycarry unconfigured placeholder bindings; a stricteridentity/weightsadapter_typecross-check can't be enforced until those get real bindings.release()d binding'squalified_nameshould ever become re-claimable (today it doesn't;release()is terminal by contract).What changed
LocalFileBindingprepare/activate/deactivate/releaseimplemented;from_catalog(name)constructor;release()is enforced-terminal (bind_backend/prepareraise if called afterrelease());prepare()retries only the failed step after a load failure instead of silently no-op'ing forever;prepare()/release()now hold the backend's exclusivity lock around the PEFT load/unload calls, matchingactivate/deactivateAdapterMixinactivate_peft_adapter/deactivate_peft_adapter(extracted from_generate_with_adapter_lock's inline PEFT calls — behaviour-preserving);_adapter_activation_lock()for the exclusivity these verbs document as requiredAdapterMixin.adapter_scope()ADAPTER_FUNCTION_PHASE_COMPLETE,ADAPTER_FUNCTION_INVOCATION_COMPLETE) instead of a no-op; a hook-dispatch failure can never strand the adapter active or mask the real outcome (including the invocation-complete hook itself);revisionin telemetry is the resolved catalogue pin, not the raw unresolvedNoneIntrinsicAdapterobtain_io_yaml/obtain_lorainstead of implicitly resolving"main"mellea/helpers/event_loop_helper.py_run_async_in_thread's shared coroutine-close handling covers all 13 real call sites instead of 3, including its own internal wrapper coroutine (not just the caller's); narrowed toexcept Exception(notBaseException) to avoid a cross-thread race onKeyboardInterruptTelemetry: what changed and why
The original branch called
start_adapter_function_span(...)directly frommellea/backends/adapters/. Wrong mechanism — since #1181, span production belongs to plugins subscribing to hooks, and library code never opens a span itself. The mechanism was more wrong than the diff made it look:span = start_generate_span(...).docs/dev/adapter_observability.md, which still namedstart_backend_span/start_action_spanas the model to mirror. This PR's first draft followed that stale doc.ADAPTER_FUNCTION_INVOCATION_COMPLETE/ADAPTER_FUNCTION_PHASE_COMPLETEare the onlyHookTypefamily with no pre/start sibling for a plugin to open a span on.So: all inline span code is removed (
mellea/telemetry/tracing.pyis back to itsupstream/mainstate), metric hooks are the only telemetry here, the doc now states the hook/plugin rule and says plainly there's no span coverage of this lifecycle until #1466, and tests assert hooks/payloads rather than span names.Deviations from the literal issue spec
intrinsic.call/intrinsic.prepare; the convention that actually merged (refactor(backends): AdapterMixin verb rename/narrow + resolve_model_options + IntrinsicMetricsPlugin (Epic #929 Phase 2) #1140/refactor(backends)!: narrow AdapterMixin verbs, centralize option resolution, add AdapterFunctionMetricsPlugin skeleton #1422) isadapter_function/adapter_function.<phase>. Moot here since spans are gone, but feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1141 and feat(telemetry): add complete adapter-function lifecycle observability #1466 are corrected so the next implementer doesn't copy the invented names.MELLEA_TRACES_CONTENT, not the issue'sMELLEA_TRACE_CONTENT.IntrinsicMetricsPlugin; the real class isAdapterFunctionMetricsPlugin.docs/docs/advanced/intrinsics.mdanddocs/examples/intrinsics/are untouched — documentingLocalFileBinding.from_catalog(...)as the construction pattern would document a path nothing in production can reach yet (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, recorded on feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1141).Testing
uv run pytest test/ -m "not qualitative"— 3931 passed, 21 skipped (hardware-gated), 0 failures.ruff format/ruff check/mypyclean.test/backends/test_adapters/test_local_file_e2e.py(GPU-gated) passes against a real model, including through the lock changes in this PR.Pre-existing failures unrelated to this diff:
test/package/test_dependency_isolation.py'stest_hooks/test_telemetry/test_telemetry_plugins_registerreproduce identically on a cleanupstream/maincheckout.Testing
Attribution
Adding a new component, requirement, sampling strategy, or tool?
If your PR adds or modifies one of the types below, check the matching box. A checklist of type-specific review items will be posted as a comment.
NOTE: Please ensure you have an issue that has been acknowledged by a core contributor and routed you to open a pull request against this repository. Otherwise, please open an issue before continuing with this pull request.