Synsemadocsv0.6.xENES

Servidor HTTP

Apps agénticas

Una app que orquesta agentes frente a una UI en vivo necesita cinco cosas que un servidor REST común no tiene: un canal bidireccional al navegador, un proceso hijo que podés mirar mientras corre, una forma de que un evento llegue a todos los clientes abiertos, una sola espera sobre todo eso, y un servidor que vence, cancela y se apaga sin dejar nada atrás. Desde el engine v0.6.7 las cinco están en el lenguaje, con las mismas reglas que todo lo demás: deny-by-default, memoria acotada, errores que se dicen.

No es sólo para agentes. Las mismas cinco primitivas son lo que necesitan un chat, un minijuego multijugador, un editor colaborativo, un dashboard en vivo, un bot de build o una terminal en el navegador: un socket por cliente, un bus por sala o tema (bus_subscribe("room." + id)), un proceso que mirás, un select por conexión y un servidor que limpia lo suyo. "Agéntica" es donde se encontró el hueco; la página aplica a cualquier programa con estado vivo compartido entre clientes.

agentic.syn
-- Doc example: the event bus (`bus_*`) and the unified `select` — the plumbing of an
-- agentic app. N subscribers receive the SAME event (fan-out — unlike signal/wait_for,
-- which one receiver consumes), queues are bounded, topics accept globs, and ONE wait
-- covers any mix of handles. Pure: no capability (same trust level as share/signal).
-- Incoming sockets (`route … socket`) and live processes (`proc_*`) need a running
-- server / an `exec` grant, so the page prose covers those with the shapes that run.
intent: "doc example: event bus + select"

let wide be bus_subscribe("agent.*")
let exact be bus_subscribe(["agent.done", "ui.*"])
let other be bus_subscribe("other")

test "publish fans out to every matching subscriber and says how many got it"
    assert_eq(bus_publish("agent.done", {"step": 1}), 2)
    let ev be bus_recv(wide, 1)
    assert_eq(ev["type"], "event")
    assert_eq(ev["topic"], "agent.done")
    assert_eq(ev["data"]["step"], 1)
    assert_eq(bus_recv(exact, 1)["topic"], "agent.done")
    assert_eq(bus_recv(other, 0.1), nothing)           -- no match → nothing at the timeout

test "select waits on any handles at once and tags the winner"
    bus_publish("ui.click", "go")
    let ev be select({"wide": wide, "exact": exact}, 1)
    assert_eq(ev["name"], "exact")
    assert_eq(ev["source"], "bus")
    assert_eq(ev["topic"], "ui.click")
    assert_eq(ev["data"], "go")
    assert_eq(select([wide, other], 0.1), nothing)      -- nothing ready → nothing

task publish_glob()
    give bus_publish("agent.*", 1)                      -- globs belong to bus_subscribe

task publish_task()
    give bus_publish("agent.done", publish_glob)        -- a task is not data

test "a published topic is literal and the payload must be data (loud, not degraded)"
    assert_error(publish_glob)
    assert_error(publish_task)

test "queues are bounded: drop_oldest keeps the newest — the publisher never blocks"
    let small be bus_subscribe("q", {"max_queue": 2})
    bus_publish("q", 1)
    bus_publish("q", 2)
    bus_publish("q", 3)
    assert_eq(bus_recv(small, 1)["data"], 2)
    assert_eq(bus_recv(small, 1)["data"], 3)
    bus_unsubscribe(small)

test "bus_topics lists live subscriptions by pattern"
    assert(some(bus_topics(), (t) => t["topic"] == "agent.*"))

Rutas WebSocket — socket§

Una ruta con un bloque socket acepta un WebSocket entrante. Adentro, socket es el handle de esta conexión — el mismo tipo de handle que devuelve ws_connect, así que toda la familia ws_* funciona sin cambios:

require serve(8080)

serve on 8080
    route "GET /ws"                              -- el handshake es un GET (RFC 6455)
        socket
            ws_send(socket, {"hello": "agent"})
            while true
                let ev be ws_recv(socket, 30)    -- {type: "text"|"binary"|"close", data}
                when ev == nothing               -- 30 s sin nada
                    stop
                when ev["type"] == "close"
                    stop
                otherwise
                    ws_send(socket, {"echo": ev["data"]})

    route "GET /private" requires auth            -- la auth corre ANTES del upgrade: 401, sin upgrade
        socket
            ws_send(socket, "hello " + request.user.name)

