> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Framework Adapter Plugins

> Extend PraisonAI with custom framework adapters via Python entry points

Framework adapter plugins enable third-party developers to add new execution frameworks to PraisonAI without modifying core code.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Framework Plugin System"
        A[📦 Third-party Package] --> B[🔌 Entry Point]
        B --> C[📝 Registry]
        C --> D[🚀 CLI Framework Selection]
    end
    
    classDef package fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef entrypoint fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef registry fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef cli fill:#10B981,stroke:#7C90A0,color:#fff
    
    class A package
    class B entrypoint
    class C registry
    class D cli

```

<Warning>
  **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.
</Warning>

## Quick Start

<Steps>
  <Step title="Programmatic Registration">
    Register a framework adapter directly in your code:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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)
    ```
  </Step>

  <Step title="Entry-Point Plugin (installable)">
    Create a pip-installable plugin — no manual `registry.register()` call needed:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # 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}"
    ```

    ```toml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # 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"
    ```

    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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.
  </Step>
</Steps>

***

## 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:

| Surface | Effect |
| - | - |
| `praisonai --framework <name>` | Your adapter name appears in the argparse `choices=` list |
| `praisonai doctor config` | `agents.yaml framework: <name>` passes validation |
| Install-hint messages | Your adapter's `install_hint` attribute is shown when the framework module is missing |

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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](/docs/docs/features/framework-availability#dynamic-framework-discovery) for more details on how the registry feeds these surfaces.

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant CLI
    participant Registry
    participant Plugin
    participant Framework
    
    User->>CLI: pip install plugin
    CLI->>Registry: Discover entry points
    Registry->>Plugin: Load adapter class
    Plugin-->>Registry: Register framework
    User->>CLI: praisonai --framework myframework agents.yaml
    CLI->>Registry: get_adapter("myframework")
    Registry->>Plugin: create() instance
    Plugin->>Framework: run(config, llm_config, topic)
    Framework-->>User: Execution result
```

The framework adapter registry provides a central point for managing framework implementations:

| Operation | Description | When Called |
| - | - | - |
| Discovery | Entry points auto-loaded on first registry access | Import time |
| Registration | Adapters registered by name | Plugin installation |
| Creation | Adapter instances created on demand | CLI framework selection |
| Availability | Framework dependencies checked | Before execution |

***

## Unsupported-field warnings (PraisonAI #3771)

When a YAML config declares a field a specific framework adapter doesn't wire — for example `approval:` on the CrewAI or AutoGen adapter — the adapter now emits a `logging.warning` naming the dropped field(s). Previously the field was silently ignored, so users who wrote `approval:` and wondered why nothing happened had no signal in the logs.

* **PraisonAI-native (`framework: praisonai`)** — no warning; the native runner supports every declared field.
* **CrewAI / AutoGen** — warning logged for each unsupported field per agent/task.

### The `SUPPORTED_YAML_FIELDS` contract (PraisonAI #4651)

