Files
deepagent/deepagents_sourcecode/libs/deepagents-cli/deepagents_cli/main.py
2026-01-11 12:05:37 +09:00

340 lines
12 KiB
Python

"""Main entry point and CLI loop for deepagents."""
# ruff: noqa: T201
import argparse
import asyncio
import contextlib
import os
import sys
from pathlib import Path
# Now safe to import agent (which imports LangChain modules)
from deepagents_cli.agent import create_cli_agent, list_agents, reset_agent
# CRITICAL: Import config FIRST to set LANGSMITH_PROJECT before LangChain loads
from deepagents_cli.config import (
console,
create_model,
settings,
)
from deepagents_cli.integrations.sandbox_factory import create_sandbox
from deepagents_cli.sessions import (
delete_thread_command,
generate_thread_id,
get_checkpointer,
get_most_recent,
get_thread_agent,
list_threads_command,
thread_exists,
)
from deepagents_cli.skills import execute_skills_command, setup_skills_parser
from deepagents_cli.tools import fetch_url, http_request, web_search
from deepagents_cli.ui import show_help
def check_cli_dependencies() -> None:
"""Check if CLI optional dependencies are installed."""
missing = []
try:
import requests # noqa: F401
except ImportError:
missing.append("requests")
try:
import dotenv # noqa: F401
except ImportError:
missing.append("python-dotenv")
try:
import tavily # noqa: F401
except ImportError:
missing.append("tavily-python")
try:
import textual # noqa: F401
except ImportError:
missing.append("textual")
if missing:
print("\n❌ Missing required CLI dependencies!")
print("\nThe following packages are required to use the deepagents CLI:")
for pkg in missing:
print(f" - {pkg}")
print("\nPlease install them with:")
print(" pip install deepagents[cli]")
print("\nOr install all dependencies:")
print(" pip install 'deepagents[cli]'")
sys.exit(1)
def parse_args() -> argparse.Namespace:
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="DeepAgents - AI Coding Assistant",
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=False,
)
subparsers = parser.add_subparsers(dest="command", help="Command to run")
# List command
subparsers.add_parser("list", help="List all available agents")
# Help command
subparsers.add_parser("help", help="Show help information")
# Reset command
reset_parser = subparsers.add_parser("reset", help="Reset an agent")
reset_parser.add_argument("--agent", required=True, help="Name of agent to reset")
reset_parser.add_argument(
"--target", dest="source_agent", help="Copy prompt from another agent"
)
# Skills command - setup delegated to skills module
setup_skills_parser(subparsers)
# Threads command
threads_parser = subparsers.add_parser("threads", help="Manage conversation threads")
threads_sub = threads_parser.add_subparsers(dest="threads_command")
# threads list
threads_list = threads_sub.add_parser("list", help="List threads")
threads_list.add_argument(
"--agent", default=None, help="Filter by agent name (default: show all)"
)
threads_list.add_argument("--limit", type=int, default=20, help="Max threads (default: 20)")
# threads delete
threads_delete = threads_sub.add_parser("delete", help="Delete a thread")
threads_delete.add_argument("thread_id", help="Thread ID to delete")
# Default interactive mode
parser.add_argument(
"--agent",
default="agent",
help="Agent identifier for separate memory stores (default: agent).",
)
# Thread resume argument - matches PR #638: -r for most recent, -r <ID> for specific
parser.add_argument(
"-r",
"--resume",
dest="resume_thread",
nargs="?",
const="__MOST_RECENT__",
default=None,
help="Resume thread: -r for most recent, -r <ID> for specific thread",
)
parser.add_argument(
"--model",
help="Model to use (e.g., claude-sonnet-4-5-20250929, gpt-5-mini). "
"Provider is auto-detected from model name.",
)
parser.add_argument(
"--auto-approve",
action="store_true",
help="Auto-approve tool usage without prompting (disables human-in-the-loop)",
)
parser.add_argument(
"--sandbox",
choices=["none", "modal", "daytona", "runloop"],
default="none",
help="Remote sandbox for code execution (default: none - local only)",
)
parser.add_argument(
"--sandbox-id",
help="Existing sandbox ID to reuse (skips creation and cleanup)",
)
parser.add_argument(
"--sandbox-setup",
help="Path to setup script to run in sandbox after creation",
)
return parser.parse_args()
async def run_textual_cli_async(
assistant_id: str,
*,
auto_approve: bool = False,
sandbox_type: str = "none",
sandbox_id: str | None = None,
model_name: str | None = None,
thread_id: str | None = None,
is_resumed: bool = False,
) -> None:
"""Run the Textual CLI interface (async version).
Args:
assistant_id: Agent identifier for memory storage
auto_approve: Whether to auto-approve tool usage
sandbox_type: Type of sandbox ("none", "modal", "runloop", "daytona")
sandbox_id: Optional existing sandbox ID to reuse
model_name: Optional model name to use
thread_id: Thread ID to use (new or resumed)
is_resumed: Whether this is a resumed session
"""
from deepagents_cli.app import run_textual_app
model = create_model(model_name)
# Show thread info
if is_resumed:
console.print(f"[green]Resuming thread:[/green] {thread_id}")
else:
console.print(f"[dim]Thread: {thread_id}[/dim]")
# Use async context manager for checkpointer
async with get_checkpointer() as checkpointer:
# Create agent with conditional tools
tools = [http_request, fetch_url]
if settings.has_tavily:
tools.append(web_search)
# Handle sandbox mode
sandbox_backend = None
sandbox_cm = None
if sandbox_type != "none":
try:
# Create sandbox context manager but keep it open
sandbox_cm = create_sandbox(sandbox_type, sandbox_id=sandbox_id)
sandbox_backend = sandbox_cm.__enter__()
except (ImportError, ValueError, RuntimeError, NotImplementedError) as e:
console.print()
console.print("[red]❌ Sandbox creation failed[/red]")
console.print(f"[dim]{e}[/dim]")
sys.exit(1)
try:
agent, composite_backend = create_cli_agent(
model=model,
assistant_id=assistant_id,
tools=tools,
sandbox=sandbox_backend,
sandbox_type=sandbox_type if sandbox_type != "none" else None,
auto_approve=auto_approve,
checkpointer=checkpointer,
)
# Run Textual app
await run_textual_app(
agent=agent,
assistant_id=assistant_id,
backend=composite_backend,
auto_approve=auto_approve,
cwd=Path.cwd(),
thread_id=thread_id,
)
except Exception as e:
console.print(f"[red]❌ Failed to create agent: {e}[/red]")
sys.exit(1)
finally:
# Clean up sandbox if we created one
if sandbox_cm is not None:
with contextlib.suppress(Exception):
sandbox_cm.__exit__(None, None, None)
def cli_main() -> None:
"""Entry point for console script."""
# Fix for gRPC fork issue on macOS
# https://github.com/grpc/grpc/issues/37642
if sys.platform == "darwin":
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "0"
# Note: LANGSMITH_PROJECT is already overridden in config.py (before LangChain imports)
# This ensures agent traces → DEEPAGENTS_LANGSMITH_PROJECT
# Shell commands → user's original LANGSMITH_PROJECT (via ShellMiddleware env)
# Check dependencies first
check_cli_dependencies()
try:
args = parse_args()
if args.command == "help":
show_help()
elif args.command == "list":
list_agents()
elif args.command == "reset":
reset_agent(args.agent, args.source_agent)
elif args.command == "skills":
execute_skills_command(args)
elif args.command == "threads":
if args.threads_command == "list":
asyncio.run(
list_threads_command(
agent_name=getattr(args, "agent", None),
limit=getattr(args, "limit", 20),
)
)
elif args.threads_command == "delete":
asyncio.run(delete_thread_command(args.thread_id))
else:
console.print("[yellow]Usage: deepagents threads <list|delete>[/yellow]")
else:
# Interactive mode - handle thread resume
thread_id = None
is_resumed = False
if args.resume_thread == "__MOST_RECENT__":
# -r (no ID): Get most recent thread
# If --agent specified, filter by that agent; otherwise get most recent overall
agent_filter = args.agent if args.agent != "agent" else None
thread_id = asyncio.run(get_most_recent(agent_filter))
if thread_id:
is_resumed = True
agent_name = asyncio.run(get_thread_agent(thread_id))
if agent_name:
args.agent = agent_name
else:
msg = (
f"No previous thread for '{args.agent}'"
if agent_filter
else "No previous threads"
)
console.print(f"[yellow]{msg}, starting new.[/yellow]")
elif args.resume_thread:
# -r <ID>: Resume specific thread
if asyncio.run(thread_exists(args.resume_thread)):
thread_id = args.resume_thread
is_resumed = True
if args.agent == "agent":
agent_name = asyncio.run(get_thread_agent(thread_id))
if agent_name:
args.agent = agent_name
else:
console.print(f"[red]Thread '{args.resume_thread}' not found.[/red]")
console.print(
"[dim]Use 'deepagents threads list' to see available threads.[/dim]"
)
sys.exit(1)
# Generate new thread ID if not resuming
if thread_id is None:
thread_id = generate_thread_id()
# Run Textual CLI
asyncio.run(
run_textual_cli_async(
assistant_id=args.agent,
auto_approve=args.auto_approve,
sandbox_type=args.sandbox,
sandbox_id=args.sandbox_id,
model_name=getattr(args, "model", None),
thread_id=thread_id,
is_resumed=is_resumed,
)
)
except KeyboardInterrupt:
# Clean exit on Ctrl+C - suppress ugly traceback
console.print("\n\n[yellow]Interrupted[/yellow]")
sys.exit(0)
if __name__ == "__main__":
cli_main()