Procesos vivos — proc_*§

run es one-shot: captura todo y devuelve al final. Un agente que corre cargo test, npm run build o un worker largo necesita ver la salida a medida que sale, mandar stdin y matarlo — un proceso como handle con eventos. Mismo gate que run (require exec(cmd), scope = el comando tal como se escribe), args como lista, nunca un shell. Pipes por defecto (una herramienta que detecta "no es una tty" se comporta como en CI); "pty": true le da una terminal real — ver Pseudo-terminal abajo.

require exec("cargo")

let p be proc_spawn("cargo", ["test"], {"cwd": "./repo"})
while true
    let ev be proc_recv(p, 60)                   -- {type: "stdout"|"stderr"|"exit", data} o nothing
    when ev == nothing
        proc_kill(p)                              -- 60 s en silencio: TERM ("KILL" para insistir)
    otherwise when ev["type"] == "exit"
        print("exit " + text(ev["data"]["exit_code"]))
        stop
    otherwise
        print(ev["type"] + ": " + ev["data"])     -- un evento por línea, sin el salto final
proc_close(p)
BuiltinDevuelve
proc_spawn(cmd, args?, opts?)handle
proc_recv(h, timeout?)próximo evento, o nothing al vencer
proc_select(lista | mapa, timeout?)primer evento listo entre procesos (select acepta cualquier handle)
proc_send(h, texto | bytes)true; error si stdin está cerrado. Es una escritura bloqueante de pipe: un hijo que nunca lee stdin te bloquea
proc_close_stdin(h)true — EOF al hijo. Sólo pipes: un pty no tiene un stdin aparte — mandá la tecla de EOF (bytes([4]) Ctrl-D, bytes([26, 13]) Ctrl-Z + Enter)
proc_resize(h, cols, rows)true — sólo pty (SIGWINCH / ResizePseudoConsole); error en un proceso por pipes
proc_status(h)"running" · "exited" · "killed" · "closed"
proc_kill(h, señal?)"TERM" (default; SIGTERM en Unix) o "KILL"; en Windows ambas terminan. Alcanza al árbol de procesos entero (v0.6.9+, ver Ciclo de vida)
proc_wait(h, timeout?){exit_code, signal} o nothing
proc_stats(h){pid, cmd, status, exit_code, pty, tree, queued, queued_bytes, dropped, uptime}tree: el kill alcanza a los nietos
proc_close(h)libera el handle; mata si sigue vivo (TERM, KILL a los 2 s, luego wait) — el árbol entero. Sin huérfanos. Idempotente

exit llega una vez, después de drenar los dos pipes, como {exit_code, signal} (exit_code -1 si murió por señal; signal es nothing en una salida normal y siempre en Windows). opts: cwd, env (hereda + sobreescribe; un valor secret es error — reveal() explícito), line_mode (true; false = chunks crudos de texto ≤ 64 KiB — data siempre es texto, UTF-8 lossy, nunca partido dentro de un carácter), stderr ("separate" | "merge"), pty (+ cols, rows, term — abajo), process_group (true: grupo de procesos / Job Object propio para que el kill alcance al árbol; false desprende a propósito un daemon que debe sobrevivir a proc_close — entonces sólo muere el hijo directo), y la cola acotada max_queue (4096) / max_queue_bytes (64 MiB) con on_full = "block" (default: el lector deja de drenar, el hijo se frena en su write — backpressure real, sin pérdida) | "drop_oldest" | "error" (el próximo recv falla, el proceso se mata).

Ciclo de vida. Cuando termina el intérprete que lo lanzó — el request bajo serve, el programa, el agente — todo proceso vivo se mata. El árbol entero, no sólo el hijo (v0.6.9+): el hijo nace en su propio grupo de procesos en Unix y en un Job Object con kill-on-close en Windows, así proc_kill/proc_close sobre sh -c "npm run dev" se llevan también al node de abajo — y en Windows hasta un crash del intérprete cierra el job y el árbol con él. proc_stats(h)["tree"] te dice que está en efecto. Un handler nunca deja un fantasma; el trabajo que debe sobrevivir a un request va en cron_after o en un agente. SYNSEMA_PROC_MAX (64, techo 1024) acota los procesos vivos por intérprete. Un nieto que retiene el pipe tras el exit del hijo no te cuelga: 1 s de gracia y se entrega el exit.

