Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a0bcdb0
📦 (adapters): Create empty registry module
Chisanan232 Apr 27, 2026
a44d5c4
📦 (tests): Create empty registry test module
Chisanan232 Apr 27, 2026
3ca4260
✨ (adapters): Add AdapterInfo dataclass contract
Chisanan232 Apr 27, 2026
db0737f
🏗️ (adapters): Add AdapterRegistry class shell with lock and state maps
Chisanan232 Apr 27, 2026
811dc9b
✨ (adapters): Add built-in framework placeholder adapter type
Chisanan232 Apr 27, 2026
c8cbe83
✨ (adapters): Pre-register built-in adapters in AdapterRegistry.__init__
Chisanan232 Apr 27, 2026
9dfb6e8
✨ (adapters): Add register() with replacement-safe behavior
Chisanan232 Apr 27, 2026
f40f596
✨ (adapters): Add unregister() with safe deactivation path
Chisanan232 Apr 27, 2026
9d546f6
✨ (adapters): Add list_active() real-time state projection
Chisanan232 Apr 27, 2026
b29310f
✨ (adapters): Add entry-point discovery for group agent_assembly.adap…
Chisanan232 Apr 27, 2026
c197d13
✨ (adapters): Add auto_detect() activation flow
Chisanan232 Apr 27, 2026
fc093c6
♻️ (adapters): Make auto_detect() idempotent across repeated calls
Chisanan232 Apr 27, 2026
8c90da2
♻️ (adapters): Track activation and discovery error status in Adapter…
Chisanan232 Apr 27, 2026
71b6f5d
♻️ (exports): Export AdapterRegistry and AdapterInfo from adapters pa…
Chisanan232 Apr 27, 2026
0a78292
✅ (tests): Add auto_detect importability isolation test with mocked i…
Chisanan232 Apr 27, 2026
dc4ebe2
✅ (tests): Add entry-point discovery test for third-party adapters
Chisanan232 Apr 27, 2026
1ca7d20
✅ (tests): Add idempotent auto_detect test
Chisanan232 Apr 27, 2026
70d8157
✅ (tests): Add list_active real-time state test
Chisanan232 Apr 27, 2026
07fec77
✅ (tests): Add concurrent register and unregister thread-safety test
Chisanan232 Apr 27, 2026
710d603
🔧 (typing): Enforce strict mypy coverage for registry module
Chisanan232 Apr 27, 2026
0e722a4
♻️ (adapters): Make entry-point auto_detect idempotent by discovery key
Chisanan232 Apr 27, 2026
35a71d3
♻️ (adapters): Use resilient noop governance interceptor in auto_detect
Chisanan232 Apr 27, 2026
f22a072
♻️ (adapters): Align built-in key to pydantic-ai with python import a…
Chisanan232 Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion agent_assembly/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor
from agent_assembly.adapters.registry import AdapterInfo, AdapterRegistry

__all__ = ["GovernanceInterceptor", "FrameworkAdapter"]
__all__ = ["GovernanceInterceptor", "FrameworkAdapter", "AdapterInfo", "AdapterRegistry"]
188 changes: 188 additions & 0 deletions agent_assembly/adapters/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
from __future__ import annotations

from dataclasses import dataclass
from importlib import metadata
from threading import Lock
from typing import Callable, Literal

from agent_assembly.adapters.base import FrameworkAdapter


@dataclass(frozen=True, slots=True)
class AdapterInfo:
name: str
version: str
status: Literal["active", "error"]
hooks_registered: int


class _BuiltinPlaceholderAdapter(FrameworkAdapter):
def __init__(self, framework_name: str, import_name: str | None = None) -> None:
self._framework_name = framework_name
self._import_name = import_name or framework_name

def get_framework_name(self) -> str:
return self._import_name

def get_supported_versions(self) -> list[str]:
return [">=0.0.0"]

def register_hooks(self, interceptor: object) -> None:
return None

def unregister_hooks(self) -> None:
return None


def _noop_interceptor_method(*args: object, **kwargs: object) -> None:
del args, kwargs
return None


