Agentes y coordinación
Memoria y estado
El estado persistente del agente se declara, no es implícito. Una línea al tope del programa nombra la memoria y habilita toda la familia — memoria, reglas del owner y progreso:
require memory("support-agent")
El nombre declarado es la identidad: el estado persiste en <dir-del-programa>/.synsema/state/<name>.db (gitignoreá .synsema/), keyed por ese nombre — no por el nombre del archivo. Un remember() en una corrida lo encuentra recall() en la siguiente; renombrar el .syn no cambia nada; dos entry points que declaran el mismo nombre comparten una memoria.
-- Doc example: DECLARED agent memory, progress, and owner rules (persist to SQLite).
-- The declared name IS the identity: state lives in .synsema/state/doc-memory.db,
-- keyed by the name below — not by this file's name. Without the declaration the
-- whole family fails with `Capability not granted: memory` and creates no files.
intent: "doc example: declared memory, namespaces, progress, rules"
require memory("doc-memory")
remember("learning", "demo note from run", ["demo"])
print("recalled → " + text((recall("learning", ["demo"])[0])["content"]))
test "remember then recall — newest first ([0] is the most recent)"
remember("learning", "API is slow on Mondays", ["api"])
let notes be recall("learning", ["api"])
assert_eq((notes[0])["content"], "API is slow on Mondays")
test "recall named args: limit caps results; from filters by writer namespace"
remember("context", "note-a", ["ns"])
remember("context", "note-b", ["ns"])
assert_eq(length(recall("context", ["ns"], limit = 1)), 1)
-- top-level writes source = "main"; agents write under their own name
assert(length(recall("context", ["ns"], from = "main")) >= 2)
assert_eq(length(recall("context", ["ns"], from = "nobody")), 0)
test "progress: resume_point returns the step to resume from"
create_progress("import", ["ingest", "validate", "load"])
start_step("import", "ingest")
complete_step("import", "ingest", "100 rows")
assert_eq(resume_point("import"), "validate")
test "owner rules: a numeric must-rule flags a violation"
add_rule("max_discount", "must", "discount <= 0.20", "pricing")
assert(length(check_rules("pricing", {"discount": 0.25})) > 0)
-- What recall returns is a value (v0.6.29+): changing it changes your copy, not the store.
-- To change what is stored, remember again (or forget_memory the old entry).
test "a recalled entry is a copy; the store changes only through remember/forget_memory"
remember("decision", "ship on Friday", ["release"])
let e be recall("decision", ["release"])[0]
set e["content"] to "edited locally"
assert_eq(recall("decision", ["release"])[0]["content"], "ship on Friday")
test "strict arity: remember takes at most 3 arguments, recall at most 6"
assert_error(() => remember("context", "x", ["t"], "extra"))
assert_error(() => recall("context", nothing, nothing, "any", 10, "main", 7))
-- Every recall is an input of the program (lineage, v0.6.29+): the query goes as a salted
-- commitment, the result as the sha256 of its canonical JSON.
test "recall is recorded in lineage — the query committed, the result hashed"
let rows be recall("learning", ["api"])
let entry be lineage()[-1]
assert_eq(entry.source, "recall")
assert(starts_with(entry.what, "query sha256-salted:"))
assert_eq(entry.encoding, "jcs")
assert_eq("0x" + entry.sha256, hex(sha256(canonical_json(rows))))La declaración (sabelo antes que nada)§
- Sin ella, nada persiste —
remember,recall,add_rule,create_progressy el resto de la familia fallan conCapability not granted: memory(que nombra la línea exacta a agregar), y no se crea ningún archivo ni directorio.memorynunca se auto-otorga, ni siquiera bajorun: escribe archivos a disco. - Un nombre por programa. Dos
require memorycon nombres distintos es un error de arranque. Los nombres siguen[a-zA-Z0-9_-]+— sin rutas, sin string vacío; un nombre inválido falla en la declaración.require memorypelado (sin nombre) es error de parseo. - Todos los contextos de ejecución la comparten:
run,test, los handlers deserve, los ticks de cron, los workers deparallel_mapy los agentes spawneados ven la única memoria declarada del programa, guardada en cada escritura (un crash no pierde nada ya escrito). - Semántica de capability como cualquier otra: denegada dentro de
sandbox;call_toolla intersecta (una tool debe declarar su propiorequire memory("<name>")); el techo del host la gatea (--cap-setsinmemorydeniega la familia y no crea archivo;--cap-set "memory=shop-*"permite nombres bajo un prefijo). - Variables de entorno:
SYNSEMA_STATE_DIRrelocaliza el directorio de estado (útil en tests).SYNSEMA_STATE_NAMEquedó deprecada y se ignora con warning — la declaración la reemplazó. - Al actualizar: si un engine anterior dejó un
<stem-del-archivo>.db, correr sin declaración imprime un warning con la línea exactarequire memory("<stem>")para conservarlo; el esquema no cambió (renombrar el.dbes toda la migración). En el REPL, tipearrequire memory("x")habilita un store en memoria solo para la sesión.
Memoria persistente§
require memory("assistant")
remember("learning", "API slow on Mondays", ["api", "performance"])
let notes be recall("learning", ["api"]) -- newest first ([0] = most recent)
let hits be recall(nothing, nothing, "Monday") -- free-text search (skip args with nothing)
forget_memory(entry_id)
Una entrada de recall es un map con id, category, content, source, tags.
recall toma 6 args — recall(category, tags, search, mode, limit, from) — todos opcionales, todos usables como args nombrados (recall("learning", limit = 10)):
| Arg | Significado |
|---|---|
category | una de las categorías fijas (abajo) |
tags | lista; OR por defecto |
search | búsqueda por substring |
mode | "all" = todos los tags deben matchear (AND) |
limit | máximo de entradas, default 200 |
from | namespace source a leer (sección siguiente) |
Las categorías son un set fijo en inglés:preference,rule,learning,decision,context. Cualquier otro string (p. ej."preferencia") da error.
remember toma como mucho 3 argumentos — remember(category, content, tags) — y recall como mucho 6: desde v0.6.29 uno de más es un error (remember() takes at most 3 arguments, got 4), no un descarte silencioso.
Lo que devuelve recall es un valor (v0.6.29+)§
Las listas y los maps son valores, así que una entrada que sacaste de recall es tu copia: set e["content"] to … la cambia a ella y a nada más. El store cambia solo a través de la familia — remember una entrada nueva, forget_memory(id) la vieja — y con él lo que ven otros agentes, requests y corridas posteriores. Lo mismo vale para una task que recibe estado: cambia su propia copia y tiene que devolverla con give (ver Multi-agente § Estado compartido).
Cada recall es una entrada — linaje (v0.6.29+)§
El engine registra cada recall en lineage(): source recall, what = query sha256-salted:<hex> (un compromiso con sal de los argumentos — un texto de búsqueda como "session://TOKEN" es dato, nunca se publica como host) y el sha256 del resultado como canonical_json (encoding: "jcs"), así que "0x" + e.sha256 == hex(sha256(canonical_json(rows))). receipt() la publica sin la sal — ver Observabilidad § Linaje.
Namespaces por agente (source / from)§
Cada entrada registra quién la escribió en source: dentro de agent X un remember escribe source = "X"; el código top-level (y los handlers de serve) escriben "main". Las lecturas están namespaceadas por defecto, así los agentes que comparten una memoria no confunden sus notas:
require memory("newsroom")
agent Writer
remember("context", "draft done") -- source = "Writer"
agent Analyzer
let mine be recall() -- only Analyzer's own entries (default)
let theirs be recall(from = "Writer") -- explicit cross-namespace read
let all be recall(from = "*") -- everything
spawn Writer
spawn Analyzer
let everything be recall() -- top-level sees ALL by default
Las reglas y el progreso no están namespaceados — un solo reglamento, un solo tablero de planes por programa.
Progreso (reanudable tras crash)§
require memory("import-agent")
create_progress("import", ["ingest", "validate", "load"])
start_step("import", "ingest")
complete_step("import", "ingest", "100 rows") -- or fail_step(...)
let next_step be resume_point("import") -- "validate" — where to resume after a restart
Reglas del owner§
require memory("pricing-agent")
add_rule("max_discount", "must", "discount <= 0.20", "pricing")
let violations be check_rules("pricing", {"discount": 0.25}) -- non-empty → violated
Niveles: must (bloqueo duro), should (warning), avoid / prefer (preferencias). Las condiciones numéricas se evalúan contra el map de contexto. get_rules(category?) las lista; memory_summary() imprime un resumen de todo lo guardado.
Estado de serve (por proceso, en memoria)§
Bajo serve, los builtins state_* (p. ej. state_incr("visits")) comparten estado en memoria entre requests; para cambiar un mapa compartido desde requests concurrentes usá los atómicos state_merge(key, map) y state_cas(key, expected, new) (motor v0.6.45+), no state_get + state_set, que pierden escrituras — ver Servidor HTTP. (Son una feature de serve, no disponibles en run plano, y no necesitan declaración de memory — nunca tocan disco.) Para estado durable, usá los builtins de memoria/progreso declarados de arriba o una base de datos.