Pseudo-terminal — pty: true§

Engine v0.6.8+. Muchos programas preguntan "¿estoy hablando con una terminal?" y, sobre un pipe, saltan la pregunta, asumen "no", se cuelgan o se niegan: prompts y/N que leen /dev/tty, contraseñas de ssh/sudo/gpg, menús con flechas, docker run -it, REPLs, barras de progreso y toda TUI (vim, htop, un CLI agéntico). "pty": true corre al hijo dentro de un pseudo-terminal real — openpty en Linux/macOS, ConPTY en Windows ≥ 10 1809. Misma API, mismos eventos, mismo gate exec(cmd): un pty no otorga ningún poder del SO que un pipe no dé; cambia cómo se comporta el hijo, no qué puede hacer. sandbox lo deniega como a cualquier exec.

require exec("npm")

let p be proc_spawn("npm", ["install"], {"pty": true, "cols": 120, "rows": 40})
let screen be ""
while true
    let ev be proc_recv(p, 60)
    when ev == nothing
        proc_kill(p)
    otherwise when ev["type"] == "exit"
        stop
    otherwise
        set screen to screen + ev["data"]              -- texto crudo en chunks, ANSI incluido
        when contains(strip_ansi(screen), "[y/N]")    -- leerlo como lo leería un humano
            proc_send(p, "y\r")                       -- teclas: Enter es \r en una tty
            set screen to ""
proc_close(p)

Qué cambia en modo pty — consecuencias de "es una terminal", no API nueva:

Una terminal web es una ruta socket y un pty en un mismo select: bytes del navegador → proc_send, eventos del proceso → socket_send, un mensaje {resize}proc_resize; xterm.js dibuja. Quién recibe ese socket es una decisión de auth de la app — el runtime no agrega ninguna capability porque no hay nada nuevo que gatear.

Probá primero el flag no interactivo (--yes, CI=1, DEBIAN_FRONTEND=noninteractive): más barato y determinista. El pty es para cuando no existe.

Bus de eventos — bus_*§

signal/wait_for es punto a punto (un receptor consume). Una UI en vivo necesita fan-out: N handlers — uno por cliente SSE o socket — reciben el mismo evento que publicó un agente, un cron u otro request, sin polling. Eso es el bus. Un bus por programa, visible desde todos lados: top-level, workers de parallel_map, ticks de cron, agentes spawneados y cada handler de cada worker del server. Sólo in-process (Redis con la misma API es el roadmap). Sin capability — mismo nivel de confianza que share/signal.

bus_publish("agent.progress", {"step": 3, "of": 10})   -- → cuántos suscriptores lo recibieron
let sub be bus_subscribe("agent.*")                     -- un topic o una lista; globs * y ?
let ev be bus_recv(sub, 25)                             -- {type: "event", topic, data, timestamp} o nothing
bus_unsubscribe(sub)
bus_topics()                                            -- [{topic, subscribers}] por patrón

La ruta SSE que todo dashboard quiere — alimentada por el bus, con el heartbeat a cargo del server:

route "GET /events"
    stream
        let sub be bus_subscribe("agent.*")
        while true
            let ev be bus_recv(sub, 25)
            when ev != nothing
                send ev["data"] as ev["topic"]

File-watch — watch§

Cambios en disco como handle con eventos (engine v0.6.9+), en el mismo hub que procesos, sockets y bus — así un solo select cubre "cambió un archivo" y "terminó el build". Gate: require file(path) — mirar un árbol es leerlo, el mismo scope file_read que list_dir.

require file("src")
require file("src/*")
require exec("cargo")

let files be watch("src", {"interval": 0.2, "ignore": ["*.tmp"]})
let build be nothing
while true
    let ev be select({"files": files, "build": build}, 60)
    when ev == nothing
        continue
    when ev["source"] == "watch"                  -- {type, path, is_dir}
        when build != nothing
            proc_close(build)                     -- mata el build anterior, árbol entero
        set build to proc_spawn("cargo", ["build"])
    otherwise when ev["type"] == "exit"
        print("build exit " + text(ev["data"]["exit_code"]))
