"""deepagents CLI의 메인 엔트리포인트 및 루프입니다. 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 for specific parser.add_argument( "-r", "--resume", dest="resume_thread", nargs="?", const="__MOST_RECENT__", default=None, help="Resume thread: -r for most recent, -r 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 err: # noqa: BLE001 console.print(f"[red]❌ Failed to create agent: {err}[/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: # noqa: PLR0912, PLR0915 """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 [/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 : 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()