Quand Hermes attend un humain, votre plugin peut désormais le savoir


Il est deux heures du matin et votre exécution Hermes sur un serveur tombe sur une commande qui réclame un mot de passe sudo, ou fait surgir une invite d’approbation pour une commande dangereuse. Personne n’est devant l’écran. L’exécution s’arrête simplement jusqu’au délai d’expiration et échoue, et à neuf heures le lendemain matin vous trouvez une froide ligne de sortie dans le terminal. Ce que vous vouliez était trivial : quand ça bloque, prévenez-moi.

C’était autrefois pénible à construire. Hermes s’arrête pour une personne à plusieurs endroits sans lien — l’invite de mot de passe sudo, une question de l’outil clarify, une approbation de commande dangereuse — et chacun vit sur son propre chemin de code. Pour obtenir une seule notification « quelqu’un est nécessaire », il fallait enregistrer un hook par type d’invite, puis faire correspondre des motifs sur les chaînes de commandes du terminal pour deviner lesquelles étaient sudo.

La PR #133171, fusionnée le 5 octobre 2026, comble cette lacune avec une paire de hooks générique qui se déclenche chaque fois que l’agent s’arrête et attend une personne.

La paire de hooks

on_human_input_request   — fires right before the prompt is shown
on_human_input_resolved  — fires once per request when the wait ends

Le hook de requête se trouve dans le code partagé par lequel passe déjà toutes les surfaces, donc CLI, Ink TUI / Desktop, ACP et les plateformes gateway (Telegram, Discord, …) sont tous couverts sans câblage propre à chaque surface. Ce qu’il couvre :

kind Quelle invite Où elle apparaît
"sudo" Invite de mot de passe sudo masquée avant l’exécution d’une commande sudo CLI interactive, Ink TUI / Desktop (carte sudo.request), repli /dev/tty
"clarify" Une question de l’outil clarify CLI, Ink TUI / Desktop, plateformes gateway
"approval" Approbation de commande dangereuse / d’écriture, consentement d’élicitation MCP, transports d’approbation de plugin CLI, Ink TUI / Desktop, ACP, plateformes gateway, plugins utilisant ctx.register_approval_transport

Deux cas ne se déclenchent délibérément pas : les décisions approvals.mode=smart prises par le modèle auxiliaire, car aucune personne n’est sollicitée du tout ; et les approbations gateway qui rejoignent une invite identique déjà en attente, car la personne voit une seule invite et la paire de hooks se déclenche une fois pour elle.

Ce que reçoit votre callback

def my_callback(kind: str, request_id: str, session_id: str, session_key: str,
                platform: str, prompt: str, **kwargs):
    ...
Paramètre Description
kind "sudo", "clarify" ou "approval" ; de nouveaux types peuvent être ajoutés, donc ignorez ceux que vous ne gérez pas
request_id Unique par invite ; le on_human_input_resolved correspondant porte la même valeur
session_id Identifiant de session Hermes quand il est connu, sinon ""
session_key Clé de routage de session/chat, pratique pour cadrer les notifications par chat
platform Plateforme gateway ("telegram", "discord", …) ou source locale ("cli", "tui", "desktop", …)
prompt Ce qu’on demande à la personne : la commande pour sudo/approval, le texte de la question pour clarify

Un détail à propos de prompt mérite son propre paragraphe. Il passe toujours par le rédacteur de secrets (redact_sensitive_text(..., force=True)), même lorsque security.redact_secrets est désactivé dans votre configuration. Si la rédaction elle-même échoue, prompt est une chaîne vide — le texte brut n’est jamais envoyé. Le mot de passe sudo que vous tapez et les réponses aux questions clarify ne sont jamais transmis à l’un ou l’autre hook ; seul le libellé du résultat l’est.

Les valeurs de retour sont ignorées, et un callback qui lève une exception est journalisé au niveau debug pendant que l’invite se poursuit — un observateur cassé ne peut pas casser une invite humaine. Les callbacks s’exécutent sur le thread qui est sur le point de bloquer, donc gardez-les rapides : confiez les appels à un service de push ou à un webhook à un thread en arrière-plan.

Valeurs de résultat

on_human_input_resolved se déclenche exactement une fois par requête, après la fin de l’attente, avec un champ supplémentaire :

kind valeurs outcome
"sudo" "provided", "skipped" (réponse vide), "timeout", "cancelled", "error"
"clarify" le résultat propre à la surface : "submitted", "timed_out", "cancelled", "undelivered", "terminated", ou "error"
"approval" "once", "session", "always", "deny", "timeout", "cancelled", "notify_failed", ou "transport_<failure>" pour un transport de plugin en échec

Un exemple vraiment utile : ne m’alerter que si personne ne répond

Le besoin courant n’est pas « me notifier à chaque invite » mais « me relancer si personne ne s’en occupe ». La paire requête/résolution rend cela naturel :

import threading

def push_notification(text):
    ...  # appelez votre service de push ici

def register(ctx):
    pending = {}

    def on_request(kind, request_id, prompt, session_key, **kwargs):
        pending[request_id] = threading.Timer(
            30, push_notification, (f"Hermes needs you ({kind}): {prompt[:80]}",)
        )
        pending[request_id].start()  # n'alerte que si personne ne répond dans les 30 s

    def on_resolved(request_id, **kwargs):
        timer = pending.pop(request_id, None)
        if timer:
            timer.cancel()

    ctx.register_hook("on_human_input_request", on_request)
    ctx.register_hook("on_human_input_resolved", on_resolved)

La propriété agréable ici est que les invites auxquelles un humain répond ne vous parviennent jamais, et seules les exécutions réellement bloquées déclenchent une alerte. Comme request_id est apparié, vous n’avez ni alertes en double ni minuteurs orphelins. Pour la forme du système de hooks dans son ensemble — hooks observateurs contre hooks intercepteurs, enregistrement et cycle de vie — voir la référence de commande hooks et le guide des hooks de plugin provider précédent.

Statut de release

La paire de hooks est sur main et dans aucune étiquette publiée pour l’instant (la release la plus récente est v0.21.5, tag v2026.9.24). Les installations Git la récupèrent en tirant le dernier main ; les images Docker et les déploiements hébergés l’obtiennent avec le prochain tag. Pour savoir comment écrire le plugin lui-même, voir la référence de commande plugins.

Pourquoi cela compte au-delà des notifications

Construire une alerte « Hermes a besoin d’un humain » exigeait autrefois de connaître les chemins de code internes de chaque type d’invite. Désormais, c’est une paire de hooks, un champ kind et un ensemble de libellés de résultat. L’idée la plus transférable est la décision de conception qui la sous-tend : traiter « arrêté et en attente d’un humain » comme un événement de premier ordre au lieu de laisser chaque appelant assembler sa propre vision. Pour l’automatisation sans surveillance, c’est généralement la différence entre découvrir une exécution bloquée le lendemain matin et en être averti dès qu’elle cale.