Skip to content

CLI / TUI / Web

源码版本v2026.7.20

Responsabilidad

La capa de interfaces es otro grupo de entradas para que el usuario alcance al agent (además de las plataformas de mensajes del gateway): CLI (hermes_cli/, sistema de subcomandos), TUI (ui-tui/, UI de terminal en TypeScript + tui_gateway/ puente en Python), Web (web/, interfaz en navegador). Los tres terminan entregando la entrada del usuario al run_conversation del agent y renderizando su salida de vuelta. Son paralelos a la capa de gateway: el gateway conecta plataformas externas; la capa de interfaces, al usuario local.

Archivos clave

CLI

TUI

Web

Superficie de subcomandos CLI

hermes_cli/_parser.py aísla el argparse de nivel superior para que módulos como relaunch.py puedan introspeccionar los flags sin correr main. El parser solo maneja el subcomando chat y las opciones globales; el resto de subcomandos se construyen in-place en main.py (fuertemente acoplados a las funciones cmd_*). El epílogo muestra el panorama de subcomandos que ve el usuario:

python
hermes                        Start interactive chat
hermes --tui                  Launch the modern TUI
hermes auth add <provider>    Add a pooled credential
hermes gateway                Run messaging gateway
hermes dashboard              Start web UI dashboard (port 9119)

El CLI es a la vez «entrada de conversación» y «panel de ops»: auth/model/config/gateway/dashboard/backup cuelgan todos del mismo árbol de subcomandos. Solo chat y sus derivados (-c/--resume) entran en run_conversation; el resto va directo a su módulo.

Bucle principal stdin/JSON-RPC del gateway TUI

La TUI deja el renderizado del terminal en el lado TypeScript (ui-tui/); el lado Python solo corre un puente ligero tui_gateway. Los dos hablan JSON-RPC por stdin/stdout. El núcleo de main() en tui_gateway/entry.py es un bucle de readline + dispatch (tui_gateway/entry.py):

python
while True:
    raw = sys.stdin.readline()
    if not raw:
        # Stdin fell through — spurious EOF (child flipped O_NONBLOCK)
        # or genuine close.  handle_spurious_eof 决定是否继续。
        if not handle_spurious_eof(_recovery_times, _log_exit):
            break
        continue
    try:
        req = json.loads(raw.strip())
    except json.JSONDecodeError:
        write_json({"jsonrpc": "2.0", "error": {"code": -32700, ...}, "id": None})
        continue
    resp = dispatch(req)

Varios detalles de ingeniería: handle_spurious_eof trata el «EOF espurio» que provoca el hijo al voltear O_NONBLOCK; _log_exit deja un log de crash si la escritura a stdout falla; SIGPIPE se ignora explícitamente (signal.signal(signal.SIGPIPE, signal.SIG_IGN)) para que un hilo en segundo plano (TTS/beep) que escriba a un pipe cerrado no arrastre todo el proceso.

Los tres terminan confluyendo en run_conversation

Sea cual sea la entrada, las peticiones de conversación terminan llamando a AIAgent.run_conversation — un mero forwarder; el bucle real vive en agent/conversation_loop.py (run_agent.py:6350):

python
def run_conversation(self, user_message, system_message=None,
                     conversation_history=None, ..., moa_config=None) -> Dict[str, Any]:
    """Forwarder — see ``agent.conversation_loop.run_conversation``."""
    from agent.conversation_loop import run_conversation
    from agent.portal_tags import set_conversation_context
    token = set_conversation_context(self._conversation_root_id())
    with scoped_runtime_main({}):
        return run_conversation(self, user_message, ...)

El ID de conversación se propaga por ContextVar — todas las llamadas LLM dentro del bucle principal (compresión (compression), visión, slot MoA, fork de revisión en segundo plano) llevan la misma etiqueta sin que cada punto de llamada tenga que pasarla explícita. La instancia del agent la ensambla init_agent (agent/agent_init.py:276); la larga lista de callbacks en la firma (step_callback/stream_delta_callback/event_callback/...) es el gancho para las tres shells — un mismo agent, distintas shells cuelgan callbacks distintos y la forma de render cambia por completo.

Flujo de datos

  1. CLI: hermes <subcommand>hermes_cli/_parser.py parsea → el módulo del subcomando (auth/backup/blueprint/…) ejecuta; los subcomandos de conversación terminan llamando a AIAgent.run_conversation:6350.
  2. TUI: ui-tui/ (frontend TS) se conecta por WebSocket a tui_gateway/ws.py; tui_gateway puentea los eventos del terminal al agent (tui_gateway/server.py mantiene el agent) y publica la salida vía tui_gateway/event_publisher.py al frontend.
  3. Web: web/ (frontend Vite) se conecta igualmente al agent por un backend y renderiza la conversación y los resultados de herramientas.
  4. La instancia de agent en los tres casos se ensambla vía init_agent:276 y comparte la misma capa de capacidades.

Motivo de diseño

¿Por qué tres shells comparten un único run_conversation en lugar de correr cada una su propio bucle? El principio es «diferencia de renderizado vs consistencia de negocio». Tokens en streaming, progreso de herramientas (tool), formato de eventos — asunto de la shell; estado de conversación, llamada a herramienta, decremento de presupuesto (budget), inyección de memoria — asunto del agent. Aislar la shell en la capa de callbacks permite cambiar de shell sin tocar el negocio y añadir lógica de negocio sin tocar las tres shells. tui_gateway existe porque el lado TS no puede tener al agent Python directamente — es un puente de lenguaje, no una capa de negocio.

Límites y fallos

  • El stdin/stdout del gateway TUI es un pipe JSON-RPC, no un log. Un print de depuración que escriba stdout rompe el parser de la TUI; los logs de Python van por stderr y la TUI los trata como eventos gateway.stderr que se llevan al panel de Activity.
  • El proceso hijo de tui_gateway no reconecta solo al caer. _CRASH_LOG es para forense posterior, no para recuperación — el usuario tiene que reiniciar hermes --tui a mano.
  • Web no sustituye a la TUI. web/ se sirve desde hermes dashboard (puerto 9119); su rol es consola local + visualización de conversaciones largas, no un producto web multiusuario remoto.

Resumen

La capa de interfaces = tres carcasas (CLI/TUI/Web) + un puente tui_gateway. No albergan lógica de negocio; solo se ocupan de la forma de entrada/salida. Son paralelas a la capa de gateway: el gateway conecta plataformas externas de mensajes; la capa de interfaces conecta al terminal/navegador local. ACP (ver aquí) conecta el IDE, completando la «superficie de contacto».

Sitio de aprendizaje comunitario no oficial. Basado en el código fuente de NousResearch/hermes-agent (licencia MIT).