340 lines
12 KiB
Python
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()
|