BuiltinDevuelve
watch(path, opts?)handle — también lo acepta select, etiquetado source: "watch"
watch_recv(h, timeout?)el próximo evento, o nothing al timeout
watch_stats(h){path, recursive, interval, entries, scans, queued, dropped}
watch_close(h)libera el handle y apaga el scanner. Idempotente

Los eventos son {type: "create" | "modify" | "delete", path, is_dir}. path usa / y queda relativa si la raíz era relativa. Un rename es un delete más un create. Los directorios sólo emiten create/delete (su mtime cambia con cada hijo — ruido). No se emite nada por lo que ya existía al arrancar el watch. opts: recursive (true), interval en segundos (0.5, piso 0.02), ignore (nombres de entrada, glob * permitido; default [".git", "node_modules", "target"] — pasá [] para incluirlos), max_entries (100 000: por encima watch() falla, y un árbol que lo supera después hace que el próximo recv falle y retira el handle), max_queue (4096; el overflow descarta lo más viejo y lo cuenta en dropped).

Cómo funciona, honestamente. Es polling con snapshot — mtime + tamaño por entrada, comparados cada interval — no inotify, FSEvents ni ReadDirectoryChangesW. Eso compra semántica idéntica en Linux, macOS y Windows, sin límites de watches del kernel y sin dependencia nueva; cuesta un recorrido del directorio por tick (de ahí ignore y max_entries) y una latencia igual a interval. Un cambio hecho y deshecho dentro de un mismo intervalo no se ve: recibís el estado, no la historia. SYNSEMA_WATCH_MAX (64, techo 1024) acota los watches vivos por intérprete; como los procesos, cada watch muere con el intérprete que lo abrió.

La terminal — term§

La terminal propia del programa como handle con eventos (engine v0.6.11+), en el mismo hub que procesos, sockets, bus y watches — así un solo select cubre "el humano tocó una tecla" y "el subagente publicó un resultado". Gate: require stdin. Es lo que necesita un chat CLI, una paleta de comandos o un editor de línea con historial: read_line te da una línea cocinada por vez; term_open te da cada tecla en el momento en que se pulsa.

require stdin
require stdout

let commands be ["/help", "/history", "/approve", "/model", "/quit"]
let term be term_open()
when term == nothing                       -- pipe / CI / serve / test: no hay terminal
    let line be read_line("> ")            -- el mismo programa, entrada plana
otherwise
    let buf be ""
    let sub be bus_subscribe("agent.*")
    while true
        let matches be where(commands, (c) => starts_with(c, buf))
        term_write(term, "\r\x1b[2K> " + buf + "   " + join(matches, "  "))
        let ev be select({"keys": term, "agent": sub}, 60)
        when ev == nothing
            continue
        when ev["source"] == "bus"
            term_write(term, "\r\n[agent] " + json_encode(ev["data"]) + "\r\n")
        otherwise when ev["type"] == "eof"
            stop
        otherwise when ev["type"] == "paste"
            set buf to buf + ev["text"]
        otherwise when ev["key"] == "char"
            set buf to buf + ev["text"]
        otherwise when ev["key"] == "backspace"
            set buf to slice(buf, 0, len(buf) - 1)
        otherwise when ev["key"] == "tab" and len(matches) == 1
            set buf to matches[0]
        otherwise when ev["key"] == "enter" and ev["alt"]
            set buf to buf + "\n"                  -- Alt+Enter = nueva línea
        otherwise when ev["key"] == "enter"
            stop
    term_close(term)

Cada tecla es un evento, así que el menú de / filtra mientras se tipea; el loop sigue vivo mientras un subagente habla por el bus.

BuiltinDevuelve
term_open(opts?)handle — también lo acepta select, etiquetado source: "term"; nothing cuando stdin o stdout no son una TTY, bajo serve, synsema test/conform y en WebAssembly
term_recv(h, timeout?)el próximo evento, o nothing al vencer (default 30 s — pasá uno explícito cuando esperás a un humano)
term_size(h){cols, rows}
term_write(h, text)escribe a stdout ya, sin pasar por el buffer de print; se permiten escapes ANSI (cursor, borrar línea, colores)
term_stats(h){kitty, paste, ansi, keys, queued, dropped}
term_close(h)restaura la terminal y apaga el lector. Idempotente — y el runtime lo hace igual al soltar el handle (fin del programa, error de runtime, stop, pánico)

Eventos (todos etiquetados source: "term", handle, name):

