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

# PraisonAI Package Integration

> Guide for integrating the PraisonAI package into your Python projects, including YAML configuration, execution modes, and the C8 wrapper–code boundary architecture

# Include praisonai package in your project

## Option 0: One-liner (simplest)

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import praisonai

result = praisonai.run("agents.yaml")
print(result)
```

That's it. `praisonai.run()` reuses the same framework auto-detection and LLM endpoint resolution the `praisonai` CLI uses, so anything that works on the command line works here.

### What it does

<Steps>
  <Step>
    Auto-detects the first installed framework in this order: `crewai` → `praisonaiagents` → `autogen`. Pass `framework="..."` to override — `ag2` and `langgraph` must be requested explicitly.
  </Step>

  <Step>
    Reads `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL_NAME` (and the standard PraisonAI key/config files) — same as the CLI.
  </Step>

  <Step>
    Returns the final task output as a string.
  </Step>
</Steps>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    User[👤 User] --> Call[📞 praisonai.run]
    Call --> Detect[🔍 Auto-detect framework]
    Call --> Resolve[🔑 Resolve LLM endpoint]
    Detect --> Gen[🤖 AgentsGenerator]
    Resolve --> Gen
    Gen --> Out[✅ Result string]

    classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef out fill:#10B981,stroke:#7C90A0,color:#fff

    class User,Out user
    class Call,Gen process
    class Detect,Resolve config
```

### Async variant for FastAPI / Jupyter

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import praisonai

async def handler():
    result = await praisonai.arun("agents.yaml")
    return result
```

`arun()` offloads the synchronous work to a thread so it never blocks the event loop.

### Optional parameters

| Parameter | Type | Default | Description |
| - | - | - | - |
| `agent_file` | `str` | required | Path to your `agents.yaml`. |
| `framework` | `str \| None` | `None` (auto-detect) | One of `"crewai"`, `"praisonai"`, `"autogen"`, `"ag2"`, `"langgraph"`. `ag2` and `langgraph` must be selected explicitly — they are not in the auto-detect chain. `ag2` is registered but its adapter is an unimplemented stub; passing `framework="ag2"` will raise `NotImplementedError` until implemented. |
| `tools` | `list \| None` | `None` | Mixed list of classes, plain functions and framework tool instances — see [Passing tools](#tools-param). |
| `agent_yaml` | `str \| None` | `None` | Inline YAML string; takes precedence over `agent_file` when both are given. |
| `cli_config` | `dict \| None` | `None` | Advanced: override the resolved CLI config (rarely needed). |

<Note>
  If no framework is installed, `run()` raises `RuntimeError` listing available adapters. Install one with `pip install praisonaiagents` (recommended for new projects). `ag2` and `langgraph` are not in the auto-detect chain; pass `framework="ag2"` or `framework="langgraph"` explicitly to select them. Note that `ag2` is an unimplemented stub and will raise `NotImplementedError` when called.
</Note>

<a id="tools-param" />

### Passing tools

`tools` accepts a mixed list — PraisonAI registers each entry under the right name and skips anything it doesn't recognise.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai import PraisonAI
from crewai_tools import SerperDevTool          # framework tool instance

def my_search(query: str) -> str:                # plain function
    return f"local search: {query}"

class MyScraper:                                 # class — instantiated for you
    def run(self, url: str) -> str:
        return f"scraped {url}"

praison = PraisonAI(
    agent_file="agents.yaml",
    tools=[my_search, SerperDevTool(), MyScraper],
)
praison.run()
```

| Entry shape | How it's registered | Example |
| - | - | - |
| Class | Instantiated: `tools_dict[MyTool.__name__] = MyTool()` | `tools=[MyTool]` |
| Plain callable (`def` or `@tool` function) | Passed through by `__name__` | `tools=[my_search]` |
| Framework tool instance (CrewAI / LangChain `BaseTool`) | Passed through by `.name` (fallback: `__name__`, then type name) | `tools=[JiraSearchTool()]` |
| Anything else (e.g. `42`, a bare dict) | Logged as a warning and skipped — the LLM never sees it | — |