Each adapter declares the extended YAML fields it honours as a class attribute — the single source of truth is the adapter that reads a field. [PraisonAI PR #4651](https://github.com/MervinPraison/PraisonAI/pull/4651) replaced the old module-level `_ADAPTER_SUPPORTED_FIELDS` dict with this per-class set.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from typing import ClassVar, FrozenSet
from praisonai.framework_adapters.base import BaseFrameworkAdapter

class MyFrameworkAdapter(BaseFrameworkAdapter):
    name = "myframework"
    # Extended fields this backend actually consumes, beyond the structural
    # keys (tools, tasks, role, goal, backstory). Empty set = "supports
    # everything" (the praisonai backend).
    SUPPORTED_YAML_FIELDS: ClassVar[FrozenSet[str]] = frozenset({"llm"})
```

`warn_unsupported_fields(adapter, extras)` reads `adapter.SUPPORTED_YAML_FIELDS` directly. Any per-agent YAML key not in this set — after the structural keys `tools`, `tasks`, `role`, `goal`, `backstory` are removed — is logged once per agent:

```
framework='crewai' ignores YAML field(s) ['approval'] for agent 'Researcher'; these are only honoured by framework=praisonai.
```

<Warning>
  **API change:** `warn_unsupported_fields(adapter, extras)` now takes the **adapter instance** as its first argument, not the adapter's name string. If you call it directly, pass `self`, not `self.name`.
</Warning>

The two built-in overrides are working templates for third-party authors:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# crewai_adapter.py
SUPPORTED_YAML_FIELDS = frozenset({
    "allow_delegation", "max_iter", "max_rpm", "max_execution_time",
    "verbose", "cache", "system_template", "prompt_template",
    "response_template", "llm", "function_calling_llm",
})

# autogen_adapter.py — shared by AutoGenAdapter, AutoGenV4Adapter,
# AG2Adapter, and AutoGenFamilyAdapter
SUPPORTED_YAML_FIELDS = frozenset({"llm", "function_calling_llm"})
```

### AutoGen callback parity

* **`agent_callback` now fires per agent (PraisonAI #3771).** The AutoGen adapter previously declared but never invoked `agent_callback`. It now runs once per agent, matching the CrewAI adapter's behaviour.
* **`task_callback` is now wired for the AutoGen v0.2 adapter ([PraisonAI PR #5403](https://github.com/MervinPraison/PraisonAI/pull/5403)),** restoring parity with CrewAI. AutoGen v0.2's `initiate_chats` has no per-step hook, so the adapter fans the callback out itself: it fires `{"event": "task_prepared", ...}` once per prepared chat task before `initiate_chats`, and `{"event": "task_completed", ...}` once per chat after `initiate_chats` returns. Sync callbacks and `async def` callbacks both work — an async callback is routed through `dispatch_maybe_awaitable` so the returned coroutine actually runs instead of being discarded. Callback exceptions are caught and logged; they never abort the chat.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

def on_task(event):
    # event ∈ {"task_prepared", "task_completed"}
    print(event["event"], event.get("summary"))
```

Wire `task_callback` the same way as the other adapter callbacks — through the orchestrator / adapter contract (see [Auto-Discovery in the CLI](#auto-discovery-in-the-cli) and the protocol table below for the exact plumbing).

#### `task_callback` event payload

| Event | Key | Type | Description |
| - | - | - | - |
| `task_prepared` | `event` | `str` | Literal `"task_prepared"`. |
| `task_prepared` | `task_spec` | `TaskSpec` | The task spec that was converted into an AutoGen chat task. |
| `task_prepared` | `agent` | `AssistantAgent` | The AutoGen assistant agent assigned to the chat. |
| `task_completed` | `event` | `str` | Literal `"task_completed"`. |
| `task_completed` | `task` | `dict` | The AutoGen chat task dict that was passed to `initiate_chats`. |
| `task_completed` | `response` | `ChatResult` | AutoGen's `ChatResult` for that chat. |
| `task_completed` | `summary` | `str \| None` | `getattr(response, "summary", None)` — convenience field. |

***

## 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:

| Surface | What it shows / validates against |
| - | - |
| `praisonai --framework <name>` argparse `choices` | `list_framework_choices(include_unavailable=True)` |
| `praisonai doctor` `agents.yaml` `framework:` field check | `list_framework_choices(include_unavailable=True)` |
| Missing-framework error → install hint | adapter'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:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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()`.

The registry now distinguishes **structural** failures (`ImportError`, `ValueError`, `TypeError` — the optional dep is genuinely not installed, cached for the process) from **transient** failures (any other exception — e.g. a racing entry-point scan, a boot-time healthcheck timeout, cached only for `_NEG_CACHE_TTL = 60 s`). A one-off flaky probe therefore no longer pins a framework to *unavailable* for the rest of the process.

| Exception from `adapter.is_available()` | Cached as `False` for | Rationale |
| - | - | - |
| `ImportError` | Whole process | Missing optional dep — will not change until reinstall + restart |
| `ValueError` / `TypeError` | Whole process | Structural adapter defect — same session cannot recover |
| Anything else | 60 s | Transient — reprobe automatically |

Adapter authors: keep `is_available()` cheap and side-effect-free. If you must raise on infrastructure hiccups (network probes, entry-point scans mid-write), raising the default `Exception` subclass is correct — the registry will retry after the TTL. **Do not** raise `ImportError` for transient causes, or the framework will be hidden for the whole process.

### Adapter Resolution Errors

When the requested framework can't be resolved — the entry-point package isn't installed, the adapter dependency is missing, the framework name is typo'd, or the generator is running in minimal/mock mode without an `_adapter_registry` — PraisonAI logs a `Could not resolve framework adapter for <framework>: <error>` warning and falls back to native execution. Previously this was swallowed silently. If you see this warning: install the plugin, correct the framework name, or drop the `--framework` flag to run natively.

***

## Helpers exposed at the package root

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

<Card title="Framework Adapter Registry API" icon="code" href="/docs/sdk/reference/praisonai/modules/adapters">
  Complete API reference for FrameworkAdapterRegistry
</Card>

### Registry Methods

| Method | Parameters | Description |
| - | - | - |
| `get_default_registry()` | None | Module-level factory; lazy creates a single process-wide registry |
| `register(name, cls)` | `name: str`, `cls: Type[FrameworkAdapter]` | Register adapter at runtime |
| `unregister(name)` | `name: str` → `bool` | Remove adapter; returns `True` if found |
| `resolve(name)` | `name: str` → `Type` | Get adapter class; raises `ValueError` if missing |
| `create(name, *args, **kwargs)` | varies | Create 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: str` → `bool` | Check adapter is registered AND its `is_available()` returns `True` |

### Adapter Protocol

Framework adapters must implement the `FrameworkAdapter` protocol:

| Method / Attribute | Required | Description |
| - | - | - |
| `name` | ✅ | Unique framework identifier |
| `install_hint` | ❌ | String shown when the framework is missing (falls back to `pip install 'praisonai-frameworks[<name>]'`) |
| `requires_tools_extra` | ❌ | `bool`, default `False`. Set `True` if the adapter needs the tools extra. |
| `is_router` | ❌ | `bool`, default `False`. Set `True` for router adapters that delegate to concrete siblings — skips `run()` protocol validation at registration time. |
| `SUPPORTS_WORKFLOW` | ❌ | `bool`, default `False`. Set `True` (class-level) to accept native workflow YAML dispatch. See [Capability Flags](#capability-flags). |
| `SUPPORTS_RUNTIME_FEATURES` | ❌ | `bool`, default `False`. Set `True` (class-level) to accept `cli_backend` / `runtime` config keys. See [Capability Flags](#capability-flags). |
| `SUPPORTS_CORE_TOOL_TIMEOUT` | ❌ | `bool`, default `False`. Set `True` (class-level) if the adapter forwards per-agent `tool_timeout` to a core `Agent` through `tool_config=ToolConfig(timeout=…)`. The wrapper then stops wrapping plain callables and lets core enforce the budget. See [Capability Flags](#capability-flags). |
| `SUPPORTED_YAML_FIELDS` | ❌ | `frozenset[str]`, default `frozenset()`. Extended YAML fields this backend honours beyond the structural keys. Empty set = "supports everything". See [Capability Flags](#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). Relying on `**kwargs` instead of naming these validates but logs a warning — see [`**kwargs`-only adapters are accepted but warned](#kwargs-only-adapters-are-accepted-but-warned). |
| `arun(config, llm_config, topic, *, tools_dict, agent_callback, task_callback, cli_config)` | ❌ | Async execution path. Validated at registration time whenever it exists — same rules as `run()`, including the [`**kwargs`-only warning](#kwargs-only-adapters-are-accepted-but-warned). `arun` remains optional; adapters without it keep the sync-only offload behaviour. 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_ASYNC` | ❌ | `bool`, 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_WORKERS` | ❌ | `int`, 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 parameters `tools_dict`, `agent_callback`, `task_callback`, and `cli_config`, instantiation fails with a per-method `TypeError`:

```
TypeError: FrameworkAdapter '<name>'.run does not implement the protocol:
missing keyword parameters ['agent_callback', 'cli_config', 'task_callback', 'tools_dict']
```

The error names the failing method (`.run` or `.arun`), so `arun()` is now validated with the same rules whenever it exists. A `run()` (or `arun()`) that declares a `**kwargs` catch-all satisfies the check without listing each protocol keyword explicitly.

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.

#### `**kwargs`-only adapters are accepted but warned

A signature that relies on `**kwargs` still validates for backward compatibility — an adapter can legitimately forward `**kwargs` to a delegate. But if `run()` / `arun()` does **not** name every one of `tools_dict`, `agent_callback`, `task_callback`, and `cli_config` explicitly and instead leans on `**kwargs`, registration now logs a warning naming the un-declared kwargs. If those kwargs aren't forwarded, their safety guarantees — `tool_timeout` wraps, `agent_callback` / `task_callback` observability hooks, and `cli_config` approval / guardrails / allowed-tools knobs — are silently dropped.

```
FrameworkAdapter 'my_adapter'.run names none of the adapter kwargs
['agent_callback', 'cli_config', 'task_callback', 'tools_dict']
explicitly and relies on **kwargs; if these are not forwarded, their
safety guarantees (tool_timeout, callbacks, approval/guardrails via
cli_config) will be silently dropped.
```

The warning fires whenever the signature has a `**kwargs` catch-all **and** is missing one or more of the four named kwargs. The log line lists exactly the missing ones. Built-in adapters (`crewai`, `autogen`, `autogen-v4`, `ag2`, `praisonai`) name all four explicitly, so no warning fires for them.

| `run` / `arun` signature | Registration behaviour |
| - | - |
| Names all four kwargs explicitly (with or without `**kwargs`) | Silent — no warning |
| Names some / none of the four kwargs, **no** `**kwargs` | `TypeError` at registration |
| Missing one or more of the four kwargs, **has** `**kwargs` | Accepted **but** `logger.warning` names the un-declared kwargs |

<Tip>
  Always name `tools_dict`, `agent_callback`, `task_callback`, and `cli_config` explicitly in `run()` / `arun()` (even if you also accept `**kwargs`) so the safety guarantees are preserved and the registry stays silent.
</Tip>

<Note>
  `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.
</Note>

<Warning>
  **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](https://github.com/MervinPraison/PraisonAI/pull/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.
</Warning>

**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.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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): ...
```

| Flag | Default | Consulted by | Effect when `False` / unchanged |
| - | - | - | - |
| `SUPPORTS_ASYNC` | `False` | `BaseFrameworkAdapter.arun()` | Sync `run()` is offloaded to the adapter's bounded thread pool and a one-line warning is logged |
| `_THREAD_OFFLOAD_MAX_WORKERS` | `4` | `_get_thread_pool()` (offload path) | Caps concurrent offloaded `run()` calls; increase for high-concurrency sync adapters |

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    ARun[⚡ arun called] --> Flag{SUPPORTS_ASYNC?}
    Flag -->|True| Native[🚀 native async run]
    Flag -->|False| Warn[⚠️ log one-line warning]
    Warn --> Pool[🧵 bounded ThreadPoolExecutor<br/>max_workers = _THREAD_OFFLOAD_MAX_WORKERS]
    Pool --> Sync[🔁 offload sync run]

    classDef start fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef decision fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef native fill:#10B981,stroke:#7C90A0,color:#fff
    classDef warn fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef pool fill:#189AB4,stroke:#7C90A0,color:#fff

    class ARun start
    class Flag decision
    class Native native
    class Warn warn
    class Pool,Sync pool
```

<Note>
  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.
</Note>

***

## Capability Flags

Capability flags are class-level booleans that tell PraisonAI which features your adapter can execute.

Four flags gate distinct wrapper features. All default to `False`, so an adapter opts in only to what it can actually run.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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
    SUPPORTS_CORE_TOOL_TIMEOUT = True  # only if run() forwards tool_config to a core Agent

    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}"
```

<Warning>
  Setting `SUPPORTS_CORE_TOOL_TIMEOUT = True` without forwarding `tool_config=ToolConfig(timeout=…)` to a core `Agent` inside your adapter's `_build_agents_and_tasks` (or equivalent) silently drops every per-tool timeout. The flag declares behaviour your adapter already implements — it does not create it.
</Warning>

| Flag | Type | Default | What it grants |
| - | - | - | - |
| `SUPPORTS_ASYNC` | `bool` | `False` | Adapter has a native `async` `run` path; skips the bounded thread-pool offload in `arun()`. |
| `SUPPORTS_WORKFLOW` | `bool` | `False` | Adapter can execute native workflow YAML. Read by `validate_workflow_framework()` and `JobExecutor._framework_supports_native`. |
| `SUPPORTS_RUNTIME_FEATURES` | `bool` | `False` | Adapter supports `cli_backend`, `runtime`, `models.*.runtime`, and `providers.*.runtime_default`. Read by `AgentsGenerator._validate_cli_backend_compatibility`. |
| `SUPPORTS_CORE_TOOL_TIMEOUT` | `bool` | `False` | Set `True` **only** if the adapter forwards the resolved per-agent `tool_timeout` to a core `Agent` via `tool_config=ToolConfig(timeout=…)`, the way the built-in `PraisonAIAdapter` does. When `True`, the wrapper leaves plain callables unwrapped and core enforces the timeout (single enforcement path). When `False`, the wrapper wraps every tool itself. See [Tool Timeout](/docs/features/tool-timeout) and PraisonAI PR #5798. |
| `SUPPORTED_YAML_FIELDS` | `frozenset[str]` | `frozenset()` | Extended YAML fields this backend honours beyond the structural keys (`tools`, `tasks`, `role`, `goal`, `backstory`). Empty set = "supports everything" (the `praisonai` adapter). Otherwise any per-agent YAML key not in this set — after structural keys are removed — is logged as `framework=%r ignores YAML field(s) %s for agent %r; these are only honoured by framework=praisonai.` Read by `warn_unsupported_fields(adapter, extras)`. |

The built-in `PraisonAIAdapter` sets `SUPPORTS_WORKFLOW`, `SUPPORTS_RUNTIME_FEATURES`, and `SUPPORTS_CORE_TOOL_TIMEOUT` to `True`. Third-party adapters registered via the `praisonai.framework_adapters` entry-point group — and any subclass of `PraisonAIAdapter`, such as a renamed fork — opt in by setting the flags on their class.

A renamed fork inherits every flag with no extra work:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai.framework_adapters.praisonai_adapter import PraisonAIAdapter

class CompanyNativeAdapter(PraisonAIAdapter):
    # Inherits SUPPORTS_WORKFLOW, SUPPORTS_RUNTIME_FEATURES, SUPPORTS_ASYNC,
    # SUPPORTS_CORE_TOOL_TIMEOUT, SUPPORTS_SESSION_CONTINUITY, SUPPORTS_STREAM_BRIDGE
    name = "company_native"
```

Each flag maps to the wrapper call site that reads it:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Capability Flags"
        WF[🚩 SUPPORTS_WORKFLOW]
        RF[🚩 SUPPORTS_RUNTIME_FEATURES]
    end
    WF --> V[🛠️ validate_workflow_framework]
    WF --> E[🛠️ JobExecutor._framework_supports_native]
    RF --> C[🛠️ AgentsGenerator._validate_cli_backend_compatibility]
    V --> R1[✅ Workflow YAML runs]
    E --> R2[✅ Native dispatch]
    C --> R3[✅ cli_backend / runtime accepted]

    classDef flag fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef site fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff

    class WF,RF flag
    class V,E,C site
    class R1,R2,R3 ok
```

<Note>
  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.
</Note>

<Warning>
  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.
</Warning>

***

## Adding an Async Path

<Steps>
  <Step title="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:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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)
    ```
  </Step>

  <Step title="Native async adapter">
    Override `arun()` with a real `async def` and set `SUPPORTS_ASYNC = True` to use the framework's native async API:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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)
    ```
  </Step>
</Steps>

### 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.

The router consults the *owning* registry — the registry that constructed this adapter — with `get_default_registry()` as the fallback when the adapter was constructed outside a registry (standalone use). `FrameworkAdapterRegistry.create()` post-attaches itself as `adapter._owning_registry`, so router adapters honour the same registry that instantiated them without any explicit plumbing. A module-level `_owning_registry(adapter)` helper returns `adapter._owning_registry` when present, else the process-default registry; `is_available()`, `resolve_alias()`, and `resolve()` all route through it.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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)
        # _owning_registry(self) returns self._owning_registry when the registry
        # that built this adapter post-attached it, else the process default.
        return _owning_registry(self).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 *owning* registry (`get_default_registry()` fallback for standalone use)
* `run()` has the full four keyword-only kwargs and raises `RuntimeError` — the orchestrator calls `resolve()` first, then calls `run()` on the returned concrete adapter

<Note>
  **Owning-registry resolution (PR [#4861](https://github.com/MervinPraison/PraisonAI/pull/4861)).** A tenant-scoped `FrameworkAdapterRegistry` passed as `adapter_registry=` is now honoured when a workflow uses the `framework: autogen` family alias — not just direct aliases like `autogen_v2`. `AutoGenAdapter.resolve()` keeps its v0.2 → v0.4 fallback: an *unregistered* `autogen_v4` name still falls back to the built-in `AutoGenV4Adapter()` stub, but if `autogen_v4` **is** registered and its loader/constructor/validation raises, that error now surfaces instead of being silently swallowed as "v0.4 absent" — the actionable-error path for anyone registering a real v0.4 replacement via entry points.
</Note>

<Note>
  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](/docs/features/autogen-yaml-tools).
</Note>

***

### 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).

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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
```

As of PR #3152, `setup()` is invoked **inside** `observability_session`, so any events emitted by `setup()` — and any exception it raises — are captured and finalized. Custom adapters should therefore **not** open their own observability session inside `setup()`.

### Orchestrator Pipeline (Post-#3152)

As of PR #3152, `AgentsGenerator` owns the observability lifecycle. Both the sync (`generate_crew_and_kickoff`) and async (`agenerate_crew_and_kickoff`) run paths bracket `adapter.setup()` **and** `adapter.run()`/`adapter.arun()` in a single `observability_session`, so setup and run share one session and finalize is impossible to forget. The orchestrator now follows this sequence:

<Note>
  The **sync** path additionally runs `adapter.run()` inside a per-call `scoped_bridge()`, isolating its `run_sync`-driven work onto its own loop+thread. The sync YAML-workflow entry (`_run_yaml_workflow`, for `process: workflow`) likewise wraps `workflow.start(...)` in `scoped_bridge()` as of PR #5228. The async paths (`adapter.arun()` and `_arun_yaml_workflow`) are unchanged. See [Async Bridge → Per-run auto-scoping](/docs/features/async-bridge#per-run-auto-scoping-sequential-hierarchical-workflow).
</Note>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Gen as AgentsGenerator
    participant Reg as Registry
    participant Adapter
    participant FamilyAdapter as FamilyAdapter (router)
    participant Obs as observability_session

    Gen->>Reg: resolve(framework)
    Reg-->>Gen: initial_adapter
    Gen->>Adapter: resolve(config=config)
    Note over Adapter,FamilyAdapter: Family adapters read autogen_version<br/>from config, then return concrete adapter
    Adapter-->>Gen: concrete_adapter
    Gen->>Gen: assert_framework_available(adapter.name, registry=self._adapter_registry)
    Gen->>Obs: enter observability_session(adapter.name)
    Gen->>Adapter: setup(framework_tag=adapter.name)
    Note over Gen,Adapter: setup now runs inside the session
    Gen->>Adapter: run(config, llm_config, topic)
    Adapter-->>Gen: result
    Gen->>Obs: exit observability_session (auto-finalize)
```

***

## Common Patterns

### Override Built-in Adapter

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

<AccordionGroup>
  <Accordion title="Name the four adapter kwargs explicitly">
    Always name `tools_dict`, `agent_callback`, `task_callback`, and `cli_config` in `run()` / `arun()` — even if you also accept `**kwargs`. This keeps the safety guarantees (`tool_timeout` wraps, callbacks, and `cli_config` approval / guardrails) intact and stops the registry logging a warning at registration.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    def run(self, config, llm_config, topic, *, tools_dict=None,
            agent_callback=None, task_callback=None, cli_config=None,
            **kwargs):
        ...
    ```

    An adapter that leans on `**kwargs` alone still registers, but logs a warning naming the un-declared kwargs — see [`**kwargs`-only adapters are accepted but warned](#kwargs-only-adapters-are-accepted-but-warned).
  </Accordion>

  <Accordion title="Handle Missing Dependencies Gracefully">
    Always implement defensive `is_available()` checks:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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
    ```
  </Accordion>

  <Accordion title="Avoid Import-Time Failures">
    Don't import heavy dependencies at module level:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # ❌ 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)
    ```
  </Accordion>

  <Accordion title="Use Structured Logging">
    Log framework events for debugging:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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
    ```
  </Accordion>

  <Accordion title="Implement Proper Resource Cleanup">
    Always clean up resources in the `cleanup()` method:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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()
    ```
  </Accordion>

  <Accordion title="Don't import heavy deps inside is_available">
    Use cached availability checks to avoid multi-second imports on every probe:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    import importlib.util

    def is_available(self) -> bool:
        return importlib.util.find_spec("myframework") is not None
    ```

    Raise `ImportError` **only** for a genuinely missing optional dep — the registry caches that for the whole process. Any other exception is treated as transient and reprobed after 60 s.
  </Accordion>

  <Accordion title="Prefer native async for high-concurrency workloads">
    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:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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)
    ```
  </Accordion>
</AccordionGroup>

***

## Registry Helpers

Registry-level helpers for building CLIs and plugins:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai.framework_adapters.registry import (
    list_framework_choices,
    list_available_frameworks,
    get_install_hint,
)
```

| Helper | Returns | Use 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, *, registry=None)` | `str` | Adapter-aware install hint string (e.g. `pip install 'praisonai-frameworks[autogen]'`). Pass `registry=` to derive the hint from an injected registry (DI-friendly); defaults to the process-default. |
| `framework_option_help()` | `str` | Auto-generated `--help` text for the `--framework` flag |

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    A[pick_default called] --> B{Walk DEFAULT_PRIORITY}
    B -->|crewai available| C[✅ Return crewai]
    B -->|praisonai available| D[✅ Return praisonai]
    B -->|autogen available| E[✅ Return autogen]
    B -->|none available| G{Any other registered adapter?}
    G -->|yes| H[✅ Return first available plugin]
    G -->|no| I[❌ RuntimeError]

    classDef priority fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef success fill:#10B981,stroke:#7C90A0,color:#fff
    classDef fail fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef decision fill:#F59E0B,stroke:#7C90A0,color:#fff

    class A,B decision
    class C,D,E,H success
    class G decision
    class I fail
```

`DEFAULT_PRIORITY` is a class attribute on `FrameworkAdapterRegistry`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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`.

<Note>
  `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.
</Note>

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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
```

<Note>
  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`.
</Note>

### `resolve_or_default()` — for optional framework inputs

When a caller may or may not pass a framework name, use `resolve_or_default()` — it returns the caller's choice if given, or the registry's `pick_default()` otherwise.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
FrameworkAdapterRegistry.resolve_or_default(name: Optional[str]) -> str
```

| Input | Returns |
| - | - |
| `"crewai"` (any truthy string, whitespace stripped) | `"crewai"` |
| `"  autogen  "` | `"autogen"` |
| `None` / `""` / `"   "` | Result of `self.pick_default()` |
| `None` and `pick_default()` raises `RuntimeError` | `DEFAULT_PRIORITY[0]` (safe fallback) |

`resolve_or_default()` now:

* **strips whitespace** from truthy strings (`"  autogen  "` → `"autogen"`), matching how `pick_default()` normalises its own output.
* **falls back to `DEFAULT_PRIORITY[0]`** when `pick_default()` raises `RuntimeError` (no adapter installed), instead of propagating — so downstream availability validation gets a chance to raise its own actionable error rather than this helper masking it.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai.framework_adapters.registry import get_default_registry

registry = get_default_registry()

registry.resolve_or_default("crewai")   # → "crewai"
registry.resolve_or_default(None)       # → whatever pick_default() picks
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    In[name arg] --> Q{name is None or empty?}
    Q -->|no| Explicit[✅ return name stripped]
    Q -->|yes| PD[pick_default]
    PD --> Ret[✅ return default]
    PD -->|RuntimeError| Fallback[✅ DEFAULT_PRIORITY 0]
    classDef q fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef in fill:#6366F1,stroke:#7C90A0,color:#fff
    class In in
    class Q q
    class Explicit,Ret,PD,Fallback ok
```

<Note>
  Wrapper entry points used to fall back to a hardcoded `"praisonai"` when no framework was passed, which bypassed the registry's default-selection policy — a problem when a custom / tenant-scoped registry was injected. `resolve_or_default()` centralises the "None → pick\_default()" step so every caller stays consistent.
</Note>

As of PR [#5039](https://github.com/MervinPraison/PraisonAI/pull/5039) (Gap 1 of [#5016](https://github.com/MervinPraison/PraisonAI/issues/5016)), every wrapper entry point that used to fall back to a hardcoded `"praisonai"` now goes through the registry:

| Call site | Purpose |
| - | - |
| `AgentsGenerator._prepare_for_run` | Selects the adapter for `agents.yaml` runs |
| `AgentsGenerator.generate_crew_and_kickoff` / `agenerate_crew_and_kickoff` | Tags workflow-YAML observability sessions with the config-resolved framework (was a hardcoded `"praisonai"` label — wrong for non-praisonai workflow adapters) |
| `AgentsGenerator._build_yaml_workflow` | Resolves the framework for `cli_backend` compatibility validation on workflow YAMLs |
| `praisonai.jobs.router.submit_job` | Resolves the framework passed on the Jobs API's `POST /api/v1/runs` request |
| `praisonai.jobs.executor._run_agent` / `_run_praisonai_agents` | Resolves the framework for job execution — first invocation is dispatched via `asyncio.to_thread` so first-time registry construction (which probes entry-point imports) does not stall the event loop |
| `praisonai.scheduler.yaml_loader.load_agent_yaml_with_schedule` | Resolves the framework recorded in scheduler-YAML configurations |
| `praisonai.framework_adapters.workflow_framework.framework_from_config` | The core workflow-YAML helper — now accepts an optional `registry=` kwarg |

***

## Inject Your Own Registry (Multi-tenant / Tests)

<Note>
  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.
</Note>

The new `adapter_registry=` kwarg on `AgentsGenerator` and `AutoGenerator` enables dependency injection for multi-tenant applications and testing:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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](/docs/features/autoagents)).

### Framework validation honours the injected registry

As of PR #3152, framework-availability validation is DI-aware — a scoped/per-tenant adapter registered on the generator's own registry is no longer rejected just because it is absent from the process-default registry.

* `assert_framework_available(name, *, registry=None)` and `get_install_hint(name, *, registry=None)` now accept an optional `registry` kwarg (default: process-default).
* `AgentsGenerator._prepare_for_run` threads its injected `_adapter_registry` through into `assert_framework_available`, so validation consults the tenant registry, not the process-default.
* The install hint is derived from the same registry that failed validation (DI end to end).
* **Fallback for resolved built-in names:** if the injected registry does not know a name but the process-default does (typical when a family router resolves `autogen → autogen_v2`), validation falls back to the default and the framework is accepted. This preserves the DI seam without rejecting resolved built-ins.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai.framework_adapters.registry import FrameworkAdapterRegistry
from praisonai.agents_generator import AgentsGenerator

tenant_registry = FrameworkAdapterRegistry()
tenant_registry.register("tenant_scoped_fw", TenantScopedAdapter)

gen = AgentsGenerator(
    agent_file="agents.yaml",
    framework="tenant_scoped_fw",
    config_list=[...],
    adapter_registry=tenant_registry,   # DI seam
)
# Validation now consults tenant_registry, not the process-default.
```

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="TypeError: <name>.run does not implement the protocol: missing keyword parameters [...]">
    Your adapter's `run()` (or `arun()`) method is missing the four parameters required by the `FrameworkAdapter` protocol. As of [PR #2083](https://github.com/MervinPraison/PraisonAI/pull/2083), `FrameworkAdapterRegistry.create()` validates these at adapter construction time. The error names the failing method:

    ```
    TypeError: FrameworkAdapter '<name>'.run does not implement the protocol:
    missing keyword parameters ['agent_callback', 'cli_config', 'task_callback', 'tools_dict']
    ```

    Update your `run()` signature to:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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.

    A `**kwargs` catch-all also passes — handy when your adapter forwards to a delegate:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    def run(self, config, llm_config, topic, **kwargs):  # ✅ now accepted
        tools_dict = kwargs.get("tools_dict")
        agent_callback = kwargs.get("agent_callback")
        ...
    ```
  </Accordion>

  <Accordion title="My adapter disappeared from the available frameworks list">
    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.

    A `**kwargs` adapter is no longer a cause of this — if your adapter still disappears, the culprit is a named-parameter signature that omits one of the four protocol kwargs, or an `ImportError` in `__init__`.
  </Accordion>

  <Accordion title="Sync adapter called from async context">
    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`:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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)
    ```
  </Accordion>

  <Accordion title="Tool timeout enforced twice on a renamed native adapter">
    **Problem:** A renamed or forked native-style adapter's plain-callable tools hit `tool_timeout` twice — once in core, once in the wrapper — and you see wrapper `ToolTimeoutError` exceptions the built-in adapter never raises.

    **Cause:** Before PraisonAI PR #5798 the wrapper hardcoded the native path to `framework == "praisonai"`. A renamed or forked adapter missed that branch, so its plain callables were double-wrapped.

    **Fix:** Upgrade to a release containing PR #5798 and set `SUPPORTS_CORE_TOOL_TIMEOUT = True` on your adapter class, or subclass `PraisonAIAdapter`, which already sets it.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Registry Dependency Injection" icon="puzzle-piece" href="/docs/features/registry-dependency-injection">
    Learn about injecting custom registries for multi-tenant isolation
  </Card>

  <Card title="CrewAI Integration" icon="users" href="/docs/framework/crewai">
    See how built-in framework adapters are implemented
  </Card>

  <Card title="Workflow YAML Framework Field" icon="diagram-project" href="/docs/features/workflow-yaml-framework-field">
    How framework: works in workflow YAML vs agents.yaml
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.