Cuando Hermes se queda esperando a una persona, tu plugin ahora puede enterarse


Son las dos de la madrugada y tu ejecución de Hermes en un servidor se topa con un comando que pide una contraseña de sudo, o lanza un prompt de aprobación para un comando peligroso. No hay nadie sentado ahí. La ejecución simplemente se detiene hasta que expira el tiempo de espera y falla, y a las nueve de la mañana siguiente te encuentras con una fría línea de salida en la terminal. Lo que querías era trivial: cuando se bloquee, avísame.

Eso solía ser incómodo de construir. Hermes se detiene a esperar a una persona en varios sitios no relacionados — el prompt de contraseña de sudo, la pregunta de una herramienta clarify, una aprobación de comando peligroso — y cada uno vive en su propia ruta de código. Para conseguir una sola notificación de “alguien es necesario” tenías que registrar un hook por tipo de prompt, y luego buscar coincidencias de patrones en cadenas de comandos de terminal para adivinar cuáles eran sudo.

El PR #133171, fusionado el 5 de octubre de 2026, cierra esa brecha con un único par de hooks genérico que se dispara siempre que el agente se detiene y espera a una persona.

El par 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

El hook de petición está en el código compartido por el que ya pasa toda superficie, así que CLI, Ink TUI / Desktop, ACP y las plataformas de gateway (Telegram, Discord, …) quedan cubiertos sin ningún cableado por superficie. Lo que cubre:

kind Qué prompt Dónde aparece
"sudo" Prompt de contraseña de sudo enmascarado antes de ejecutar un comando sudo CLI interactiva, Ink TUI / Desktop (tarjeta sudo.request), fallback /dev/tty
"clarify" Pregunta de una herramienta clarify CLI, Ink TUI / Desktop, plataformas de gateway
"approval" Aprobación de comando peligroso / escritura, consentimiento de elicited MCP, transportes de aprobación de plugins CLI, Ink TUI / Desktop, ACP, plataformas de gateway, plugins que usan ctx.register_approval_transport

Dos casos deliberadamente no se disparan: las decisiones de approvals.mode=smart tomadas por el modelo auxiliar, porque no se pregunta a ninguna persona; y las aprobaciones del gateway que se unen a un prompt idéntico ya pendiente, porque la persona ve un solo prompt y el par de hooks se dispara una sola vez para él.

Qué recibe tu callback

def my_callback(kind: str, request_id: str, session_id: str, session_key: str,
                platform: str, prompt: str, **kwargs):
    ...
Parámetro Descripción
kind "sudo", "clarify" o "approval"; pueden añadirse tipos nuevos, así que ignora los que no manejes
request_id Único por prompt; el on_human_input_resolved correspondiente lleva el mismo valor
session_id Id de sesión de Hermes cuando se conoce; si no, ""
session_key Clave de enrutamiento de sesión/chat, útil para acotar las notificaciones por chat
platform Plataforma de gateway ("telegram", "discord", …) u origen local ("cli", "tui", "desktop", …)
prompt Qué se le pregunta a la persona: el comando para sudo/aprobación, el texto de la pregunta para clarify

Un detalle sobre prompt merece su propio párrafo. Siempre pasa por el redactor de secretos (redact_sensitive_text(..., force=True)), incluso cuando security.redact_secrets está desactivado en tu configuración. Si el propio redactado falla, prompt queda como cadena vacía — el texto en crudo nunca se envía. La contraseña de sudo que escribes y las respuestas a las preguntas de clarify nunca se pasan a ninguno de los dos hooks; solo la etiqueta del resultado.

Los valores de retorno se ignoran, y un callback que lanza una excepción se registra a nivel debug mientras el prompt sigue adelante — un observador roto no puede romper un prompt humano. Los callbacks se ejecutan en el hilo que está a punto de bloquearse, así que mantenlos rápidos: delega las llamadas a servicios push o webhooks a un hilo en segundo plano.

Valores de resultado

on_human_input_resolved se dispara exactamente una vez por petición, tras terminar la espera, con un campo extra:

kind valores de outcome
"sudo" "provided", "skipped" (respuesta vacía), "timeout", "cancelled", "error"
"clarify" el resultado propio de la superficie: "submitted", "timed_out", "cancelled", "undelivered", "terminated" o "error"
"approval" "once", "session", "always", "deny", "timeout", "cancelled", "notify_failed", o "transport_<failure>" para un transporte de plugin fallido

Un ejemplo realmente útil: avísame solo si nadie responde

La necesidad habitual no es “notifícame en cada prompt”, sino “dame un toque si nadie se ocupa”. El par request/resolve lo hace natural:

import threading

def push_notification(text):
    ...  # llama aquí a tu servicio push

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()  # solo avisa si nadie responde en 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 propiedad agradable aquí es que los prompts que responde una persona nunca te llegan, y solo avisan las ejecuciones genuinamente atascadas. Como request_id está emparejado, no obtienes avisos duplicados ni temporizadores huérfanos. Para la forma del sistema de hooks en su conjunto — hooks observadores frente a hooks interceptores, registro y ciclo de vida — consulta la referencia del comando hooks y la anterior guía de hooks de plugins proveedores.

Estado de la versión

El par de hooks está en main y todavía en ninguna etiqueta publicada (la versión más reciente es v0.21.5, tag v2026.9.24). Las instalaciones vía Git lo recogen al hacer pull de la última main; las imágenes Docker y los despliegues alojados lo reciben con la próxima etiqueta. Para saber cómo escribir el propio plugin, consulta la referencia del comando plugins.

Por qué esto importa más allá de las notificaciones

Construir una alerta de “Hermes necesita a una persona” solía requerir conocer rutas de código internas para cada tipo de prompt. Ahora es un par de hooks, un campo kind y un conjunto de etiquetas de resultado. La idea más transferible es la decisión de diseño que hay detrás: tratar “se detuvo y espera a una persona” como un evento de primera clase en lugar de dejar que cada llamador arme su propia visión. Para la automatización desatendida, eso suele ser la diferencia entre descubrir una ejecución atascada a la mañana siguiente y enterarte en el momento en que se detiene.