Skip to content

CLI / TUI / Web

源码版本v2026.7.20

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

TUI

Web

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:

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)

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):

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 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):

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, ...)

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

  1. CLI: hermes <subcommand>hermes_cli/_parser.py parsed → das Subcommand-Modul (auth/backup/blueprint/…) führt aus; dialogorientierte Commands rufen letztlich AIAgent.run_conversation:6350 auf.
  2. TUI: ui-tui/ (TS-Frontend) verbindet sich per WebSocket mit tui_gateway/ws.py; tui_gateway übersetzt Terminal-Events an den Agenten (tui_gateway/server.py hält den Agenten), Output wird über tui_gateway/event_publisher.py ans Frontend gepusht.
  3. Web: web/ (Vite-Frontend) bindet sich ebenfalls über ein Backend an den Agenten und rendert Dialog und Werkzeugergebnisse (tool results).
  4. Die Agent-Instanz aller drei wird über init_agent:276 zusammengebaut 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 als gateway.stderr-Event ans Activity-Panel geliefert.
  • tui_gateway-Kindprozess abgestürzt, TUI verbindet sich nicht automatisch neu. _CRASH_LOG dient der nachträglichen Untersuchung, nicht der Wiederherstellung — der Nutzer muss hermes --tui manuell neu starten.
  • Web ist kein Ersatz für das TUI. web/ läuft über hermes 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».

Inoffizielle Community-Lernseite. Basiert auf dem MIT-lizenzierten NousResearch/hermes-agent-Quellcode.