Synsema docsENES

Humano en el loop

Los gates de aprobación y las preguntas son primitivas del lenguaje — devuelven valores sobre

los que ramificás. El runtime garantiza una cosa en todos lados: **un gate jamás se

auto-aprueba en silencio**. Donde un humano puede responder, Synsema lo espera; donde no hay

ninguno, deniega fail-closed y lo dice.

human.syn
-- Doc example: human interaction. The primitives are interactive; this shows the
-- documented NON-TTY behavior (CI/tests/pipes), which is deterministic.
intent: "doc example: human interaction (non-TTY)"

print("ask picked → " + (ask "Pick an environment" with ["staging", "prod"]))    -- no TTY → first

test "ask with options takes the FIRST option when there is no TTY (CI/tests/pipes)"
    let choice be ask "Pick an environment" with ["staging", "prod"]
    assert_eq(choice, "staging")

Primitivas

let ok be approve "¿Deploy a producción?"               -- gate sí/no (devuelve un bool)
confirm "¿Mandar email a 500 clientes?"                 -- confirmación
let env be ask "¿Qué entorno?" with ["staging", "prod"]
show data as "Preview"                                  -- mostrar a un humano

Usalas como expresiones:

when approve "Pago grande: $" + text(amount)
    process_payment()
otherwise
    cancel()

Timeouts: within

approve, confirm y ask aceptan un within <n><s|m|h|d> opcional — la espera máxima

por la respuesta humana antes de denegar fail-closed:

let ok be approve "¿Borrar la tabla de producción?" within 2h
let v be ask "¿Color del tema?" with ["azul", "oscuro"] within 90s

Precedencia: el within del gate > el knob SYNSEMA_HUMAN_TIMEOUT (segundos, env/.env) >

default de 300s. Al vencer, el gate devuelve false (o el fallback documentado de ask)

y un aviso único por stderr explica que ningún humano respondió — el programa sigue, jamás

queda colgado para siempre.

De dónde llega la respuesta

ContextoComportamiento
synsema run en una terminal (TTY)El gate pregunta ahí mismo ([approve] … (y/n):) y te espera — sin timeout; Ctrl+C corta, EOF deniega.
synsema run sin TTY (pipes, CI, manejado por un agente)Deniega al instante, fail-closed, con un aviso único por stderr que le dice explícitamente a los agentes de IA que debe aprobar un HUMANO — un agente no puede fingir tu aprobación. ask cae al fallback (primera opción / "").
synsema serveEl gate se encola y el request bloquea hasta que un humano responde fuera de banda o vence el deadline (el vencimiento deniega). Ver abajo.
synsema test / conformDeterminista: los gates auto-pasan para que las suites nunca bloqueen en un prompt.

Aprobaciones bajo serve

Cuando un handler llega a un gate, el server imprime una línea en su consola con un

token de un solo uso y el comando listo:

[synsema] approval pending interact_1 — "¿Borrar la tabla de producción?" (expires in 7200s).
A HUMAN can respond with: POST /approvals/interact_1 {"decision": true|false, "token": "<64-hex>"}

Dos rutas reservadas (atendidas antes que las tuyas, como /llms.txt):

incluye tokens.

{"token": "...", "value": "texto"} para ask) → 200; token incorrecto → 403 y el

gate sigue esperando; id inexistente/vencido/ya respondido → 404; body malformado → 400.

El token se genera por aprobación (32 bytes aleatorios), se consume al usarse y expira con el

deadline — poseer la consola del server es lo que te autoriza a responder. Un gate bloqueado

retiene su hilo del request toda la espera, así que el within bajo serve es para minutos,

no días.

Notificá cualquier canal: webhooks + links de decisión

Seteá SYNSEMA_HUMAN_WEBHOOK=<url> y cada gate encolado dispara además un **webhook

firmado** — un POST plano (el mismo patrón de los webhooks de Stripe/GitHub), así que lo

recibe CUALQUIER cosa: otro programa Synsema, n8n, una lambda, un bot de chat. El payload

lleva id, mensaje, vencimiento, token y — con SYNSEMA_HUMAN_PUBLIC_URL seteada — **links

de decisión listos para reenviar**:

{"id": "interact_1", "type": "approve", "message": "¿Borrar la tabla de producción?",
 "expires_at": 1786500000, "token": "<64-hex>",
 "respond_path": "/approvals/interact_1",
 "respond_url": "https://mi-app.com/approvals/interact_1",
 "respond_link_yes": "https://mi-app.com/approvals/interact_1/<token>?d=yes",
 "respond_link_no": "https://mi-app.com/approvals/interact_1/<token>?d=no"}

Tu canal reenvía los links por SMS/chat/email; el humano decide abriendo uno

(GET /approvals/{id}/{token}?d=yes|no — un solo uso, devuelve una pagina mínima de

confirmación). Con SYNSEMA_HUMAN_WEBHOOK_SECRET seteada, el body va firmado con HMAC-SHA256

en X-Synsema-Signature: sha256=<hex> para que tu receptor verifique el origen — en

producción seteala siempre. El envío es fire-and-forget (un intento, 10s): un canal caído

jamás bloquea el gate — quedan la consola y GET /approvals de fallback. Un canal escrito en

Synsema son ~6 líneas: una ruta que hace json_decode(body of request) y reenvía

respond_link_yes/no adonde quieras.

Sin TTY (pipes / CI / tests)

Sin canal humano, el ask "q" de texto libre devuelve "" y ask "q" with [opts] toma la

primera opción (como muestra el doctest de arriba) — con un aviso único de que ningún

humano respondió de verdad. No confíes en el ask de texto libre para entrada ahí. Para stdin

que funciona con pipes, usá read_line(prompt?); para entrada estilo config trivial de

testear, usá env("NAME", "default").