Skip to main content
Framework adapter plugins enable third-party developers to add new execution frameworks to PraisonAI without modifying core code.
from praisonaiagents import Agent

agent = Agent(name="adapter-agent", instructions="Use framework adapter plugins.")
agent.start("Enable the LangChain adapter plugin for this workflow.")
The user installs a third-party adapter package; entry-point discovery registers it for CLI framework selection.
Deprecated: Direct imports of concrete built-in adapter classes from praisonai.framework_adapters (CrewAIAdapter, AutoGenAdapter, AutoGenV4Adapter, AG2Adapter) now emit DeprecationWarning. Use the registry (get_default_registry().create("crewai")) or register via the entry-point group instead.

Quick Start

1

Programmatic Registration

Register a framework adapter directly in your code:
from typing import Any, Callable, Dict, List, Optional
from praisonaiagents import BaseFrameworkAdapter
from praisonai.framework_adapters.registry import get_default_registry

class MyFrameworkAdapter(BaseFrameworkAdapter):
    name = "myframework"

    def is_available(self) -> bool:
        try:
            import myframework
            return True
        except ImportError:
            return False

    def run(
        self,
        config: Dict[str, Any],
        llm_config: List[Dict],
        topic: str,
        *,
        tools_dict: Optional[Dict[str, Any]] = None,
        agent_callback: Optional[Callable] = None,
        task_callback: Optional[Callable] = None,
        cli_config: Optional[Dict[str, Any]] = None,
    ) -> str:
        return "Framework execution result"

# Register the adapter (preferred for app code)
registry = get_default_registry()
registry.register("myframework", MyFrameworkAdapter)
2

Entry-Point Plugin (installable)

Create a pip-installable plugin — no manual registry.register() call needed:
# my_framework_adapter.py
from praisonaiagents import BaseFrameworkAdapter

class MyFrameworkAdapter(BaseFrameworkAdapter):
    name = "my_framework"
    install_hint = "pip install my-framework-bridge"
    requires_tools_extra = False

    def is_available(self) -> bool:
        return True

    def run(
        self,
        config,
        llm_config,
        topic,
        *,
        tools_dict=None,
        agent_callback=None,
        task_callback=None,
        cli_config=None,
    ) -> str:
        return f"my_framework ran: {topic}"
# pyproject.toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "my-framework-bridge"
version = "0.0.1"
dependencies = ["praisonaiagents"]

[tool.setuptools]
py-modules = ["my_framework_adapter"]

[project.entry-points."praisonai.framework_adapters"]
my_framework = "my_framework_adapter:MyFrameworkAdapter"
pip install -e ./examples/python/frameworks/custom_adapter
praisonai --framework my_framework agents.yaml
After pip install, the adapter appears in praisonai --framework choices and praisonai doctor automatically — no manual registry.register() call needed.

What the Registry Controls

Registering through the praisonai.framework_adapters entry-point group gives your adapter automatic visibility across three surfaces — no extra wiring required:
SurfaceEffect
praisonai --framework <name>Your adapter name appears in the argparse choices= list
praisonai doctor configagents.yaml framework: <name> passes validation
Install-hint messagesYour adapter’s install_hint attribute is shown when the framework module is missing
# Example: after pip install praisonai-langgraph
$ praisonai --framework langgraph agents.yaml   # accepted
$ praisonai doctor
 agents.yaml framework='langgraph' is a registered adapter
See Framework Availability — Dynamic Discovery for more details on how the registry feeds these surfaces.

How It Works

The framework adapter registry provides a central point for managing framework implementations:
OperationDescriptionWhen Called
DiscoveryEntry points auto-loaded on first registry accessImport time
RegistrationAdapters registered by namePlugin installation
CreationAdapter instances created on demandCLI framework selection
AvailabilityFramework dependencies checkedBefore execution

Auto-Discovery in the CLI

