External CLI Integrations

Drive external AI CLIs like Claude Code from a PraisonAI agent

Drive external AI CLIs from a PraisonAI agent — Claude Code is the flagship integration.

python
from praisonaiagents import Agent
from praisonai.integrations import ClaudeCodeIntegration

agent = Agent(
    name="coder",
    tools=[ClaudeCodeIntegration().as_tool()],
)
agent.start("Summarise the README in this repo")

The user asks in chat; the agent invokes Claude Code or another external CLI through the integration tool.

Diagram

👤 User

🤖 Agent

🔌 ClaudeCodeIntegration

💻 claude CLI

✅ Result

Quick Start

  1. Attach Claude Code as an agent tool
    python
    from praisonaiagents import Agent
    from praisonai.integrations import ClaudeCodeIntegration
    
    agent = Agent(
        name="coder",
        tools=[ClaudeCodeIntegration().as_tool()],
    )
    agent.start("Summarise the README in this repo")
  2. One-shot call via the integration API
    python
    import asyncio
    from praisonai.integrations import ClaudeCodeIntegration
    
    integration = ClaudeCodeIntegration()
    result = asyncio.run(integration.execute("Summarise the README in this repo"))
    print(result)
  3. Continue a previous Claude Code session
    python
    # First turn
    await integration.execute("List the open bugs")
    
    # Continue the same session — you MUST pass continue_session=True explicitly
    await integration.execute("Now suggest fixes for the first one", continue_session=True)
  4. Stream events live
    python
    async for event in integration.stream("Refactor utils.py"):
        print(event)   # parsed JSON events from the CLI

    The timeout argument is enforced end-to-end: stream_async(timeout=...) applies a monotonic deadline to every stdout read and the final drain, so a stalled claude/gemini/codex/cursor subprocess raises TimeoutError: Stream timed out after {timeout}s: {cmd} instead of hanging forever.

  5. Per-call output format
    python
    await integration.execute("prompt", output_format="json")
    await integration.execute("prompt", output_format="text")
  6. Register as a native async tool
    python
    from praisonaiagents import Agent
    from praisonai.integrations import ClaudeCodeIntegration
    
    agent = Agent(
        name="coder",
        tools=[ClaudeCodeIntegration().as_async_tool()],  # awaited on the running loop
    )
    
    # Use the async entrypoint on the agent to keep everything on one loop:
    await agent.astart("Summarise the README in this repo")

    as_async_tool() returns a coroutine tool named <cli>_atool (for example claude_atool). It is functionally equivalent to as_tool(), but with no thread hop — measurable when the agent invokes the CLI many times in one turn.


How It Works

Diagram
Resultclaude CLIClaudeCodeIntegrationUserResultclaude CLIClaudeCodeIntegrationUserexecute(prompt, continue_session=?, output_format=?)_build_command() adds –continue only when askedsubprocess with assembled argsparsed responsereturn to callerfinal result

The integration builds CLI arguments dynamically per call, with no instance state mutated during execution.


Stateless Session Management

Old (removed / ignored)New explicit parameter
self._session_active = True (auto)continue_session=True per call
self.output_format mutated during stream()output_format="stream-json" passed per call
integration.reset_session()No longer needed — deprecated no-op

Before (stateful):

python
# ❌ Old pattern — relied on implicit session state
integration = ClaudeCodeIntegration()
await integration.execute("step 1")  # marks _session_active = True
await integration.execute("step 2")  # implicitly continued previous session
integration.reset_session()           # cleared state

After (stateless):

python
# ✅ New pattern — explicit, stateless, safe for concurrent use
integration = ClaudeCodeIntegration()
await integration.execute("step 1")
await integration.execute("step 2", continue_session=True)   # explicit
# reset_session() is no longer needed; it is a deprecated no-op

CLI: Manager Delegation (Default)

PraisonAI CLI offers two execution modes for external agents, providing flexibility between automated reasoning and direct execution.

Diagram

Yes

No

User Command

Need planning/reasoning?

Manager Delegation

Direct Proxy

–external-agent

–external-agent-direct

Manager Agent + Subagent Tool

Pass-through CLI

ModeFlagBehaviourBest for
Manager delegation (default)--external-agent XManager Agent wraps the CLI as a tool, reasons, then calls itMulti-step tasks, planning, aggregation
Direct proxy--external-agent X --external-agent-directPass-through — CLI runs the prompt verbatimFast single-shot calls, scripting, when you don’t want a manager LLM

Usage Examples:

bash
# Manager delegation - manager reasons, then calls claude as tool
praisonai "Fix the bug in auth.py" --external-agent claude

# Direct proxy - no manager overhead, straight to claude  
praisonai "Fix the bug in auth.py" --external-agent claude --external-agent-direct

Configuration Options

OptionTypeDefaultWhereDescription
workspacestr"."constructorWorking directory passed to the CLI
timeoutint300constructorPer-call timeout (seconds), enforced on streaming reads — a stalled subprocess raises TimeoutError
output_formatstr"json"constructor or per-call"text" \"json" \"stream-json"
use_sdkboolFalse (True if SDK available)constructorUse the Claude Code SDK when installed, otherwise fall back to subprocess
model`str \None`NoneconstructorPassed through to the CLI via --model
continue_sessionboolFalseper-callAdds --continue to the CLI invocation
skip_permissionsboolTrueconstructorSkip permission prompts with --dangerously-skip-permissions
system_prompt`str \None`NoneconstructorCustom system prompt to append
allowed_tools`List[str] \None`NoneconstructorList of allowed tools (e.g., [“Read”, “Write”, “Bash”])
disallowed_tools`List[str] \None`NoneconstructorList of disallowed tools

Registry

The ExternalAgentRegistry manages all available CLI integrations with a thread-safe registry pattern with lazy module-level default. You can construct your own ExternalAgentRegistry() for test isolation or multi-tenant runs:

python
from praisonai.integrations.registry import get_default_registry

# Default registry (recommended for app code)
registry = get_default_registry()

# List available integrations
available = await registry.get_available()
print(available)  # {'claude': True, 'gemini': False, ...}

# Option 1 — soft lookup: returns None if the integration is missing/unavailable
claude = registry.try_create("claude", workspace="/path/to/project")
if claude is None:
    print("Integration 'claude' not found or unavailable")

# Option 2 — strict: raises ValueError with diagnostics if not registered
claude = registry.create("claude", workspace="/path/to/project")

The existing helpers get_registry(), register_integration(), create_integration(), and get_available_integrations() are still exported and still work — they now delegate to get_default_registry(). Backward-compat code does not need to change.

Invalidating the availability cache

BaseCLIIntegration caches the result of shutil.which(cli_command) for each CLI. If a CLI is installed/uninstalled mid-run (e.g. in tests, or after a setup step), invalidate the cache:

python
from praisonai.integrations.base import BaseCLIIntegration

# Clear cache for one command
BaseCLIIntegration.invalidate_availability("claude")

# Clear the entire cache
BaseCLIIntegration.invalidate_availability()

The cache is thread-safe; invalidation is also thread-safe.

Logging for embedders

The registry logs through the named logger praisonai.integrations.registry — it never configures the root logger, so importing or calling the registry will not install a handler on an embedding application’s root logger.

Two warnings route through this logger:

MessageWhen
Failed to check availability for <name>: <error>An integration raised while its availability was being probed
Skipping external agent '<name>': failed to load (<error>)An optional plugin failed to import and was dropped from the catalog

Silence or redirect them:

python
import logging

logging.getLogger("praisonai.integrations.registry").setLevel(logging.ERROR)

Registering the integration as a tool

Both methods wrap the same underlying integration.execute(query) coroutine — pick the one that matches how your agent is invoked.

