CLI / TUI / Web
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
hermes_cli/__init__.py— entrada del paquete_parser.py— parsing de argumentosauth.py/auth_commands.py— autenticación (Nous Portal, etc.)active_sessions.py— gestión de sesiones activasbackup.py/checkpoints.py— backups y checkpointsblueprint_cmd.py— comando de blueprintsbrowser_connect.py/bundles.py/callbacks.pybanner.py/build_info.py— banner e información de build
TUI
ui-tui/README— UI de terminal (TypeScript, con packages/src)ui-tui/package.json— dependencias y scriptstui_gateway/entry.py— entrada del gateway TUIserver.py— servidor del TUItransport.py/ws.py— transporte y WebSocketevent_publisher.py— publicación de eventoshost_supervisor.py/compute_host.py— supervisión del hostslash_worker.py/synthetic_turn.py/render.py/project_tree.py_stdin_recovery.py— recuperación de stdin
Web
web/README— interfaz en navegadorweb/index.html/vite.config.ts— entrada de Viteweb/src/— código fuente del frontend
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:
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):
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):
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
- CLI:
hermes <subcommand>→hermes_cli/_parser.pyparsea → el módulo del subcomando (auth/backup/blueprint/…) ejecuta; los subcomandos de conversación terminan llamando aAIAgent.run_conversation:6350. - TUI:
ui-tui/(frontend TS) se conecta por WebSocket atui_gateway/ws.py;tui_gatewaypuentea los eventos del terminal al agent (tui_gateway/server.pymantiene el agent) y publica la salida víatui_gateway/event_publisher.pyal frontend. - Web:
web/(frontend Vite) se conecta igualmente al agent por un backend y renderiza la conversación y los resultados de herramientas. - La instancia de agent en los tres casos se ensambla vía
init_agent:276y 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
printde depuración que escriba stdout rompe el parser de la TUI; los logs de Python van por stderr y la TUI los trata como eventosgateway.stderrque se llevan al panel de Activity. - El proceso hijo de
tui_gatewayno reconecta solo al caer._CRASH_LOGes para forense posterior, no para recuperación — el usuario tiene que reiniciarhermes --tuia mano. - Web no sustituye a la TUI.
web/se sirve desdehermes 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».