@aidalinfo/hermes-bridge
v0.1.4
Published
Relais MCP pour la communication synchrone entre agents Hermes.
Readme
hermes-bridge
Relais MCP pour la communication synchrone et multi-tour entre agents Hermes (framework Nous Research). Un bot Hermes peut déléguer une tâche ou poser une question à un autre bot Hermes connu du relais et attendre sa réponse — sans dépendre d'un service tiers (pas de Teams, pas de ntfy, pas de Raft).
Architecture
bot A (adapter) relais (src/server/) bot B (adapter)
────────────── ──────────────────── ──────────────
tool ask_agent(to=B,...) ───HTTP/MCP──▶ handleAskAgent
│ registry.has(B)?
│ ConversationStore.createRequest ── wake JSON ──WS──▶ _on_wake()
│ (request_id, timer=ask_timeout_ms) │
│ │ tour d'inférence
│ ◀── heartbeat {request_id} ──WS── │ (post_tool_call/
│ extendRequest (ré-arme le timer) │ post_llm_call)
│ │
│ ◀── tool reply(request_id, answer) ───HTTP/MCP────────┘
ask_agent() se résout ◀── answer ────┘ resolveRequest- Le relais (
src/server/) expose trois tools MCP —ask_agent,reply,list_agents— surmcp_servers(transport HTTP), et un endpoint WebSocket (/bridge/connect) que chaque bot rejoint en sortant (jamais l'inverse : le relais n'a besoin d'aucun accès réseau vers les bots). - L'adapter (
adapter/) est un plugin "platform" Hermes installé dans/opt/data/plugins/hermes-bridge/de chaque bot — il réveille l'agent (déclenche un tour d'inférence) quand un message arrive, sans toucher au core Hermes ni nécessiter un rebuild d'image.- ⚠️ Le chemin compte : c'est
<HERMES_HOME>/plugins/<name>/, pas<HERMES_HOME>/.hermes/plugins/<name>/.get_hermes_home()(Hermes) n'ajoute.hermesque quandHERMES_HOMEest absent (défaut natif~/.hermes) — l'image Docker des bots fixeHERMES_HOME=/opt/dataexplicitement, donc le dossier de scan réel est/opt/data/plugins.npx @aidalinfo/hermes-bridge installgère ça correctement depuis la 0.1.1 ; si un bot a été installé avant, relancerinstallpour corriger l'emplacement, puis redémarrer le conteneur.
- ⚠️ Le chemin compte : c'est
ask_agentbloque jusqu'à ce que l'agent cible appellereply, ou jusqu'au timeout (défaut 120s, configurable viaask_timeout_ms). Réutiliser le mêmeconversation_idpermet un échange multi-tour séquentiel ; Hermes conserve l'historique automatiquement via sonchat_idde session.- Timeout intelligent (heartbeat) :
ask_timeout_msn'est qu'un filet de sécurité contre un agent réellement bloqué/planté, pas une estimation à deviner pour les réponses lentes (plusieurs tool calls, lookup mémoire…). L'adapter de l'agent cible s'abonne aux hooks Hermespre_llm_call/post_tool_call/post_llm_call(les mêmes points d'extension que le statut « busy » natif de Hermes, et le même pattern que l'adapterraftbundlé). Tant que la session ouverte par le wake est active, l'adapter envoie une frame{"type":"heartbeat","request_id":"..."}sur la même connexion WebSocket sortante (pas un nouveau canal), throttlée à 1 toutes les 5s par session. Le relais (ConversationStore.extendRequest) ré-arme alors le timer de cerequest_idpour une fenêtre complète.on_session_endnettoie le suivi quand le tour se termine. Résultat : le délai ne compte vraiment que si l'agent s'est arrêté de travailler, pas s'il est juste lent.- ⚠️ Le point piégeux : les hooks Hermes exposent
session_id = agent.session_id, un identifiant généré à chaque run d'agent (f"{timestamp}_{uuid}") — sans aucun rapport avec la clé de session que l'adapter calcule lui-même pour le routage (gateway.session.build_session_key, utilisée pour la queue de wakes, jamais exposée aux hooks). Impossible donc de précalculer la correspondancesession_id → request_idau moment du wake. La solution :wake.build_wake_text()embarque déjàrequest_id=<id>en clair dans le texte injecté ; le hookpre_llm_call(seul à fournir à la foissession_idetuser_message) relit cet identifiant dans le texte (wake.extract_request_id) et fixe la correspondance à ce moment précis — lespost_tool_call/post_llm_callsuivants du même run la réutilisent. Autre piège du même hook :platformy est le membre d'enumgateway.config.Platform(pas une chaîne) —platform_value()le déballe avant toute comparaison, sans quoi le filtre== "hermes-bridge"est toujours faux. Sans ces deux corrections, le heartbeat ne se déclenche jamais (échec silencieux — aucune erreur, juste des frames qui ne partent jamais), etask_timeout_msreste un mur fixe malgré un adapter et un relais à jour. - ⚠️ Ce mécanisme est entièrement côté adapter + relais — aucune action requise de l'agent/LLM cible (il ne « sait » même pas que ça existe).
- ⚠️ Le relais doit être redéployé pour que le heartbeat fonctionne :
publier une nouvelle version npm de l'adapter ne suffit pas, le serveur
(
src/server/bridge-ws.ts+conversations.ts) doit tourner avec le code à jour pour comprendre les framesheartbeat.
- ⚠️ Le point piégeux : les hooks Hermes exposent
Détails de conception complets : voir manageai/docs/superpowers/specs/2026-06-30-hermes-bridge-design.md.
Déployer le relais
docker build -t hermes-bridge .
docker run -d -p 8787:8787 -v $(pwd)/config.yaml:/app/config.yaml:ro hermes-bridgeconfig.yaml (voir config.example.yaml) :
agents:
- name: daniel-bot
token: <token-secret-par-bot>
- name: helpdesk-bot
token: <token-secret-par-bot>
ask_timeout_ms: 120000Installer l'adapter sur un bot
docker exec -it -u hermes <bot> npx @aidalinfo/hermes-bridge install \
--token=<token-du-bot> \
--relay-url=wss://<host-du-relais>/bridge/connectPuis redémarrer le conteneur du bot pour charger le plugin.
Ajouter le relais aux mcp_servers du bot
mcp_servers:
hermes-bridge:
enabled: true
transport: http
url: https://<host-du-relais>/mcp
headers:
Authorization: Bearer ${HERMES_BRIDGE_TOKEN}
access_mode: read_writePersistance (mode db)
Par défaut, l'historique des échanges vit en mémoire (maxHistory=200,
telemetry.ts) et disparaît à chaque redémarrage du relais — y compris
un redeploy Coolify normal sur push. Pour une traçabilité durable (audit,
« qu'est-ce que daniel-bot a répondu à helpdesk-bot mardi dernier ? »),
configurez une base — seul postgres est implémenté, et c'est le driver par
défaut :
db:
driver: postgres # défaut si omis
connection_string: postgresql://user:pass@host:5432/hermes_bridgeconnection_string peut aussi venir de la variable d'env DATABASE_URL
(recommandé — évite de committer un secret dans config.yaml ; dans ce cas
le bloc db: peut être omis entièrement). Le mode db s'active dès que
config.db.connection_string ou DATABASE_URL est renseigné.
Ce que ça change concrètement :
- La table
hermes_bridge_exchangesest créée automatiquement au démarrage (src/server/db.ts,CREATE TABLE IF NOT EXISTS) — aucune migration manuelle. - Chaque
recordStart/recordEndécrit dans la base en plus de la mémoire, en fire-and-forget (comme l'export Langfuse existant) : une panne db ne bloque jamais unask_agent/reply, juste unconsole.warn(throttlé à une fois). /uiet/ui/api/statelisent depuis la base quand le mode db est actif (pas depuis la mémoire) — c'est ce qui les rend durables : le flux affiché après un redémarrage n'est plus vide, il reprend l'historique. En cas d'échec de lecture db, repli silencieux sur la mémoire (mieux vaut un historique tronqué qu'une page cassée).- Sans
dbconfiguré, comportement strictement inchangé (mémoire uniquement, comme avant cette fonctionnalité).
Observabilité
Chaque échange ask_agent → reply (ou timeout/déconnexion) peut être
exporté vers une instance Langfuse existante
(cloud ou self-hosted), regroupé par conversation_id — les échanges
multi-tours d'une même conversation apparaissent comme plusieurs spans
d'une seule trace :
langfuse:
public_key: pk-lf-...
secret_key: sk-lf-...
base_url: https://cloud.langfuse.com # optionnel, défaut cloud LangfuseSans cette section, le relais fonctionne normalement sans appel réseau vers
Langfuse. Langfuse et le mode db sont indépendants — Langfuse pour tracer
en externe, le mode db pour l'audit local//ui durable — activables
séparément ou ensemble.
Le relais expose aussi une page /ui (ex: http://<host-du-relais>:8787/ui),
« Conversations entre agents » — layout et styles Forma importés du
projet Claude Design
Visualiser les conversations d'agents
(voir src/server/ui.ts, réimplémenté en HTML/JS sans dépendance, branché sur
les vraies données au lieu des exemples du prototype) :
- Un badge par agent connu (en ligne / hors ligne, point de couleur), rangée du haut.
- Une recherche texte (message + réponse/erreur) et un filtre par agent.
- Un flux des échanges les plus récents en premier, chacun avec
from → to, durée, badge de statut (ok,timeout,agent hors ligne,agent déconnecté,agent inconnu,conversation inconnue,en cours), message tronqué à 180 caractères avec un bouton Voir plus/moins qui révèle la réponse (ou « En attente de réponse… » tant que c'estpending). - Rafraîchissement automatique (
fetch('/ui/api/state')toutes les 3s) sans perdre la recherche/le filtre/les échanges dépliés en cours.
Cette page n'est pas authentifiée — elle affiche le contenu intégral des messages/réponses. Si le relais est exposé au-delà d'un LAN de confiance, mettez-la derrière un reverse-proxy protégé.
Développement
npm install
npm test # tests TypeScript (vitest)
pytest adapter/test # tests Python (wake.py — logique pure, sans dépendance Hermes)
npm run dev # lance le relais localement (HERMES_BRIDGE_CONFIG, PORT)