Skip to content

feat(intrinsics): introduce Adapter/Identity/IOContract/WeightsBinding scaffolding (Epic #929 Phase 0)#1158

Merged
planetf1 merged 3 commits into
generative-computing:mainfrom
planetf1:worktree-issue-1134
Jun 5, 2026
Merged

feat(intrinsics): introduce Adapter/Identity/IOContract/WeightsBinding scaffolding (Epic #929 Phase 0)#1158
planetf1 merged 3 commits into
generative-computing:mainfrom
planetf1:worktree-issue-1134

Conversation

@planetf1

@planetf1 planetf1 commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #1134 — Epic #929 Phase 0, Wave 1.

Why

Today's adapter hierarchy in Mellea is AdapterLocalHFAdapterIntrinsicAdapterEmbeddedIntrinsicAdapter / CustomIntrinsicAdapter. The class that a backend gets handed encodes where the weights live (locally, embedded in the model artefact, server-mediated), so every caller that wants to load, activate, or unload an adapter ends up doing isinstance branching to figure out which storage reality applies. Seven recent fix-up commits trace directly to that structure, and adding new realities (e.g. the work blocked by #1018) means new branches in every caller.

The epic's resolution is to break the adapter into three independent concerns — identity (name, type, capability), I/O contract (how to build the prompt and parse the output), and weights binding (how the bytes get loaded). Each lives behind its own type, and the storage-reality variation moves from the class hierarchy into a pluggable WeightsBinding. Callers stop branching; new realities become a new binding subclass.

This PR does only the type scaffolding — the new dataclasses and ABCs, plus three NotImplementedError stubs for the three storage realities. No existing class is modified, no existing caller is migrated. That keeps this PR small and reviewable; the migration is the next phase's work.

Where this fits in Epic #929

This PR is Phase 0, Wave 1 of Epic #929 — paired with #1135 (PR #1157, catalogue revision pinning), which lands in parallel. See #929 for the full phase plan.

Deferred from this PR (do not flag as missing):

Reviewers should expect scaffolding only: ABCs that raise on instantiation, three subclasses that raise NotImplementedError, and one alias module that re-exports Intrinsic under its new name. Anything that uses this scaffolding to actually load weights is deliberately out of scope.

What changed

File Contents
mellea/backends/adapters/_core.py New: Adapter dataclass; Identity (frozen, capability-warning); IOContract ABC (build_prompt, parse); WeightsBinding ABC (prepare, activate, deactivate, release); LocalFileBinding, EmbeddedBinding, ServerMediatedBinding stubs; AdapterSchemaMismatchError (picklable).
mellea/backends/adapters/capabilities.py New: KNOWN_CAPABILITIES advisory frozenset, derived from _INTRINSICS_CATALOG_ENTRIES so it cannot drift.
mellea/stdlib/components/adapter_based_component/__init__.py New: re-exports Intrinsic as AdapterBasedComponent. The chosen name; the old import path remains valid.
mellea/backends/adapters/__init__.py Updated: re-exports all new public names.
test/backends/test_adapters/test_core_types.py New: 23 unit tests covering construction, ABC enforcement, frozen / hashable behaviour, stub NotImplementedError, exception format and pickle round-trip, capability-warning behaviour.
test/stdlib/components/test_adapter_based_component.py New: identity test (AdapterBasedComponent is Intrinsic) and old-import-path test.

Naming-collision resolution

The issue called out a potential collision between the existing Adapter ABC (mellea/backends/adapters/adapter.py:24) and the new Adapter dataclass.

In practice there is none: the existing ABC was never exported from mellea/backends/adapters/__init__.py — only AdapterMixin, AdapterType, IntrinsicAdapter, LocalHFAdapter, fetch_intrinsic_metadata, and get_adapter_for_intrinsic were. The new Adapter dataclass lives in _core.py and is the first thing to be exported under that name on the public surface. Both classes coexist inside the package until shim removal in Phase 4; no caller's import resolves differently.

Deviations from the literal issue spec

The review pass surfaced a few quality concerns. I tightened the spec rather than ship the looser version:

  • Identity and Adapter are @dataclass(frozen=True) — the issue just said "dataclass". Frozen makes both hashable (so they can key into adapter caches) and stops __post_init__ validation being bypassed by post-construction assignment. Phase 0 is the cheapest moment to lock this down.
  • IOContract.parse -> dict[str, object] — the issue said -> dict. Bare dict is dict[Any, Any] and violates AGENTS.md §5 strict-typing on a public ABC.
  • IOContract.build_prompt(**kwargs: object) — the issue used (...). Same typing reason.
  • Identity.capability instead of Identity.role — renamed during review. "role" implies exact matching and is an overloaded term; "capability" reads as "this adapter does X" and supports several adapters sharing one. Done now while it is pure scaffolding with no callers bound to it.
  • Stub NotImplementedError raises now name the class and point to Phase 2 ("LocalFileBinding is a Phase 0 stub; implementation lands in Epic #929 Phase 2.") so a confused user gets a useful traceback.
  • The adapter_based_component docstring asserts AdapterBasedComponent as the chosen name rather than hedging about a pending IBM rename. The "placeholder" framing in feat(intrinsics): introduce Adapter/Identity/IOContract/WeightsBinding scaffolding (Epic #929 Phase 0) #1134 is design-tracker language only, not real ambiguity; if IBM eventually picks a different upstream term, the rename is scheduled in Phase 4. Issue feat(intrinsics): introduce Adapter/Identity/IOContract/WeightsBinding scaffolding (Epic #929 Phase 0) #1134 has a correction note at the top documenting this.

Acceptance criteria

  • Adapter, Identity, IOContract, WeightsBinding importable from mellea.backends.adapters
  • IOContract ABC enforces both build_prompt and parse as abstract
  • WeightsBinding ABC enforces all four verbs as abstract
  • LocalFileBinding, EmbeddedBinding, ServerMediatedBinding raise NotImplementedError on each verb
  • AdapterSchemaMismatchError has correct attributes and message format (and is picklable)
  • from mellea.stdlib.components.adapter_based_component import AdapterBasedComponent works
  • AdapterBasedComponent is Intrinsic evaluates True
  • Old import mellea.stdlib.components.intrinsic still works
  • Naming-collision resolution documented (above)
  • KNOWN_CAPABILITIES importable; Identity(capability="unknown-capability") emits UserWarning; Identity(capability="answerability") does not
  • All existing tests pass unchanged
  • ruff format, ruff check, mypy clean (one pre-existing cpex import-not-found in mellea/plugins/policies.py — not from this PR)

Testing

uv run pytest test/backends/test_adapters/test_core_types.py test/stdlib/components/test_adapter_based_component.py -v

25 tests pass. The full non-qualitative suite (uv run pytest -m "not qualitative") shows no regressions caused by this PR — the only failures are the pre-existing Ollama-connectivity tests that need a running server.

Attribution

  • AI coding assistants used: Claude Code

@github-actions github-actions Bot added the enhancement New feature or request label May 26, 2026
planetf1 added a commit to planetf1/mellea that referenced this pull request May 26, 2026
…ng (generative-computing#1134)

Fixes flagged by the multi-reviewer pass on PR generative-computing#1158:

- `AdapterSchemaMismatchError` now passes structured fields (not the formatted
  message) to `Exception.__init__`, so `pickle.dumps(err)` round-trips through
  worker / process boundaries. `__str__` is overridden to keep the existing
  user-visible message format. Adds a `pickle` round-trip test.
- `IOContract.parse` now returns `dict[str, object]` (was bare `dict`) and
  `IOContract.build_prompt` typed as `**kwargs: object` to satisfy the
  AGENTS.md §5 strict-typing rule on the public ABC.
- `Identity` and `Adapter` are now `@dataclass(frozen=True)` so the `__post_init__`
  validation cannot be bypassed by post-construction assignment, and both are
  hashable (usable as dict keys / set members). New tests cover the frozen and
  hashable behaviour.
- `WeightsBinding` docstring now documents the lifecycle as an informal state
  machine (prepare → activate → deactivate → release) so Phase 2 implementations
  share a contract.
- Stub bindings (`LocalFileBinding`, `EmbeddedBinding`, `ServerMediatedBinding`)
  now raise `NotImplementedError` with a message pointing to Epic generative-computing#929 Phase 2.
- `KNOWN_ROLES` is now derived from `_INTRINSICS_CATALOG_ENTRIES` rather than
  hand-copied, eliminating the silent drift risk.
- `Identity.__post_init__` carries an inline comment explaining why the runtime
  check is needed alongside the `Literal` annotation.
- The placeholder docstring on `adapter_based_component/__init__.py` no longer
  hedges — the module asserts `AdapterBasedComponent` as the chosen name.
- `test_stub_binding_subclasses_raise_not_implemented` parametrises over verbs
  for clearer per-verb failure attribution.
- `test_identity_known_role_no_warning` no longer uses `simplefilter("error")`,
  which would have failed on any unrelated `DeprecationWarning` from imports.

All 25 unit tests pass; ruff and mypy clean on the new files.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
planetf1 added a commit to planetf1/mellea that referenced this pull request May 26, 2026
…ng (generative-computing#1134)

Fixes flagged by the multi-reviewer pass on PR generative-computing#1158:

- `AdapterSchemaMismatchError` now passes structured fields (not the formatted
  message) to `Exception.__init__`, so `pickle.dumps(err)` round-trips through
  worker / process boundaries. `__str__` is overridden to keep the existing
  user-visible message format. Adds a `pickle` round-trip test.
- `IOContract.parse` now returns `dict[str, object]` (was bare `dict`) and
  `IOContract.build_prompt` typed as `**kwargs: object` to satisfy the
  AGENTS.md §5 strict-typing rule on the public ABC.
- `Identity` and `Adapter` are now `@dataclass(frozen=True)` so the `__post_init__`
  validation cannot be bypassed by post-construction assignment, and both are
  hashable (usable as dict keys / set members). New tests cover the frozen and
  hashable behaviour.
- `WeightsBinding` docstring now documents the lifecycle as an informal state
  machine (prepare → activate → deactivate → release) so Phase 2 implementations
  share a contract.
- Stub bindings (`LocalFileBinding`, `EmbeddedBinding`, `ServerMediatedBinding`)
  now raise `NotImplementedError` with a message pointing to Epic generative-computing#929 Phase 2.
- `KNOWN_ROLES` is now derived from `_INTRINSICS_CATALOG_ENTRIES` rather than
  hand-copied, eliminating the silent drift risk.
- `Identity.__post_init__` carries an inline comment explaining why the runtime
  check is needed alongside the `Literal` annotation.
- The placeholder docstring on `adapter_based_component/__init__.py` no longer
  hedges — the module asserts `AdapterBasedComponent` as the chosen name.
- `test_stub_binding_subclasses_raise_not_implemented` parametrises over verbs
  for clearer per-verb failure attribution.
- `test_identity_known_role_no_warning` no longer uses `simplefilter("error")`,
  which would have failed on any unrelated `DeprecationWarning` from imports.

All 25 unit tests pass; ruff and mypy clean on the new files.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
@planetf1
planetf1 force-pushed the worktree-issue-1134 branch from 92b9e7e to 8ecefaf Compare May 26, 2026 16:44
@planetf1
planetf1 marked this pull request as ready for review May 26, 2026 17:16
@planetf1
planetf1 requested a review from a team as a code owner May 26, 2026 17:16

@jakelorocco jakelorocco left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@planetf1, I also think that we should just have one big feature branch in generative-computing/mellea that these are merged into. I think it's super helpful to be able to review individual PRs that correspond to phases, but I don't think we want unfinished versions of these things in the code base.

Comment thread mellea/backends/adapters/_core.py
Comment on lines +197 to +215
def prepare(self) -> None:
raise NotImplementedError(
_PHASE_2_NOT_IMPLEMENTED.format(cls="EmbeddedBinding")
)

def activate(self) -> None:
raise NotImplementedError(
_PHASE_2_NOT_IMPLEMENTED.format(cls="EmbeddedBinding")
)

def deactivate(self) -> None:
raise NotImplementedError(
_PHASE_2_NOT_IMPLEMENTED.format(cls="EmbeddedBinding")
)

def release(self) -> None:
raise NotImplementedError(
_PHASE_2_NOT_IMPLEMENTED.format(cls="EmbeddedBinding")
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I mentioned this in the design document, but I'd like more clarification on how this works for EmbeddedBindings. It makes sense to me that prepare / release will grab / delete just the io.yaml file for these embedded adapters.

For activate / deactivate, it seems like they will just be no-ops. But how then do we indicate that to utilize the embedded adapter we need to pass along a special chat template kwarg? Would that live in the IOContract? If so, then the IOContract and EmbeddedBinding are linked in some way and not quite as mix-and-matchable as it originally seems. Is that expected?

@planetf1 planetf1 May 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question — activate()/deactivate() for EmbeddedBinding are indeed no-ops. The chat template kwarg (intrinsic_name injected into chat_template_kwargs) is already handled by the backend, not by IOContract. Today the backend dispatches on adapter shape (isinstance(adapter, EmbeddedIntrinsicAdapter) in openai.py); in the new architecture that maps to the WeightsBinding type — i.e. EmbeddedBinding — and the backend stays the glue between IOContract and WeightsBinding, so the two don't need to know about each other.

It's the same separation we discussed above for build_prompt: IOContract declares what the adapter needs, the backend decides how to format it. Mix-and-matchability holds at the contract level.

(Edit: corrected an earlier claim that the dispatch keys off Identity.adapter_type == "alora" — it's actually keyed off the embedded-adapter shape, which maps to EmbeddedBinding in the new architecture. Both LoRA and aLoRA can be embedded.)

@jakelorocco jakelorocco May 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so when a new adapter type is introduced, all backends (that support adapters or that specific type of adapter) need to get updated to handle that new adapter? Is that the contract / process we are buying into here?

@planetf1 planetf1 Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, intentional. Each backend isinstance-checks the WeightsBinding and handles the types it supports — a new binding type means updating the backends that should support it, not a registry doing it for free.

Mix-and-match is at the contract level (any IOContract + any WeightsBinding); a new binding can't auto-run everywhere since backends load weights differently (HF in-process vs. a remote OpenAI server).

On "does this get messy with more backends" — it grows linearly (one branch per backend per supported type), but the downside is the "what supports what" knowledge ends up scattered across backend files. If that bites, the clean fix mirrors what we already do with AdapterMixin (backends declare adapter support by implementing the ABC): each backend declares which bindings it supports and fails fast on the rest. Going explicit now keeps Phase 0 small and leaves that door open.

Does that sound OK to you?

Comment thread mellea/backends/adapters/roles.py Outdated
@psschwei

Copy link
Copy Markdown
Member

@planetf1, I also think that we should just have one big feature branch in generative-computing/mellea that these are merged into. I think it's super helpful to be able to review individual PRs that correspond to phases, but I don't think we want unfinished versions of these things in the code base.

Are the things we're implementing here stand-alone enough that they could live in contribs until they're finished? Would be a nice way to dogfood the idea of contribs for experiments (+ eventual migration to core), but could also be way more trouble than it's worth (I haven't really thought it through so feel free to just say no)

@planetf1

Copy link
Copy Markdown
Contributor Author

A feature branch does add some complexity — you'd want automated builds/CI running on it, a rebase strategy, and if it's a shared branch you need cross-team coordination that's not unlike managing main itself. Then there's the question of when to promote. I'm not sure what it actually buys here.

Feature branches for a specific feature make sense when the work is long-running and owned by multiple people — reviewed as a complete unit before the merge. But that requires a genuine workstream with enough people engaged to ensure quality before a big lump lands on main.

The purpose of breaking this refactor into phases is to slide changes into main incrementally — not breaking anything, and hopefully sized sensibly for review. Not too small, not too large (which brings its own problems with conflicts). This is essentially what trunk-based development calls branch by abstraction, and it's how LlamaIndex handled their v0.10 refactor — 300+ integrations extracted to main incrementally behind compatibility shims.

If a PR doesn't regress the code and is clearly positioned, it feels like an effective way to get things in. In the case of intrinsics, the wiring-up comes later — you do the enablement pieces first, carefully, then activate the new function. Feature flags are an option where needed.

@planetf1

Copy link
Copy Markdown
Contributor Author

On contribs — I think for that to genuinely work you'd need real plug points in the core: interfaces, registries, something that lets a contrib slot in cleanly without touching core code. Without that, it's essentially a different folder with the same coupling and the same review overhead, plus an extra migration step when it graduates to core.

The adapter scaffolding this PR is introducing is part of building those extension points — so it's a bit circular to suggest housing it in contribs. Once the lifecycle interfaces are in place, contribs becomes a much more viable home for experimental adapters that want to develop independently before being considered for inclusion.

@planetf1

Copy link
Copy Markdown
Contributor Author

One reflection — the obvious alternative was a single big PR. But to do that well you'd need all the design decisions locked down upfront, and a diff that large tends to get a cursory pass rather than a close read. The phased approach was deliberate for exactly this reason: catching things like the capability naming and IOContract signature early, when course-correcting is cheap. A long-lived feature branch arguably makes this worse, not better — changes pile up out of sight and the quality gate moves to the end. This thread is a good example of why incremental works.

@jakelorocco

Copy link
Copy Markdown
Contributor

One reflection — the obvious alternative was a single big PR. But to do that well you'd need all the design decisions locked down upfront, and a diff that large tends to get a cursory pass rather than a close read. The phased approach was deliberate for exactly this reason: catching things like the capability naming and IOContract signature early, when course-correcting is cheap. A long-lived feature branch arguably makes this worse, not better — changes pile up out of sight and the quality gate moves to the end. This thread is a good example of why incremental works.

My argument was that we can still evaluate PRs into the feature branch and review as we are doing now. I don't think that requires much more testing (probably just a manual job every PR). As long as there are no API changes until the whole epic is complete, I would be okay with incremental merge as well.

@planetf1

planetf1 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Works for me. Nothing public changes in these PRs yet, it's internal scaffolding with no callers wired up, so I'll keep the incremental merges to that line. We can revisit if a later phase needs to touch the public surface before the epic wraps.

planetf1 added a commit to planetf1/mellea that referenced this pull request Jun 2, 2026
…omputing#1134)

Per review thread on PR generative-computing#1158: "role" implies exact matching and is
already an overloaded term. "capability" reads as "this adapter does X"
and naturally supports multiple adapters sharing one.

- Identity.role -> Identity.capability
- KNOWN_ROLES -> KNOWN_CAPABILITIES (module roles.py -> capabilities.py)
- warning message and tests updated to match

This is the vocabulary half of the thread. Decoupling the capability
registry from catalog entry names (an explicit capability field on
CatalogEntry) is deferred to a follow-up under Epic generative-computing#929, sequenced
after generative-computing#1157 to avoid catalog.py churn.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
planetf1 added a commit to planetf1/mellea that referenced this pull request Jun 3, 2026
…ng (generative-computing#1134)

Fixes flagged by the multi-reviewer pass on PR generative-computing#1158:

- `AdapterSchemaMismatchError` now passes structured fields (not the formatted
  message) to `Exception.__init__`, so `pickle.dumps(err)` round-trips through
  worker / process boundaries. `__str__` is overridden to keep the existing
  user-visible message format. Adds a `pickle` round-trip test.
- `IOContract.parse` now returns `dict[str, object]` (was bare `dict`) and
  `IOContract.build_prompt` typed as `**kwargs: object` to satisfy the
  AGENTS.md §5 strict-typing rule on the public ABC.
- `Identity` and `Adapter` are now `@dataclass(frozen=True)` so the `__post_init__`
  validation cannot be bypassed by post-construction assignment, and both are
  hashable (usable as dict keys / set members). New tests cover the frozen and
  hashable behaviour.
- `WeightsBinding` docstring now documents the lifecycle as an informal state
  machine (prepare → activate → deactivate → release) so Phase 2 implementations
  share a contract.
- Stub bindings (`LocalFileBinding`, `EmbeddedBinding`, `ServerMediatedBinding`)
  now raise `NotImplementedError` with a message pointing to Epic generative-computing#929 Phase 2.
- `KNOWN_ROLES` is now derived from `_INTRINSICS_CATALOG_ENTRIES` rather than
  hand-copied, eliminating the silent drift risk.
- `Identity.__post_init__` carries an inline comment explaining why the runtime
  check is needed alongside the `Literal` annotation.
- The placeholder docstring on `adapter_based_component/__init__.py` no longer
  hedges — the module asserts `AdapterBasedComponent` as the chosen name.
- `test_stub_binding_subclasses_raise_not_implemented` parametrises over verbs
  for clearer per-verb failure attribution.
- `test_identity_known_role_no_warning` no longer uses `simplefilter("error")`,
  which would have failed on any unrelated `DeprecationWarning` from imports.

All 25 unit tests pass; ruff and mypy clean on the new files.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
planetf1 added a commit to planetf1/mellea that referenced this pull request Jun 3, 2026
…omputing#1134)

Per review thread on PR generative-computing#1158: "role" implies exact matching and is
already an overloaded term. "capability" reads as "this adapter does X"
and naturally supports multiple adapters sharing one.

- Identity.role -> Identity.capability
- KNOWN_ROLES -> KNOWN_CAPABILITIES (module roles.py -> capabilities.py)
- warning message and tests updated to match

This is the vocabulary half of the thread. Decoupling the capability
registry from catalog entry names (an explicit capability field on
CatalogEntry) is deferred to a follow-up under Epic generative-computing#929, sequenced
after generative-computing#1157 to avoid catalog.py churn.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
@planetf1
planetf1 force-pushed the worktree-issue-1134 branch from 315ed01 to f13058e Compare June 3, 2026 13:32
@planetf1

planetf1 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Agreed — continuing with incremental merges. Separate discussion if needed.

planetf1 added 3 commits June 4, 2026 12:48
…g scaffolding (generative-computing#1134)

Implements Epic generative-computing#929 Phase 0 (Wave 1):

- `mellea/backends/adapters/_core.py`: new composable `Adapter` dataclass,
  `Identity`, `IOContract` ABC, `WeightsBinding` ABC, three stub bindings
  (`LocalFileBinding`, `EmbeddedBinding`, `ServerMediatedBinding`), and
  `AdapterSchemaMismatchError` exception.
- `mellea/backends/adapters/roles.py`: `KNOWN_ROLES` advisory registry
  (frozenset of known role strings seeded from the intrinsics catalog).
- `mellea/stdlib/components/adapter_based_component/__init__.py`: placeholder
  module re-exporting `Intrinsic` as `AdapterBasedComponent`; old import path
  remains valid.
- `mellea/backends/adapters/__init__.py`: all new public names exported.

Naming-collision resolution: the existing `Adapter` ABC in `adapter.py` was
already absent from `__init__.py__'s public surface. The new `Adapter`
dataclass is introduced in `_core.py` and re-exported, so no namespace
collision exists on the public API. Both coexist until shim removal in 4.1.

Closes generative-computing#1134

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…ng (generative-computing#1134)

Fixes flagged by the multi-reviewer pass on PR generative-computing#1158:

- `AdapterSchemaMismatchError` now passes structured fields (not the formatted
  message) to `Exception.__init__`, so `pickle.dumps(err)` round-trips through
  worker / process boundaries. `__str__` is overridden to keep the existing
  user-visible message format. Adds a `pickle` round-trip test.
- `IOContract.parse` now returns `dict[str, object]` (was bare `dict`) and
  `IOContract.build_prompt` typed as `**kwargs: object` to satisfy the
  AGENTS.md §5 strict-typing rule on the public ABC.
- `Identity` and `Adapter` are now `@dataclass(frozen=True)` so the `__post_init__`
  validation cannot be bypassed by post-construction assignment, and both are
  hashable (usable as dict keys / set members). New tests cover the frozen and
  hashable behaviour.
- `WeightsBinding` docstring now documents the lifecycle as an informal state
  machine (prepare → activate → deactivate → release) so Phase 2 implementations
  share a contract.
- Stub bindings (`LocalFileBinding`, `EmbeddedBinding`, `ServerMediatedBinding`)
  now raise `NotImplementedError` with a message pointing to Epic generative-computing#929 Phase 2.
- `KNOWN_ROLES` is now derived from `_INTRINSICS_CATALOG_ENTRIES` rather than
  hand-copied, eliminating the silent drift risk.
- `Identity.__post_init__` carries an inline comment explaining why the runtime
  check is needed alongside the `Literal` annotation.
- The placeholder docstring on `adapter_based_component/__init__.py` no longer
  hedges — the module asserts `AdapterBasedComponent` as the chosen name.
- `test_stub_binding_subclasses_raise_not_implemented` parametrises over verbs
  for clearer per-verb failure attribution.
- `test_identity_known_role_no_warning` no longer uses `simplefilter("error")`,
  which would have failed on any unrelated `DeprecationWarning` from imports.

All 25 unit tests pass; ruff and mypy clean on the new files.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
…omputing#1134)

Per review thread on PR generative-computing#1158: "role" implies exact matching and is
already an overloaded term. "capability" reads as "this adapter does X"
and naturally supports multiple adapters sharing one.

- Identity.role -> Identity.capability
- KNOWN_ROLES -> KNOWN_CAPABILITIES (module roles.py -> capabilities.py)
- warning message and tests updated to match

This is the vocabulary half of the thread. Decoupling the capability
registry from catalog entry names (an explicit capability field on
CatalogEntry) is deferred to a follow-up under Epic generative-computing#929, sequenced
after generative-computing#1157 to avoid catalog.py churn.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
@planetf1
planetf1 force-pushed the worktree-issue-1134 branch 2 times, most recently from f13058e to 2e161f9 Compare June 4, 2026 11:54
@planetf1
planetf1 enabled auto-merge June 4, 2026 13:03
@planetf1

planetf1 commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@jakelorocco @psschwei are you ok with the latest updates? anything else needed prior to merge?

@jakelorocco jakelorocco left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm; sorry thought I had approved after my last comment

@planetf1
planetf1 added this pull request to the merge queue Jun 5, 2026
Merged via the queue into generative-computing:main with commit 538a02b Jun 5, 2026
12 of 17 checks passed
@planetf1
planetf1 deleted the worktree-issue-1134 branch June 5, 2026 13:22
planetf1 added a commit to planetf1/mellea that referenced this pull request Jun 16, 2026
…all_intrinsic (Epic generative-computing#929 Phase 1)

Old classes (IntrinsicAdapter, EmbeddedIntrinsicAdapter, CustomIntrinsicAdapter)
now inherit from the new Adapter dataclass introduced in Phase 0 (PR generative-computing#1158).
Each emits DeprecationWarning on construction and exposes an Identity triple,
making isinstance(x, Adapter) return True alongside their existing type checks.

call_intrinsic is rewritten to go through backend.resolve_adapter() and
backend.adapter_scope() (no-op stub in Phase 1), removing the direct
IntrinsicAdapter / EmbeddedIntrinsicAdapter branching from stdlib code.

Requirement rerouting in OpenAIBackend and LocalHFBackend is updated to use
Identity.capability for adapter lookup instead of isinstance + AdapterType
checks. This removes the last internal callers that branched on the old class
hierarchy and unblocks Phase 3 (issues generative-computing#1137, generative-computing#1138, generative-computing#1139, generative-computing#1140, generative-computing#1141).

Closes generative-computing#1136

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
planetf1 added a commit to planetf1/mellea that referenced this pull request Jun 16, 2026
…all_intrinsic (Epic generative-computing#929 Phase 1)

Old classes (IntrinsicAdapter, EmbeddedIntrinsicAdapter, CustomIntrinsicAdapter)
now inherit from the new Adapter dataclass introduced in Phase 0 (PR generative-computing#1158).
Each emits DeprecationWarning on construction and exposes an Identity triple,
making isinstance(x, Adapter) return True alongside their existing type checks.

call_intrinsic is rewritten to go through backend.resolve_adapter() and
backend.adapter_scope() (no-op stub in Phase 1), removing the direct
IntrinsicAdapter / EmbeddedIntrinsicAdapter branching from stdlib code.

Requirement rerouting in OpenAIBackend and LocalHFBackend is updated to use
Identity.capability for adapter lookup instead of isinstance + AdapterType
checks. This removes the last internal callers that branched on the old class
hierarchy and unblocks Phase 3 (issues generative-computing#1137, generative-computing#1138, generative-computing#1139, generative-computing#1140, generative-computing#1141).

Closes generative-computing#1136

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
planetf1 added a commit to planetf1/mellea that referenced this pull request Jun 16, 2026
…all_intrinsic (Epic generative-computing#929 Phase 1)

Old classes (IntrinsicAdapter, EmbeddedIntrinsicAdapter, CustomIntrinsicAdapter)
now inherit from the new Adapter dataclass introduced in Phase 0 (PR generative-computing#1158).
Each emits DeprecationWarning on construction and exposes an Identity triple,
making isinstance(x, Adapter) return True alongside their existing type checks.

call_intrinsic is rewritten to go through backend.resolve_adapter() and
backend.adapter_scope() (no-op stub in Phase 1), removing the direct
IntrinsicAdapter / EmbeddedIntrinsicAdapter branching from stdlib code.

Requirement rerouting in OpenAIBackend and LocalHFBackend is updated to use
Identity.capability for adapter lookup instead of isinstance + AdapterType
checks. This removes the last internal callers that branched on the old class
hierarchy and unblocks Phase 3 (issues generative-computing#1137, generative-computing#1138, generative-computing#1139, generative-computing#1140, generative-computing#1141).

Closes generative-computing#1136

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
planetf1 added a commit to planetf1/mellea that referenced this pull request Jun 17, 2026
…all_intrinsic (Epic generative-computing#929 Phase 1)

Old classes (IntrinsicAdapter, EmbeddedIntrinsicAdapter, CustomIntrinsicAdapter)
now inherit from the new Adapter dataclass introduced in Phase 0 (PR generative-computing#1158).
Each emits DeprecationWarning on construction and exposes an Identity triple,
making isinstance(x, Adapter) return True alongside their existing type checks.

call_intrinsic is rewritten to go through backend.resolve_adapter() and
backend.adapter_scope() (no-op stub in Phase 1), removing the direct
IntrinsicAdapter / EmbeddedIntrinsicAdapter branching from stdlib code.

Requirement rerouting in OpenAIBackend and LocalHFBackend is updated to use
Identity.capability for adapter lookup instead of isinstance + AdapterType
checks. This removes the last internal callers that branched on the old class
hierarchy and unblocks Phase 3 (issues generative-computing#1137, generative-computing#1138, generative-computing#1139, generative-computing#1140, generative-computing#1141).

Closes generative-computing#1136

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
planetf1 added a commit to planetf1/mellea that referenced this pull request Jun 17, 2026
…all_intrinsic (Epic generative-computing#929 Phase 1)

Old classes (IntrinsicAdapter, EmbeddedIntrinsicAdapter, CustomIntrinsicAdapter)
now inherit from the new Adapter dataclass introduced in Phase 0 (PR generative-computing#1158).
Each emits DeprecationWarning on construction and exposes an Identity triple,
making isinstance(x, Adapter) return True alongside their existing type checks.

call_intrinsic is rewritten to go through backend.resolve_adapter() and
backend.adapter_scope() (no-op stub in Phase 1), removing the direct
IntrinsicAdapter / EmbeddedIntrinsicAdapter branching from stdlib code.

Requirement rerouting in OpenAIBackend and LocalHFBackend is updated to use
Identity.capability for adapter lookup instead of isinstance + AdapterType
checks. This removes the last internal callers that branched on the old class
hierarchy and unblocks Phase 3 (issues generative-computing#1137, generative-computing#1138, generative-computing#1139, generative-computing#1140, generative-computing#1141).

Closes generative-computing#1136

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
planetf1 added a commit to planetf1/mellea that referenced this pull request Jun 23, 2026
…all_intrinsic (Epic generative-computing#929 Phase 1)

Old classes (IntrinsicAdapter, EmbeddedIntrinsicAdapter, CustomIntrinsicAdapter)
now inherit from the new Adapter dataclass introduced in Phase 0 (PR generative-computing#1158).
Each emits DeprecationWarning on construction and exposes an Identity triple,
making isinstance(x, Adapter) return True alongside their existing type checks.

call_intrinsic is rewritten to go through backend.resolve_adapter() and
backend.adapter_scope() (no-op stub in Phase 1), removing the direct
IntrinsicAdapter / EmbeddedIntrinsicAdapter branching from stdlib code.

Requirement rerouting in OpenAIBackend and LocalHFBackend is updated to use
Identity.capability for adapter lookup instead of isinstance + AdapterType
checks. This removes the last internal callers that branched on the old class
hierarchy and unblocks Phase 3 (issues generative-computing#1137, generative-computing#1138, generative-computing#1139, generative-computing#1140, generative-computing#1141).

Closes generative-computing#1136

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
AngeloDanducci pushed a commit to AngeloDanducci/mellea that referenced this pull request Jun 23, 2026
…insic rewritten (Epic generative-computing#929 Phase 1) (generative-computing#1269)

* feat(intrinsics): shim old adapters onto Adapter dataclass; rewrite call_intrinsic (Epic generative-computing#929 Phase 1)

Old classes (IntrinsicAdapter, EmbeddedIntrinsicAdapter, CustomIntrinsicAdapter)
now inherit from the new Adapter dataclass introduced in Phase 0 (PR generative-computing#1158).
Each emits DeprecationWarning on construction and exposes an Identity triple,
making isinstance(x, Adapter) return True alongside their existing type checks.

call_intrinsic is rewritten to go through backend.resolve_adapter() and
backend.adapter_scope() (no-op stub in Phase 1), removing the direct
IntrinsicAdapter / EmbeddedIntrinsicAdapter branching from stdlib code.

Requirement rerouting in OpenAIBackend and LocalHFBackend is updated to use
Identity.capability for adapter lookup instead of isinstance + AdapterType
checks. This removes the last internal callers that branched on the old class
hierarchy and unblocks Phase 3 (issues generative-computing#1137, generative-computing#1138, generative-computing#1139, generative-computing#1140, generative-computing#1141).

Closes generative-computing#1136

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* test(intrinsics): fix stale patch + add resolve_adapter unit tests

Fix test_huggingface_unit.py: remove dead patch of get_adapter_for_intrinsic
(removed from huggingface.py imports in parent commit) and pre-populate
_added_adapters with a proper Identity so the new capability-based lookup finds
the adapter. Add two unit tests to test_shims.py covering the find-existing and
no-model-id error paths of resolve_adapter.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* nit: drop line number from comment (stale-proof)

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* fix(docs): align resolve_adapter Returns: type with annotation

Docstring quality gate (--fail-on-quality) requires the Returns: prefix
to match the return type annotation exactly. Annotation is _AdapterCore;
docstring incorrectly said Adapter.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* refactor(adapters): extract _find_adapter helper; clarify shim docs

Add AdapterMixin._find_adapter(capability, adapter_types) to centralise
the capability-based lookup that was repeated inline four times across
huggingface.py and openai.py.  resolve_adapter's duplicate search loops
also collapse to single _find_adapter calls.

Clarify IntrinsicAdapter and EmbeddedIntrinsicAdapter docstrings: replace
the bare identity/io_contract/weights Attributes entries with a .. note::
explaining these are Phase 1 internal scaffolding not meant for consumer use.

Update test fixtures to wire _find_adapter through the real AdapterMixin
implementation, and fix a misleading comment in test_huggingface_unit.py.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* docs(dev): update Phase 1 code example to use _find_adapter

The inline next(...) example was superseded by the _find_adapter helper
extracted in the previous commit.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* fix(adapters): validate technology before warning; document _find_adapter ordering

EmbeddedIntrinsicAdapter previously emitted DeprecationWarning before
validating the technology argument, so an invalid call (e.g. technology='qlora')
would both warn and raise. Validation now runs first.

Add a Note: section to _find_adapter documenting that insertion-order is used
and the Phase 0 preference-ordering semantics are not preserved; Phase 2
(issue generative-computing#1138) will revisit if multiple entries per capability become possible.

Test updated: test_embedded_invalid_technology now asserts that no
DeprecationWarning fires on an invalid-technology construction attempt.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* fix(adapters): honour adapter type preference order; remove adapter_scope from call_intrinsic

_find_adapter previously accepted set[str] and returned the first hit in
_added_adapters insertion order, ignoring any caller-specified type preference.
Change the signature to tuple[str, ...] and iterate preferred types first so
aLoRA is returned before LoRA when both are registered for the same capability,
matching Phase 0's get_adapter_for_intrinsic behaviour.

The HF backend was also discarding the preference order at the call site by
building a set from action.adapter_types before passing to _find_adapter.
Both call sites in huggingface.py now pass a tuple so the preference flows
end-to-end from call_intrinsic through to adapter selection.

Remove the adapter_scope wrapper from call_intrinsic. The HF backend already
handles adapter activation inside _generate_with_adapter_lock under the
generation lock, immediately before generate_func is called. Activating at
the call_intrinsic level would be outside that lock and could race with
concurrent async requests. Add a comment explaining this so the pattern is
clear for Phase 2 implementors.

Add test_find_adapter_honours_type_preference_order to verify aLoRA wins over
LoRA regardless of insertion order — the previous set-based implementation
would have passed all prior tests since none registered both types for the
same capability.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* fix(adapters): fix openai.py call sites to pass tuple not set to _find_adapter

Missed in the initial fix — openai.py had the same set[str] call sites as
huggingface.py at lines 486 and 571.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* docs(dev): fix set-literal in requirement_aLoRA_rerouting Phase 1 example

The "After (Phase 1)" snippet showed {"alora"} (set) but _find_adapter
takes tuple[str, ...] for preference-ordered iteration.  A set has no
guaranteed order, defeating the aLoRA-before-LoRA preference the method
provides.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* fix(adapters): raise NotImplementedError in _ShimWeightsBinding; document resolve_adapter contracts

_ShimWeightsBinding methods were silent no-ops while _ShimIOContract already
raised NotImplementedError.  The asymmetry means Phase 2 lifecycle code
accidentally calling WeightsBinding.activate/deactivate on an un-migrated
shim would silently succeed rather than surface the gap.  Raise consistently.

Also adds three inline comments to resolve_adapter:
- warns that warnings.catch_warnings() is not async/thread-safe (Phase 2 gap)
- notes the embedded branch is only valid for backends whose add_adapter
  accepts the full Adapter type (not LocalHFBackend)
- documents that the AdapterType.LORA default is intentional (mirrors the
  pre-Phase-1 _util.py default) and is Phase 2 scope to improve

Adds test_resolve_adapter_lazy_creates_and_returns covering the previously
untested lazy-creation branch of resolve_adapter.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* test(adapters): mock obtain_io_yaml in test_resolve_adapter_lazy_creates_and_returns

The test patched fetch_intrinsic_metadata but resolve_adapter calls
IntrinsicAdapter without a config_dict, causing __init__ to fall through
to intrinsics.obtain_io_yaml() and make a real HF Hub request.  Mock
obtain_io_yaml and builtins.open to keep this unit test fully offline,
consistent with the hf_skip() pattern from PR generative-computing#1199.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

* docs(adapters): update adapter.py docstrings to reflect Phase 1 shim changes

- Rewrite module-level docstring to name resolve_adapter/_find_adapter as
  primary surface and LocalHFAdapter/IntrinsicAdapter/EmbeddedIntrinsicAdapter
  as deprecation shims; removes stale description of get_adapter_for_intrinsic
- Expand catch_warnings race comment to name the concrete failure mode:
  concurrent first-time call_intrinsic calls interleaving catch_warnings
  contexts, causing DeprecationWarning to surface in user code
- Make _ShimWeightsBinding NotImplementedError messages consistent with
  _ShimIOContract by including "Phase 2 (issue generative-computing#1138)" navigation context

Addresses markstur review comments on PR generative-computing#1269.

Assisted-by: Claude Code
Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>

---------

Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(intrinsics): introduce Adapter/Identity/IOContract/WeightsBinding scaffolding (Epic #929 Phase 0)

3 participants