class _NoopGovernanceInterceptor:
def __getattr__(self, name: str) -> Callable[..., None]:
del name
return _noop_interceptor_method


_NOOP_GOVERNANCE_INTERCEPTOR = _NoopGovernanceInterceptor()


class AdapterRegistry:
def __init__(self) -> None:
self._lock = Lock()
self._registered: dict[str, FrameworkAdapter] = {}
self._active: dict[str, FrameworkAdapter] = {}
self._errors: dict[str, str] = {}
self._discovered_entry_points: set[str] = set()
builtin_frameworks = [
("langchain", "langchain"),
("langgraph", "langgraph"),
("crewai", "crewai"),
("pydantic-ai", "pydantic_ai"),
]
for registry_name, import_name in builtin_frameworks:
self._registered[registry_name] = _BuiltinPlaceholderAdapter(
framework_name=registry_name,
import_name=import_name,
)

def register(self, adapter: FrameworkAdapter) -> None:
adapter_name = adapter.get_framework_name()
with self._lock:
self._registered[adapter_name] = adapter
self._errors.pop(adapter_name, None)
if adapter_name in self._active and self._active[adapter_name] is not adapter:
self._active.pop(adapter_name, None)

def unregister(self, name: str) -> None:
with self._lock:
active_adapter = self._active.pop(name, None)
self._registered.pop(name, None)
self._errors.pop(name, None)

if active_adapter is not None:
active_adapter.unregister_hooks()

def list_active(self) -> list[AdapterInfo]:
with self._lock:
active_items = list(self._active.items())
error_names = set(self._errors.keys())

result: list[AdapterInfo] = []
for name, adapter in active_items:
hooks_registered = getattr(adapter, "_hooks_registered_count", 0)
if not isinstance(hooks_registered, int):
hooks_registered = 0

result.append(
AdapterInfo(
name=name,
version=adapter.get_active_version() or "",
status="active",
hooks_registered=hooks_registered,
)
)

active_names = {name for name, _ in active_items}
for name in sorted(error_names - active_names):
result.append(
AdapterInfo(
name=name,
version="",
status="error",
hooks_registered=0,
)
)

return sorted(result, key=lambda info: info.name)

def _discover_entry_point_adapters(self) -> list[str]:
discovered: list[str] = []
entry_points = metadata.entry_points()
adapter_entry_points = entry_points.select(group="agent_assembly.adapters")

for entry_point in adapter_entry_points:
with self._lock:
if entry_point.name in self._discovered_entry_points:
continue

try:
loaded = entry_point.load()
except Exception as error: # pragma: no cover - guarded by tests via monkeypatch
with self._lock:
self._errors[entry_point.name] = str(error)
continue

if not isinstance(loaded, type):
with self._lock:
self._errors[entry_point.name] = "Entry point did not load a class."
continue

if not issubclass(loaded, FrameworkAdapter):
with self._lock:
self._errors[entry_point.name] = "Entry point class is not a FrameworkAdapter."
continue

try:
adapter = loaded()
except Exception as error: # pragma: no cover - guarded by tests via monkeypatch
with self._lock:
self._errors[entry_point.name] = str(error)
continue

self.register(adapter)
with self._lock:
self._discovered_entry_points.add(entry_point.name)
discovered.append(adapter.get_framework_name())

return discovered

def auto_detect(self) -> list[str]:
self._discover_entry_point_adapters()

with self._lock:
registered_items = list(self._registered.items())

activated: list[str] = []
for name, adapter in registered_items:
if not adapter.is_available():
continue

with self._lock:
if self._active.get(name) is adapter:
continue

try:
adapter.register(_NOOP_GOVERNANCE_INTERCEPTOR)
except Exception as error:
with self._lock:
self._errors[name] = str(error)
continue

with self._lock:
self._active[name] = adapter
self._errors.pop(name, None)
activated.append(name)

return activated
3 changes: 3 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ strict_concatenate = True

[mypy-agent_assembly.adapters.base]
strict = True

[mypy-agent_assembly.adapters.registry]
strict = True
Loading