Synsemadocsv0.6.xENES

Agentes y coordinación

Multi-agente

Los agentes corren concurrentemente en threads reales, cada uno en su propio intérprete aislado. Se coordinan por un blackboard compartido y señales — no por llamadas directas. (Esta es la capa de concurrencia; para un modelo que elige tools, ver Tool calling.)

agents.syn
-- Doc example: the blackboard (share / observe) — thread-safe shared state.
-- spawn / signal / wait_for run real threads (non-deterministic), shown in the prose.
intent: "doc example: blackboard"

share 42 as "demo_key"
observe "demo_key" as demo_v
print("blackboard: demo_key → " + text(demo_v))

test "the blackboard is shared, synchronous state (share publishes, observe reads)"
    share 42 as "answer"
    observe "answer" as a
    assert_eq(a, 42)
    share "hi" as "result_1"
    observe "result_1" as g
    assert_eq(g, "hi")

-- Lists and maps are values (v0.6.29+): share publishes a copy, observe reads a copy, and a
-- task that changes a map it received changes its own copy until it gives it back.
test "share publishes a copy; a task gives back what it changes"
    let cfg be {"n": 1}
    share cfg as "cfg"
    set cfg["n"] to 2                  -- the local map only
    observe "cfg" as seen
    assert_eq(seen["n"], 1)            -- the blackboard kept what was shared
    set seen["n"] to 99                -- a copy too: the blackboard does not change
    observe "cfg" as again
    assert_eq(again["n"], 1)
    task bump(state)
        set state["n"] to state["n"] + 1
        give state
    set cfg to bump(cfg)
    assert_eq(cfg["n"], 3)

Definir y spawnear§

Definir un agente lo registra; el cuerpo corre solo al spawnearlo (en un thread nuevo). El padre sigue de inmediato.

agent Researcher
    require net("*.wikipedia.org")
    let data be fetch("https://en.wikipedia.org/api/...")
    share data as "research"
    signal "done"

spawn Researcher with query = "AI safety"

Las tasks/valores top-level se snapshotean (una copia) dentro del agente. Lo que el agente no ve: las tasks definidas dentro de módulos importados (sólo viajan los bindings top-level del programa de entrada — re-exportá como task top-level lo que el agente necesite), y una task pasada como argumento de spawn llega como texto (los closures no cruzan hilos: pasá datos). Cada agente es un intérprete nuevo con su propio set de capabilities — necesita sus propios require, acotados por el techo del host. Un agente que falla queda contenido (estado ERROR); synsema run espera a los agentes antes de salir y sale con código ≠0 si alguno terminó en ERROR.

Probar agentes. synsema test cablea el mismo swarm real que run (engine v0.6.10+): dentro de un bloque test, spawn arranca el agente en su propio hilo, agents()/agent_stop existen, blackboard y señales funcionan. Al terminar el bloque el runner espera a los agentes de ese bloque; un agente que terminó en ERROR hace fallar ese test (Agent error [<id>]: …) y el bloque siguiente arranca limpio. En engines ≤ 0.6.9 test no tenía swarm (spawn corría el cuerpo in-process, bloqueante).

Blackboard — share / observe§

share value as "key" publica; observe "key" as var lee. Thread-safe y versionado; la key es una expresión ("result_" + text(id)).

El estado compartido es explícito — las listas y los maps son valores (v0.6.29+)§

Una lista o un map es un valor: let ys be xs, pasarlo a una task, share y observe dan todos una copia lógica (copy-on-write, así que no se copia nada hasta que alguien escribe). Lo que cambia el blackboard es share, nunca un set sobre un map que compartiste u observaste:

let cfg be {"n": 1}
share cfg as "cfg"
set cfg["n"] to 2               -- changes the local map only
observe "cfg" as seen           -- seen["n"] is 1: share published a copy

task bump(state)
    set state["n"] to state["n"] + 1
    give state                  -- a change leaves a task with give
set cfg to bump(cfg)

Una task que hace set dentro de un map que recibió cambia su propia copia; antes de v0.6.29 cambiaba el del que la llamó. synsema check avisa (v0.6.30+) cuando esa escritura se pierde — la task ni devuelve el valor, ni lo pasa, ni lee lo que escribió — y nombra el arreglo: give state y set x to bump(x). El estado que tienen que ver varios agentes o requests pasa por los canales hechos para eso: el blackboard, la memoria, el bus de abajo, o state_* bajo serve.

Señales — signal / wait_for§

signal "done"                       -- emit (a consumable queue, not a latch)
signal "done" with data             -- emit with a payload
wait_for "done" as result           -- blocks until a signal arrives (default 30s), CONSUMES it
wait_for "done" timeout 2 as r      -- bound the wait; returns nothing on timeout

Acotá wait_for con timeout dentro de un handler de ruta para que un request no se cuelgue. El canal es una expresión — usá "cancel:" + text(job_id) para un canal push por job.

Bus de eventos — bus_publish / bus_subscribe (fan-out) — engine v0.6.7+§

Las señales las consume un solo receptor. Cuando N partes tienen que ver el mismo evento (cada cliente SSE/socket de una UI en vivo), publicá en el bus: uno por programa, visible desde el top-level, los workers de parallel_map, los ticks de cron, los agentes spawneados y cada handler de serve; in-process, acotado por suscriptor, topics con glob, sin capability.

bus_publish("agent.done", {"id": 7})          -- → subscribers reached
let sub be bus_subscribe("agent.*")
let ev be bus_recv(sub, 30)                    -- {type: "event", topic, data, timestamp} or nothing

Semántica completa, select sobre sockets + procesos + bus, y el patrón SSE: Apps agénticas.

Observar y detener agentes — agents() / agent_stop — engine v0.6.7+§

agents()                  -- [{id, name, state, error, started_at, finished_at}]
agent_stop(id, reason?)   -- cooperative cancellation; true if it was alive → state "stopped"

Un agente detenido levanta cancelled: <motivo> antes de su próximo statement y despierta de cualquier espera (wait_for, sleep, select…). Funciona bajo run y serve, sin capability. Bajo serve, un agente spawneado recibe el mismo cableado que un tick de cron (state_*, DB, approvals, bus, memoria), corre bajo el techo del host (serve --sandbox | --cap-set) y un shutdown ordenado lo detiene.

Inspeccionar una corrida§

synsema conform --swarm program.syn    -- dump JSON: blackboard + estados por agente