Files
hive/core/framework/runner/cli.py
T

2087 lines
69 KiB
Python

"""CLI commands for agent runner."""
import argparse
import asyncio
import json
import sys
from pathlib import Path
def register_commands(subparsers: argparse._SubParsersAction) -> None:
"""Register runner commands with the main CLI."""
# run command
run_parser = subparsers.add_parser(
"run",
help="Run an exported agent",
description="Execute an exported agent with the given input.",
)
run_parser.add_argument(
"agent_path",
type=str,
help="Path to agent folder (containing agent.json)",
)
run_parser.add_argument(
"--input",
"-i",
type=str,
help="Input context as JSON string",
)
run_parser.add_argument(
"--input-file",
"-f",
type=str,
help="Input context from JSON file",
)
run_parser.add_argument(
"--output",
"-o",
type=str,
help="Write results to file instead of stdout",
)
run_parser.add_argument(
"--quiet",
"-q",
action="store_true",
help="Only output the final result JSON",
)
run_parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Show detailed execution logs (steps, LLM calls, etc.)",
)
run_parser.add_argument(
"--tui",
action="store_true",
help="Launch interactive terminal dashboard",
)
run_parser.add_argument(
"--model",
"-m",
type=str,
default=None,
help="LLM model to use (any LiteLLM-compatible name)",
)
run_parser.add_argument(
"--resume-session",
type=str,
default=None,
help="Resume from a specific session ID",
)
run_parser.add_argument(
"--checkpoint",
type=str,
default=None,
help="Resume from a specific checkpoint (requires --resume-session)",
)
run_parser.set_defaults(func=cmd_run)
# info command
info_parser = subparsers.add_parser(
"info",
help="Show agent information",
description="Display details about an exported agent.",
)
info_parser.add_argument(
"agent_path",
type=str,
help="Path to agent folder (containing agent.json)",
)
info_parser.add_argument(
"--json",
action="store_true",
help="Output as JSON",
)
info_parser.set_defaults(func=cmd_info)
# validate command
validate_parser = subparsers.add_parser(
"validate",
help="Validate an exported agent",
description="Check that an exported agent is valid and runnable.",
)
validate_parser.add_argument(
"agent_path",
type=str,
help="Path to agent folder (containing agent.json)",
)
validate_parser.set_defaults(func=cmd_validate)
# list command
list_parser = subparsers.add_parser(
"list",
help="List available agents",
description="List all exported agents in a directory.",
)
list_parser.add_argument(
"directory",
type=str,
nargs="?",
default="exports",
help="Directory to search (default: exports)",
)
list_parser.set_defaults(func=cmd_list)
# dispatch command (multi-agent)
dispatch_parser = subparsers.add_parser(
"dispatch",
help="Dispatch request to multiple agents",
description="Route a request to the best agent(s) using the orchestrator.",
)
dispatch_parser.add_argument(
"agents_dir",
type=str,
nargs="?",
default="exports",
help="Directory containing agent folders (default: exports)",
)
dispatch_parser.add_argument(
"--input",
"-i",
type=str,
required=True,
help="Input context as JSON string",
)
dispatch_parser.add_argument(
"--intent",
type=str,
help="Description of what you want to accomplish",
)
dispatch_parser.add_argument(
"--agents",
"-a",
type=str,
nargs="+",
help="Specific agent names to use (default: all in directory)",
)
dispatch_parser.add_argument(
"--quiet",
"-q",
action="store_true",
help="Only output the final result JSON",
)
dispatch_parser.set_defaults(func=cmd_dispatch)
# shell command (interactive agent session)
shell_parser = subparsers.add_parser(
"shell",
help="Interactive agent session",
description="Start an interactive REPL session with agents.",
)
shell_parser.add_argument(
"agent_path",
type=str,
nargs="?",
help="Path to agent folder (optional, can select interactively)",
)
shell_parser.add_argument(
"--agents-dir",
type=str,
default="exports",
help="Directory containing agents (default: exports)",
)
shell_parser.add_argument(
"--multi",
action="store_true",
help="Enable multi-agent mode with orchestrator",
)
shell_parser.add_argument(
"--no-approve",
action="store_true",
help="Disable human-in-the-loop approval (auto-approve all steps)",
)
shell_parser.set_defaults(func=cmd_shell)
# tui command (interactive agent dashboard)
tui_parser = subparsers.add_parser(
"tui",
help="Launch interactive TUI dashboard",
description="Browse available agents and launch the terminal dashboard.",
)
tui_parser.add_argument(
"--model",
"-m",
type=str,
default=None,
help="LLM model to use (any LiteLLM-compatible name)",
)
tui_parser.set_defaults(func=cmd_tui)
# code command (Hive Coder — framework agent builder)
code_parser = subparsers.add_parser(
"code",
help="Launch Hive Coder to build agents",
description="Interactive agent builder. Describe what you want and Hive Coder builds it.",
)
code_parser.add_argument(
"--model",
"-m",
type=str,
default=None,
help="LLM model to use (any LiteLLM-compatible name)",
)
code_parser.set_defaults(func=cmd_code)
# sessions command group (checkpoint/resume management)
sessions_parser = subparsers.add_parser(
"sessions",
help="Manage agent sessions",
description="List, inspect, and manage agent execution sessions.",
)
sessions_subparsers = sessions_parser.add_subparsers(
dest="sessions_cmd",
help="Session management commands",
)
# sessions list
sessions_list_parser = sessions_subparsers.add_parser(
"list",
help="List agent sessions",
description="List all sessions for an agent.",
)
sessions_list_parser.add_argument(
"agent_path",
type=str,
help="Path to agent folder",
)
sessions_list_parser.add_argument(
"--status",
choices=["all", "active", "failed", "completed", "paused"],
default="all",
help="Filter by session status (default: all)",
)
sessions_list_parser.add_argument(
"--has-checkpoints",
action="store_true",
help="Show only sessions with checkpoints",
)
sessions_list_parser.set_defaults(func=cmd_sessions_list)
# sessions show
sessions_show_parser = sessions_subparsers.add_parser(
"show",
help="Show session details",
description="Display detailed information about a specific session.",
)
sessions_show_parser.add_argument(
"agent_path",
type=str,
help="Path to agent folder",
)
sessions_show_parser.add_argument(
"session_id",
type=str,
help="Session ID to inspect",
)
sessions_show_parser.add_argument(
"--json",
action="store_true",
help="Output as JSON",
)
sessions_show_parser.set_defaults(func=cmd_sessions_show)
# sessions checkpoints
sessions_checkpoints_parser = sessions_subparsers.add_parser(
"checkpoints",
help="List session checkpoints",
description="List all checkpoints for a session.",
)
sessions_checkpoints_parser.add_argument(
"agent_path",
type=str,
help="Path to agent folder",
)
sessions_checkpoints_parser.add_argument(
"session_id",
type=str,
help="Session ID",
)
sessions_checkpoints_parser.set_defaults(func=cmd_sessions_checkpoints)
# pause command
pause_parser = subparsers.add_parser(
"pause",
help="Pause running session",
description="Request graceful pause of a running agent session.",
)
pause_parser.add_argument(
"agent_path",
type=str,
help="Path to agent folder",
)
pause_parser.add_argument(
"session_id",
type=str,
help="Session ID to pause",
)
pause_parser.set_defaults(func=cmd_pause)
# resume command
resume_parser = subparsers.add_parser(
"resume",
help="Resume session from checkpoint",
description="Resume a paused or failed session from a checkpoint.",
)
resume_parser.add_argument(
"agent_path",
type=str,
help="Path to agent folder",
)
resume_parser.add_argument(
"session_id",
type=str,
help="Session ID to resume",
)
resume_parser.add_argument(
"--checkpoint",
"-c",
type=str,
help="Specific checkpoint ID to resume from (default: latest)",
)
resume_parser.add_argument(
"--tui",
action="store_true",
help="Resume in TUI dashboard mode",
)
resume_parser.set_defaults(func=cmd_resume)
# setup-credentials command
setup_creds_parser = subparsers.add_parser(
"setup-credentials",
help="Interactive credential setup",
description="Guide through setting up required credentials for an agent.",
)
setup_creds_parser.add_argument(
"agent_path",
type=str,
nargs="?",
help="Path to agent folder (optional - runs general setup if not specified)",
)
setup_creds_parser.set_defaults(func=cmd_setup_credentials)
# serve command (HTTP API server)
serve_parser = subparsers.add_parser(
"serve",
help="Start HTTP API server",
description="Start an HTTP server exposing REST + SSE APIs for agent control.",
)
serve_parser.add_argument(
"--host",
type=str,
default="127.0.0.1",
help="Host to bind (default: 127.0.0.1)",
)
serve_parser.add_argument(
"--port",
"-p",
type=int,
default=8787,
help="Port to listen on (default: 8787)",
)
serve_parser.add_argument(
"--agent",
"-a",
type=str,
action="append",
default=[],
help="Agent path to preload (repeatable)",
)
serve_parser.add_argument(
"--model",
"-m",
type=str,
default=None,
help="LLM model for preloaded agents",
)
serve_parser.add_argument(
"--open",
action="store_true",
help="Open dashboard in browser after server starts",
)
serve_parser.set_defaults(func=cmd_serve)
def _load_resume_state(
agent_path: str, session_id: str, checkpoint_id: str | None = None
) -> dict | None:
"""Load session or checkpoint state for headless resume.
Args:
agent_path: Path to the agent folder (e.g., exports/my_agent)
session_id: Session ID to resume from
checkpoint_id: Optional checkpoint ID within the session
Returns:
session_state dict for executor, or None if not found
"""
agent_name = Path(agent_path).name
agent_work_dir = Path.home() / ".hive" / "agents" / agent_name
session_dir = agent_work_dir / "sessions" / session_id
if not session_dir.exists():
return None
if checkpoint_id:
# Checkpoint-based resume: load checkpoint and extract state
cp_path = session_dir / "checkpoints" / f"{checkpoint_id}.json"
if not cp_path.exists():
return None
try:
cp_data = json.loads(cp_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
return {
"resume_session_id": session_id,
"memory": cp_data.get("shared_memory", {}),
"paused_at": cp_data.get("next_node") or cp_data.get("current_node"),
"execution_path": cp_data.get("execution_path", []),
"node_visit_counts": {},
}
else:
# Session state resume: load state.json
state_path = session_dir / "state.json"
if not state_path.exists():
return None
try:
state_data = json.loads(state_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
progress = state_data.get("progress", {})
paused_at = progress.get("paused_at") or progress.get("resume_from")
return {
"resume_session_id": session_id,
"memory": state_data.get("memory", {}),
"paused_at": paused_at,
"execution_path": progress.get("path", []),
"node_visit_counts": progress.get("node_visit_counts", {}),
}
def _prompt_before_start(agent_path: str, runner, model: str | None = None):
"""Prompt user to start agent or update credentials.
Returns:
Updated runner if user proceeds, None if user aborts.
"""
from framework.credentials.setup import CredentialSetupSession
from framework.runner import AgentRunner
while True:
print()
try:
choice = input("Press Enter to start agent, or 'u' to update credentials: ").strip()
except (EOFError, KeyboardInterrupt):
print()
return None
if choice == "":
return runner
elif choice.lower() == "u":
session = CredentialSetupSession.from_agent_path(agent_path)
result = session.run_interactive()
if result.success:
# Reload runner with updated credentials
try:
runner = AgentRunner.load(agent_path, model=model)
except Exception as e:
print(f"Error reloading agent: {e}")
return None
# Loop back to prompt again
elif choice.lower() == "q":
return None
def cmd_run(args: argparse.Namespace) -> int:
"""Run an exported agent."""
import logging
from framework.credentials.models import CredentialError
from framework.runner import AgentRunner
# Set logging level (quiet by default for cleaner output)
if args.quiet:
logging.basicConfig(level=logging.ERROR, format="%(message)s")
elif getattr(args, "verbose", False):
logging.basicConfig(level=logging.INFO, format="%(message)s")
else:
logging.basicConfig(level=logging.WARNING, format="%(message)s")
# Load input context
context = {}
if args.input:
try:
context = json.loads(args.input)
except json.JSONDecodeError as e:
print(f"Error parsing --input JSON: {e}", file=sys.stderr)
return 1
elif args.input_file:
try:
with open(args.input_file, encoding="utf-8") as f:
context = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Error reading input file: {e}", file=sys.stderr)
return 1
# Run the agent (with TUI or standard)
if getattr(args, "tui", False):
from framework.tui.app import AdenTUI
async def run_with_tui():
try:
# Load runner inside the async loop to ensure strict loop affinity
# (only one load — avoids spawning duplicate MCP subprocesses)
# AgentRunner handles credential setup interactively when stdin is a TTY.
try:
runner = AgentRunner.load(
args.agent_path,
model=args.model,
)
except CredentialError as e:
print(f"\n{e}", file=sys.stderr)
return
except Exception as e:
print(f"Error loading agent: {e}")
return
# Prompt before starting (allows credential updates)
if sys.stdin.isatty():
runner = _prompt_before_start(args.agent_path, runner, args.model)
if runner is None:
return
# Force setup inside the loop
if runner._agent_runtime is None:
try:
runner._setup()
except CredentialError as e:
print(f"\n{e}", file=sys.stderr)
return
# Start runtime before TUI so it's ready for user input
if runner._agent_runtime and not runner._agent_runtime.is_running:
await runner._agent_runtime.start()
app = AdenTUI(
runner._agent_runtime,
resume_session=getattr(args, "resume_session", None),
resume_checkpoint=getattr(args, "checkpoint", None),
)
# TUI handles execution via ChatRepl — user submits input,
# ChatRepl calls runtime.trigger_and_wait(). No auto-launch.
await app.run_async()
except Exception as e:
import traceback
traceback.print_exc()
print(f"TUI error: {e}")
await runner.cleanup_async()
return None
asyncio.run(run_with_tui())
print("TUI session ended.")
return 0
else:
# Standard execution — load runner here (not shared with TUI path)
# AgentRunner handles credential setup interactively when stdin is a TTY.
try:
runner = AgentRunner.load(
args.agent_path,
model=args.model,
)
except CredentialError as e:
print(f"\n{e}", file=sys.stderr)
return 1
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
# Prompt before starting (allows credential updates)
if sys.stdin.isatty() and not args.quiet:
runner = _prompt_before_start(args.agent_path, runner, args.model)
if runner is None:
return 1
# Load session/checkpoint state for resume (headless mode)
session_state = None
resume_session = getattr(args, "resume_session", None)
checkpoint = getattr(args, "checkpoint", None)
if resume_session:
session_state = _load_resume_state(args.agent_path, resume_session, checkpoint)
if session_state is None:
print(
f"Error: Could not load session state for {resume_session}",
file=sys.stderr,
)
return 1
if not args.quiet:
resume_node = session_state.get("paused_at", "unknown")
if checkpoint:
print(f"Resuming from checkpoint: {checkpoint}")
else:
print(f"Resuming session: {resume_session}")
print(f"Resume point: {resume_node}")
print()
# Auto-inject user_id if the agent expects it but it's not provided
entry_input_keys = runner.graph.nodes[0].input_keys if runner.graph.nodes else []
if "user_id" in entry_input_keys and context.get("user_id") is None:
import os
context["user_id"] = os.environ.get("USER", "default_user")
if not args.quiet:
info = runner.info()
print(f"Agent: {info.name}")
print(f"Goal: {info.goal_name}")
print(f"Steps: {info.node_count}")
print(f"Input: {json.dumps(context)}")
print()
print("=" * 60)
print("Executing agent...")
print("=" * 60)
print()
result = asyncio.run(runner.run(context, session_state=session_state))
# Format output
output = {
"success": result.success,
"steps_executed": result.steps_executed,
"output": result.output,
}
if result.error:
output["error"] = result.error
if result.paused_at:
output["paused_at"] = result.paused_at
# Output results
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
json.dump(output, f, indent=2, default=str)
if not args.quiet:
print(f"Results written to {args.output}")
else:
if args.quiet:
print(json.dumps(output, indent=2, default=str))
else:
print()
print("=" * 60)
status_str = "SUCCESS" if result.success else "FAILED"
print(f"Status: {status_str}")
print(f"Steps executed: {result.steps_executed}")
print(f"Path: {''.join(result.path)}")
print("=" * 60)
if result.success:
print("\n--- Results ---")
# Show only meaningful output keys (skip internal/intermediate values)
meaningful_keys = ["final_response", "response", "result", "answer", "output"]
# Try to find the most relevant output
shown = False
for key in meaningful_keys:
if key in result.output:
value = result.output[key]
if isinstance(value, str) and len(value) > 10:
print(value)
shown = True
break
elif isinstance(value, (dict, list)):
print(json.dumps(value, indent=2, default=str))
shown = True
break
# If no meaningful key found, show all non-internal keys
if not shown:
for key, value in result.output.items():
if not key.startswith("_") and key not in [
"user_id",
"request",
"memory_loaded",
"user_profile",
"recent_context",
]:
if isinstance(value, (dict, list)):
print(f"\n{key}:")
value_str = json.dumps(value, indent=2, default=str)
if len(value_str) > 300:
value_str = value_str[:300] + "..."
print(value_str)
else:
val_str = str(value)
if len(val_str) > 200:
val_str = val_str[:200] + "..."
print(f"{key}: {val_str}")
elif result.error:
print(f"\nError: {result.error}")
runner.cleanup()
return 0 if result.success else 1
def cmd_info(args: argparse.Namespace) -> int:
"""Show agent information."""
from framework.credentials.models import CredentialError
from framework.runner import AgentRunner
try:
runner = AgentRunner.load(args.agent_path)
except CredentialError as e:
print(f"\n{e}", file=sys.stderr)
return 1
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
info = runner.info()
if args.json:
print(
json.dumps(
{
"name": info.name,
"description": info.description,
"goal_name": info.goal_name,
"goal_description": info.goal_description,
"node_count": info.node_count,
"nodes": info.nodes,
"edges": info.edges,
"success_criteria": info.success_criteria,
"constraints": info.constraints,
"required_tools": info.required_tools,
"has_tools_module": info.has_tools_module,
},
indent=2,
)
)
else:
print(f"Agent: {info.name}")
print(f"Description: {info.description}")
print()
print(f"Goal: {info.goal_name}")
print(f" {info.goal_description}")
print()
print(f"Nodes ({info.node_count}):")
for node in info.nodes:
inputs = f" [in: {', '.join(node['input_keys'])}]" if node.get("input_keys") else ""
outputs = f" [out: {', '.join(node['output_keys'])}]" if node.get("output_keys") else ""
print(f" - {node['id']}: {node['name']}{inputs}{outputs}")
print()
print(f"Success Criteria ({len(info.success_criteria)}):")
for sc in info.success_criteria:
print(f" - {sc['description']} ({sc['metric']} = {sc['target']})")
print()
print(f"Constraints ({len(info.constraints)}):")
for c in info.constraints:
print(f" - [{c['type']}] {c['description']}")
print()
print(f"Required Tools ({len(info.required_tools)}):")
for tool in info.required_tools:
status = "" if runner._tool_registry.has_tool(tool) else ""
print(f" {status} {tool}")
print()
print(f"Tools Module: {'✓ tools.py found' if info.has_tools_module else '✗ no tools.py'}")
runner.cleanup()
return 0
def cmd_validate(args: argparse.Namespace) -> int:
"""Validate an exported agent."""
from framework.credentials.models import CredentialError
from framework.runner import AgentRunner
try:
runner = AgentRunner.load(args.agent_path)
except CredentialError as e:
print(f"\n{e}", file=sys.stderr)
return 1
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
validation = runner.validate()
if validation.valid:
print("✓ Agent is valid")
else:
print("✗ Agent has errors:")
for error in validation.errors:
print(f" ERROR: {error}")
if validation.warnings:
print("\nWarnings:")
for warning in validation.warnings:
print(f" WARNING: {warning}")
if validation.missing_tools:
print("\nMissing tool implementations:")
for tool in validation.missing_tools:
print(f" - {tool}")
print("\nTo fix: Create tools.py in the agent folder or register tools programmatically")
runner.cleanup()
return 0 if validation.valid else 1
def cmd_list(args: argparse.Namespace) -> int:
"""List available agents."""
from framework.runner import AgentRunner
directory = Path(args.directory)
if not directory.exists():
# FIX: Handle missing directory gracefully on fresh install
print(f"No agents found in {directory}")
return 0
agents = []
for path in directory.iterdir():
if _is_valid_agent_dir(path):
try:
runner = AgentRunner.load(path)
info = runner.info()
agents.append(
{
"path": str(path),
"name": info.name,
"description": info.description[:60] + "..."
if len(info.description) > 60
else info.description,
"nodes": info.node_count,
"tools": len(info.required_tools),
}
)
runner.cleanup()
except Exception as e:
agents.append(
{
"path": str(path),
"error": str(e),
}
)
if not agents:
print(f"No agents found in {directory}")
return 0
print(f"Agents in {directory}:\n")
for agent in agents:
if "error" in agent:
print(f" {agent['path']}: ERROR - {agent['error']}")
else:
print(f" {agent['name']}")
print(f" Path: {agent['path']}")
print(f" Description: {agent['description']}")
print(f" Nodes: {agent['nodes']}, Tools: {agent['tools']}")
print()
return 0
def cmd_dispatch(args: argparse.Namespace) -> int:
"""Dispatch request to multiple agents via orchestrator."""
from framework.runner import AgentOrchestrator
# Parse input
try:
context = json.loads(args.input)
except json.JSONDecodeError as e:
print(f"Error parsing --input JSON: {e}", file=sys.stderr)
return 1
# Find agents
agents_dir = Path(args.agents_dir)
if not agents_dir.exists():
print(f"Directory not found: {agents_dir}", file=sys.stderr)
return 1
# Create orchestrator and register agents
orchestrator = AgentOrchestrator()
agent_paths = []
if args.agents:
# Use specific agents
for agent_name in args.agents:
agent_path = agents_dir / agent_name
if not _is_valid_agent_dir(agent_path):
print(f"Agent not found: {agent_path}", file=sys.stderr)
return 1
agent_paths.append((agent_name, agent_path))
else:
# Discover all agents
for path in agents_dir.iterdir():
if _is_valid_agent_dir(path):
agent_paths.append((path.name, path))
if not agent_paths:
print(f"No agents found in {agents_dir}", file=sys.stderr)
return 1
# Register agents
for name, path in agent_paths:
try:
orchestrator.register(name, path)
if not args.quiet:
print(f"Registered agent: {name}")
except Exception as e:
print(f"Failed to register {name}: {e}", file=sys.stderr)
if not args.quiet:
print()
print(f"Input: {json.dumps(context)}")
if args.intent:
print(f"Intent: {args.intent}")
print()
print("=" * 60)
print("Dispatching to agents...")
print("=" * 60)
print()
# Dispatch
result = asyncio.run(orchestrator.dispatch(context, intent=args.intent))
# Output results
if args.quiet:
output = {
"success": result.success,
"handled_by": result.handled_by,
"results": result.results,
"error": result.error,
}
print(json.dumps(output, indent=2, default=str))
else:
print()
print("=" * 60)
print(f"Success: {result.success}")
print(f"Handled by: {', '.join(result.handled_by) or 'none'}")
if result.error:
print(f"Error: {result.error}")
print("=" * 60)
if result.results:
print("\n--- Results by Agent ---")
for agent_name, data in result.results.items():
print(f"\n{agent_name}:")
status = data.get("status", "unknown")
print(f" Status: {status}")
if "completed_steps" in data:
print(f" Steps: {len(data['completed_steps'])}")
if "results" in data:
results_preview = json.dumps(data["results"], default=str)
if len(results_preview) > 200:
results_preview = results_preview[:200] + "..."
print(f" Results: {results_preview}")
if not args.quiet:
print(f"\nMessage trace: {len(result.messages)} messages")
orchestrator.cleanup()
return 0 if result.success else 1
def _interactive_approval(request):
"""Interactive approval callback for HITL mode."""
from framework.graph import ApprovalDecision, ApprovalResult
print()
print("=" * 60)
print("🔔 APPROVAL REQUIRED")
print("=" * 60)
print(f"\nStep: {request.step_id}")
print(f"Description: {request.step_description}")
if request.approval_message:
print(f"\nMessage: {request.approval_message}")
if request.preview:
print(f"\nPreview:\n{request.preview}")
if request.context:
print("\n--- Content to be sent ---")
for key, value in request.context.items():
print(f"\n[{key}]:")
if isinstance(value, (dict, list)):
import json
value_str = json.dumps(value, indent=2, default=str)
# Show more content for approval - up to 2000 chars
if len(value_str) > 2000:
value_str = value_str[:2000] + "\n... (truncated)"
print(value_str)
else:
value_str = str(value)
if len(value_str) > 500:
value_str = value_str[:500] + "... (truncated)"
print(f" {value_str}")
print()
print("Options:")
print(" [a] Approve - Execute as planned")
print(" [r] Reject - Skip this step")
print(" [s] Skip all - Reject and skip dependent steps")
print(" [x] Abort - Stop entire execution")
print()
while True:
try:
choice = input("Your choice (a/r/s/x): ").strip().lower()
except (EOFError, KeyboardInterrupt):
print("\nAborting...")
return ApprovalResult(decision=ApprovalDecision.ABORT, reason="User interrupted")
if choice == "a":
print("✓ Approved")
return ApprovalResult(decision=ApprovalDecision.APPROVE)
elif choice == "r":
reason = input("Reason (optional): ").strip() or "Rejected by user"
print(f"✗ Rejected: {reason}")
return ApprovalResult(decision=ApprovalDecision.REJECT, reason=reason)
elif choice == "s":
print("✗ Rejected (skipping dependent steps)")
return ApprovalResult(decision=ApprovalDecision.REJECT, reason="User skipped")
elif choice == "x":
reason = input("Reason (optional): ").strip() or "Aborted by user"
print(f"⛔ Aborted: {reason}")
return ApprovalResult(decision=ApprovalDecision.ABORT, reason=reason)
else:
print("Invalid choice. Please enter a, r, s, or x.")
def _format_natural_language_to_json(
user_input: str, input_keys: list[str], agent_description: str, session_context: dict = None
) -> dict:
"""Use Haiku to convert natural language input to JSON based on agent's input schema."""
import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
# Build prompt for Haiku
session_info = ""
if session_context:
# Extract the main field (usually 'objective') that we'll append to
main_field = input_keys[0] if input_keys else "objective"
existing_value = session_context.get(main_field, "")
session_info = (
f'\n\nExisting {main_field}: "{existing_value}"\n\n'
f"The user is providing ADDITIONAL information. Append this new "
f"information to the existing {main_field} to create an enriched, "
"more detailed version."
)
prompt = f"""You are formatting user input for an agent that requires specific input fields.
Agent: {agent_description}
Required input fields: {", ".join(input_keys)}{session_info}
User input: {user_input}
{"If this is a follow-up, APPEND new info to the existing field value." if session_context else ""}
Output ONLY valid JSON, no explanation:"""
try:
message = client.messages.create(
model="claude-haiku-4-5-20251001", # Fast and cheap
max_tokens=500,
messages=[{"role": "user", "content": prompt}],
)
json_str = message.content[0].text.strip()
# Remove markdown code blocks if present
if json_str.startswith("```"):
json_str = json_str.split("```")[1]
if json_str.startswith("json"):
json_str = json_str[4:]
json_str = json_str.strip()
return json.loads(json_str)
except Exception:
# Fallback: try to infer the main field
if len(input_keys) == 1:
return {input_keys[0]: user_input}
else:
# Put it in the first field as fallback
return {input_keys[0]: user_input}
def cmd_shell(args: argparse.Namespace) -> int:
"""Start an interactive agent session."""
import logging
from framework.credentials.models import CredentialError
from framework.runner import AgentRunner
# Configure logging to show runtime visibility
logging.basicConfig(
level=logging.INFO,
format="%(message)s", # Simple format for clean output
)
agents_dir = Path(args.agents_dir)
# Multi-agent mode with orchestrator
if args.multi:
return _interactive_multi(agents_dir)
# Single agent mode
agent_path = args.agent_path
if not agent_path:
# List available agents and let user choose
agent_path = _select_agent(agents_dir)
if not agent_path:
return 1
try:
runner = AgentRunner.load(agent_path)
except CredentialError as e:
print(f"\n{e}", file=sys.stderr)
return 1
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
# Set up approval callback by default (unless --no-approve is set)
if not getattr(args, "no_approve", False):
runner.set_approval_callback(_interactive_approval)
print("\n🔔 Human-in-the-loop mode enabled")
print(" Steps marked for approval will pause for your review")
else:
print("\n⚠️ Auto-approve mode: all steps will execute without review")
info = runner.info()
# Get entry node's input keys for smart formatting
entry_node = next((n for n in info.nodes if n["id"] == info.entry_node), None)
entry_input_keys = entry_node["input_keys"] if entry_node else []
print(f"\n{'=' * 60}")
print(f"Agent: {info.name}")
print(f"Goal: {info.goal_name}")
print(f"Description: {info.description[:100]}...")
print(f"{'=' * 60}")
print("\nInteractive mode. Enter natural language or JSON:")
print(" /info - Show agent details")
print(" /nodes - Show agent nodes")
print(" /reset - Reset conversation state")
print(" /quit - Exit interactive mode")
print(" {...} - JSON input to run agent")
print(" anything else - Natural language (auto-formatted with Haiku)")
print()
# Session state: accumulate context across multiple inputs
session_memory = {}
conversation_history = []
agent_session_state = None # Track paused agent state
while True:
try:
user_input = input(">>> ").strip()
except (EOFError, KeyboardInterrupt):
print("\nExiting...")
break
if not user_input:
continue
if user_input == "/quit":
break
if user_input == "/info":
print(f"\nAgent: {info.name}")
print(f"Goal: {info.goal_name}")
print(f"Description: {info.goal_description}")
print(f"Nodes: {info.node_count}")
print(f"Edges: {info.edge_count}")
print(f"Required tools: {', '.join(info.required_tools)}")
print()
continue
if user_input == "/nodes":
print("\nAgent nodes:")
for node in info.nodes:
inputs = f" [in: {', '.join(node['input_keys'])}]" if node.get("input_keys") else ""
outputs = (
f" [out: {', '.join(node['output_keys'])}]" if node.get("output_keys") else ""
)
print(f" {node['id']}: {node['name']}{inputs}{outputs}")
print(f" {node['description']}")
print()
continue
if user_input == "/reset":
session_memory = {}
conversation_history = []
agent_session_state = None # Clear agent's internal state too
print("✓ Conversation state and agent session cleared")
print()
continue
# Try to parse as JSON first
try:
context = json.loads(user_input)
print("✓ Parsed as JSON")
except json.JSONDecodeError:
# Not JSON - check for key=value format
if "=" in user_input and " " not in user_input.split("=")[0]:
context = {}
for part in user_input.split():
if "=" in part:
key, value = part.split("=", 1)
context[key] = value
print("✓ Parsed as key=value")
else:
# Natural language - use Haiku to format
print("🤖 Formatting with Haiku...")
try:
context = _format_natural_language_to_json(
user_input,
entry_input_keys,
info.description,
session_context=session_memory,
)
print(f"✓ Formatted to: {json.dumps(context)}")
except Exception as e:
print(f"Error formatting input: {e}")
print("Please try JSON format: {...} or key=value format")
continue
# Handle context differently based on whether we're resuming or starting fresh
if agent_session_state:
# RESUMING: Pass only the new input in the "input" key
# The executor will restore all session memory automatically
# The resume node expects fresh input, not merged session context
run_context = {"input": user_input} # Pass raw user input for resume nodes
print(f"\n🔄 Resuming from paused state: {agent_session_state.get('paused_at')}")
print(f"User's answer: {user_input}")
else:
# STARTING FRESH: Merge new input with accumulated session memory
run_context = {**session_memory, **context}
# Auto-inject user_id if missing (for personal assistant agents)
if "user_id" in entry_input_keys and run_context.get("user_id") is None:
import os
run_context["user_id"] = os.environ.get("USER", "default_user")
# Add conversation history to context if agent expects it
if conversation_history:
run_context["_conversation_history"] = conversation_history.copy()
print(f"\nRunning with: {json.dumps(context)}")
if session_memory:
print(f"Session context: {json.dumps(session_memory)}")
print("-" * 40)
# Pass agent session state to enable resumption
result = asyncio.run(runner.run(run_context, session_state=agent_session_state))
status_str = "SUCCESS" if result.success else "FAILED"
print(f"\nStatus: {status_str}")
print(f"Steps executed: {result.steps_executed}")
print(f"Path: {''.join(result.path)}")
# Show clean output - prioritize meaningful keys
if result.output:
meaningful_keys = ["final_response", "response", "result", "answer", "output"]
shown = False
for key in meaningful_keys:
if key in result.output:
value = result.output[key]
if isinstance(value, str) and len(value) > 10:
print(f"\n{value}\n")
shown = True
break
if not shown:
print("\nOutput:")
for key, value in result.output.items():
if not key.startswith("_"):
val_str = str(value)[:200]
print(f" {key}: {val_str}")
if result.error:
print(f"\nError: {result.error}")
if result.total_tokens > 0:
print(f"\nTokens used: {result.total_tokens}")
print(f"Latency: {result.total_latency_ms}ms")
# Update agent session state if paused
if result.paused_at:
agent_session_state = result.session_state
print(f"⏸ Agent paused at: {result.paused_at}")
print(" Next input will resume from this point")
else:
# Execution completed (not paused), clear session state
agent_session_state = None
# Update session memory with outputs from this run
# This allows follow-up inputs to reference previous context
if result.output:
for key, value in result.output.items():
# Don't store internal keys or very large values
if not key.startswith("_") and len(str(value)) < 5000:
session_memory[key] = value
# Track conversation history
conversation_history.append(
{
"input": context,
"output": result.output if result.output else {},
"status": "success" if result.success else "failed",
"paused_at": result.paused_at,
}
)
print()
runner.cleanup()
return 0
def _get_framework_agents_dir() -> Path:
"""Resolve the framework agents directory relative to this file."""
return Path(__file__).resolve().parent.parent / "agents"
def _launch_agent_tui(
agent_path: str | Path,
model: str | None = None,
) -> int:
"""Load an agent and launch the TUI. Shared by cmd_tui and cmd_code."""
from framework.credentials.models import CredentialError
from framework.runner import AgentRunner
from framework.tui.app import AdenTUI
async def run_with_tui():
# AgentRunner handles credential setup interactively when stdin is a TTY.
try:
runner = AgentRunner.load(
agent_path,
model=model,
)
except CredentialError as e:
print(f"\n{e}", file=sys.stderr)
return
except Exception as e:
print(f"Error loading agent: {e}")
return
if runner._agent_runtime is None:
try:
runner._setup()
except CredentialError as e:
print(f"\n{e}", file=sys.stderr)
return
if runner._agent_runtime and not runner._agent_runtime.is_running:
await runner._agent_runtime.start()
app = AdenTUI(runner._agent_runtime)
try:
await app.run_async()
except Exception as e:
import traceback
traceback.print_exc()
print(f"TUI error: {e}")
await runner.cleanup_async()
asyncio.run(run_with_tui())
print("TUI session ended.")
return 0
def cmd_tui(args: argparse.Namespace) -> int:
"""Launch the interactive TUI dashboard with in-app agent picker."""
import logging
logging.basicConfig(level=logging.WARNING, format="%(message)s")
from framework.tui.app import AdenTUI
async def run_tui():
app = AdenTUI(
model=args.model,
)
await app.run_async()
asyncio.run(run_tui())
print("TUI session ended.")
return 0
def cmd_code(args: argparse.Namespace) -> int:
"""Launch Hive Coder with multi-graph support.
Unlike ``_launch_agent_tui``, this sets up graph lifecycle tools and
assigns ``graph_id="hive_coder"`` so the coder can load, supervise,
and restart secondary agent graphs within the same session.
"""
import logging
logging.basicConfig(level=logging.WARNING, format="%(message)s")
framework_agents_dir = _get_framework_agents_dir()
hive_coder_path = framework_agents_dir / "hive_coder"
if not (hive_coder_path / "agent.py").exists():
print("Error: Hive Coder agent not found.", file=sys.stderr)
return 1
# Ensure framework agents dir is on sys.path for import
fa_str = str(framework_agents_dir)
if fa_str not in sys.path:
sys.path.insert(0, fa_str)
from framework.credentials.models import CredentialError
from framework.runner import AgentRunner
from framework.tools.session_graph_tools import register_graph_tools
from framework.tui.app import AdenTUI
async def run_with_tui():
try:
runner = AgentRunner.load(hive_coder_path, model=args.model)
except CredentialError as e:
print(f"\n{e}", file=sys.stderr)
return
except Exception as e:
print(f"Error loading agent: {e}")
return
if runner._agent_runtime is None:
try:
runner._setup()
except CredentialError as e:
print(f"\n{e}", file=sys.stderr)
return
runtime = runner._agent_runtime
# -- Multi-graph setup --
# Tag the primary graph so events carry graph_id="hive_coder"
runtime._graph_id = "hive_coder"
runtime._active_graph_id = "hive_coder"
# Register graph lifecycle tools (load_agent, unload_agent, etc.)
register_graph_tools(runner._tool_registry, runtime)
# Refresh tool schemas AND executor so streams see the new tools.
# The executor closure references the registry dict by ref, but
# refreshing both is robust against any copy-on-read behavior.
runtime._tools = list(runner._tool_registry.get_tools().values())
runtime._tool_executor = runner._tool_registry.get_executor()
if not runtime.is_running:
await runtime.start()
app = AdenTUI(runtime)
try:
await app.run_async()
except Exception as e:
import traceback
traceback.print_exc()
print(f"TUI error: {e}")
await runner.cleanup_async()
asyncio.run(run_with_tui())
print("TUI session ended.")
return 0
def _extract_python_agent_metadata(agent_path: Path) -> tuple[str, str]:
"""Extract name and description from a Python-based agent's config.py.
Uses AST parsing to safely extract values without executing code.
Returns (name, description) tuple, with fallbacks if parsing fails.
"""
import ast
config_path = agent_path / "config.py"
fallback_name = agent_path.name.replace("_", " ").title()
fallback_desc = "(Python-based agent)"
if not config_path.exists():
return fallback_name, fallback_desc
try:
with open(config_path, encoding="utf-8") as f:
tree = ast.parse(f.read())
# Find AgentMetadata class definition
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef) and node.name == "AgentMetadata":
name = fallback_name
desc = fallback_desc
# Extract default values from class body
for item in node.body:
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
field_name = item.target.id
if item.value:
# Handle simple string constants
if isinstance(item.value, ast.Constant):
if field_name == "name":
name = item.value.value
elif field_name == "description":
desc = item.value.value
# Handle parenthesized multi-line strings (concatenated)
elif isinstance(item.value, ast.JoinedStr):
# f-strings - skip, use fallback
pass
elif isinstance(item.value, ast.BinOp):
# String concatenation with + - try to evaluate
try:
result = _eval_string_binop(item.value)
if result and field_name == "name":
name = result
elif result and field_name == "description":
desc = result
except Exception:
pass
return name, desc
return fallback_name, fallback_desc
except Exception:
return fallback_name, fallback_desc
def _eval_string_binop(node) -> str | None:
"""Recursively evaluate a BinOp of string constants."""
import ast
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return node.value
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
left = _eval_string_binop(node.left)
right = _eval_string_binop(node.right)
if left is not None and right is not None:
return left + right
return None
def _is_valid_agent_dir(path: Path) -> bool:
"""Check if a directory contains a valid agent (agent.json or agent.py)."""
if not path.is_dir():
return False
return (path / "agent.json").exists() or (path / "agent.py").exists()
def _has_agents(directory: Path) -> bool:
"""Check if a directory contains any valid agents (folders with agent.json or agent.py)."""
if not directory.exists():
return False
return any(_is_valid_agent_dir(p) for p in directory.iterdir())
def _getch() -> str:
"""Read a single character from stdin without waiting for Enter."""
try:
if sys.platform == "win32":
import msvcrt
ch = msvcrt.getch()
return ch.decode("utf-8", errors="ignore")
else:
import termios
import tty
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
except Exception:
return ""
def _read_key() -> str:
"""Read a key, handling arrow key escape sequences."""
ch = _getch()
if ch == "\x1b": # Escape sequence start
ch2 = _getch()
if ch2 == "[":
ch3 = _getch()
if ch3 == "C": # Right arrow
return "RIGHT"
elif ch3 == "D": # Left arrow
return "LEFT"
return ch
def _select_agent(agents_dir: Path) -> str | None:
"""Let user select an agent from available agents with pagination."""
AGENTS_PER_PAGE = 10
if not agents_dir.exists():
print(f"Directory not found: {agents_dir}", file=sys.stderr)
# fixes issue #696, creates an exports folder if it does not exist
agents_dir.mkdir(parents=True, exist_ok=True)
print(f"Created directory: {agents_dir}", file=sys.stderr)
# return None
agents = []
for path in agents_dir.iterdir():
if _is_valid_agent_dir(path):
agents.append(path)
agents.sort(key=lambda p: p.name)
if not agents:
print(f"No agents found in {agents_dir}", file=sys.stderr)
return None
# Pagination setup
page = 0
total_pages = (len(agents) + AGENTS_PER_PAGE - 1) // AGENTS_PER_PAGE
while True:
start_idx = page * AGENTS_PER_PAGE
end_idx = min(start_idx + AGENTS_PER_PAGE, len(agents))
page_agents = agents[start_idx:end_idx]
# Show page header with indicator
if total_pages > 1:
print(f"\nAvailable agents in {agents_dir} (Page {page + 1}/{total_pages}):\n")
else:
print(f"\nAvailable agents in {agents_dir}:\n")
# Display agents for current page (with global numbering)
for i, agent_path in enumerate(page_agents, start_idx + 1):
try:
name, desc = _extract_python_agent_metadata(agent_path)
desc = desc[:50] + "..." if len(desc) > 50 else desc
print(f" {i}. {name}")
print(f" {desc}")
except Exception as e:
print(f" {i}. {agent_path.name} (error: {e})")
# Build navigation options
nav_options = []
if total_pages > 1:
nav_options.append("←/→ or p/n=navigate")
nav_options.append("q=quit")
print()
if total_pages > 1:
print(f" [{', '.join(nav_options)}]")
print()
# Show prompt
print("Select agent (number), use arrows to navigate, or q to quit: ", end="", flush=True)
try:
key = _read_key()
if key == "RIGHT" and page < total_pages - 1:
page += 1
print() # Newline before redrawing
elif key == "LEFT" and page > 0:
page -= 1
print()
elif key == "q":
print()
return None
elif key in ("n", ">") and page < total_pages - 1:
page += 1
print()
elif key in ("p", "<") and page > 0:
page -= 1
print()
elif key.isdigit():
# Build number with support for backspace
buffer = key
print(key, end="", flush=True)
while True:
ch = _getch()
if ch in ("\r", "\n"):
# Enter pressed - submit
print()
break
elif ch in ("\x7f", "\x08"):
# Backspace (DEL or BS)
if buffer:
buffer = buffer[:-1]
# Erase character: move back, print space, move back
print("\b \b", end="", flush=True)
elif ch.isdigit():
buffer += ch
print(ch, end="", flush=True)
elif ch == "\x1b":
# Escape - cancel input
print()
buffer = ""
break
elif ch == "\x03":
# Ctrl+C
print()
return None
# Ignore other characters
if buffer:
try:
idx = int(buffer) - 1
if 0 <= idx < len(agents):
return str(agents[idx])
print("Invalid selection")
except ValueError:
print("Invalid input")
elif key == "\r" or key == "\n":
print() # Just pressed enter, redraw
else:
print()
print("Invalid input")
except (EOFError, KeyboardInterrupt):
print()
return None
def _interactive_multi(agents_dir: Path) -> int:
"""Interactive multi-agent mode with orchestrator."""
from framework.runner import AgentOrchestrator
if not agents_dir.exists():
print(f"Directory not found: {agents_dir}", file=sys.stderr)
return 1
orchestrator = AgentOrchestrator()
agent_count = 0
# Register all agents
for path in agents_dir.iterdir():
if _is_valid_agent_dir(path):
try:
orchestrator.register(path.name, path)
agent_count += 1
except Exception as e:
print(f"Warning: Failed to register {path.name}: {e}")
if agent_count == 0:
print(f"No agents found in {agents_dir}", file=sys.stderr)
return 1
print(f"\n{'=' * 60}")
print("Multi-Agent Interactive Mode")
print(f"Registered {agent_count} agents")
print(f"{'=' * 60}")
print("\nCommands:")
print(" /agents - List registered agents")
print(" /quit - Exit")
print(" {...} - JSON input to dispatch")
print()
while True:
try:
user_input = input(">>> ").strip()
except (EOFError, KeyboardInterrupt):
print("\nExiting...")
break
if not user_input:
continue
if user_input == "/quit":
break
if user_input == "/agents":
print("\nRegistered agents:")
for agent in orchestrator.list_agents():
print(f" - {agent['name']}: {agent['description'][:60]}...")
print()
continue
# Parse intent if provided
intent = None
if user_input.startswith("/intent "):
parts = user_input.split(" ", 2)
if len(parts) >= 3:
intent = parts[1]
user_input = parts[2]
# Try to parse as JSON
try:
context = json.loads(user_input)
except json.JSONDecodeError:
print("Error: Invalid JSON input. Use {...} format.")
continue
print(f"\nDispatching: {json.dumps(context)}")
if intent:
print(f"Intent: {intent}")
print("-" * 40)
result = asyncio.run(orchestrator.dispatch(context, intent=intent))
print(f"\nSuccess: {result.success}")
print(f"Handled by: {', '.join(result.handled_by) or 'none'}")
if result.error:
print(f"Error: {result.error}")
if result.results:
print("\nResults by agent:")
for agent_name, data in result.results.items():
print(f"\n {agent_name}:")
status = data.get("status", "unknown")
print(f" Status: {status}")
if "results" in data:
results_preview = json.dumps(data["results"], default=str)
if len(results_preview) > 150:
results_preview = results_preview[:150] + "..."
print(f" Results: {results_preview}")
print(f"\nMessage trace: {len(result.messages)} messages")
print()
orchestrator.cleanup()
return 0
def cmd_sessions_list(args: argparse.Namespace) -> int:
"""List agent sessions."""
print("⚠ Sessions list command not yet implemented")
print("This will be available once checkpoint infrastructure is complete.")
print(f"\nAgent: {args.agent_path}")
print(f"Status filter: {args.status}")
print(f"Has checkpoints: {args.has_checkpoints}")
return 1
def cmd_sessions_show(args: argparse.Namespace) -> int:
"""Show detailed session information."""
print("⚠ Session show command not yet implemented")
print("This will be available once checkpoint infrastructure is complete.")
print(f"\nAgent: {args.agent_path}")
print(f"Session: {args.session_id}")
return 1
def cmd_sessions_checkpoints(args: argparse.Namespace) -> int:
"""List checkpoints for a session."""
print("⚠ Session checkpoints command not yet implemented")
print("This will be available once checkpoint infrastructure is complete.")
print(f"\nAgent: {args.agent_path}")
print(f"Session: {args.session_id}")
return 1
def cmd_pause(args: argparse.Namespace) -> int:
"""Pause a running session."""
print("⚠ Pause command not yet implemented")
print("This will be available once executor pause integration is complete.")
print(f"\nAgent: {args.agent_path}")
print(f"Session: {args.session_id}")
return 1
def cmd_resume(args: argparse.Namespace) -> int:
"""Resume a session from checkpoint."""
print("⚠ Resume command not yet implemented")
print("This will be available once checkpoint resume integration is complete.")
print(f"\nAgent: {args.agent_path}")
print(f"Session: {args.session_id}")
if args.checkpoint:
print(f"Checkpoint: {args.checkpoint}")
if args.tui:
print("Mode: TUI")
return 1
def cmd_setup_credentials(args: argparse.Namespace) -> int:
"""Interactive credential setup for an agent."""
from framework.credentials.setup import CredentialSetupSession
agent_path = getattr(args, "agent_path", None)
if agent_path:
# Setup credentials for a specific agent
session = CredentialSetupSession.from_agent_path(agent_path)
else:
# No agent specified - show usage
print("Usage: hive setup-credentials <agent_path>")
print()
print("Examples:")
print(" hive setup-credentials exports/my-agent")
print(" hive setup-credentials examples/templates/deep_research_agent")
return 1
result = session.run_interactive()
return 0 if result.success else 1
def _open_browser(url: str) -> None:
"""Open URL in the default browser (best-effort, non-blocking)."""
import subprocess
import sys
try:
if sys.platform == "darwin":
subprocess.Popen(
["open", url],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
encoding="utf-8",
)
elif sys.platform == "linux":
subprocess.Popen(
["xdg-open", url],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
encoding="utf-8",
)
except Exception:
pass # Best-effort — don't crash if browser can't open
def _build_frontend() -> bool:
"""Build the frontend if source is newer than dist. Returns True if dist exists."""
import subprocess
# Find the frontend directory relative to this file or cwd
candidates = [
Path("core/frontend"),
Path(__file__).resolve().parent.parent.parent / "frontend",
]
frontend_dir: Path | None = None
for c in candidates:
if (c / "package.json").is_file():
frontend_dir = c.resolve()
break
if frontend_dir is None:
return False
dist_dir = frontend_dir / "dist"
src_dir = frontend_dir / "src"
# Skip build if dist is up-to-date (newest src file older than dist index.html)
index_html = dist_dir / "index.html"
if index_html.exists() and src_dir.is_dir():
dist_mtime = index_html.stat().st_mtime
needs_build = False
for f in src_dir.rglob("*"):
if f.is_file() and f.stat().st_mtime > dist_mtime:
needs_build = True
break
if not needs_build:
return True
# Need to build
print("Building frontend...")
try:
# Ensure deps are installed
subprocess.run(
["npm", "install", "--no-fund", "--no-audit"],
encoding="utf-8",
cwd=frontend_dir,
check=True,
capture_output=True,
)
subprocess.run(
["npm", "run", "build"],
encoding="utf-8",
cwd=frontend_dir,
check=True,
capture_output=True,
)
print("Frontend built.")
return True
except FileNotFoundError:
print("Node.js not found — skipping frontend build.")
return dist_dir.is_dir()
except subprocess.CalledProcessError as exc:
stderr = exc.stderr.decode(errors="replace") if exc.stderr else ""
print(f"Frontend build failed: {stderr[:500]}")
return dist_dir.is_dir()
def cmd_serve(args: argparse.Namespace) -> int:
"""Start the HTTP API server."""
import logging
from aiohttp import web
_build_frontend()
from framework.server.app import create_app
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
model = getattr(args, "model", None)
app = create_app(model=model)
async def run_server():
manager = app["manager"]
# Preload agents specified via --agent
for agent_path in args.agent:
try:
session = await manager.create_session_with_worker(agent_path, model=model)
info = session.worker_info
name = info.name if info else session.worker_id
print(f"Loaded agent: {session.worker_id} ({name})")
except Exception as e:
print(f"Error loading {agent_path}: {e}")
# Start server using AppRunner/TCPSite (same pattern as webhook_server.py)
runner = web.AppRunner(app, access_log=None)
await runner.setup()
site = web.TCPSite(runner, args.host, args.port)
await site.start()
# Check if frontend is being served
dist_candidates = [
Path("frontend/dist"),
Path("core/frontend/dist"),
]
has_frontend = any((c / "index.html").exists() for c in dist_candidates if c.is_dir())
dashboard_url = f"http://{args.host}:{args.port}"
print()
print(f"Hive API server running on {dashboard_url}")
if has_frontend:
print(f"Dashboard: {dashboard_url}")
print(f"Health: {dashboard_url}/api/health")
print(f"Agents loaded: {sum(1 for s in manager.list_sessions() if s.worker_runtime)}")
print()
print("Press Ctrl+C to stop")
# Auto-open browser if --open flag is set and frontend exists
if getattr(args, "open", False) and has_frontend:
_open_browser(dashboard_url)
# Run forever until interrupted
try:
await asyncio.Event().wait()
except asyncio.CancelledError:
pass
finally:
await manager.shutdown_all()
await runner.cleanup()
try:
asyncio.run(run_server())
except KeyboardInterrupt:
print("\nServer stopped.")
return 0