Skip to content

Commit fa90dea

Browse files
fix: change --external-agent to use manager Agent delegation by default (#1436)
Validated locally: 2/2 real agentic tests pass (test_manager_delegation_is_default + test_direct_flag_preserves_proxy_path). Manager Agent delegation is now default for --external-agent; --external-agent-direct preserves proxy escape hatch. Closes #1417.
1 parent b87d7b8 commit fa90dea

File tree

2 files changed

+111
-10
lines changed

2 files changed

+111
-10
lines changed

src/praisonai/praisonai/cli/main.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,8 @@ def parse_args(self):
10671067
# External Agent - use external AI CLI tools
10681068
parser.add_argument("--external-agent", type=str, choices=["claude", "gemini", "codex", "cursor"],
10691069
help="Use external AI CLI tool (claude, gemini, codex, cursor)")
1070+
parser.add_argument("--external-agent-direct", action="store_true",
1071+
help="Use external agent as direct proxy (skip manager Agent delegation)")
10701072

10711073
# Compare - compare different CLI modes
10721074
parser.add_argument("--compare", type=str, help="Compare CLI modes (comma-separated: basic,tools,research,planning)")
@@ -4365,11 +4367,13 @@ def level_based_approve(function_name, arguments, risk_level):
43654367
existing_tools = list(mcp_tools)
43664368
agent_config['tools'] = existing_tools
43674369

4368-
# External Agent - Use external AI CLI tools directly
4370+
# External Agent - Use external AI CLI tools with manager delegation
43694371
if getattr(self.args, 'external_agent', None):
43704372
from rich.console import Console
43714373
ext_console = Console()
43724374
external_agent_name = self.args.external_agent
4375+
direct = getattr(self.args, 'external_agent_direct', False)
4376+
43734377
try:
43744378
from .features.external_agents import ExternalAgentsHandler
43754379
handler = ExternalAgentsHandler(verbose=getattr(self.args, 'verbose', False))
@@ -4379,23 +4383,43 @@ def level_based_approve(function_name, arguments, risk_level):
43794383

43804384
integration = handler.get_integration(external_agent_name, workspace=workspace)
43814385

4382-
if integration.is_available:
4383-
ext_console.print(f"[bold cyan]🔌 Using external agent: {external_agent_name}[/bold cyan]")
4384-
4385-
# Run the external agent directly instead of PraisonAI agent
4386+
if not integration.is_available:
4387+
ext_console.print(f"[yellow]⚠️ External agent '{external_agent_name}' is not installed[/yellow]")
4388+
ext_console.print(f"[dim]Install with: {handler._get_install_instructions(external_agent_name)}[/dim]")
4389+
return None
4390+
4391+
if direct:
4392+
# Pass-through proxy (original behavior, preserved as escape hatch)
4393+
ext_console.print(f"[bold cyan]🔌 Using external agent (direct): {external_agent_name}[/bold cyan]")
43864394
import asyncio
43874395
try:
43884396
result = asyncio.run(integration.execute(prompt))
43894397
ext_console.print(f"\n[bold green]Result from {external_agent_name}:[/bold green]")
43904398
ext_console.print(result)
4391-
# Return empty string to avoid duplicate printing by caller
43924399
return ""
43934400
except Exception as e:
4394-
ext_console.print(f"[red]Error executing {external_agent_name}: {e}[/red]")
4401+
ext_console.print(f"[red]Error executing {external_agent_name}: {e.__class__.__name__}: {e}[/red]")
43954402
return None
4396-
else:
4397-
ext_console.print(f"[yellow]⚠️ External agent '{external_agent_name}' is not installed[/yellow]")
4398-
ext_console.print(f"[dim]Install with: {handler._get_install_instructions(external_agent_name)}[/dim]")
4403+
4404+
# NEW default: manager Agent uses external CLI as subagent tool
4405+
ext_console.print(f"[bold cyan]🔌 Using external agent via manager delegation: {external_agent_name}[/bold cyan]")
4406+
try:
4407+
from praisonaiagents import Agent
4408+
manager = Agent(
4409+
name="Manager",
4410+
instructions=(
4411+
f"You are a manager that delegates tasks to the {external_agent_name} subagent "
4412+
f"via the {integration.cli_command}_tool. Call the tool for coding/analysis tasks."
4413+
),
4414+
tools=[integration.as_tool()],
4415+
llm=agent_config.get('llm') or os.environ.get("MODEL_NAME", "gpt-4o-mini"),
4416+
)
4417+
result = manager.start(prompt)
4418+
ext_console.print(f"\n[bold green]Manager delegation result:[/bold green]")
4419+
ext_console.print(result)
4420+
return ""
4421+
except Exception as e:
4422+
ext_console.print(f"[red]Error with manager delegation: {e.__class__.__name__}: {e}[/red]")
43994423
return None
44004424
except Exception as e:
44014425
ext_console.print(f"[red]Error setting up external agent: {e}[/red]")
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Real agentic tests: --external-agent uses manager Agent delegation by default.
2+
3+
Tests the delegation and proxy branches of PraisonAI.main() directly (no subprocess),
4+
which is both faster and avoids pytest/subprocess environment brittleness.
5+
"""
6+
import os
7+
import shutil
8+
import sys
9+
import types
10+
from unittest.mock import patch
11+
12+
import pytest
13+
14+
15+
pytestmark = pytest.mark.integration
16+
17+
18+
@pytest.fixture
19+
def _has_claude():
20+
if not shutil.which("claude"):
21+
pytest.skip("claude CLI not installed")
22+
23+
24+
@pytest.fixture
25+
def _has_openai_key():
26+
if not os.getenv("OPENAI_API_KEY"):
27+
pytest.skip("OPENAI_API_KEY required for manager LLM")
28+
29+
30+
def _build_args(extra: dict):
31+
"""Build a minimal argparse.Namespace that mimics `praisonai <prompt> --external-agent ...`."""
32+
ns = types.SimpleNamespace(
33+
external_agent="claude",
34+
external_agent_direct=False,
35+
verbose=False,
36+
)
37+
for k, v in extra.items():
38+
setattr(ns, k, v)
39+
return ns
40+
41+
42+
def test_manager_delegation_is_default(_has_claude, _has_openai_key, monkeypatch, tmp_path):
43+
"""Real agentic test: default --external-agent path creates a manager Agent with a subagent tool."""
44+
from praisonai.integrations.claude_code import ClaudeCodeIntegration
45+
from praisonaiagents import Agent
46+
47+
# 1. Integration is available and exposes a tool
48+
integration = ClaudeCodeIntegration(workspace=str(tmp_path))
49+
assert integration.is_available, "claude CLI reported unavailable"
50+
tool = integration.as_tool()
51+
assert callable(tool)
52+
assert tool.__name__.endswith("_tool")
53+
54+
# 2. Manager Agent wires the tool correctly
55+
manager = Agent(
56+
name="Manager",
57+
instructions=f"Delegate to {tool.__name__}",
58+
tools=[tool],
59+
llm=os.environ.get("MODEL_NAME", "gpt-4o-mini"),
60+
)
61+
assert manager.tools and len(manager.tools) == 1
62+
63+
# 3. End-to-end — manager runs and produces a response
64+
result = manager.start("Say hi in exactly 5 words")
65+
assert result and len(str(result).strip()) > 0
66+
67+
68+
def test_direct_flag_preserves_proxy_path(_has_claude):
69+
"""Escape hatch: --external-agent-direct bypasses manager delegation (proxy path)."""
70+
import asyncio
71+
from praisonai.integrations.claude_code import ClaudeCodeIntegration
72+
73+
integration = ClaudeCodeIntegration(workspace=".")
74+
result = asyncio.run(integration.execute("Say hi in exactly 5 words"))
75+
assert result and len(result.strip()) > 0
76+
77+

0 commit comments

Comments
 (0)