Skip to content

Feature: External agents (claude/gemini/codex/cursor) as subagents across all praisonai ui entry points #1418

@MervinPraison

Description

@MervinPraison

Feature: External agents (claude/gemini/codex/cursor) as subagents across all praisonai ui entry points

@claude please implement.

Supersedes #1416 (closed). Companion to #1415 (CLI integration bugs) and #1417 (manager→subagent CLI). This issue covers the UI layer only — no overlap.

Overview

PraisonAI has four UI entry points (praisonai ui, ui chat, ui code, ui agents). Three of them have zero wiring to the external-agent integrations that live in praisonai/integrations/*; the fourth (ui code) has a duplicate subprocess reimplementation of Claude Code that bypasses ClaudeCodeIntegration. This issue consolidates the gap analysis and proposes a single DRY fix.

Background

praisonai/integrations/ already provides working, production-grade wrappers:

Integration File Availability check Tool wrapper
Claude Code src/praisonai/praisonai/integrations/claude_code.py .is_available .as_tool()
Gemini CLI src/praisonai/praisonai/integrations/gemini_cli.py
Codex CLI src/praisonai/praisonai/integrations/codex_cli.py
Cursor CLI src/praisonai/praisonai/integrations/cursor_cli.py

And a CLI-level flag handler at src/praisonai/praisonai/cli/features/external_agents.py:134-179 (ExternalAgentsHandler.apply_to_agent_config) that already knows how to inject an integration's .as_tool() into an agent's tools list. Reuse this — don't duplicate.

Architecture Analysis — Current Implementation

Entry-point matrix

Entry point File Has Agent? Has external agents? Notes
praisonai ui (new clean chat) src/praisonai/praisonai/ui_chat/default_app.py:1-77 ❌ Calls openai.AsyncOpenAI directly ❌ None Bypasses PraisonAI Agent entirely
praisonai ui chat src/praisonai/praisonai/ui/chat.py:403-448 Agent(name="PraisonAI Assistant", …) ❌ None No external-agent switches in ChatSettings
praisonai ui code src/praisonai/praisonai/ui/code.py:417-473 Agent(name="PraisonAI Code Assistant", …) ⚠️ Claude only — and duplicated via subprocess See DRY violation below
praisonai ui agents src/praisonai/praisonai/ui/agents.py ✅ AgentTeam flow ❌ None No switches
praisonai ui realtime src/praisonai/praisonai/ui/realtime.py ✅ Realtime voice ❌ None Out of scope (voice-only)
praisonai ui gradio (gradio) ❌ None Out of scope unless trivial

The DRY violation in ui/code.py

# src/praisonai/praisonai/ui/code.py:249-290   — reimplements Claude Code via subprocess
async def claude_code_tool(query: str) -> str:
    subprocess = _get_subprocess()
    ...
    claude_cmd = ["claude", "--dangerously-skip-permissions", "-p", query]
    ...
    result = subprocess.run(claude_cmd, cwd=repo_path, ...)

This is a direct duplicate of ClaudeCodeIntegration.execute() at src/praisonai/praisonai/integrations/claude_code.py. Replacing it with ClaudeCodeIntegration(...).as_tool() removes ~50 lines AND automatically picks up any fixes from issue #1415 (stderr surfacing, env-var model config, etc.).

Grep evidence

$ grep -rn "external_agent\|ExternalAgent\|GeminiCLI\|CodexCLI\|CursorCLI" \
    src/praisonai/praisonai/ui/ \
    src/praisonai/praisonai/ui_chat/ --include="*.py" | grep -v __pycache__ | wc -l
0

Zero references outside the integrations module.

Gap Analysis

Critical Gaps

# Gap File Impact Severity
G1 ui_chat/default_app.py bypasses PraisonAI Agent — no tools, no memory, no integrations possible ui_chat/default_app.py:42-71 New default UI can never use external agents 🔴 Critical
G2 ui/chat.py Agent has no external-agent tools and no switch in ChatSettings ui/chat.py:465-478 Users cannot delegate to Claude/Gemini/Codex/Cursor from chat UI 🔴 Critical
G3 ui/code.py has Claude only, missing Gemini/Codex/Cursor ui/code.py:494-512 Partial solution; 3 of 4 integrations unreachable 🟠 High
G4 ui/code.py reimplements Claude via subprocess — duplicates ClaudeCodeIntegration ui/code.py:250-290 Bugs fixed in #1415 will not propagate; ~50 LOC duplication 🟠 High
G5 ui/agents.py (AgentTeam flow) has no external-agent switches ui/agents.py Multi-agent team cannot include external CLIs as specialized subagents 🟡 Medium
G6 No shared UI helper for "installed external agents + switches" — each UI file must duplicate all UI files Future drift; DRY violation waiting to happen 🟡 Medium
G7 Settings persistence: claude_code_enabled is stored in session DB (ui/code.py:483) but other agents will need the same pattern database_config.py Each new agent needs schema change unless generalized 🟢 Low

Feature Gaps (per entry point)

Feature ui ui chat ui code ui agents
Agent wiring
Claude switch ⚠️ (subprocess dup)
Gemini switch
Codex switch
Cursor switch
Availability gate (hide if not installed)
@mention routing (@claude, @gemini, etc.)

Proposed Implementation — DRY + Minimal

Phase 1 — Shared helper (new file, ~80 LOC)

New file: src/praisonai/praisonai/ui/_external_agents.py

"""Shared helpers for wiring external agents into any UI entry point.

Single source of truth for:
- Listing installed external agents (lazy, cached)
- Rendering Chainlit Switch widgets / aiui settings entries
- Building the tools list from enabled agents
"""

from functools import lru_cache
from typing import Any, Dict, List

# Map of UI toggle id → (integration class path, pretty label)
EXTERNAL_AGENTS: Dict[str, Dict[str, str]] = {
    "claude_enabled": {"module": "claude_code", "cls": "ClaudeCodeIntegration",
                       "label": "Claude Code (coding, file edits)", "cli": "claude"},
    "gemini_enabled": {"module": "gemini_cli", "cls": "GeminiCLIIntegration",
                       "label": "Gemini CLI (analysis, search)", "cli": "gemini"},
    "codex_enabled":  {"module": "codex_cli",  "cls": "CodexCLIIntegration",
                       "label": "Codex CLI (refactoring)",       "cli": "codex"},
    "cursor_enabled": {"module": "cursor_cli", "cls": "CursorCLIIntegration",
                       "label": "Cursor CLI (IDE tasks)",        "cli": "cursor-agent"},
}


@lru_cache(maxsize=1)
def installed_external_agents() -> List[str]:
    """Return toggle ids of external agents whose CLI is on PATH."""
    import shutil
    return [toggle_id for toggle_id, meta in EXTERNAL_AGENTS.items()
            if shutil.which(meta["cli"])]


def external_agent_tools(settings: Dict[str, Any], workspace: str = ".") -> list:
    """Build tools list from settings dict of toggle_id → bool."""
    import importlib
    tools = []
    for toggle_id, enabled in settings.items():
        if not enabled or toggle_id not in EXTERNAL_AGENTS:
            continue
        meta = EXTERNAL_AGENTS[toggle_id]
        mod = importlib.import_module(f"praisonai.integrations.{meta['module']}")
        integration = getattr(mod, meta["cls"])(workspace=workspace)
        if integration.is_available:
            tools.append(integration.as_tool())
    return tools


def chainlit_switches(current_settings: Dict[str, bool]):
    """Return Chainlit Switch widgets for installed external agents only."""
    from chainlit.input_widget import Switch
    return [
        Switch(id=toggle_id, label=EXTERNAL_AGENTS[toggle_id]["label"],
               initial=current_settings.get(toggle_id, False))
        for toggle_id in installed_external_agents()
    ]

Phase 2 — Wire into each UI entry point

ui/chat.py

# at top: from praisonai.ui._external_agents import chainlit_switches, external_agent_tools

# in @cl.on_chat_start (around line 465):
settings = cl.ChatSettings([
    TextInput(id="model_name", ...),
    Switch(id="tools_enabled", ...),
    *chainlit_switches(load_external_agent_settings()),   # ← NEW
])

# in _get_or_create_agent (around line 420):
tools = _get_interactive_tools() + external_agent_tools(
    session_settings, workspace=os.environ.get("PRAISONAI_WORKSPACE", ".")
)

ui/code.py

# REMOVE duplicate subprocess function claude_code_tool (lines 249-290)
# REPLACE with shared helper:
from praisonai.ui._external_agents import chainlit_switches, external_agent_tools

# in @cl.on_chat_start (around line 502-511):
settings = cl.ChatSettings([
    TextInput(...),
    Switch(id="tools_enabled", ...),
    *chainlit_switches(load_external_agent_settings()),   # replaces single claude switch
])

# in _get_or_create_agent: use external_agent_tools() instead of manual claude_code_tool append

ui_chat/default_app.py (clean chat)

# REPLACE direct openai.AsyncOpenAI call with PraisonAI Agent + external tools
from praisonaiagents import Agent
from praisonai.ui._external_agents import external_agent_tools, installed_external_agents

_agent = None

@aiui.settings
async def on_settings(new_settings):
    global _agent
    _agent = None  # invalidate cache on change

def _get_agent(settings):
    global _agent
    if _agent is None:
        _agent = Agent(
            name="PraisonAI",
            instructions="You are a helpful assistant. Delegate coding/analysis tasks to external subagents when available.",
            llm=os.getenv("MODEL_NAME", "gpt-4o-mini"),
            tools=external_agent_tools(settings),
        )
    return _agent

@aiui.reply
async def on_message(message: str, settings: dict = None):
    agent = _get_agent(settings or {})
    result = await agent.achat(str(message))
    for chunk in str(result).split(" "):
        await aiui.stream_token(chunk + " ")

ui/agents.py

Add the same chainlit_switches to the ChatSettings and pass external_agent_tools(...) into each Agent constructor in the AgentTeam.

Phase 3 — Optional @mention routing (nice-to-have)

In each UI's message handler, pre-process the message: if starts with @claude, @gemini, @codex, or @cursor, strip the mention and temporarily override the Agent's instructions to "Always call {cli}_tool". Default behavior (no mention) delegates automatically based on instructions.

Files to Create / Modify

New files

File Purpose
src/praisonai/praisonai/ui/_external_agents.py Shared DRY helper (~80 LOC)
src/praisonai/tests/integration/test_ui_external_agents.py Tests (see below)

Modified files

File Change Approx LOC
src/praisonai/praisonai/ui_chat/default_app.py Replace direct OpenAI call with Agent + external tools; add @aiui.settings handler ~30
src/praisonai/praisonai/ui/chat.py Inject chainlit_switches + external_agent_tools ~10
src/praisonai/praisonai/ui/code.py Delete duplicate claude_code_tool (lines 250-290); use shared helper; add 3 new switches ~-50 / +15
src/praisonai/praisonai/ui/agents.py Add switches + inject into AgentTeam agents ~10

Net change: ≈ −20 LOC (shared helper eliminates more duplication than it adds).

Technical Considerations

  • Lazy imports: installed_external_agents() only uses shutil.which; external_agent_tools() imports the integration module lazily per toggle. No module-level heavy imports.
  • Performance: @lru_cache(maxsize=1) on availability list → constant-time after first call. UI cold-start impact: <5 ms.
  • Hide unavailable: chainlit_switches() returns widgets ONLY for installed CLIs → no misleading toggles.
  • Bug fix propagation: By routing through ClaudeCodeIntegration instead of duplicate subprocess, ui/code.py automatically inherits Bug: --external-agent silently returns empty for gemini/codex/cursor (4 root causes) #1415 fixes (stderr surfacing, env-var models, --skip-git-repo-check, etc.).
  • Multi-agent safety: Each Chainlit session gets its own Agent instance (already done); external integrations are stateless.
  • Settings persistence: Reuse existing save_setting / load_setting helpers in ui/chat.py:save_setting_with_retry and ui/code.py; add one row per toggle id. No schema migration needed.
  • Backward compat: claude_code_enabled legacy key must keep working — alias to claude_enabled in the helper.

Acceptance Criteria

  • praisonai ui shows installed external agents as toggles in settings (auto-hide uninstalled)
  • praisonai ui chat shows installed external agents as Chainlit switches
  • praisonai ui code switches now offer Claude + Gemini + Codex + Cursor (when installed)
  • praisonai ui agents AgentTeam members can opt into external agents via switches
  • Duplicate claude_code_tool in ui/code.py:249-290 removed — replaced by ClaudeCodeIntegration.as_tool()
  • Toggling an external agent on, sending a message, and verifying via logs that {cli}_tool was invoked
  • praisonai ui cold-start does not regress (±500 ms of baseline)
  • claude_code_enabled legacy setting key still works (backward compat)
  • Tests in tests/integration/test_ui_external_agents.py cover: availability gating, settings → tools mapping, backward-compat key

Verification Commands

# Unit + smoke
cd /Users/praison/praisonai-package
pytest src/praisonai/tests/integration/test_ui_external_agents.py -v

# Start each UI and verify switches
praisonai ui &             # check browser for subagent toggles
praisonai ui chat &
praisonai ui code &
praisonai ui agents &

# Real agentic — manager delegates to external tool
python -c "
from praisonaiagents import Agent
from praisonai.ui._external_agents import external_agent_tools
agent = Agent(
    name='UI Manager',
    instructions='Delegate coding to claude_tool.',
    tools=external_agent_tools({'claude_enabled': True}),
    llm='gpt-4o-mini',
)
print(agent.start('Use claude_tool to say hello in 5 words'))
"

# Perf: UI import time unchanged
python -c "import time; t=time.time(); import praisonai.ui._external_agents; print(f'{(time.time()-t)*1000:.1f}ms')"

Principles (from AGENTS.md)

  • Agent-centric: UI wires external agents as Agent tools (subagents), not proxies
  • Protocol-driven core: no changes to praisonaiagents (core SDK); all changes in praisonai wrapper
  • DRY: single helper replaces 4 pending duplications + removes 1 existing duplication
  • No perf impact: lazy imports, cached availability, no module-level heavy deps
  • Async-safe / multi-agent safe: each session = own Agent; integrations stateless
  • Minimal code change: ≈ −20 net LOC; one new helper file, four small wire-ups
  • Easy for non-developers: flip a toggle in Settings → subagent is active

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclaudeAuto-trigger Claude analysisenhancementNew feature or requestperformance

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions