Files
teng-lin--notebooklm-py/docs/adr/0002-capability-protocol-pattern.md
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

86 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0002: Capability Protocol pattern (`SessionCapabilities` fat union)
> **Current state (2026-05-29).** This ADR is **Superseded** and documents a
> pre-cutover pattern for historical context only. Since it was written, the
> concrete `Session` facade class, `_session.py`, and `_core.py` have all been
> **deleted**, and the surviving shared Protocols now live in
> `src/notebooklm/_runtime/contracts.py`
> (see [ADR-0014](0014-feature-local-runtime-adapters.md) and
> [`docs/architecture.md`](../architecture.md) for the live shape). The
> feature-local composite Protocols `ChatRuntime` and `ArtifactsRuntime`
> referenced in the Status line below were also **retired** — feature APIs now
> take their narrow collaborators by keyword-only constructor argument. The
> live client also has later-added namespaces such as `mind_maps` and `labels`.
> Read
> in-body references to `Session`, `_core.py`, `_session_contracts.py`,
> `ChatRuntime`, `ArtifactsRuntime`, and exact `file.py:NNN` line numbers as
> historical — they do not point at live code.
## Status
Superseded by [`arch-d2-cutover`](https://github.com/teng-lin/notebooklm-py/pull/835) (#835). The `SessionCapabilities` adapter and the transitional `ChatStreamingProvider` Protocol were deleted at cutover time; the later composable-capabilities arc continued in [ADR-0013](0013-composable-session-capabilities.md) (#866) and then retired the concrete `Session` facade itself. Current feature APIs depend on direct collaborators and the small shared Protocol set in `src/notebooklm/_runtime/contracts.py` (`Kernel`, `RpcCaller`, `LoopGuard`); single-consumer Protocols stay local to their owners. `NotebookLMClient` wires those collaborators in `src/notebooklm/_client_assembly.py`.
This ADR documents the pre-cutover pattern for historical context. The "Decision" section below describes the state prior to D2 cutover; the "Alternatives considered" section describes the replacement now adopted.
## Context
At the original baseline, `NotebookLMClient` exposed eight namespaced feature APIs (`notebooks`, `sources`, `artifacts`, `chat`, `research`, `notes`, `settings`, `sharing`). The live client has since added namespaces such as `mind_maps` and `labels`; this ADR describes the earlier pattern that shaped the cutover away from a broad `SessionCapabilities` adapter. Each feature API is implemented in its own module (`_notebooks.py`, `_sources.py`, etc.) and needed structured access to `Session` collaborators (RPC dispatch, auth routing, request-id allocation, polling registry, transport bookkeeping, upload concurrency, etc.).
Two non-negotiable forces shaped the original design:
- **Sub-clients must not import `Session` directly.** Doing so would create a circular dependency (`Session` imports sub-clients to expose them on `NotebookLMClient.notebooks` etc.; sub-clients importing `Session` would close the loop). Mypy enforces the boundary via `TYPE_CHECKING` gates today.
- **Sub-clients should be typeable.** When a sub-client calls `executor.rpc_call(...)`, mypy needs to verify the signature; passing `Any` defeats the type system at exactly the place where method-ID drift would otherwise break silently.
The codebase resolved both forces with a *capability Protocol* pattern. Ten narrow `Protocol` classes describe individual collaborator surfaces:
```text
CoreRPCProvider · SourceListProvider · CoreReqIdProvider
ChatStreamingProvider · PollRegistryProvider · AuthRouteProvider
CookieJarProvider · TransportOperationProvider
UploadConcurrencyProvider · LoopAffinityProvider
```
Each Protocol describes the smallest collaborator surface the audit could identify at the time of extraction. A concrete adapter class `SessionCapabilities` then multi-inherits *all ten* Protocols and forwards every method to an underlying `Session` instance (`src/notebooklm/_capabilities.py:149-160`). Sub-clients accept a `SessionCapabilities` parameter in their constructors and rely on it for every collaborator interaction.
An internal architecture audit (disease D2) classified the result as a *fat-union god-interface wearing a Protocol mask*. The Protocols are narrow individually, but every sub-client takes the union, so the *effective* contract sub-clients depend on is the full ten-Protocol surface. The hoped-for narrowing never materialized because the adapter pre-merges them.
## Decision
For the current arc (tier-10 baseline), the pattern is:
1. Define narrow capability `Protocol` classes in `src/notebooklm/_capabilities.py`, one per collaborator surface.
2. Define a single concrete adapter `SessionCapabilities` that multi-inherits every Protocol and forwards to `Session`.
3. Sub-client constructors accept a `SessionCapabilities` instance, not a `Session` instance.
4. `NotebookLMClient` constructs one `SessionCapabilities` adapter at open time and threads it into every sub-client.
The pattern is *Accepted* today because:
- It provides a single import path for sub-clients (`from ._capabilities import SessionCapabilities`), avoiding the "every sub-client lists its own Protocol grab-bag" boilerplate.
- It guarantees that every Protocol has at least one structural implementer (`Session`, through the adapter), so mypy verifies the contract end-to-end.
- It survived the tier-7 thread-safety arc and the tier-8 RPC/VCR arc without churn, which means the surface is empirically stable.
## Consequences
**Wanted:**
- A single, mypy-verified seam between sub-clients and `Session`.
- The capability Protocols documented the collaborator graph in one place, which was useful for comparison. The live graph now lives in [`docs/architecture.md`](../architecture.md).
**Unwanted (and the reason for the sunset clause):**
- Every sub-client depends on the *union*, not the subset it actually needs. `NotebooksAPI` and `SettingsAPI` do not need `UploadConcurrencyProvider` or `ChatStreamingProvider`; today they advertise both anyway.
- `Session` cannot shrink below ~1,300 lines while the union pins it. Every method named in any Protocol must remain on `Session` (or on the adapter, which delegates to `Session`).
- The property-bridge zoo in `_core.py:450-774` exists partly because the union forces `Session` to expose attributes that have been physically relocated into seams — see ADR-0001.
- The adapter has begun to leak private internals. `_capabilities.py:230` forwards `_core._begin_transport_post`, an underscore-prefixed method. The narrow-protocol contract has already started to bend into private territory.
- `ChatStreamingProvider`'s docstring openly self-describes as transitional: *"Chat-aware error mapping still lives on `Session.query_post` until that is extracted into a chat-owned transport."* The fat union is documented as not-yet-done work.
The audit's recommendation is to *type each sub-client on its actual capability subset* and delete the `SessionCapabilities` adapter. `Session` will structurally satisfy each narrow Protocol; no runtime adapter is needed because structural sub-typing is enough. That work is sequenced as the D2 cutover (Wave 3 of the architecture-disease-remediation arc).
## Alternatives considered
- **Per-sub-client narrow `Protocol` class — chosen replacement for the D2 cutover.** Each sub-client declares its own `Protocol` listing only the collaborator surfaces it actually uses. `Session` is not modified; structural sub-typing means `Session` automatically satisfies each per-sub-client `Protocol`. Effect: `NotebooksAPI` depends on `CoreRPC + AuthRoute` only; the type checker enforces narrowing. Cost: each sub-client owns one extra type definition; ~8 new Protocol classes overall. Was not chosen at the time of the original extraction because the team prioritised "one import path for sub-clients" over "minimum coupling per sub-client"; the audit re-prioritises after observing the long-term coupling cost.
- **Constructor injection of individual collaborator dataclasses.** Rejected. Would force every sub-client constructor to take 47 typed parameters, and every test to construct that many fakes. The Protocol pattern is strictly more ergonomic and only the *unioned shape* is wrong, not the structural-typing approach.
- **Type sub-clients on `Session` directly.** Rejected. Creates the circular-import problem and defeats the layering. The Protocol pattern is the canonical Python answer to "type me on something I cannot import."
- **`typing.Protocol` with `runtime_checkable=True` + `isinstance` guards instead of an adapter class.** Rejected. `isinstance` checks against `runtime_checkable` Protocols are slow and not actually safer (they do not inspect method signatures). The cost without benefit was clear at the time.
- **Delete `SessionCapabilities` outright in this ADR.** Rejected. Removal must be paired with the per-sub-client Protocol introduction; deleting it first would force every sub-client to type its `core` parameter as `Any` during the migration window, which loses the type-safety benefit the pattern was designed to deliver. The D2 cutover sequences the swap atomically.