MethodReturnsSync agentAsync agentRegistered name
integration.as_tool()def tool_func(query: str) -> str✅✅ (safe since PraisonAI PR #4022)<cli>_tool
integration.as_async_tool()async def atool_func(query: str) -> strNot intended (coroutine)✅ Preferred — no thread hop<cli>_atool

<cli> is the integration’s cli_command: claude_tool/claude_atool, gemini_tool/gemini_atool, codex_tool/codex_atool, and cursor-agent_tool/cursor-agent_atool.


Register a custom external agent

Register your own external agent once and it becomes reachable from every surface — --external-agent, the praisonai_code CLI handler, and the UI toggles — with no PraisonAI code changes.

Diagram

🔌 Registered Agent

📝 ExternalAgentRegistry

💻 –external-agent

⚙️ CLI Handler

🔘 UI Toggle

  1. Ship as a pip plugin (recommended)

    Publish your integration class to the praisonai.external_agents entry-point group:

    toml
    # your-plugin/pyproject.toml
    [project.entry-points."praisonai.external_agents"]
    aider = "acme_aider:AiderIntegration"

    After pip install your-plugin, aider appears in praisonai --external-agent choices, in the praisonai_code CLI handler, and as a UI toggle — no PraisonAI code changes.

  2. Or register at runtime
    python
    from praisonai.integrations.registry import get_default_registry
    from praisonai.integrations.base import BaseCLIIntegration
    
    class AiderIntegration(BaseCLIIntegration):
        display_label = "Aider (pair programming)"
        install_hint = "pip install aider-chat"
    
        @property
        def cli_command(self) -> str:
            return "aider"
    
        async def execute(self, prompt: str, **options) -> str: ...
        async def stream(self, prompt: str, **options): ...
    
    get_default_registry().register("aider", AiderIntegration)

    register() raises ValueError if the class does not inherit BaseCLIIntegration.

Presentation metadata

Two optional class attributes drive help text, UI labels, and install hints on every surface:

AttributeTypeDefaultDescription
display_label`str \None`f"{name} CLI"UI label and CLI help text
install_hint`str \None`"See documentation"Install instruction shown when the CLI is missing

Built-ins declare them too:

Namedisplay_labelinstall_hint
claudeClaude Code (coding, file edits)npm install -g @anthropic-ai/claude-code
geminiGemini CLI (analysis, search)npm install -g @anthropic-ai/gemini-cli
codexCodex CLI (refactoring)npm install -g @openai/codex
cursorCursor CLI (IDE tasks)Download from cursor.com

Single source of truth

Two registry helpers back every surface that lists external agents:

python
from praisonai.integrations.registry import (
    list_external_agents,
    external_agent_catalog,
)

# Names of every registered external agent (built-ins + plugins)
list_external_agents()
# ['claude', 'gemini', 'codex', 'cursor', 'aider']

# Presentation metadata per agent: {"cls", "label", "cli", "install"}
external_agent_catalog()["aider"]
# {'cls': AiderIntegration, 'label': 'Aider (pair programming)',
#  'cli': 'aider', 'install': 'pip install aider-chat'}

Built-ins keep declaration order (claude, gemini, codex, cursor); plugins are appended in sorted order behind them. A faulty optional plugin is logged and skipped — it never blanks the catalog. Name collisions between a third-party plugin and a built-in are resolved in favour of the built-in — a plugin can extend the catalog but never replace claude, gemini, codex, or cursor.

A listed name is always a usable name. list_external_agents() is derived from external_agent_catalog(), so a plugin that fails to import disappears from both surfaces at once. A broken plugin is never advertised as a --external-agent choice and then rejected by the handler — if the name shows up in --help, in ExternalAgentsHandler().list_integrations(), or as a UI toggle, it loads.

Diagram

loads

fails to load

📦 Registered names

🔍 external_agent_catalog
loads each class

📋 list_external_agents

⚠️ skipped + logged

💬 –external-agent choices

✅ UI toggles

✅ Handler

Which entry-point group?

Two entry-point groups exist and are easy to confuse — pick by outcome:

GroupOutcomeDocumented in
praisonai.external_agentsShort-name --external-agent choices, registry, and UI togglesThis page
praisonai.integrationsModule importability (from praisonai.integrations import acme) — does not feed short-name --external-agent lookupIntegration Registry

Use praisonai.external_agents for a selectable --external-agent short name and a UI toggle. Use praisonai.integrations only to make a class importable from praisonai.integrations.


Prerequisites

The claude CLI must be available on PATH or the Claude Code SDK must be installed if use_sdk=True. The integration performs a cached, thread-safe shutil.which(...) check via is_available().

bash
# Install Claude Code CLI (option 1)
# Follow Claude Code installation instructions

# Or install SDK (option 2)  
pip install claude-agent-sdk

Decision Flow

Diagram

One-shot answer

Stream events live

Continue prior session

Structured JSON back

Register with a sync or async agent

Register with a native async agent

How do I use Claude Code?

await integration.execute prompt

async for event in integration.stream prompt

pass continue_session=True per call

pass output_format=”json” per call

integration.as_tool

integration.as_async_tool


Best Practices

Always pass continue_session=True explicitly

Don’t rely on instance state for session continuation. Always be explicit about when you want to continue a previous session.

python
# ✅ Good - explicit session control
await integration.execute("step 1")
await integration.execute("step 2", continue_session=True)

# ❌ Bad - relying on removed implicit state
await integration.execute("step 1")
await integration.execute("step 2")  # No longer continues session
Set output_format per call or once in constructor

Do not mutate the integration.output_format attribute between calls. Use the per-call parameter or set it once during initialization.

python
# ✅ Good - per-call parameter
await integration.execute("prompt", output_format="json")
await integration.execute("prompt", output_format="text")

# ✅ Good - set once in constructor
integration = ClaudeCodeIntegration(output_format="json")

# ❌ Bad - mutating instance attribute
integration.output_format = "json"  # Don't do this
Remove reset_session() calls

The reset_session() method is now a no-op. Remove these calls from your code as they serve no purpose.

python
# ❌ Bad - calling deprecated no-op
integration.reset_session()

# ✅ Good - just omit the call
# Session state is automatically stateless
Safe for concurrent use

A single ClaudeCodeIntegration instance can be shared across concurrent tasks safely since no instance state is mutated during calls.

python
# ✅ Good - concurrent safe
integration = ClaudeCodeIntegration()

async def task1():
    return await integration.execute("task 1")

async def task2(): 
    return await integration.execute("task 2")

# Both tasks can run concurrently safely
results = await asyncio.gather(task1(), task2())

Troubleshooting

My plugin doesn't appear in –external-agent

The plugin failed to import, so it was dropped from the catalog and never offered as a choice. The reason is logged as Skipping external agent '<name>': failed to load (<error>) on logger praisonai.integrations.registry.

See the exact error:

python
import logging
logging.basicConfig(level=logging.WARNING)

from praisonai.integrations.registry import list_external_agents
list_external_agents()

Fix the import error the log reports (a missing dependency, a bad entry-point path, a syntax error in the plugin), then re-run — the name reappears once the class loads.

–external-agent says the wrapper is too old

You see:

your installed 'praisonai' wrapper is too old for --external-agent (missing external_agent_catalog); upgrade with: pip install -U praisonai

This means your praisonai-code is newer than your praisonai wrapper — a mixed-version install. Upgrade the wrapper:

bash
pip install -U praisonai

Using from PraisonAI UI


Related