opts: paste (true, bracketed paste), kitty (true: pide el kitty keyboard protocol cuando la terminal lo soporta — Windows Terminal, kitty, WezTerm, foot, Ghostty — que es lo que hace distinguible Shift+Enter; sin él Shift+Enter es un Enter común, así que hacé de Alt+Enter tu atajo multilínea y de Shift+Enter un bonus), ctrl_c ("exit": Ctrl+C restaura la terminal y termina el proceso con código 130, como haría SIGINT — el raw mode si no se lo tragaría; "key": llega como {key: "char", text: "c", ctrl: true} y el programa decide), max_queue (16384, descarta los más viejos).

Lo que se sostiene con una terminal abierta: print/log siguen funcionando (el runtime escribe \r\n por vos, sin escalera); ask de texto libre, approve y confirm funcionan — el handler de consola suspende el raw mode mientras el humano contesta y lo reanuda; una terminal por proceso — un agente spawneado no puede abrirla mientras la tiene el programa principal. Restaurar la terminal es tarea del runtime, nunca del script: lo que activaste vos (cursor oculto, colores) lo deshacés vos.

Una sola espera — select§

let ev be select({"sock": socket, "child": child, "feed": feed, "cancel": sub}, 60)

targets es una lista de handles o un mapa nombre → handle — cualquier mezcla de ws_connect, socket, proc_spawn, bus_subscribe, watch, term_open. Devuelve el primer evento listo, etiquetado con source ("ws" | "proc" | "bus" | "watch" | "term"), handle y name (forma mapa), más los campos propios del evento (type/data en sockets y procesos; topic/data/timestamp en el bus). nothing al vencer o cuando todos los targets desaparecieron. Equitativo (round-robin), duerme en el poller del kernel mientras no hay nada, falla rápido con un error atrapable cuando un target muere por error de protocolo/cola, y despierta al instante ante una cancelación. ws_select también acepta cualquier handle (conserva su etiqueta conn); proc_select es la misma espera restringida a procesos.

Todo junto — una conexión, un hijo, una suscripción, un loop:

require serve(8080)
require exec("sh")

serve on 8080
    route "GET /console"
        socket
            let sub be bus_subscribe("agent.*")
            let child be proc_spawn("sh", ["-c", "for i in 1 2 3; do echo step $i; sleep 1; done"])
            while true
                let ev be select({"ui": socket, "child": child, "bus": sub}, 60)
                when ev == nothing
                    ws_send(socket, "idle")
                otherwise when ev["name"] == "ui"
                    when ev["type"] == "close"
                        stop
                    otherwise
                        ws_send(socket, "you said " + ev["data"])
                otherwise when ev["name"] == "child"
                    when ev["type"] == "exit"
                        ws_send(socket, "exit " + text(ev["data"]["exit_code"]))
                    otherwise
                        ws_send(socket, ev["type"] + ": " + ev["data"])
                otherwise
                    ws_send(socket, "[" + ev["topic"] + "] " + json_encode(ev["data"]))

La versión completa, con un feed SSE /events y un POST /announce que publica, es examples/agent_console.syn en el repo del engine.

Timeouts y cancelación§

Por default un handler no tiene límite de tiempo. Declaralo en el bloque serve (default de todas las rutas) y/o por ruta:

serve on 8080
    timeout 30                    -- segundos, default de todas las rutas
    route "POST /think"
        timeout 300               -- override de ruta: al principio del cuerpo, una sola vez
        give reason "…" given request.json
    route "GET /events"
        timeout none              -- esta ruta se exime del default del bloque
        stream

Shutdown ordenado§

Ante SIGINT (Ctrl-C) — y SIGTERM en Unix, lo que mandan docker stop y systemd:

1. El listener se cierra: las conexiones nuevas se rechazan; un request sobre una conexión keep-alive abierta recibe 503 {"error": "server shutting down"} + Retry-After: 2. 2. Log: [serve] shutting down: draining N in-flight request(s), grace 10s (SYNSEMA_SHUTDOWN_GRACE). El cron deja de programar, los agentes vivos se cancelan (agent_stop, motivo server shutting down), streams y sockets se cancelan de inmediato (SSE event: error cancelled: server shutting down; sockets Close 1001). 3. Los requests sized en vuelo tienen hasta SYNSEMA_SHUTDOWN_GRACE segundos (default 10; 0 = inmediato); lo que queda se cancela; [serve] stopped; código de salida 0 (fue pedido). Un segundo Ctrl-C durante el drain sale ya con 130. 4. Con varios bloques serve on el proceso sale cuando drenó el último.

