From d5263166872b40df8a02333a3ebf1af03f6f1653 Mon Sep 17 00:00:00 2001 From: sawradip Date: Fri, 7 Nov 2025 18:31:36 +0600 Subject: [PATCH 1/3] feat: removed capacity tracking of local agents --- runagent/cli/commands/db.py | 82 +---- runagent/cli/commands/delete.py | 4 - runagent/cli/commands/serve.py | 13 - runagent/sdk/db.py | 509 +------------------------------- runagent/utils/schema.py | 10 - 5 files changed, 6 insertions(+), 612 deletions(-) diff --git a/runagent/cli/commands/db.py b/runagent/cli/commands/db.py index 8f4d694..99d2359 100644 --- a/runagent/cli/commands/db.py +++ b/runagent/cli/commands/db.py @@ -52,77 +52,11 @@ def db(): @db.command() @click.option("--cleanup-days", type=int, help="Clean up records older than N days") @click.option("--agent-id", help="Show detailed info for specific agent") -@click.option("--capacity", is_flag=True, help="Show detailed capacity information") -def status(cleanup_days, agent_id, capacity): +def status(cleanup_days, agent_id): """Show local database status and statistics (ENHANCED with invocation stats)""" try: sdk = RunAgent() - if capacity: - # Show detailed capacity info - capacity_info = sdk.db_service.get_database_capacity_info() - - console.print(f"\n[bold]Database Capacity Information[/bold]") - console.print( - f"Current: [cyan]{capacity_info.get('current_count', 0)}/5[/cyan] agents" - ) - console.print( - f"Remaining slots: [green]{capacity_info.get('remaining_slots', 0)}[/green]" - ) - - status = "[red]FULL[/red]" if capacity_info.get("is_full") else "[green]Available[/green]" - console.print(f"Status: {status}") - - agents = capacity_info.get("agents", []) - if agents: - console.print(f"\n[bold]Deployed Agents (by age):[/bold]") - - # Create table for agents - table = Table(title="Agents by Deployment Age") - table.add_column("#", style="dim", width=3) - table.add_column("Status", width=8) - table.add_column("Agent ID", style="magenta", width=36) - table.add_column("Framework", style="green", width=12) - table.add_column("Deployed At", style="cyan", width=20) - table.add_column("Age Note", style="yellow", width=10) - - for i, agent in enumerate(agents): - status_text = ( - "[green]deployed[/green]" - if agent["status"] == "deployed" - else "[red]error[/red]" if agent["status"] == "error" else "[yellow]other[/yellow]" - ) - age_label = ( - "oldest" - if i == 0 - else "newest" if i == len(agents) - 1 else "" - ) - - table.add_row( - str(i+1), - status_text, - agent['agent_id'], - agent['framework'], - agent['deployed_at'] or "Unknown", - age_label - ) - - console.print(table) - - if capacity_info.get("is_full"): - oldest = capacity_info.get("oldest_agent", {}) - console.print( - f"\n[yellow]To deploy new agent, replace oldest:[/yellow]" - ) - console.print( - f" [cyan]runagent serve --folder --replace {oldest.get('agent_id', '')}[/cyan]" - ) - console.print( - f" [cyan]runagent delete --id {oldest.get('agent_id', '')}[/cyan]" - ) - - return - if agent_id: # Show agent-specific details including invocations result = sdk.get_agent_info(agent_id, local=True) @@ -143,19 +77,11 @@ def status(cleanup_days, agent_id, capacity): # Show general database stats stats = sdk.db_service.get_database_stats() - capacity_info = sdk.db_service.get_database_capacity_info() console.print("\n[bold]Local Database Status[/bold]") - current_count = capacity_info.get("current_count", 0) - is_full = capacity_info.get("is_full", False) - status = "FULL" if is_full else "OK" - console.print( - f"Agent Capacity: [cyan]{current_count}/5[/cyan] agents ([red]{status}[/red])" - if is_full - else f"Agent Capacity: [cyan]{current_count}/5[/cyan] agents ([green]{status}[/green])" - ) - + total_agents = stats.get("total_agents", 0) + console.print(f"Total Agents: [cyan]{total_agents}[/cyan]") console.print(f"Total Agent Runs: [cyan]{stats.get('total_runs', 0)}[/cyan]") console.print( f"Database Size: [yellow]{stats.get('database_size_mb', 0)} MB[/yellow]" @@ -235,7 +161,7 @@ def status(cleanup_days, agent_id, capacity): console.print(f" • [cyan]runagent db invocation [/cyan] - Show specific invocation") console.print(f" • [cyan]runagent db cleanup[/cyan] - Clean up old records") console.print(f" • [cyan]runagent db status --agent-id [/cyan] - Agent-specific info") - console.print(f" • [cyan]runagent db status --capacity[/cyan] - Capacity management info") + console.print(f" • [cyan]runagent db status --agent-id [/cyan] - Agent-specific info") # Cleanup if requested (keep existing logic) if cleanup_days: diff --git a/runagent/cli/commands/delete.py b/runagent/cli/commands/delete.py index 3590479..82a4033 100644 --- a/runagent/cli/commands/delete.py +++ b/runagent/cli/commands/delete.py @@ -104,10 +104,6 @@ def delete(agent_id, yes): if result["success"]: console.print(f"\n✅ [green]Agent {agent_id} deleted successfully![/green]") - - # Show updated capacity - capacity_info = sdk.db_service.get_database_capacity_info() - console.print(f"Updated capacity: [cyan]{capacity_info.get('current_count', 0)}/5[/cyan] agents") else: console.print(f"❌ [red]Failed to delete agent:[/red] {format_error_message(result.get('error'))}") import sys diff --git a/runagent/cli/commands/serve.py b/runagent/cli/commands/serve.py index fbb76b3..a0bd9ee 100644 --- a/runagent/cli/commands/serve.py +++ b/runagent/cli/commands/serve.py @@ -11,7 +11,6 @@ from rich.console import Console from rich.table import Table -from runagent import RunAgent from runagent.sdk.exceptions import ( # RunAgentError,; ConnectionError AuthenticationError, TemplateError, @@ -79,18 +78,6 @@ def serve(port, host, debug, reload, no_animation, animation_style, path): if not no_animation: show_simple_serve_progress("Initializing server") - sdk = RunAgent() - - # Check capacity - capacity_info = sdk.db_service.get_database_capacity_info() - if capacity_info["is_full"]: - console.print("❌ [red]Database is full![/red]") - oldest_agent = capacity_info.get("oldest_agent", {}) - if oldest_agent: - console.print(f"[yellow]Suggested command:[/yellow]") - console.print(f" Delete: [cyan]runagent delete --id {oldest_agent.get('agent_id', '')}[/cyan]") - raise click.ClickException("Database at capacity. Use 'runagent delete' to free space.") - console.print("[bold]Starting local server with auto port allocation...[/bold]") # Show progress while creating server diff --git a/runagent/sdk/db.py b/runagent/sdk/db.py index fa41fe3..f8e5e6f 100644 --- a/runagent/sdk/db.py +++ b/runagent/sdk/db.py @@ -292,181 +292,6 @@ def __init__(self, db_path: Path = None, rest_client=None): self.db_manager = DBManager(db_path) self.rest_client = rest_client - # System resource allocation limits based on deployment tier - self._deployment_config = self._load_system_constraints() - - # Cache for limits to avoid repeated API calls (5 minutes cache) - self._limits_cache = None - self._cache_expiry = None - - def _load_system_constraints(self) -> Dict[str, Any]: - """Load system deployment constraints and resource limits""" - # Obfuscated calculation to make default limit less obvious - tier_multiplier = 0x1 # Base tier - resource_factor = int("101", 2) # Convert binary string to int (5) - - # Calculate deployment limits based on system architecture - base_limit = resource_factor # This gives us 5 - deployment_ceiling = base_limit # Keep it as 5 - - # Apply enterprise scaling factor (no-op in this case) - enterprise_modifier = lambda x: x - final_limit = enterprise_modifier(deployment_ceiling) - - return { - "max_concurrent_instances": final_limit, - "resource_tier": "standard", - "allocation_strategy": "fixed_pool", - } - - def _get_default_limit(self) -> int: - """Get the default agent limit (obfuscated calculation)""" - return self._deployment_config["max_concurrent_instances"] - - def _should_check_api_before_adding(self, current_count: int) -> bool: - """Determine if we should check API before adding an agent""" - default_limit = self._get_default_limit() - - # Only check API if we're at or exceeding default limits - return current_count >= default_limit - - def _check_enhanced_limits_with_fallback(self) -> Dict: - """ - Check enhanced limits via RestClient API with fallback to default limits - - Returns: - Dictionary with limit information - """ - # Check cache first (5 minutes cache) - if ( - self._limits_cache - and self._cache_expiry - and datetime.now() < self._cache_expiry - ): - return self._limits_cache - - default_limit = self._get_default_limit() - - # If no RestClient available, use default limits - if not self.rest_client: - result = { - "limit": default_limit, - "enhanced": False, - "source": "default", - "api_available": False, - "api_validated": False, - "error": "No RestClient configured", - } - return result - - try: - console.print("🔍 [dim]Checking enhanced limits via API...[/dim]") - - # Use RestClient to get limits - limits_data = self.rest_client.get_local_db_limits() - - if limits_data.get("success"): - max_agents = limits_data.get("max_agents", default_limit) - enhanced = limits_data.get("enhanced_limits", False) - - # Handle unlimited case - if max_agents == -1: - max_agents = 999 # Practical unlimited - enhanced = True - - result = { - "limit": max_agents, - "enhanced": enhanced, - "source": "api" if enhanced else "default", - "api_available": True, - "api_validated": limits_data.get("api_validated", False), - "tier_info": limits_data.get("tier_info", {}), - "features": limits_data.get("features", []), - "expires_at": limits_data.get("expires_at"), - "unlimited": max_agents == 999, - } - - # Cache for 5 minutes - self._limits_cache = result - self._cache_expiry = datetime.now() + timedelta(minutes=5) - - if enhanced: - console.print( - f"[green]Enhanced limits active: {max_agents} agents[/green]" - ) - else: - console.print( - f"[yellow]Using default limits: {max_agents} agents[/yellow]" - ) - - return result - - else: - # API call failed, use default limits - error_msg = limits_data.get("error", "Unknown API error") - result = { - "limit": default_limit, - "enhanced": False, - "source": "default", - "api_available": True, - "api_validated": limits_data.get("api_validated", False), - "error": error_msg, - } - - console.print( - f"[yellow]⚠️ API limit check failed: {error_msg} - using default limits[/yellow]" - ) - return result - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - # Exception during API call, fallback to default - result = { - "limit": default_limit, - "enhanced": False, - "source": "default", - "api_available": False, - "api_validated": False, - "error": f"API check exception: {str(e)}", - } - - console.print( - f"[red]❌ Error checking enhanced limits: {str(e)} - using default limits[/red]" - ) - return result - - def clear_limits_cache(self): - """Clear the cached enhanced limits to force refresh""" - self._limits_cache = None - self._cache_expiry = None - if self.rest_client: - self.rest_client.clear_limits_cache() - console.print("🔄 [dim]Enhanced limits cache cleared[/dim]") - - def get_current_tier_info(self) -> Dict: - """Get information about current deployment tier and limits""" - default_limit = self._get_default_limit() - - # Get current limit info (may trigger API call) - limit_info = self._check_enhanced_limits_with_fallback() - - return { - "default_limit": default_limit, - "current_limit": limit_info["limit"], - "enhanced_via_api": limit_info.get("enhanced", False), - "limit_source": limit_info.get("source", "default"), - "api_available": limit_info.get("api_available", False), - "api_validated": limit_info.get("api_validated", False), - "tier_info": limit_info.get("tier_info", {}), - "unlimited": limit_info.get("unlimited", False), - "api_error": limit_info.get("error"), - "cache_expires": ( - self._cache_expiry.isoformat() if self._cache_expiry else None - ), - "rest_client_configured": self.rest_client is not None, - } - def start_invocation( self, agent_id: str, @@ -703,144 +528,7 @@ def cleanup_old_invocations(self, days_old: int = 30) -> int: session.rollback() console.print(f"Error cleaning up old invocations: {e}") return 0 - - - def _load_system_constraints(self) -> Dict[str, Any]: - """Load system deployment constraints and resource limits""" - # Obfuscated calculation to make default limit less obvious - tier_multiplier = 0x1 # Base tier - resource_factor = int("101", 2) # Convert binary string to int (5) - - # Calculate deployment limits based on system architecture - base_limit = resource_factor # This gives us 5 - deployment_ceiling = base_limit # Keep it as 5 - - # Apply enterprise scaling factor (no-op in this case) - enterprise_modifier = lambda x: x - final_limit = enterprise_modifier(deployment_ceiling) - - return { - "max_concurrent_instances": final_limit, - "resource_tier": "standard", - "allocation_strategy": "fixed_pool", - } - - def _get_default_limit(self) -> int: - """Get the default agent limit (obfuscated calculation)""" - return self._deployment_config["max_concurrent_instances"] - - def _should_check_api_before_adding(self, current_count: int) -> bool: - """Determine if we should check API before adding an agent""" - default_limit = self._get_default_limit() - - # Only check API if we're at or exceeding default limits - return current_count >= default_limit - - def _check_enhanced_limits_with_fallback(self) -> Dict: - """ - Check enhanced limits via RestClient API with fallback to default limits - - Returns: - Dictionary with limit information - """ - # Check cache first (5 minutes cache) - if ( - self._limits_cache - and self._cache_expiry - and datetime.now() < self._cache_expiry - ): - return self._limits_cache - - default_limit = self._get_default_limit() - - # If no RestClient available, use default limits - if not self.rest_client: - result = { - "limit": default_limit, - "enhanced": False, - "source": "default", - "api_available": False, - "api_validated": False, - "error": "No RestClient configured", - } - return result - - try: - console.print("🔍 [dim]Checking enhanced limits via API...[/dim]") - - # Use RestClient to get limits - limits_data = self.rest_client.get_local_db_limits() - - if limits_data.get("success"): - max_agents = limits_data.get("max_agents", default_limit) - enhanced = limits_data.get("enhanced_limits", False) - - # Handle unlimited case - if max_agents == -1: - max_agents = 999 # Practical unlimited - enhanced = True - - result = { - "limit": max_agents, - "enhanced": enhanced, - "source": "api" if enhanced else "default", - "api_available": True, - "api_validated": limits_data.get("api_validated", False), - "tier_info": limits_data.get("tier_info", {}), - "features": limits_data.get("features", []), - "expires_at": limits_data.get("expires_at"), - "unlimited": max_agents == 999, - } - - # Cache for 5 minutes - self._limits_cache = result - self._cache_expiry = datetime.now() + timedelta(minutes=5) - - if enhanced: - console.print( - f"[green]Enhanced limits active: {max_agents} agents[/green]" - ) - else: - console.print( - f"[yellow]Using default limits: {max_agents} agents[/yellow]" - ) - - return result - - else: - # API call failed, use default limits - error_msg = limits_data.get("error", "Unknown API error") - result = { - "limit": default_limit, - "enhanced": False, - "source": "default", - "api_available": True, - "api_validated": limits_data.get("api_validated", False), - "error": error_msg, - } - - console.print( - f"[yellow]⚠️ API limit check failed: {error_msg} - using default limits[/yellow]" - ) - return result - - except Exception as e: - if os.getenv('DISABLE_TRY_CATCH'): - raise - # Exception during API call, fallback to default - result = { - "limit": default_limit, - "enhanced": False, - "source": "default", - "api_available": False, - "api_validated": False, - "error": f"API check exception: {str(e)}", - } - - console.print( - f"[red]❌ Error checking enhanced limits: {str(e)} - using default limits[/red]" - ) - return result + def add_agent( self, @@ -884,9 +572,6 @@ def add_agent( with self.db_manager.get_session() as session: try: - # Check current agent count - current_count = session.query(Agent).count() - # Check if agent already exists existing_agent = ( session.query(Agent).filter(Agent.agent_id == agent_id).first() @@ -898,96 +583,6 @@ def add_agent( "code": "AGENT_EXISTS", } - # Smart limit checking - default_limit = self._get_default_limit() - - # Phase 1: Check if we're within default limits (no API call needed) - if current_count < default_limit: - console.print( - f"[green]Adding agent within default limits ({current_count + 1}/{default_limit})[/green]" - ) - - # Proceed without API check - new_agent = Agent( - agent_id=agent_id, - agent_path=str(agent_path), - host=host, - port=port, - framework=framework, - status=status, - remote_status="initialized", # Default remote status - agent_name=agent_name, - description=description, - template=template, - version=version, - initialized_at=initialized_at, - config_fingerprint=config_fingerprint, - project_id=project_id, - ) - - session.add(new_agent) - session.commit() - - return { - "success": True, - "message": f"Agent {agent_id} added successfully (within default limits)", - "current_count": current_count + 1, - "limit_source": "default", - "api_check_performed": False, - } - - # Phase 2: At or above default limit - check API for enhanced limits - console.print( - f"[yellow]At default limit ({current_count}/{default_limit}) - checking for enhanced limits...[/yellow]" - ) - - limit_info = self._check_enhanced_limits_with_fallback() - max_capacity = limit_info["limit"] - - # Check if we can still add within enhanced limits - if current_count >= max_capacity: - oldest_agent = ( - session.query(Agent).order_by(Agent.deployed_at).first() - ) - - # Provide helpful error message based on API availability - if not self.rest_client: - error_message = f"Maximum {max_capacity} agents allowed. Configure RestClient with API key for enhanced limits." - suggestion = "Configure RestClient with valid API key to potentially increase limits" - elif not limit_info.get("api_validated", False): - error_message = f"Maximum {max_capacity} agents allowed. API key invalid or not configured." - suggestion = "Verify API key configuration in RestClient" - else: - error_message = f"Maximum {max_capacity} agents allowed. Database is at capacity ({current_count}/{max_capacity} agents)." - suggestion = ( - f"Consider replacing the oldest agent: {oldest_agent.agent_id}" - if oldest_agent - else "Database cleanup needed" - ) - - return { - "success": False, - "error": error_message, - "code": "DATABASE_FULL", - "current_count": current_count, - "max_allowed": max_capacity, - "limit_info": limit_info, - "oldest_agent": ( - { - "agent_id": oldest_agent.agent_id, - "deployed_at": ( - oldest_agent.deployed_at.isoformat() - if oldest_agent.deployed_at - else None - ), - } - if oldest_agent - else None - ), - "suggestion": suggestion, - } - - # Enhanced limits allow the addition new_agent = Agent( agent_id=agent_id, agent_path=str(agent_path), @@ -1008,23 +603,9 @@ def add_agent( session.add(new_agent) session.commit() - limit_source = "enhanced" if limit_info.get("enhanced") else "default" - console.print( - f"🟢 Agent added with {limit_source} limits ({current_count + 1}/{max_capacity})" - ) - return { "success": True, - "message": f"Agent {agent_id} added successfully ({limit_source} limits)", - "current_count": current_count + 1, - "max_allowed": max_capacity, - "remaining_slots": ( - max_capacity - (current_count + 1) - if max_capacity != 999 - else "unlimited" - ), - "limit_info": limit_info, - "api_check_performed": True, + "message": f"Agent {agent_id} added successfully", } except Exception as e: @@ -1085,92 +666,6 @@ def delete_agent(self, agent_id: str) -> bool: console.print(f"💡 Each agent has a unique immutable ID. Create a new agent with 'runagent init' or 'runagent serve'.") return False - def get_database_capacity_info(self) -> Dict: - """Get database capacity information with smart limit checking""" - with self.db_manager.get_session() as session: - try: - current_count = session.query(Agent).count() - default_limit = self._get_default_limit() - - # Only check API if we're at or near limits - if self._should_check_api_before_adding(current_count): - limit_info = self._check_enhanced_limits_with_fallback() - max_capacity = limit_info["limit"] - enhanced_info = limit_info - else: - max_capacity = default_limit - enhanced_info = { - "limit": default_limit, - "enhanced": False, - "source": "default", - "api_available": bool(self.rest_client), - "api_validated": False, - } - - agents = session.query(Agent).order_by(Agent.deployed_at).all() - - return { - "current_count": current_count, - "max_capacity": max_capacity, - "default_limit": default_limit, - "remaining_slots": ( - max(0, max_capacity - current_count) - if max_capacity != 999 - else "unlimited" - ), - "is_full": current_count >= max_capacity, - "limit_info": enhanced_info, - "agents": [ - { - "agent_id": agent.agent_id, - "deployed_at": ( - agent.deployed_at.isoformat() - if agent.deployed_at - else None - ), - "framework": agent.framework, - "status": agent.status, - } - for agent in agents - ], - "oldest_agent": ( - { - "agent_id": agents[0].agent_id, - "deployed_at": ( - agents[0].deployed_at.isoformat() - if agents[0].deployed_at - else None - ), - } - if agents - else None - ), - "newest_agent": ( - { - "agent_id": agents[-1].agent_id, - "deployed_at": ( - agents[-1].deployed_at.isoformat() - if agents[-1].deployed_at - else None - ), - } - if agents - else None - ), - "rest_client_configured": self.rest_client is not None, - } - except Exception as e: - default_limit = self._get_default_limit() - return { - "error": f"Failed to get capacity info: {str(e)}", - "current_count": 0, - "max_capacity": default_limit, - "default_limit": default_limit, - "remaining_slots": default_limit, - "is_full": False, - "rest_client_configured": self.rest_client is not None, - } - def update_api_key(self, api_key: str) -> Dict: """ Update API key and refresh limits (deprecated - use RestClient directly) diff --git a/runagent/utils/schema.py b/runagent/utils/schema.py index a1b8679..9912787 100644 --- a/runagent/utils/schema.py +++ b/runagent/utils/schema.py @@ -198,16 +198,6 @@ class AgentRunResponseV2(BaseModel): request_id: str -class CapacityInfo(BaseModel): - """Database capacity information""" - - current_count: int - max_capacity: int - remaining_slots: int - is_full: bool - agents: t.List[t.Dict[str, t.Any]] - - class AgentInfo(BaseModel): """Agent information and endpoints""" From 1c1aeaa549d4b9e9e6622a0bef5a79a4d5223dc8 Mon Sep 17 00:00:00 2001 From: sawradip Date: Fri, 7 Nov 2025 18:31:51 +0600 Subject: [PATCH 2/3] feat: improve init message --- runagent/cli/commands/init.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/runagent/cli/commands/init.py b/runagent/cli/commands/init.py index a55b4fc..e588bb8 100644 --- a/runagent/cli/commands/init.py +++ b/runagent/cli/commands/init.py @@ -379,9 +379,13 @@ def init(path, template, blank, from_template, from_github, use_auth, name, desc # Simple next steps console.print("\n[bold]Next Steps:[/bold]") if relative_path != Path("."): - console.print(f" 1. [cyan]cd {relative_path}[/cyan]") + path_str = str(relative_path) + console.print(f" 1. [cyan]cd {path_str}[/cyan]") console.print(f" 2. Install dependencies: [cyan]pip install -r requirements.txt[/cyan]") console.print(f" 3. Serve locally: [cyan]runagent serve .[/cyan]") + console.print( + f" (or run [cyan]runagent serve {path_str}[/cyan] from the current directory)" + ) else: console.print(f" 1. Install dependencies: [cyan]pip install -r requirements.txt[/cyan]") console.print(f" 2. Serve locally: [cyan]runagent serve .[/cyan]") From b845d7e13e8cbee074bde342068cdd1e7d844c8a Mon Sep 17 00:00:00 2001 From: sawradip Date: Sat, 8 Nov 2025 01:05:47 +0600 Subject: [PATCH 3/3] feat: enhance error handling and suggestions in deployment and upload commands --- runagent/cli/commands/deploy.py | 18 +++++--- runagent/cli/commands/run.py | 11 ++++- runagent/cli/commands/upload.py | 18 +++++--- runagent/client/client.py | 77 +++++++++++++++++++++++++++++++-- runagent/sdk/rest_client.py | 64 ++++++++++++++++++--------- runagent/sdk/socket_client.py | 43 ++++++++++-------- 6 files changed, 176 insertions(+), 55 deletions(-) diff --git a/runagent/cli/commands/deploy.py b/runagent/cli/commands/deploy.py index abd92bf..8556856 100644 --- a/runagent/cli/commands/deploy.py +++ b/runagent/cli/commands/deploy.py @@ -31,13 +31,12 @@ def format_error_message(error_info): """Format error information from API responses""" if isinstance(error_info, dict) and "message" in error_info: - # New format with ErrorDetail object error_message = error_info.get("message", "Unknown error") - error_code = error_info.get("code", "UNKNOWN_ERROR") - return f"[{error_code}] {error_message}" - else: - # Fallback to old format for backward compatibility - return str(error_info) if error_info else "Unknown error" + error_code = error_info.get("code") + if error_code: + return f"[{error_code}] {error_message}" + return error_message + return str(error_info) if error_info else "Unknown error" # ============================================================================ @@ -90,7 +89,12 @@ def deploy(path: Path, overwrite: bool): console.print(f"Agent ID: [bold magenta]{result.get('agent_id')}[/bold magenta]") console.print(f"Endpoint: [link]{result.get('endpoint')}[/link]") else: - console.print(f"❌ [red]Deployment failed:[/red] {format_error_message(result.get('error'))}") + error_info = result.get("error") + console.print(f"❌ [red]Deployment failed:[/red] {format_error_message(error_info)}") + if isinstance(error_info, dict): + suggestion = error_info.get("suggestion") + if suggestion: + console.print(f"[cyan]Suggestion: {suggestion}[/cyan]") import sys sys.exit(1) diff --git a/runagent/cli/commands/run.py b/runagent/cli/commands/run.py index 215d2c4..a20a6c9 100644 --- a/runagent/cli/commands/run.py +++ b/runagent/cli/commands/run.py @@ -17,7 +17,7 @@ TemplateError, ValidationError, ) -from runagent.client.client import RunAgentClient +from runagent.client.client import RunAgentClient, RunAgentExecutionError from runagent.sdk.server.local_server import LocalServer from runagent.utils.agent import detect_framework from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner @@ -225,6 +225,15 @@ def run(ctx, agent_id, host, port, input_file, local, tag, timeout): result = ra_client.run(**input_params) console.print(result) + except RunAgentExecutionError as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + console.print(f"[bold red]❌ {e.message}[/bold red]") + if e.suggestion: + console.print(f"[cyan]Suggestion: {e.suggestion}[/cyan]") + import sys + sys.exit(1) + except Exception as e: if os.getenv('DISABLE_TRY_CATCH'): raise diff --git a/runagent/cli/commands/upload.py b/runagent/cli/commands/upload.py index 7f060a3..f8dea93 100644 --- a/runagent/cli/commands/upload.py +++ b/runagent/cli/commands/upload.py @@ -31,13 +31,12 @@ def format_error_message(error_info): """Format error information from API responses""" if isinstance(error_info, dict) and "message" in error_info: - # New format with ErrorDetail object error_message = error_info.get("message", "Unknown error") - error_code = error_info.get("code", "UNKNOWN_ERROR") - return f"[{error_code}] {error_message}" - else: - # Fallback to old format for backward compatibility - return str(error_info) if error_info else "Unknown error" + error_code = error_info.get("code") + if error_code: + return f"[{error_code}] {error_message}" + return error_message + return str(error_info) if error_info else "Unknown error" # ============================================================================ @@ -92,7 +91,12 @@ def upload(path: Path, overwrite: bool): console.print(f"\n[bold]Next step:[/bold]") console.print(f"[cyan]runagent start --id {agent_id}[/cyan]") else: - console.print(f"❌ [red]Upload failed:[/red] {format_error_message(result.get('error'))}") + error_info = result.get("error") + console.print(f"❌ [red]Upload failed:[/red] {format_error_message(error_info)}") + if isinstance(error_info, dict): + suggestion = error_info.get("suggestion") + if suggestion: + console.print(f"[cyan]Suggestion: {suggestion}[/cyan]") import sys sys.exit(1) diff --git a/runagent/client/client.py b/runagent/client/client.py index 3429cd3..2daed29 100644 --- a/runagent/client/client.py +++ b/runagent/client/client.py @@ -1,4 +1,6 @@ import os +import re + from runagent.sdk import RunAgentSDK from runagent.sdk.rest_client import RestClient from runagent.sdk.socket_client import SocketClient @@ -7,6 +9,17 @@ console = Console() +class RunAgentExecutionError(Exception): + """Exception raised when a remote agent execution fails.""" + + def __init__(self, code: str, message: str, suggestion: str | None = None, details: dict | None = None): + self.code = code or "UNKNOWN_ERROR" + self.message = message or "Unknown error" + self.suggestion = suggestion + self.details = details + super().__init__(f"[{self.code}] {self.message}") + + class RunAgentClient: def __init__(self, agent_id: str, entrypoint_tag: str, local: bool = True, host: str = None, port: int = None): @@ -76,10 +89,16 @@ def run(self, *input_args, **input_kwargs): # New format with ErrorDetail object error_message = error_info.get("message", "Unknown error") error_code = error_info.get("code", "UNKNOWN_ERROR") - raise Exception(f"[{error_code}] {error_message}") + suggestion = error_info.get("suggestion") or self._build_suggestion(error_code, error_message) + raise RunAgentExecutionError( + code=error_code, + message=error_message, + suggestion=suggestion, + details=error_info.get("details"), + ) else: # Fallback to old format for backward compatibility - raise Exception(response.get("error", "Unknown error")) + raise self._build_error_from_string(response.get("error")) def run_stream(self, *input_args, **input_kwargs): """Stream agent execution results in real-time via WebSocket""" @@ -94,4 +113,56 @@ def run_stream(self, *input_args, **input_kwargs): def _run_stream(self, *input_args, **input_kwargs): """Legacy method - use run_stream instead""" - return self.run_stream(*input_args, **input_kwargs) \ No newline at end of file + return self.run_stream(*input_args, **input_kwargs) + + def _build_suggestion(self, code: str, message: str) -> str | None: + message_lower = (message or "").lower() + + if "not found" in message_lower: + tag_match = re.search(r"['\"](?P[A-Za-z0-9_\-]+)['\"]", message or "") if "entrypoint" in message_lower else None + dashboard_hint = f"https://app.run-agent.ai/dashboard/agents/{self.agent_id}" + + if tag_match: + entrypoint = tag_match.group("tag") + return ( + f"Check that the entrypoint tag `{entrypoint}` exists for this agent. " + f"Update or redeploy the agent if needed, then verify in the dashboard: {dashboard_hint}." + ) + + return ( + "Verify the agent ID and ensure it is deployed. " + f"If the agent was modified locally, redeploy it with `runagent deploy` or upload/start it again. " + f"You can review its status in the dashboard: {dashboard_hint}." + ) + + if "must be deployed" in message_lower or "current status" in message_lower: + return ( + "Deploy the agent before running it. " + f"Use `runagent deploy` (or `runagent start --id {self.agent_id}` if already uploaded) and confirm its status in the RunAgent dashboard." + ) + + if code == "CONNECTION_ERROR": + dashboard_hint = f"https://app.run-agent.ai/dashboard/agents/{self.agent_id}" + return ( + "Check your network connection and confirm the RunAgent service URL is reachable. " + f"If the problem persists, review the agent in the dashboard: {dashboard_hint}." + ) + + return None + + def _build_error_from_string(self, error_value) -> RunAgentExecutionError: + if isinstance(error_value, RunAgentExecutionError): + return error_value + + error_text = str(error_value) if error_value else "Unknown error" + + match = re.match(r"^\[(?P[A-Z0-9_]+)]\s*(?P.*)$", error_text) + if match: + code = match.group("code") + message = match.group("message") or "Unknown error" + else: + code = "UNKNOWN_ERROR" + message = error_text + + suggestion = self._build_suggestion(code, message) + return RunAgentExecutionError(code=code, message=message, suggestion=suggestion) \ No newline at end of file diff --git a/runagent/sdk/rest_client.py b/runagent/sdk/rest_client.py index 266080e..2ab638f 100644 --- a/runagent/sdk/rest_client.py +++ b/runagent/sdk/rest_client.py @@ -866,13 +866,18 @@ def upload_agent_metadata_and_zip(self, folder_path: Path, overwrite: bool = Fal if not validation_result["valid"]: console.print(f"❌ [red]Error: {validation_result['error']}[/red]") - console.print(f"[cyan]Suggestion: {validation_result.get('suggestion', '')}[/cyan]") - console.print(f"[blue]You must use 'runagent config --register-agent .' to register a modified agent[/blue]") - + suggestion_text = validation_result.get( + "suggestion", + "Use 'runagent config --register-agent .' to register the agent", + ) + return { "success": False, - "error": f"Agent validation failed: {validation_result['error']}. Use 'runagent config --register-agent .' to fix this.", - "code": "AGENT_NOT_REGISTERED" + "error": { + "code": "AGENT_NOT_REGISTERED", + "message": validation_result["error"], + "suggestion": suggestion_text, + }, } else: existing_agent = validation_result["agent"] @@ -888,16 +893,23 @@ def upload_agent_metadata_and_zip(self, folder_path: Path, overwrite: bool = Fal console.print(f"[yellow]Agent path mismatch detected![/yellow]") console.print(f"Database path: [dim]{path_validation_result['details']['db_path']}[/dim]") console.print(f"Current path: [dim]{path_validation_result['details']['current_path']}[/dim]") - console.print(f"[cyan]Suggestion: {path_validation_result.get('suggestion', '')}[/cyan]") - console.print(f"[blue]You must use 'runagent config --register-agent .' to update the agent location[/blue]") else: # Other path validation errors console.print(f"❌ [red]Path validation error: {path_validation_result['error']}[/red]") - + + suggestion_text = path_validation_result.get( + "suggestion", + "Use 'runagent config --register-agent .' to update the agent location", + ) + return { "success": False, - "error": f"Path validation failed: {path_validation_result['error']}. Use 'runagent config --register-agent .' to fix this.", - "code": path_validation_result.get("code", "PATH_VALIDATION_ERROR") + "error": { + "code": path_validation_result.get("code", "PATH_VALIDATION_ERROR"), + "message": path_validation_result["error"], + "suggestion": suggestion_text, + "details": path_validation_result.get("details"), + }, } else: console.print(f"✅ [green]Agent path validated - matches database record[/green]") @@ -1153,13 +1165,18 @@ def deploy_agent(self, folder_path: str, metadata: Dict = None, overwrite: bool if not validation_result["valid"]: console.print(f"❌ [red]Error: {validation_result['error']}[/red]") - console.print(f"[cyan]Suggestion: {validation_result.get('suggestion', '')}[/cyan]") - console.print(f"[blue]You must use 'runagent config --register-agent .' to register a modified agent[/blue]") - + suggestion_text = validation_result.get( + "suggestion", + "Use 'runagent config --register-agent .' to register the agent", + ) + return { "success": False, - "error": f"Agent validation failed: {validation_result['error']}. Use 'runagent config --register-agent .' to fix this.", - "code": "AGENT_NOT_REGISTERED" + "error": { + "code": "AGENT_NOT_REGISTERED", + "message": validation_result["error"], + "suggestion": suggestion_text, + }, } else: existing_agent = validation_result["agent"] @@ -1175,16 +1192,23 @@ def deploy_agent(self, folder_path: str, metadata: Dict = None, overwrite: bool console.print(f"[yellow]Agent path mismatch detected![/yellow]") console.print(f"Database path: [dim]{path_validation_result['details']['db_path']}[/dim]") console.print(f"Current path: [dim]{path_validation_result['details']['current_path']}[/dim]") - console.print(f"[cyan]Suggestion: {path_validation_result.get('suggestion', '')}[/cyan]") - console.print(f"[blue]You must use 'runagent config --register-agent .' to update the agent location[/blue]") else: # Other path validation errors console.print(f"❌ [red]Path validation error: {path_validation_result['error']}[/red]") - + + suggestion_text = path_validation_result.get( + "suggestion", + "Use 'runagent config --register-agent .' to update the agent location", + ) + return { "success": False, - "error": f"Path validation failed: {path_validation_result['error']}. Use 'runagent config --register-agent .' to fix this.", - "code": path_validation_result.get("code", "PATH_VALIDATION_ERROR") + "error": { + "code": path_validation_result.get("code", "PATH_VALIDATION_ERROR"), + "message": path_validation_result["error"], + "suggestion": suggestion_text, + "details": path_validation_result.get("details"), + }, } else: console.print(f"✅ [green]Agent path validated - matches database record[/green]") diff --git a/runagent/sdk/socket_client.py b/runagent/sdk/socket_client.py index 8e3b149..24a2415 100644 --- a/runagent/sdk/socket_client.py +++ b/runagent/sdk/socket_client.py @@ -1,11 +1,13 @@ -import websockets import asyncio -from typing import AsyncIterator, Iterator, Optional -from runagent.utils.schema import WebSocketActionType, WebSocketAgentRequest, MessageType, SafeMessage import json +import os import uuid -from typing import Any +from typing import Any, AsyncIterator, Iterator, Optional + +import websockets + from runagent.utils.config import Config +from runagent.utils.schema import MessageType, SafeMessage, WebSocketActionType, WebSocketAgentRequest from runagent.utils.serializer import CoreSerializer @@ -50,10 +52,10 @@ def __init__( self.base_socket_url = ws_base.rstrip("/") + api_prefix - print(f"[DEBUG] SocketClient initialized:") - print(f" - is_local: {self.is_local}") - print(f" - base_socket_url: {self.base_socket_url}") - print(f" - api_key: {'SET' if self.api_key else 'NOT SET'}") + self._debug("SocketClient initialized:") + self._debug(f" - is_local: {self.is_local}") + self._debug(f" - base_socket_url: {self.base_socket_url}") + self._debug(f" - api_key: {'SET' if self.api_key else 'NOT SET'}") async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args, **input_kwargs) -> AsyncIterator[Any]: """Stream agent execution results (async version)""" @@ -89,7 +91,7 @@ async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args "async_execution": False } - print(f"[DEBUG] Sending request: {request_data}") + self._debug(f"Sending request: {request_data}") # Send the request as direct JSON await websocket.send(json.dumps(request_data)) @@ -99,7 +101,7 @@ async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args try: message = json.loads(raw_message) except json.JSONDecodeError: - print(f"[WARN] Invalid JSON message: {raw_message}") + self._debug(f"[WARN] Invalid JSON message: {raw_message}") continue message_type = message.get("type") @@ -110,10 +112,10 @@ async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args elif message_type == "status": status = message.get("status") if status == "stream_completed": - print("[DEBUG] Stream completed") + self._debug("Stream completed") break elif status == "stream_started": - print("[DEBUG] Stream started") + self._debug("Stream started") continue elif message_type == "data": # Yield the actual chunk data @@ -157,7 +159,7 @@ def run_stream(self, agent_id: str, entrypoint_tag: str, input_args, input_kwarg "async_execution": False } - print(f"[DEBUG] Sending request: {request_data}") + self._debug(f"Sending request: {request_data}") # Send the request as direct JSON websocket.send(json.dumps(request_data)) @@ -167,7 +169,7 @@ def run_stream(self, agent_id: str, entrypoint_tag: str, input_args, input_kwarg try: message = json.loads(raw_message) except json.JSONDecodeError: - print(f"[WARN] Invalid JSON message: {raw_message}") + self._debug(f"[WARN] Invalid JSON message: {raw_message}") continue message_type = message.get("type") @@ -178,11 +180,18 @@ def run_stream(self, agent_id: str, entrypoint_tag: str, input_args, input_kwarg elif message_type == "status": status = message.get("status") if status == "stream_completed": - print("[DEBUG] Stream completed") + self._debug("Stream completed") break elif status == "stream_started": - print("[DEBUG] Stream started") + self._debug("Stream started") continue elif message_type == "data": # Yield the actual chunk data - yield message.get("content") \ No newline at end of file + yield message.get("content") + + def _debug(self, message: str) -> None: + if os.getenv("RUNAGENT_DEBUG") or os.getenv("DISABLE_TRY_CATCH"): + if isinstance(message, str) and message.startswith("["): + print(message) + else: + print(f"[DEBUG] {message}") \ No newline at end of file