Once your adapter is registered (in-process via register() or via the praisonai.framework_adapters entry-point group), it appears automatically wherever framework names are listed or validated — no further wiring needed:
SurfaceWhat it shows / validates against
praisonai --framework <name> argparse choiceslist_framework_choices(include_unavailable=True)
praisonai doctor agents.yaml framework: field checklist_framework_choices(include_unavailable=True)
Missing-framework error → install hintadapter’s own install_hint (falls back to pip install 'praisonai-frameworks[<name>]')
The single source of truth is praisonai.framework_adapters.list_framework_choices(). When include_unavailable=False (the default) only adapters whose is_available() returns True are returned; the CLI uses include_unavailable=True so users see every registered framework and get a friendly error if its dependencies are missing.

Declaring an install hint

Surface a tailored install command for your adapter so users don’t see the generic fallback:
class MyFrameworkAdapter:
    name = "myframework"
    install_hint = 'pip install "myframework-praisonai[gpu]"'

    def is_available(self) -> bool: ...
    def run(self, config, llm_config, topic, *, tools_dict=None,
            agent_callback=None, task_callback=None, cli_config=None): ...
    def cleanup(self) -> None: ...
When myframework is selected but not installed, the CLI now raises:
ImportError: 'myframework' was requested but is not installed.
Install with: pip install "myframework-praisonai[gpu]"
If you omit install_hint, the wrapper falls back to pip install 'praisonai-frameworks[myframework]'.

Missing optional dependencies no longer leak

FrameworkAdapterRegistry.is_available() now catches ImportError raised when an adapter’s constructor touches a missing optional dependency, alongside the existing ValueError/TypeError. A plugin with unmet deps reports as unavailable rather than crashing the CLI list, the doctor check, or pick_default().

Helpers exposed at the package root

from praisonai.framework_adapters import (
    list_framework_choices,   # canonical name list (sorted, optionally filtered to available)
    get_install_hint,         # adapter-declared hint, or fallback
    get_default_registry,
)

Configuration

Framework Adapter Registry API

Complete API reference for FrameworkAdapterRegistry

Registry Methods

