---
slug: 60-agents
title: Multi-agente
description: Los agentes concurrentes de Synsema se coordinan por un blackboard thread-safe y señales — threads reales, intérpretes aislados, fallas contenidas.
example_ids: [agents]
---

# 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](/es/0.6.x/51-llm-tool-calling)**.)

```synsema
-- 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")
```

## Definir y spawnear

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

```synsema
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)`).

## Señales — `signal` / `wait_for`

```synsema
signal "done"                       -- emitir (una cola consumible, no un latch)
signal "done" with data             -- emitir con payload
wait_for "done" as result           -- bloquea hasta que llega una señal (default 30s), la CONSUME
wait_for "done" timeout 2 as r      -- acotar la espera; devuelve nothing al 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.

```synsema
bus_publish("agent.done", {"id": 7})          -- → suscriptores alcanzados
let sub be bus_subscribe("agent.*")
let ev be bus_recv(sub, 30)                    -- {type: "event", topic, data, timestamp} o nothing
```

Semántica completa, `select` sobre sockets + procesos + bus, y el patrón SSE: [Apps agénticas](/es/0.6.x/47-agentic-apps).

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

```synsema
agents()                  -- [{id, name, state, error, started_at, finished_at}]
agent_stop(id, motivo?)   -- cancelación cooperativa; true si estaba vivo → estado "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

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