8.9 KiB
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
Sessionfacade class,_session.py, and_core.pyhave all been deleted, and the surviving shared Protocols now live insrc/notebooklm/_runtime/contracts.py(see ADR-0014 anddocs/architecture.mdfor the live shape). The feature-local composite ProtocolsChatRuntimeandArtifactsRuntimereferenced 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 asmind_mapsandlabels. Read in-body references toSession,_core.py,_session_contracts.py,ChatRuntime,ArtifactsRuntime, and exactfile.py:NNNline numbers as historical — they do not point at live code.
Status
Superseded by arch-d2-cutover (#835). The SessionCapabilities adapter and the transitional ChatStreamingProvider Protocol were deleted at cutover time; the later composable-capabilities arc continued in ADR-0013 (#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
Sessiondirectly. Doing so would create a circular dependency (Sessionimports sub-clients to expose them onNotebookLMClient.notebooksetc.; sub-clients importingSessionwould close the loop). Mypy enforces the boundary viaTYPE_CHECKINGgates today. - Sub-clients should be typeable. When a sub-client calls
executor.rpc_call(...), mypy needs to verify the signature; passingAnydefeats 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:
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:
- Define narrow capability
Protocolclasses insrc/notebooklm/_capabilities.py, one per collaborator surface. - Define a single concrete adapter
SessionCapabilitiesthat multi-inherits every Protocol and forwards toSession. - Sub-client constructors accept a
SessionCapabilitiesinstance, not aSessioninstance. NotebookLMClientconstructs oneSessionCapabilitiesadapter 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.
Unwanted (and the reason for the sunset clause):
- Every sub-client depends on the union, not the subset it actually needs.
NotebooksAPIandSettingsAPIdo not needUploadConcurrencyProviderorChatStreamingProvider; today they advertise both anyway. Sessioncannot shrink below ~1,300 lines while the union pins it. Every method named in any Protocol must remain onSession(or on the adapter, which delegates toSession).- The property-bridge zoo in
_core.py:450-774exists partly because the union forcesSessionto expose attributes that have been physically relocated into seams — see ADR-0001. - The adapter has begun to leak private internals.
_capabilities.py:230forwards_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 onSession.query_postuntil 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
Protocolclass — chosen replacement for the D2 cutover. Each sub-client declares its ownProtocollisting only the collaborator surfaces it actually uses.Sessionis not modified; structural sub-typing meansSessionautomatically satisfies each per-sub-clientProtocol. Effect:NotebooksAPIdepends onCoreRPC + AuthRouteonly; 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 4–7 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
Sessiondirectly. 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.Protocolwithruntime_checkable=True+isinstanceguards instead of an adapter class. Rejected.isinstancechecks againstruntime_checkableProtocols are slow and not actually safer (they do not inspect method signatures). The cost without benefit was clear at the time.- Delete
SessionCapabilitiesoutright 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 itscoreparameter asAnyduring the migration window, which loses the type-safety benefit the pattern was designed to deliver. The D2 cutover sequences the swap atomically.