CLI / TUI / Web
Verantwortung
Die UI-Schicht ist ein weiterer Satz Eingänge, über die Nutzer den Agenten erreichen (neben den Gateway (gateway)-Nachrichtenplattformen): CLI (hermes_cli/, Befehlszeilen-Subcommand-System), TUI (ui-tui/, TypeScript-Terminal-UI + tui_gateway/-Python-Brücke), Web (web/, Browser-Oberfläche). Alle drei leiten die Nutzereingaben letztlich in run_conversation des Agenten und rendern die Ausgabe zurück. Sie stehen parallel zur Gateway-Schicht – das Gateway bindet externe Plattformen an, die UI-Schicht bindet lokale Nutzer an.
Schlüsseldateien
CLI
hermes_cli/__init__.py— Package-Einstieg_parser.py— Kommandozeilen-Argument-Parsingauth.py/auth_commands.py— Authentifizierung (Nous Portal u. a.)active_sessions.py— aktive Sitzungen (sessions) verwaltenbackup.py/checkpoints.py— Backup und Checkpointsblueprint_cmd.py— Blaupausen-Commandbrowser_connect.py/bundles.py/callbacks.pybanner.py/build_info.py— Banner und Build-Info
TUI
ui-tui/README— Terminal-UI (TypeScript, inkl. packages/src)ui-tui/package.json— Abhängigkeiten und Skriptetui_gateway/entry.py— TUI-Gateway-Einstiegserver.py— TUI-Servertransport.py/ws.py— Transport und WebSocketevent_publisher.py— Event-Publishinghost_supervisor.py/compute_host.py— Host-Aufsichtslash_worker.py/synthetic_turn.py/render.py/project_tree.py_stdin_recovery.py— Stdin-Wiederherstellung
Web
web/README— Browser-Oberflächeweb/index.html/vite.config.ts— Vite-Einstiegweb/src/— Frontend-Quellen
CLI-Subcommand-Oberfläche
hermes_cli/_parser.py kapselt den Top-Level-argparse separat, sodass Module wie relaunch.py introspektieren können, welche Flags existieren, ohne main laufen zu lassen. Der Parser kümmert sich nur um das chat-Subcommand und die globalen Optionen; die anderen Subcommands werden in main.py direkt konstruiert (eng gekoppelt mit den cmd_*-Funktionen). Das _EPILOGUE gibt die Subcommand-Panorama-Sicht, die der Nutzer sieht:
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)Die CLI ist sowohl «Dialog-Einstieg» als auch «Ops-Panel» — auth/model/config/gateway/dashboard/backup hängen alle am selben Subcommand-Baum. Nur chat und seine Derivate (-c/--resume) gehen in run_conversation; die anderen laufen direkt durch ihre jeweiligen Module.
stdin/JSON-RPC-Hauptschleife des TUI-Gateways
Das TUI verlagert die Terminal-Renderierung auf die TypeScript-Seite (ui-tui/); auf der Python-Seite läuft nur eine schmale tui_gateway-Brücke. Beide kommunizieren über JSON-RPC auf stdin/stdout. Der Kern von main() in tui_gateway/entry.py ist eine readline + dispatch-Schleife (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 entscheidet, ob weiter.
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)Ein paar Engineering-Details: handle_spurious_eof fängt das «falsche EOF» ab, das entsteht, wenn der Kindprozess O_NONBLOCK umdreht; _log_exit schreibt ein Crash-Log, wenn der Schreibversuch auf stdout scheitert; SIGPIPE wird explizit ignoriert (signal.signal(signal.SIGPIPE, signal.SIG_IGN)), damit ein Hintergrund-Thread (TTS/Beep), der auf eine bereits geschlossene Pipe schreibt, nicht den ganzen Prozess mitreißt.
Alle drei laufen am Ende in run_conversation zusammen
Egal, über welchen Einstieg man kommt — dialogorientierte Requests landen letztlich bei AIAgent.run_conversation; das ist nur ein Forwarder; die echte Hauptschleife steht in 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, ...)Die Sitzungs-ID wird über ein ContextVar nach unten gereicht — alle LLM-Aufrufe innerhalb der Hauptschleife (Kompression (compression), Vision, MoA-Slot, Background-Review-Fork) tragen automatisch dasselbe Tag, ohne dass an jedem Aufrufpunkt explizit übergeben werden muss. Die Agent-Instanz wird von init_agent (agent/agent_init.py:276) zusammengebaut; die lange Liste von Callback-Parametern in der Signatur (step_callback/stream_delta_callback/event_callback/…) sind die Haken der drei Hüllen — derselbe Agent, verschiedene Hüllen hängen verschiedene Callbacks ein, und die Render-Form unterscheidet sich komplett.
Datenfluss
- CLI:
hermes <subcommand>→hermes_cli/_parser.pyparsed → das Subcommand-Modul (auth/backup/blueprint/…) führt aus; dialogorientierte Commands rufen letztlichAIAgent.run_conversation:6350auf. - TUI:
ui-tui/(TS-Frontend) verbindet sich per WebSocket mittui_gateway/ws.py;tui_gatewayübersetzt Terminal-Events an den Agenten (tui_gateway/server.pyhält den Agenten), Output wird übertui_gateway/event_publisher.pyans Frontend gepusht. - Web:
web/(Vite-Frontend) bindet sich ebenfalls über ein Backend an den Agenten und rendert Dialog und Werkzeugergebnisse (tool results). - Die Agent-Instanz aller drei wird über
init_agent:276zusammengebaut und teilt sich die Fähigkeitsschicht.
Designmotiv
Warum teilen sich drei Hüllen ein einziges run_conversation statt jede ihre eigene Schleife? Kern ist «Render-Unterschied vs Geschäfts-Konsistenz». Streaming-Token, Werkzeug-Fortschritt (tool progress), Event-Format sind Sache der Hülle; Sitzungszustand, Werkzeugaufruf, Budget (budget)-Abbuch, Memory-Injektion sind Sache des Agenten. Die Hülle an der Callback-Schicht zu isolieren, erlaubt einen Hüllenwechsel ohne Geschäftsänderung — und Geschäftsänderung, ohne drei Stellen anzufassen. tui_gateway existiert separat, weil die TS-Seite den Python-Agenten nicht direkt halten kann — es ist eine Sprachbrücke, keine Geschäftsschicht.
Grenzen und Fehler
- TUIs stdin/stdout ist eine JSON-RPC-Pipe, kein Log.
print-Debugging, das wild auf stdout schreibt, bringt das TUI-Parsing zum Absturz; Python-seitige Logs laufen über stderr und werden vom TUI alsgateway.stderr-Event ans Activity-Panel geliefert. tui_gateway-Kindprozess abgestürzt, TUI verbindet sich nicht automatisch neu._CRASH_LOGdient der nachträglichen Untersuchung, nicht der Wiederherstellung — der Nutzer musshermes --tuimanuell neu starten.- Web ist kein Ersatz für das TUI.
web/läuft überhermes dashboard(Port 9119) und ist als lokale Konsole + Lange-Sitzungen-Betrachter gedacht, nicht als Web-Produkt für Multi-User-Remote-Zugriff.
Zusammenfassung
UI-Schicht = drei Hüllen (CLI/TUI/Web) + eine tui_gateway-Brücke. Sie tragen keine Geschäftslogik, sondern nur Ein-/Ausgabeform. Parallel zum Gateway: das Gateway bindet externe Nachrichtenplattformen an, die UI-Schicht bindet lokales Terminal/Browser an. ACP (siehe hier) bindet die IDE an und komplettiert die «Erreichungsfläche».