Framework adapters must implement the FrameworkAdapter protocol:
Method
Required
Description
name
✅
Unique framework identifier
is_available()
✅
Check framework dependencies
resolve()
❌
Pick the concrete adapter variant (default returns self)
setup(*, framework_tag)
❌
Framework-specific pre-run hooks (default no-op)
run(config, llm_config, topic)
✅
Execute framework logic
cleanup()
✅
Clean up resources
Single authoritative arun() (PR #1857): The Protocol declares arun() once; BaseFrameworkAdapter provides a single default that offloads run() to a worker thread via asyncio.to_thread. Sync-only adapters (crewai, autogen v0.2) inherit this and need do nothing. Native-async adapters override arun(). Earlier revisions of the codebase declared arun() twice on both the Protocol and the base class; those duplicate declarations have been removed and there is now exactly one default to override.
After the existing “Adapter protocol” section, two new optional methods were added in PR #1763:resolve() — pick a concrete variant.
def resolve(self) -> "FrameworkAdapter": """Pick the concrete adapter variant. Default implementation returns self. Override if your adapter is a 'family' (e.g. multiple SDK versions) and you want to pick at runtime. """ return self
setup(*, framework_tag) — pre-run hook.
def setup(self, *, framework_tag: str) -> None: """Framework-specific pre-run hooks (SDK init, etc.). Default implementation is a no-op. Use this for one-time per-run setup that depends on the resolved framework name (logging context, SDK init). Note: observability init is handled centrally by init_observability(), you don't need to call agentops.init() here. """ pass
Always clean up resources in the cleanup() method:
class ResourceAwareAdapter(BaseFrameworkAdapter): def __init__(self): super().__init__() self._connections = [] self._temp_files = [] def run(self, config, llm_config, topic): # Create resources during execution conn = self._create_connection() self._connections.append(conn) temp_file = self._create_temp_file() self._temp_files.append(temp_file) # Use resources... return result def cleanup(self) -> None: # Close connections for conn in self._connections: try: conn.close() except Exception: pass # Clean up temp files for temp_file in self._temp_files: try: temp_file.unlink() except Exception: pass self._connections.clear() self._temp_files.clear()
Don't import heavy deps inside is_available
Use cached availability checks to avoid multi-second imports on every probe:
from praisonai._framework_availability import is_available as check_availabilitydef is_available(self) -> bool: return check_availability("myframework") # cached, uses importlib.util.find_spec
The is_available() method is called on every CLI startup and framework probe. Full imports here cause multi-second delays. The centralized availability check uses cached importlib.util.find_spec for fast, repeated checks.
If you don’t pass adapter_registry, you get the process-default registry shared by all callers in the same Python process. Use a custom registry for tenant isolation, sandboxed tests, or registering one-off adapters that should not leak across runs.
The new adapter_registry= kwarg on AgentsGenerator and AutoGenerator enables dependency injection for multi-tenant applications and testing:
from praisonai.framework_adapters.registry import FrameworkAdapterRegistryfrom praisonai.agents_generator import AgentsGenerator# Tenant-isolated registry — NOT shared with other tenants in the same processtenant_registry = FrameworkAdapterRegistry()tenant_registry.register("crewai", CustomCrewAIAdapter)generator = AgentsGenerator( agent_file="agents.yaml", framework="crewai", config_list=[...], adapter_registry=tenant_registry, # <-- new in #1639)