<Warning>
  **Fixed in [PR #5809](https://github.com/MervinPraison/PraisonAI/pull/5809).** Before this fix, only *class* entries were registered. Plain callables and framework tool instances were silently dropped, so the agent ran without the tool. Upgrade if you pass anything other than classes.
</Warning>

### Advanced CLI options from Python

`run()` and `arun()` accept any extra keyword argument the CLI accepts. Two loose names get a friendly Python alias; everything else is forwarded through the same `cli_config` bridge the CLI already uses.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import praisonai

# Same as: praisonai run agents.yaml --llm gpt-4o
result = praisonai.run("agents.yaml", model="gpt-4o")

# Same as: praisonai run agents.yaml --resume abc123
result = praisonai.run("agents.yaml", session="abc123")

# Any other CLI option — forwarded through cli_config
result = praisonai.run("agents.yaml", approval="plan", verbose=True)
```

| Loose kwarg | Maps to | Notes |
| - | - | - |
| `model=` | `config_list[0]["model"]` (like `--llm`) | Also accepts the alias `llm=` |
| `session=` | `resume_session` (like `--resume`) | Renamed from the CLI-side `--session` for Python readability |
| Any other kwarg | `cli_config[key]` | Explicit `cli_config` values still win over loose kwargs |

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Run[📞 praisonai.run] --> Kwargs{Loose kwarg?}
    Kwargs -->|model= / llm=| Model[Set config_list model]
    Kwargs -->|session=| Resume[Map to resume_session]
    Kwargs -->|anything else| Bridge[Forward via cli_config]
    Model --> Resolve[🔑 Resolve LLM endpoint]
    Resume --> Resolve
    Bridge --> Gen[🤖 AgentsGenerator]
    Resolve --> Gen

    classDef call fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef out fill:#10B981,stroke:#7C90A0,color:#fff

    class Run call
    class Kwargs check
    class Model,Resume,Bridge,Resolve config
    class Gen out
```

<Note>
  Prior to release **v4.6.163** the Python API accepted only the five documented parameters and silently dropped any other kwarg. If you're on an older version, upgrade before relying on `model=` / `session=`.
</Note>

### When to use `PraisonAI(...)` instead

Reach for the full `PraisonAI` class (Option 1 below) when you need:

* Streaming / approval-system integration
* Custom backends or the gradio UI
* The `auto="..."` natural-language-to-YAML mode
* Repeated runs against the same generator without re-parsing YAML

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start{Need to run an<br/>agents.yaml?} -->|Yes, just once| Simple[praisonai.run]
    Start -->|Need streaming/<br/>approvals/auto-YAML| Full[PraisonAI class]
    Start -->|Inside async handler| Async[praisonai.arun]

    classDef question fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef choice fill:#10B981,stroke:#7C90A0,color:#fff

    class Start question
    class Simple,Full,Async choice
```

## Option 1: Using RAW YAML

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai import PraisonAI

# Example agent_yaml content
agent_yaml = """
framework: "crewai"
topic: "Space Exploration"

agents:  # Canonical: use 'agents' instead of 'roles'
  astronomer:
    role: "Space Researcher"
    goal: "Discover new insights about {topic}"
    instructions:  # Canonical: use 'instructions' instead of 'backstory' "You are a curious and dedicated astronomer with a passion for unraveling the mysteries of the cosmos."
    tasks:
      investigate_exoplanets:
        description: "Research and compile information about exoplanets discovered in the last decade."
        expected_output: "A summarized report on exoplanet discoveries, including their size, potential habitability, and distance from Earth."
"""

# Create a PraisonAI instance with the agent_yaml content
praisonai = PraisonAI(agent_yaml=agent_yaml)

# Run PraisonAI
result = praisonai.run()

# Print the result
print(result)
```

## Option 2: Using separate agents.yaml file

Note: Please create agents.yaml file before hand. If you only need to run an `agents.yaml` once and want zero ceremony, see [Option 0: One-liner](#option-0-one-liner-simplest) above.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai import PraisonAI

def basic(): # Basic Mode
    praisonai = PraisonAI(agent_file="agents.yaml")
    praisonai.run()

if __name__ == "__main__":
    basic()
```

## Other options

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai import PraisonAI

def basic(): # Basic Mode
    praisonai = PraisonAI(agent_file="agents.yaml")
    praisonai.run()
    
def advanced(): # Advanced Mode with options
    praisonai = PraisonAI(
        agent_file="agents.yaml",
        framework="autogen", # use AG2 framework (Formerly AutoGen)
    )
    praisonai.run()
    
def auto(): # Full Automatic Mode
    praisonai = PraisonAI(
        auto="Create a movie script about car in mars",
        framework="autogen" # use AG2 framework
    )
    print(praisonai.framework)
    praisonai.run()

if __name__ == "__main__":
    basic()
    advanced()
    auto()
```

## Lifecycle / cleanup

Each `AgentsGenerator` lazily creates a bounded thread-pool to run synchronous tools under per-call timeouts. The pool is **owned by the instance**, not the process — concurrent sessions in a multi-tenant runtime never share workers.

When you're done with a generator, release the pool with `.close()` or use it as a context manager:

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

# Pattern A: context manager (recommended)
with AgentsGenerator(agent_file="agents.yaml", framework="crewai", config_list=[...]) as gen:
    gen.generate_crew_and_kickoff()
# pool is shut down on exit, even if generate_crew_and_kickoff raises

# Pattern B: explicit close()
gen = AgentsGenerator(agent_file="agents.yaml", framework="crewai", config_list=[...])
try:
    gen.generate_crew_and_kickoff()
finally:
    gen.close()
```

`close()` is idempotent — calling it twice is safe.

<Note>
  **Sync runs are now auto-scoped.** `generate_crew_and_kickoff()` wraps its `adapter.run(...)` call in `scoped_bridge()` automatically, so each sync run isolates its `run_sync`-driven work onto its own loop+thread. Multi-tenant sync embedders no longer need to add their own `scoped_bridge()` around `generate_crew_and_kickoff` — a stuck coroutine in one tenant's run cannot park the shared loop for another. The same isolation now covers the YAML workflow sync entry (`_run_yaml_workflow` / `praisonai workflow run`) — `workflow.start(...)` runs under its own `scoped_bridge()` too. `agenerate_crew_and_kickoff()` and the async workflow path are intentionally not wrapped (they already run on the caller's loop). See [Async Bridge → Per-run auto-scoping](/docs/features/async-bridge#per-run-auto-scoping-sequential-hierarchical-workflow).
</Note>

### Cross-generator tool isolation

Multiple `AgentsGenerator` instances can wrap the **same shared framework tool object** (common with CrewAI / LangChain tools registered once and reused across tenants) without interfering with each other. Each generator installs its own per-instance `_TimeoutBoundTool` proxy — a lightweight wrapper that delegates schema attributes (`name`, `description`, `args_schema`, …) to the shared inner tool but routes `_run` / `run` through its own executor. The shared inner object is never mutated in place, so shutting down one generator's pool cannot break another generator's calls on the same tool.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Inner[Shared framework tool]:::agent
    ProxyA[Generator A<br/>_TimeoutBoundTool proxy]:::process
    ProxyB[Generator B<br/>_TimeoutBoundTool proxy]:::process
    ExecA[Executor A]:::config
    ExecB[Executor B]:::config

    ProxyA -->|delegates schema| Inner
    ProxyB -->|delegates schema| Inner
    ProxyA -->|_run / run| ExecA
    ProxyB -->|_run / run| ExecB

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
```

The proxy is built as an instance of a dynamic subclass of the wrapped tool's class, so `isinstance(proxy, BaseTool)` (and any framework-specific base class check) keeps returning `True`. This preserves the `isinstance`-based dispatch that `praisonaiagents.tool_execution`, CrewAI, and LangChain executors rely on to call `.run` — without it, a `BaseTool` plugin tool with `tool_timeout` set would silently execute nothing.

<Note>
  **`cli_config` templates stay clean.** As of PR #5228, the generator's per-run `_tool_timeout_wrap` / `_agent_tool_wrap_resolver` closures are **not** stamped onto `self.cli_config` any more. They live on a private `self._run_ctx` and are merged into the adapter's `cli_config` kwarg only at dispatch, via `AgentsGenerator._dispatch_cli_config()`. Multi-tenant embedders passing a shared `cli_config` template (per-tenant, per-request, or captured from `vars(args)`) no longer see it mutated after a run — it never comes back holding instance-bound lambdas. Adapters still read `_tool_timeout_wrap` / `_agent_tool_wrap_resolver` off the `cli_config` kwarg they receive, so no adapter signature changes.
</Note>

### Injecting a shared executor (multi-tenant)

If you already manage a thread-pool (e.g. one per tenant), inject it via `tool_timeout_executor=`. The generator will use the pool but **never shut it down** — ownership stays with you.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import concurrent.futures
from praisonai.agents_generator import AgentsGenerator

tenant_pool = concurrent.futures.ThreadPoolExecutor(
    max_workers=8,
    thread_name_prefix=f"tenant-{tenant_id}",
)

with AgentsGenerator(
    agent_file="agents.yaml",
    framework="crewai",
    config_list=[...],
    tool_timeout_executor=tenant_pool,   # borrowed; close() will NOT shut it down
) as gen:
    gen.generate_crew_and_kickoff()

# you remain responsible for tenant_pool.shutdown()
```

Each owned pool is named `praisonai-tool-timeout-<hexid>` for easy enumeration in `top` / `py-spy` / thread dumps.

### `tool_timeout_executor` parameter

| Parameter | Type | Default | Description |
| - | - | - | - |
| `tool_timeout_executor` | `concurrent.futures.Executor \| None` | `None` (lazy, instance-owned) | Optional executor for per-call tool timeouts. When `None`, the generator lazily creates and owns a bounded `ThreadPoolExecutor` (default 32 workers, override with `PRAISONAI_TOOL_TIMEOUT_WORKERS`). When supplied, it is **borrowed** — the generator never shuts it down. |
| `strict_validation` | `bool \| None` | `None` (falls back to `PRAISONAI_VALIDATE_STRICT`) | Per-run override that promotes YAML validation warnings to `ValueError`. Precedence: this arg > `cli_config["strict_validation"]` > `PRAISONAI_VALIDATE_STRICT` env var. Lets multi-tenant hosts scope strictness per request without mutating `os.environ`. |
| `tool_timeout_workers` | `int \| None` | `None` (falls back to `PRAISONAI_TOOL_TIMEOUT_WORKERS`, default `32`) | Per-run override for the sync-tool timeout thread pool size. Precedence: this arg > `cli_config["tool_timeout_workers"]` > `PRAISONAI_TOOL_TIMEOUT_WORKERS` env var. Invalid values fall back to the env/default with a warning. |

`strict_validation` and `tool_timeout_workers` are also recognised keys inside `cli_config`, so `run()` / `arun()` forward them through the same `**kwargs` bridge.

<Note>
  Env vars still work. Set the per-run override only when you need multi-tenant isolation.
</Note>

<AccordionGroup>
  <Accordion title="Always close or use `with`">
    Leaked tool-timeout pools accumulate threads named `praisonai-tool-timeout-<hexid>` across instances. Use `with AgentsGenerator(...) as gen:` or call `gen.close()` to release them. Check `top` / `py-spy` if you suspect thread accumulation.
  </Accordion>

  <Accordion title="Borrowed pools stay yours">
    Passing `tool_timeout_executor=` means `close()` won't shut it down. You manage the lifecycle — call `your_pool.shutdown()` when you're done with it.
  </Accordion>

  <Accordion title="One generator per session in multi-tenant">
    Don't share `AgentsGenerator` instances across tenants. Share the executor instead — inject the same pool via `tool_timeout_executor=` into each per-tenant generator.

    Sharing an `AgentsGenerator` across tenants is still discouraged (per-instance timeout executor lifecycles are per-instance), but sharing the underlying **tool objects** across generators is now safe — the per-generator proxy guarantees that A's timeout wrapper cannot leak into B's calls, and closing A's pool cannot break B's calls on the same tool.
  </Accordion>

  <Accordion title="Unique task names per workflow">
    With the CrewAI framework, `context=[...]` lookup keys on the task name. Two roles defining the same task name now raise `ValueError` — pick names like `<role>_<verb>` for clarity.
  </Accordion>
</AccordionGroup>

***

## Logging in scripts

If you want PraisonAI to configure your application's logging, call `configure_cli_logging()` once at startup:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai import PraisonAI
from praisonai._logging import configure_cli_logging

configure_cli_logging("INFO")  # opt in to root-logger setup
PraisonAI(agent_file="agents.yaml").run()
```

If you want PraisonAI to leave your application's logging untouched, just don't call `configure_cli_logging` — only namespaced `praisonai.*` loggers will be used.

***

## C9 Architecture: Four-Tier Package Model (Developer Reference)

<Note>
  This section covers the internal four-tier package architecture introduced in C9 (PR #2633). It is relevant to contributors and maintainers, not end-users.
</Note>

### Sibling: praisonai-bot

As of C9 (merged 2026-07-03), bots, gateway, and channel CLI live in a new sibling PyPI package **`praisonai-bot`** (module `praisonai_bot`). The `praisonai.bots`, `praisonai.gateway`, and `praisonai.daemon` paths remain as `alias_package` shims and are the stable public API.

<CardGroup cols={2}>
  <Card title="praisonai-bot Migration Guide" icon="package" href="/docs/guides/praisonai-bot-migration">
    Install options, CLI reference, backward-compat guarantees, and channel extension guide
  </Card>

  <Card title="Built-in Channels" icon="robot" href="/docs/features/messaging-bots">
    Telegram, Discord, Slack, WhatsApp, Linear, Email, AgentMail
  </Card>
</CardGroup>

### Four-Tier Model

PraisonAI uses a strict four-tier package model with a one-way dependency rule:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    subgraph tier1 ["Tier 1 — praisonaiagents (Core SDK)"]
        PROTO[Protocols / Hooks / Base Classes]
        CLI_PROTO[cli/protocols.py]
    end

    subgraph tier2a ["Tier 2 — praisonai-code (Terminal CLI)"]
        RUN[run / chat / code / serve]
        BRIDGE[_wrapper_bridge.py]
    end

    subgraph tier2b ["Tier 2 — praisonai-bot (Bots / Gateway)"]
        BOTS[bots / gateway / daemon / claw]
        WBRIDGE[_wrapper_bridge.py]
    end

    subgraph tier3 ["Tier 3 — praisonai (Wrapper)"]
        CMDS[Repatriated Commands]
        FEATS[Repatriated Features]
        ADAPT[adapters/]
        SHIMS[alias_package shims for bots/gateway/daemon]
    end

    PROTO --> RUN
    PROTO --> BOTS
    CLI_PROTO --> ADAPT
    RUN -.->|"lazy bridge only — no PyPI dep"| CMDS
    BRIDGE -.-> CMDS
    BRIDGE -.-> FEATS
    WBRIDGE -.->|"lazy bridge only — no PyPI dep"| CMDS

    classDef core fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef code fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef wrapper fill:#4a5568,stroke:#7C90A0,color:#fff
    classDef shim fill:#F59E0B,stroke:#7C90A0,color:#fff

    class PROTO,CLI_PROTO core
    class RUN,BRIDGE,BOTS,WBRIDGE code
    class CMDS,FEATS,ADAPT wrapper
    class SHIMS shim
```

**Invariant:** Both `praisonai-code` and `praisonai-bot` declare **no** PyPI dependency on `praisonai`. All cross-tier access goes through `_wrapper_bridge` only.

### alias\_package shim: lazy vs eager

The `praisonai.bots` / `praisonai.gateway` / `praisonai.daemon` `alias_package` shims default to an **eager** subtree walk at shim-install time. This preserves `old_name.sub is new_name.sub` — the module-identity guarantee that keeps `unittest.mock.patch("old.sub.attr")` and `isinstance` checks working after the C9 migration, even when the running code imports the moved package under its **new** name.

Set `PRAISONAI_SHIM_LAZY=1` to opt **out** of the eager walk (pure `_AliasFinder` lazy resolution). The trade-off: faster shim install and no eager pull of optional heavy deps — but `mock.patch` against the old dotted path can miss when code imports via the new name.

As of [PR #4261](https://github.com/MervinPraison/PraisonAI/pull/4261), a failed eager import is no longer silently swallowed — it logs a warning with traceback (`shim: eager import of %s failed; will be resolved lazily`) and is left for `_AliasFinder` to resolve lazily. `None` placeholders in `sys.modules` are skipped instead of poisoning the old dotted alias. The warning is a useful debugging aid when a downstream `AttributeError` on the alias would otherwise have no traceback pointing back at the shim.

<AccordionGroup>
  <Accordion title="When to keep the default (eager)">
    Keep eager registration whenever anything in the process relies on `mock.patch` against the old dotted path (`praisonai.bots.*`, `praisonai.gateway.*`, `praisonai.daemon.*`) or on `old_name.sub is new_name.sub` identity.
  </Accordion>

  <Accordion title="When to set PRAISONAI_SHIM_LAZY=1">
    Use it only when no `mock.patch` targets the old dotted path and minimal-container startup speed matters more than mock-patch parity. See [Security Environment Variables → `PRAISONAI_SHIM_LAZY`](/docs/features/security-environment-variables#praisonai_shim_lazy).
  </Accordion>
</AccordionGroup>

### C8 Architecture: Wrapper–Code Boundary (Historical Reference)

<Note>
  The following section documents the C8 three-tier model that preceded C9. The three-tier diagram and metrics are preserved for historical reference.
</Note>

#### Three-Tier Model (pre-C9 / C8)

Before C9, PraisonAI used a three-tier package model:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    subgraph tier1 ["Tier 1 — praisonaiagents (Core SDK)"]
        PROTO[Protocols / Hooks / Base Classes]
        CLI_PROTO[cli/protocols.py]
    end

    subgraph tier2 ["Tier 2 — praisonai-code (Terminal CLI)"]
        RUN[run / chat / code / serve]
        BRIDGE[_wrapper_bridge.py]
    end

    subgraph tier3 ["Tier 3 — praisonai (Wrapper)"]
        CMDS[Repatriated Commands]
        FEATS[Repatriated Features]
        ADAPT[adapters/]
    end

    PROTO --> RUN
    CLI_PROTO --> ADAPT
    RUN -.->|"lazy bridge only — no PyPI dep"| CMDS
    BRIDGE -.-> CMDS
    BRIDGE -.-> FEATS

    classDef core fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef code fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef wrapper fill:#4a5568,stroke:#7C90A0,color:#fff

    class PROTO,CLI_PROTO core
    class RUN,BRIDGE code
    class CMDS,FEATS,ADAPT wrapper
```

**Invariant:** `praisonai-code/pyproject.toml` declares **no** PyPI dependency on `praisonai`. All `praisonai-code` → `praisonai` wrapper access goes through `praisonai_code._wrapper_bridge` only.

### C8 Metrics (post-C8, merged 2026-07-02)

| Metric | Pre-C8 | Post-C8 |
| - | - | - |
| Wrapper import lines in `praisonai-code` | 225 | **0** |
| Regression baseline in `check_c7_imports.sh` | 225 | **50** |
| Allowlisted files | 47 | **0** |

### `_wrapper_bridge` — The Only Cross-Tier Path

`praisonai_code._wrapper_bridge` is the **sole** permitted mechanism for `praisonai-code` to call into `praisonai`. It lazy-loads the wrapper at runtime and never causes an import-time error when the wrapper is absent.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai_code._wrapper_bridge import get_wrapper_module

mod = get_wrapper_module("praisonai.cli.commands.train")
if mod:
    mod.train_command()
```

Direct `import praisonai` or `from praisonai import …` inside `praisonai-code` is forbidden and enforced by `scripts/check_c7_imports.sh`.

### `_WRAPPER_RESIDENT_COMMANDS`

Commands that live in the `praisonai` wrapper but are registered in the `praisonai-code` CLI router. The variable was renamed from `_WRAPPER_COMMANDS` in C8.1 (backward-compat alias retained).

`get_command()` lazy-loads these via the absolute `praisonai.cli.commands.*` path through the bridge.

**C8.2 Repatriated Commands** (moved from `praisonai-code` back to `praisonai`):

| Batch | Commands |
| - | - |
| A | `langfuse`, `langextract`, `flow`, `n8n`, `replay`, `langfuse_client` |
| B | `train`, `managed`, `examples`, `standardise` |
| C | `docs`, `schedule`, `batch` |
| D | `rag`, `knowledge`, `realtime`, `profile`, `audit`, `app` |
| E | `serve` wrapper paths (SDK subcommands remain in `praisonai-code`) |

### C8.3 Repatriated Features

The following `cli/features/*` modules moved from `praisonai-code` to `praisonai/cli/features/`:

`recipe`, `templates`, `deploy`, `recipe_optimizer`, `persistence`, `eval`, `agent_scheduler`, `acp`, `registry`, `sandbox_cli`, `ollama`, `workflow`, `tui/app`, `interactive/async_tui`, `interactive/core`

Also repatriated: `commands/recipe`, `context`, `mcp`, `validate`.

### Protocols & Adapters (C8.5)

C8.5 introduces typed protocol contracts to enforce the tier boundary. The `SessionStoreProtocol` (session persistence) already ships in `praisonaiagents`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.session.protocols import SessionStoreProtocol
```

`TemplateStoreProtocol` and `ServeHandlerProtocol` are defined in the C8.5 design but their physical extraction is **deferred** (same milestone as the `PraisonAI` class split).

| Protocol | Location | Purpose |
| - | - | - |
| `SessionStoreProtocol` | `praisonaiagents.session.protocols` | Session persistence backend contract (`add_message`, `get_chat_history`, …) |
| `TemplateStoreProtocol` | *deferred* | Template persistence for repatriated CLI features |
| `ServeHandlerProtocol` | *deferred* | Dispatch serve subcommands without direct wrapper imports |

The `praisonai.adapters` module lazily re-exports existing adapter classes (`AutoReader`, `ChromaVectorStore`, `BasicRetriever`, `FusionRetriever`, `LLMReranker`, and registration helpers) to preserve backward compatibility. Registration into the core-SDK singletons is now automatic at the wrapper entry points (`praisonai.run`, `praisonai.arun`, and every `praisonai` CLI command that runs an agent file) — `register_default_adapters()` is called for you before the agent starts, so YAML declarations like `retriever: fusion` and `reranker: llm` resolve out of the box (PraisonAI 4.7.9+). Library-mode Python callers importing `praisonai.adapters.*` directly still call `register_default_adapters()` themselves (idempotent + lock-guarded, so a subsequent auto-call is a no-op). Multi-tenant hosts can pre-register custom adapters under built-in names — the wrapper defaults preserve any existing entry (register-only-if-absent) and never overwrite.

### Writing a custom managed backend

`ManagedBackendBase` provides the shared `async stream()` producer-thread + sentinel-queue pump. `AnthropicManagedAgent` and `LocalManagedAgent` both inherit from it; a third managed backend (E2B, Modal, Fly.io, …) implements two hooks instead of copying the queue loop.

<Note>
  `ManagedBackendBase` lives in `praisonai.integrations._managed_base` — the leading underscore marks it as an extension point rather than a stable public import path. Cite the class name and subclass it; if a top-level `praisonai.integrations` re-export lands later, switch to that.
</Note>

| Method | Type | Required? | Purpose |
| - | - | - | - |
| `_ensure_session(self) -> str` | sync | ✅ | Return a session id; called on the producer thread before `_iter_events` |
| `_iter_events(self, session_id, prompt) -> Iterator[str]` | sync generator | ✅ | Yield text chunks for `prompt` on `session_id`; runs on the producer thread so blocking SDK calls are fine |
| `_after_stream(self, full: str) -> None` | sync | ⬜ | Post-stream bookkeeping (persist the assembled assistant turn, sync usage). Default: no-op |

`_ensure_session` runs *before* the first `_iter_events` call. A backend that snapshots session state should populate `agent_id` / `environment_id` inside `_ensure_session` — `LocalManagedAgent._ensure_session()` creates the inner agent first for exactly this reason, so the first persisted snapshot is complete rather than orphaned.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonai.integrations._managed_base import ManagedBackendBase

class MyManagedAgent(ManagedBackendBase):
    def _ensure_session(self) -> str:
        # Create/lookup your session with your provider; return its id.
        return self._session_id or self._provision_session()

    def _iter_events(self, session_id, prompt):
        # Runs on the producer thread — blocking SDK calls are fine here.
        for chunk in self._provider_client.stream(session_id, prompt):
            if chunk.text:
                yield chunk.text

    def _after_stream(self, full: str) -> None:
        # Optional: persist the assembled assistant turn + usage.
        self._persist_message("assistant", full)
```

<Warning>
  Do NOT override `async def stream(self, prompt, **kwargs)` — the base class owns it, including the `run_in_executor` hop over `queue.get`. Overriding it re-introduces the duplication this refactor removed, and future protocol fixes will bypass your backend.
</Warning>

### C8.4 Legacy Structure

* `praisonai/cli/legacy/inbuilt_tools.py`, `framework_run.py` — extraction targets for lazy loaders.
* `praisonai-code/cli/legacy/prompt_dispatch.py` — standalone-safe helpers.
* `main.py` still contains the `PraisonAI` class body; wrapper access is normalised via the bridge. The physical 7k-line split is **deferred** (out of scope of C8).

#### Developer Tooling (C8)

| Script | Purpose |
| - | - |
| `scripts/check_c7_imports.sh` | Counts wrapper import lines in `praisonai-code`; enforces baseline ≤ 50 |
| `scripts/c8_bridge_convert.py` | Converts direct `from praisonai …` imports to `_wrapper_bridge` calls |
| `scripts/c8_repatriate.py` | Moves a command/feature implementation from `praisonai-code` → `praisonai` |

#### Deferred Work (C8)

The physical extraction of the `PraisonAI` class (\~7k lines in `main.py`) to `praisonai/cli/legacy/praison_class.py` was explicitly deferred from C8. The `docs/concepts/architecture.mdx` page (HUMAN-ONLY) also needs a maintainer update to reflect the C8 metrics.


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