Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 4 additions & 78 deletions runagent/cli/commands/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path> --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)
Expand All @@ -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]"
Expand Down Expand Up @@ -235,7 +161,7 @@ def status(cleanup_days, agent_id, capacity):
console.print(f" • [cyan]runagent db invocation <id>[/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 <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 <id>[/cyan] - Agent-specific info")

# Cleanup if requested (keep existing logic)
if cleanup_days:
Expand Down
4 changes: 0 additions & 4 deletions runagent/cli/commands/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 11 additions & 7 deletions runagent/cli/commands/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


# ============================================================================
Expand Down Expand Up @@ -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)

Expand Down
6 changes: 5 additions & 1 deletion runagent/cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]")
Expand Down
11 changes: 10 additions & 1 deletion runagent/cli/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 0 additions & 13 deletions runagent/cli/commands/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
18 changes: 11 additions & 7 deletions runagent/cli/commands/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


# ============================================================================
Expand Down Expand Up @@ -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)

Expand Down
77 changes: 74 additions & 3 deletions runagent/client/client.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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"""
Expand All @@ -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)
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<tag>[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<code>[A-Z0-9_]+)]\s*(?P<message>.*)$", 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)
Loading