synsema run no cambia (no tiene listener). Bajo systemd poné TimeoutStopSec por encima de la gracia.

Desde el programa (engine v0.6.18+): shutdown(reason?) pide exactamente este drain — desde una ruta, un bloque socket, un job de cron o un agente. El log dice [serve] shutdown requested by the program: <motivo>, después corren los pasos de arriba y el proceso sale con 0. Idempotente (una segunda llamada no hace nada); error bajo synsema run (un programa run termina con su top level — stop sale de un loop o de una task) y antes de que algo escuche; sin capability (apagarse no es un recurso del host); un secret como motivo se rechaza (el motivo se loguea). Es como una app de escritorio se apaga cuando se cierra su última ventana — Tu app en el escritorio.

Agentes bajo serve§

Un agente spawneado desde un handler o un tick de cron recibe el mismo cableado que un tick de cronstate_*, la base compartida, la cola de approvals, cron, el bus, la memoria declarada — no una isla con builtins frescos. Corre bajo el techo del host de synsema serve --sandbox o --cap-set "<lista>" (un require adentro nunca excede lo que fijó el operador, exactamente como bajo run; --sandbox acá es stdout,time + serve), y un shutdown ordenado lo detiene.

agents()                  -- [{id, name, state, error, started_at, finished_at}]
agent_stop(id, motivo?)   -- true si estaba vivo; el agente termina en estado "stopped"

id es la instancia (Researcher_0), name el agente declarado; estados idle · starting · working · waiting · done · error · stopped. agent_stop es cancelación cooperativa: el agente levanta cancelled: <motivo> antes de su próximo statement y cualquier espera despierta al instante — un agente en while true ya no es inmortal. Los dos funcionan bajo run y serve, sin capability (introspección del propio proceso). Ojo: give agents() pagina como todo give <lista> — leé items, o give {"agents": agents()}.

Knobs del server (entorno del proceso, no .env)§

synsema init los lista, comentados, en .env.example — con la advertencia de que los lee el entorno del proceso (export, Environment= de systemd, -e de Docker):

VariableDefaultQué
SYNSEMA_SERVE_WORKERScores (mín. 2)pool de intérpretes para handlers sized (streams/sockets tienen hilo propio)
SYNSEMA_SHUTDOWN_GRACE10segundos de drain ante SIGINT/SIGTERM; 0 = inmediato
SYNSEMA_SSE_KEEPALIVE15segundos sin frames antes de un comentario SSE : keepalive; 0 apaga
SYNSEMA_WS_SERVER_PING30intervalo del ping del server en rutas socket; sin pong en 2 intervalos → close; 0 apaga
SYNSEMA_WS_SUBPROTOCOLSsubprotocolos que el server puede acordar (coma-separados); sin la variable no acuerda ninguno
SYNSEMA_WS_MAX_MESSAGE16MBtamaño máximo de un mensaje entrante en rutas socket (techo 64MB)
SYNSEMA_WS_MAX_CONNS4096handles WebSocket vivos por intérprete (ws_connect + entrantes)
SYNSEMA_PROC_MAX64hijos proc_spawn vivos por intérprete (techo 1024)
SYNSEMA_WATCH_MAX64handles watch vivos por intérprete (techo 1024; cada uno es un hilo scanner)
SYNSEMA_RUN_PROGRAM_MAX_DEPTH4profundidad máxima de anidamiento de run_program dentro de run_program (v0.6.14+)

Correr Synsema que generaste — run_program (v0.6.14+)§

Una app agéntica que escribe Synsema (una tool que emite el LLM, el plugin de un usuario, un loop que se auto-repara) lo corre con run_program(source, opts) — un proceso hijo del mismo binario bajo un techo ∩ el del padre, con su propio env/cwd/timeout y su audit devuelto como un valor (require sandbox_run). Sin exec, sin synsema en el PATH, sin parsear stderr; el hijo nunca puede exceder al padre. Es la forma segura de "correr el código que generó el modelo" — modelo completo en Sandbox, firma en Builtins.

Lo que a propósito no está§

Mirá Servidor HTTP, Cliente WebSocket, Multi-agente y Deploy.