MethodParametersDescription
get_default_registry()NoneModule-level factory; lazy creates a single process-wide registry
register(name, cls)name: str, cls: Type[FrameworkAdapter]Register adapter at runtime
unregister(name)name: strboolRemove adapter; returns True if found
resolve(name)name: strTypeGet adapter class; raises ValueError if missing
create(name, *args, **kwargs)variesCreate an adapter instance; validates the run() signature (PR #2083) and raises TypeError if the four protocol kwargs are missing
list_names()None → list[str]Sorted list of registered names
list_registered()None → list[str]Backward-compat alias of list_names()
is_available(name)name: strboolCheck adapter is registered AND its is_available() returns True

Adapter Protocol

Framework adapters must implement the FrameworkAdapter protocol:
Method / AttributeRequiredDescription
nameUnique framework identifier
install_hintString shown when the framework is missing (falls back to pip install 'praisonai-frameworks[<name>]')
requires_tools_extrabool, default False. Set True if the adapter needs the tools extra.
is_routerbool, default False. Set True for router adapters that delegate to concrete siblings — skips run() protocol validation at registration time.
SUPPORTS_WORKFLOWbool, default False. Set True (class-level) to accept native workflow YAML dispatch. See Capability Flags.
SUPPORTS_RUNTIME_FEATURESbool, default False. Set True (class-level) to accept cli_backend / runtime config keys. See Capability Flags.
is_available()Check framework dependencies
resolve(*, config=None)Pick the concrete adapter variant from YAML config (keyword-only config; default returns self)
resolve_alias()Return the concrete adapter name to dispatch to (string). Default returns self.name. Override when your adapter is a router that picks a sibling adapter at runtime by name.
setup(*, framework_tag)Framework-specific pre-run hooks (default no-op)
run(config, llm_config, topic, *, tools_dict, agent_callback, task_callback, cli_config)Execute framework logic (all four trailing kwargs are validated at registration time — see Troubleshooting)
arun(config, llm_config, topic, *, tools_dict, agent_callback, task_callback, cli_config)Async execution path. When SUPPORTS_ASYNC is False (default), it offloads run() to this adapter’s bounded thread pool and logs a one-line warning. Override with a native async def for a fully async path.
cleanup()Release resources after execution (default no-op; also shuts down the offload pool)
SUPPORTS_ASYNCbool, default False. Set True when you override arun() with a native async def. When False, arun() offloads run() to the adapter’s bounded thread pool and logs a one-line warning.
_THREAD_OFFLOAD_MAX_WORKERSint, default 4. Size of this adapter’s private ThreadPoolExecutor used by arun()’s sync-offload path. Override on the class or instance for high-concurrency workloads.

Protocol Validation (PR #2083)

Since PR #2083, FrameworkAdapterRegistry.create() validates every adapter at construction time. If the run() method does not accept the four keyword-only parameters tools_dict, agent_callback, task_callback, and cli_config, instantiation fails with:
TypeError: FrameworkAdapter '<name>' does not implement the protocol:
missing keyword-only parameters ['agent_callback', 'cli_config', 'task_callback', 'tools_dict']
Adapters that fail validation also report registry.is_available("<name>") == False instead of raising — so a broken plugin will simply disappear from the available-framework list rather than crashing the CLI.
resolve_alias() (PR #2086) and resolve(*, config=None) (PR #2070) serve different roles. Use resolve(*, config=...) when variant selection depends on YAML keys such as autogen_version. Use resolve_alias() when routing depends on environment or runtime state. Family adapters may combine both.
Migrating from resolve_variant: If you authored a custom framework adapter against PR #1988 (resolve_variant(config, registry)), rename your method to resolve and drop the registry parameter. The orchestrator calls adapter.resolve(config=config). Accept config as keyword-only: *, config=None. Instantiate sibling adapters directly (e.g. AutoGenV4Adapter()) rather than calling registry.create(...).As of PR #2257 the no-op resolve_variant default has been removed from both the FrameworkAdapter Protocol and BaseFrameworkAdapter. Old adapters that still define resolve_variant will have it silently ignored — only resolve(*, config=...) and resolve_alias() are invoked.
Default arun() offloads run() to a bounded per-adapter thread pool, allowing sync adapters to be called from async contexts without blocking the event loop. Unlike asyncio.to_thread (which shares the process-wide executor), each adapter gets its own ThreadPoolExecutor sized by _THREAD_OFFLOAD_MAX_WORKERS (default 4), so one slow framework run cannot starve every other async caller. Override arun() with a native async def and set SUPPORTS_ASYNC = True to skip the thread offload entirely and use the framework’s own async API.

Async capability flags

Set SUPPORTS_ASYNC = True when you provide a native async def arun(); leave it False to inherit the bounded thread-pool offload.
from praisonaiagents import BaseFrameworkAdapter

class MyFrameworkAdapter(BaseFrameworkAdapter):
    name = "myframework"
    SUPPORTS_ASYNC = True             # native async arun() below
    _THREAD_OFFLOAD_MAX_WORKERS = 8   # only used when SUPPORTS_ASYNC is False

    def is_available(self) -> bool: ...

    def run(self, config, llm_config, topic, *, tools_dict=None,
            agent_callback=None, task_callback=None, cli_config=None): ...

    async def arun(self, config, llm_config, topic, *, tools_dict=None,
                   agent_callback=None, task_callback=None, cli_config=None): ...
FlagDefaultConsulted byEffect when False / unchanged
SUPPORTS_ASYNCFalseBaseFrameworkAdapter.arun()Sync run() is offloaded to the adapter’s bounded thread pool and a one-line warning is logged
_THREAD_OFFLOAD_MAX_WORKERS4_get_thread_pool() (offload path)Caps concurrent offloaded run() calls; increase for high-concurrency sync adapters
Only PraisonAIAdapter sets SUPPORTS_ASYNC = True today — its arun() awaits the native async run path directly. The built-in CrewAIAdapter and AutoGenAdapter keep SUPPORTS_ASYNC = False and use the bounded thread-pool offload.

Capability Flags

Capability flags are class-level booleans that tell PraisonAI which features your adapter can execute. Three flags gate distinct wrapper features. All default to False, so an adapter opts in only to what it can actually run.
from praisonaiagents import BaseFrameworkAdapter

class MyWorkflowFrameworkAdapter(BaseFrameworkAdapter):
    name = "myframework"
    SUPPORTS_ASYNC = True              # native async run() path
    SUPPORTS_WORKFLOW = True           # accept workflow YAML dispatch
    SUPPORTS_RUNTIME_FEATURES = True   # accept cli_backend / runtime keys

    def is_available(self) -> bool:
        return True

    def run(self, config, llm_config, topic, *,
            tools_dict=None, agent_callback=None,
            task_callback=None, cli_config=None) -> str:
        return f"myframework ran: {topic}"
FlagTypeDefaultWhat it grants
SUPPORTS_ASYNCboolFalseAdapter has a native async run path; skips the bounded thread-pool offload in arun().
SUPPORTS_WORKFLOWboolFalseAdapter can execute native workflow YAML. Read by validate_workflow_framework() and JobExecutor._framework_supports_native.
SUPPORTS_RUNTIME_FEATURESboolFalseAdapter supports cli_backend, runtime, models.*.runtime, and providers.*.runtime_default. Read by AgentsGenerator._validate_cli_backend_compatibility.
The built-in PraisonAIAdapter sets both SUPPORTS_WORKFLOW and SUPPORTS_RUNTIME_FEATURES to True. Third-party adapters registered via the praisonai.framework_adapters entry-point group opt in by setting the flags on their subclass. Each flag maps to the wrapper call site that reads it:
The default False is intentional. An adapter that never sets the flags cannot accidentally accept a workflow YAML or cli_backend config it has no code to execute — the wrapper rejects the config with an actionable error instead of failing mid-run.
Set capability flags as class-level attributes. Some call sites read the flag off the adapter class before an instance is fully configured, so instance-level assignment (e.g. inside __init__) can be bypassed.

Adding an Async Path

1

Sync-only adapter (default)

Leave arun() unoverridden and SUPPORTS_ASYNC at its False default. The base class offloads run() to the adapter’s bounded thread pool automatically:
from praisonaiagents import BaseFrameworkAdapter

class MySyncAdapter(BaseFrameworkAdapter):
    name = "my_sync_framework"

    def is_available(self) -> bool:
        try:
            import my_sync_framework
            return True
        except ImportError:
            return False

    def run(
        self,
        config,
        llm_config,
        topic,
        *,
        tools_dict=None,
        agent_callback=None,
        task_callback=None,
        cli_config=None,
    ) -> str:
        import my_sync_framework
        return my_sync_framework.execute(config, topic)
2

Native async adapter

Override arun() with a real async def and set SUPPORTS_ASYNC = True to use the framework’s native async API:
from praisonaiagents import BaseFrameworkAdapter

class MyAsyncAdapter(BaseFrameworkAdapter):
    name = "my_async_framework"
    SUPPORTS_ASYNC = True

    def is_available(self) -> bool:
        try:
            import my_async_framework
            return True
        except ImportError:
            return False

    def run(
        self,
        config,
        llm_config,
        topic,
        *,
        tools_dict=None,
        agent_callback=None,
        task_callback=None,
        cli_config=None,
    ) -> str:
        import asyncio
        return asyncio.run(self.arun(config, llm_config, topic))

    async def arun(
        self,
        config,
        llm_config,
        topic,
        *,
        tools_dict=None,
        agent_callback=None,
        task_callback=None,
        cli_config=None,
    ) -> str:
        import my_async_framework
        return await my_async_framework.aexecute(config, topic)

Family Router Pattern — AutoGenFamilyAdapter as Canonical Example

A family adapter routes to a concrete sibling adapter based on environment or config. It overrides resolve_alias() to return an adapter name, then resolve() uses that name to look up and return the concrete instance.
import os
import logging
from typing import Dict, Any, Optional
from praisonaiagents import BaseFrameworkAdapter

logger = logging.getLogger(__name__)

class AutoGenFamilyAdapter(BaseFrameworkAdapter):
    """Router adapter for AutoGen family (v0.2, v0.4, AG2)."""

    name = "autogen"
    is_router = True

    def is_available(self) -> bool:
        v2 = AutoGenAdapter()
        v4 = AutoGenV4Adapter()
        ag2 = AG2Adapter()
        return v2.is_available() or v4.is_available() or ag2.is_available()

    def resolve_alias(self, config: Optional[Dict[str, Any]] = None) -> str:
        """Pick adapter name from config or AUTOGEN_VERSION env var."""
        requested = str(
            (config or {}).get("autogen_version")
            or os.getenv("AUTOGEN_VERSION", "auto")
        ).strip().lower()
        v2_available = AutoGenAdapter().is_available()
        v4_available = AutoGenV4Adapter().is_available()
        ag2_available = AG2Adapter().is_available()

        if requested == "v0.2":
            if v2_available:
                return "autogen_v2"
            raise ImportError(
                "AUTOGEN_VERSION=v0.2 was requested, but the AutoGen v0.2 adapter "
                "is not available. Install with: pip install 'praisonai-frameworks[autogen]'."
            )
        if requested == "v0.4":
            if v4_available:
                return "autogen_v4"
            raise ImportError(
                "AUTOGEN_VERSION=v0.4 was requested, but the v0.4 adapter is not "
                "registered or available. Install/register an autogen_v4 adapter "
                "(pip install 'praisonai-frameworks[autogen-v4]'), or unset AUTOGEN_VERSION "
                "to use auto-selection."
            )
        if requested == "ag2":
            if ag2_available:
                return "ag2"
            raise ImportError(
                "AUTOGEN_VERSION=ag2 was requested, but the AG2 adapter is not "
                "registered or available. Install/register an AG2 adapter "
                "(pip install 'praisonai-frameworks[ag2]'), or unset AUTOGEN_VERSION to use "
                "auto-selection."
            )

        # auto: prefer v0.2 (v0.4 and ag2 are currently unimplemented)
        if v2_available:
            return "autogen_v2"
        if v4_available:
            return "autogen_v4"
        if ag2_available:
            return "ag2"

        raise ImportError(
            "No runnable AutoGen variant is available. Install with:\n"
            "  pip install 'praisonai-frameworks[autogen]' for v0.2\n"
            "  pip install 'praisonai-frameworks[autogen-v4]' for v0.4\n"
            "  pip install 'praisonai-frameworks[ag2]' for AG2"
        )

    def resolve(self, *, config: Optional[Dict[str, Any]] = None) -> "BaseFrameworkAdapter":
        adapter_name = self.resolve_alias(config)
        from praisonai.framework_adapters.registry import get_default_registry
        return get_default_registry().create(adapter_name)

    def run(
        self,
        config,
        llm_config,
        topic,
        *,
        tools_dict=None,
        agent_callback=None,
        task_callback=None,
        cli_config=None,
    ) -> str:
        raise RuntimeError(
            "AutoGenFamilyAdapter.run() should not be called directly. "
            "The resolve() method should have been called first to get the concrete adapter."
        )
Key points:
  • resolve_alias(config) accepts optional config dict — config autogen_version key wins over AUTOGEN_VERSION env var
  • resolve() passes config to resolve_alias(), then fetches the concrete adapter from the registry
  • run() has the full four keyword-only kwargs and raises RuntimeError — the orchestrator calls resolve() first, then calls run() on the returned concrete adapter
As of PR #2654, AutoGen v0.2 now wires YAML tools: into the chat via register_for_llm + register_for_execution, and translates a per-call ToolTimeoutError into an LLM-readable string in _wrap_tool_for_execution. Custom adapters that run tools inside their own loop should follow the same _wrap_tool_for_execution pattern for timeout translation. See AutoGen v0.2 YAML Tools.

Optional Adapter Hooks

After the existing “Adapter protocol” section, two new optional methods were added in PR #1763: resolve(*, config=None) — pick a concrete variant (PR #2070).
def resolve(self, *, config: Optional[Dict[str, Any]] = None) -> "FrameworkAdapter":
    """Pick the concrete adapter variant (e.g. autogen v0.2 vs v0.4).

    Args:
        config: YAML configuration that may contain version preferences

    Returns:
        The resolved adapter instance (self or a different adapter)
    """
    return self
Worked example — AutoGenAdapter.resolve: the orchestrator passes YAML config; the adapter instantiates siblings directly:
from praisonaiagents import BaseFrameworkAdapter

def resolve(self, *, config: Optional[Dict[str, Any]] = None) -> "BaseFrameworkAdapter":
    version = "auto"
    if config and config.get("autogen_version"):
        version = str(config["autogen_version"]).lower()
    else:
        version = os.environ.get("AUTOGEN_VERSION", "auto").lower()

    v4_adapter = AutoGenV4Adapter()  # direct instantiation — no registry.create()
    v2_adapter = self

    if version == "v0.4" and v4_adapter.is_available():
        return v4_adapter
    elif version == "v0.2" and v2_adapter.is_available():
        return v2_adapter
    # ... auto-detect and fallback ...
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

Orchestrator Pipeline (Post-#1763, updated for #2086)

The orchestrator (AgentsGenerator.generate_crew_and_kickoff) now follows this sequence:

Common Patterns

Override Built-in Adapter

from typing import Any, Callable, Dict, List, Optional
from praisonaiagents import BaseFrameworkAdapter
from praisonai.framework_adapters.registry import get_default_registry

class CustomCrewAIAdapter(BaseFrameworkAdapter):
    name = "crewai"

    def is_available(self) -> bool:
        try:
            import crewai
            return True
        except ImportError:
            return False

    def run(
        self,
        config: Dict[str, Any],
        llm_config: List[Dict],
        topic: str,
        *,
        tools_dict: Optional[Dict[str, Any]] = None,
        agent_callback: Optional[Callable] = None,
        task_callback: Optional[Callable] = None,
        cli_config: Optional[Dict[str, Any]] = None,
    ) -> str:
        # Custom CrewAI execution logic
        return "Custom CrewAI result"

    def cleanup(self) -> None:
        super().cleanup()

# Override built-in CrewAI adapter
registry = get_default_registry()
registry.register("crewai", CustomCrewAIAdapter)

Subprocess Framework Wrapper

import subprocess
from typing import Any, Callable, Dict, List, Optional
from praisonaiagents import BaseFrameworkAdapter

class NonPythonAdapter(BaseFrameworkAdapter):
    name = "external_framework"

    def is_available(self) -> bool:
        try:
            subprocess.run(["external_framework", "--version"],
                         capture_output=True, check=True)
            return True
        except (subprocess.CalledProcessError, FileNotFoundError):
            return False

    def run(
        self,
        config: Dict[str, Any],
        llm_config: List[Dict],
        topic: str,
        *,
        tools_dict: Optional[Dict[str, Any]] = None,
        agent_callback: Optional[Callable] = None,
        task_callback: Optional[Callable] = None,
        cli_config: Optional[Dict[str, Any]] = None,
    ) -> str:
        # Convert config to external format
        cmd = ["external_framework", "run", "--topic", topic]
        result = subprocess.run(cmd, capture_output=True, text=True)
        return result.stdout

Conditional Dependencies

import logging
from typing import Any, Callable, Dict, List, Optional
from praisonaiagents import BaseFrameworkAdapter

logger = logging.getLogger(__name__)

class OptionalDepsAdapter(BaseFrameworkAdapter):
    name = "advanced_framework"

    def is_available(self) -> bool:
        try:
            # Check multiple optional dependencies
            import advanced_framework
            import optional_plugin
            return True
        except ImportError as e:
            logger.debug(f"Framework not available: {e}")
            return False

    def run(
        self,
        config: Dict[str, Any],
        llm_config: List[Dict],
        topic: str,
        *,
        tools_dict: Optional[Dict[str, Any]] = None,
        agent_callback: Optional[Callable] = None,
        task_callback: Optional[Callable] = None,
        cli_config: Optional[Dict[str, Any]] = None,
    ) -> str:
        if not self.is_available():
            raise RuntimeError("Framework dependencies not installed")

        import advanced_framework
        return advanced_framework.execute(config, llm_config, topic)

Best Practices

Always implement defensive is_available() checks:
def is_available(self) -> bool:
    try:
        # Check all required dependencies
        import required_framework
        import optional_dependency
        
        # Verify minimum versions if needed
        if hasattr(required_framework, 'version'):
            version = required_framework.version
            if version < (1, 0, 0):
                return False
        
        return True
    except ImportError:
        # Log debug info, don't raise
        logging.getLogger(__name__).debug(
            "Framework dependencies not available"
        )
        return False
Don’t import heavy dependencies at module level:
# ❌ Bad - imports at module level
import heavy_framework
from expensive.module import Component

class BadAdapter(BaseFrameworkAdapter):
    pass

# ✅ Good - lazy imports
class GoodAdapter(BaseFrameworkAdapter):
    def run(self, config, llm_config, topic):
        # Import only when needed
        import heavy_framework
        from expensive.module import Component
        return heavy_framework.run(config)
Log framework events for debugging:
import logging
from praisonaiagents import BaseFrameworkAdapter

class LoggingAdapter(BaseFrameworkAdapter):
    def __init__(self):
        super().__init__()
        self.logger = logging.getLogger(__name__)
    
    def run(self, config, llm_config, topic):
        self.logger.info(f"Starting {self.name} execution for topic: {topic}")
        try:
            result = self._execute_framework(config, llm_config, topic)
            self.logger.info(f"Execution completed successfully")
            return result
        except Exception as e:
            self.logger.error(f"Framework execution failed: {e}")
            raise
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, *, tools_dict=None,
            agent_callback=None, task_callback=None, cli_config=None):
        conn = self._create_connection()
        self._connections.append(conn)
        
        temp_file = self._create_temp_file()
        self._temp_files.append(temp_file)
        
        return result
    
    def cleanup(self) -> None:
        for conn in self._connections:
            try:
                conn.close()
            except Exception:
                pass
        
        for temp_file in self._temp_files:
            try:
                temp_file.unlink()
            except Exception:
                pass
        
        self._connections.clear()
        self._temp_files.clear()
        super().cleanup()
Use cached availability checks to avoid multi-second imports on every probe:
import importlib.util

def is_available(self) -> bool:
    return importlib.util.find_spec("myframework") is not None
The default arun() offloads run() to the adapter’s bounded thread pool (_THREAD_OFFLOAD_MAX_WORKERS, default 4). For high-concurrency workloads, either raise the pool size or override arun() with a native async def (and set SUPPORTS_ASYNC = True) to avoid offload contention:
from praisonaiagents import BaseFrameworkAdapter

class ScalableAsyncAdapter(BaseFrameworkAdapter):
    name = "scalable_framework"
    SUPPORTS_ASYNC = True

    def is_available(self) -> bool:
        return True

    def run(self, config, llm_config, topic, *, tools_dict=None,
            agent_callback=None, task_callback=None, cli_config=None) -> str:
        import asyncio
        return asyncio.run(self.arun(config, llm_config, topic))

    async def arun(self, config, llm_config, topic, *, tools_dict=None,
                   agent_callback=None, task_callback=None, cli_config=None) -> str:
        import scalable_framework
        return await scalable_framework.arun(config, topic)

Registry Helpers

Registry-level helpers for building CLIs and plugins:
from praisonai.framework_adapters.registry import (
    list_framework_choices,
    list_available_frameworks,
    get_install_hint,
)
HelperReturnsUse case
list_framework_choices(*, include_unavailable=False)list[str]Build the CLI --framework choice list (registered adapters; pass include_unavailable=True for the full list)
list_available_frameworks()list[str]Filter to just those whose is_available() returns True
get_install_hint(name)strAdapter-aware install hint string (e.g. pip install 'praisonai-frameworks[autogen]')
framework_option_help()strAuto-generated --help text for the --framework flag
from praisonai.framework_adapters.registry import list_framework_choices, get_install_hint

# See which frameworks appear in --framework choices
choices = list_framework_choices()
print(choices)  # ['crewai', 'praisonai', 'autogen', 'my_framework']

# Get install instructions for a missing framework
hint = get_install_hint("crewai")
print(hint)  # pip install 'praisonai-frameworks[crewai]'

Default Framework Selection

FrameworkAdapterRegistry.pick_default() selects the default framework when none is specified. DEFAULT_PRIORITY is a class attribute on FrameworkAdapterRegistry:
DEFAULT_PRIORITY: tuple[str, ...] = ("crewai", "praisonai", "autogen")
pick_default() walks this tuple and returns the first available adapter. If none of the three built-ins are installed, it falls back to any other registered adapter — including entry-point plugins. If no adapter is available at all, it raises RuntimeError.
ag2 is intentionally omitted from DEFAULT_PRIORITY because the AG2 adapter is an unimplemented stub. Users can still select it explicitly via --framework ag2 or framework: ag2 in YAML, but it will not be chosen automatically — and calling it will raise NotImplementedError until an implementation lands. The invariant is enforced by the test_ag2_not_in_default_builtins regression test.
from praisonai.framework_adapters.registry import get_default_registry

registry = get_default_registry()
default = registry.pick_default()   # returns "crewai", "praisonai", etc.
print(f"Using framework: {default}")
_entrypoint.run() and arun() now delegate to pick_default(). This means an entry-point adapter can become the default framework once the four built-ins are uninstalled:
from praisonai.framework_adapters.registry import get_default_registry

# Scenario: only your plugin adapter is installed
registry = get_default_registry()
default = registry.pick_default()  # returns your plugin adapter name
The praisonaiagents → praisonai rename is reflected in DEFAULT_PRIORITY — the tuple uses "praisonai", not "praisonaiagents". Any YAML or code that relied on the old probe tuple ("crewai", "praisonaiagents", "autogen", "ag2") now uses pick_default() instead.The static fallback list used by the CLI only when the adapter layer itself cannot be imported is ["ag2", "autogen", "crewai", "praisonai"] (alphabetical) — ag2 still appears in this install-hint list for discoverability, but is not in the runtime DEFAULT_PRIORITY tuple. This prevents pick_default() from routing to ag2 and hitting a NotImplementedError.

Inject Your Own Registry (Multi-tenant / Tests)

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 FrameworkAdapterRegistry
from praisonai.agents_generator import AgentsGenerator

# Tenant-isolated registry — NOT shared with other tenants in the same process
tenant_registry = FrameworkAdapterRegistry()
tenant_registry.register("crewai", CustomCrewAIAdapter)

generator = AgentsGenerator(
    agent_file="agents.yaml",
    framework="crewai",
    config_list=[...],
    adapter_registry=tenant_registry,  # <-- new in #1639
)
For AutoGenerator:
from praisonai.framework_adapters.registry import FrameworkAdapterRegistry
from praisonai.auto import AutoGenerator

reg = FrameworkAdapterRegistry()
auto = AutoGenerator(
    topic="...",
    agent_file="test.yaml",
    framework="crewai",
    config_list=[...],
    adapter_registry=reg,
)
For workflow YAML, use WorkflowAutoGenerator — its framework= kwarg is honoured end-to-end (see AutoAgents).

Troubleshooting

Your adapter’s run() method is missing the four keyword-only parameters required by the FrameworkAdapter protocol. As of PR #2083, FrameworkAdapterRegistry.create() validates these at adapter construction time.Update your run() signature to:
def run(
    self,
    config,
    llm_config,
    topic,
    *,
    tools_dict=None,
    agent_callback=None,
    task_callback=None,
    cli_config=None,
):
    ...
Your code can still ignore the values — the signature just needs to accept them. The four parameters can be either KEYWORD_ONLY (after *) or POSITIONAL_OR_KEYWORD; either form passes validation.
If praisonai --list-frameworks (or registry.list_names()) no longer shows your adapter, run registry.is_available("<your-adapter-name>"). Since PR #2083, is_available() catches TypeError from the new protocol validator and returns False rather than raising — so a malformed plugin is silently filtered out of the available list. Since PR #2415, is_available() also catches ImportError raised during adapter construction (e.g., when the adapter’s __init__ touches an optional dependency). Check the application logs for the error message above and fix the run() signature or ensure your dependencies are properly guarded.
The default arun() offloads sync run() calls to the adapter’s bounded thread pool and logs a one-line warning. This is safe for most workloads. If you need a fully async path, set SUPPORTS_ASYNC = True and override arun() with a native async def:
class MyAdapter(BaseFrameworkAdapter):
    SUPPORTS_ASYNC = True

    async def arun(self, config, llm_config, topic, *, tools_dict=None,
                   agent_callback=None, task_callback=None, cli_config=None) -> str:
        return await my_native_async_call(config, topic)

Registry Dependency Injection

Learn about injecting custom registries for multi-tenant isolation

CrewAI Integration

See how built-in framework adapters are implemented

Workflow YAML Framework Field

How framework: works in workflow YAML vs agents.yaml