Synsemadocsv0.6.xENES

Agents & coordination

Memory & state

Persistent agent state is declared, not implicit. One line at the top of the program names the memory and unlocks the whole family — memory, owner rules, and progress:

require memory("support-agent")

The declared name is the identity: state persists in <program-dir>/.synsema/state/<name>.db (gitignore .synsema/), keyed by that name — not by the filename. remember() in one run is found by recall() in the next; renaming the .syn file changes nothing; two entry points that declare the same name share one memory.

memory.syn
-- 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))))

The declaration (know this before anything else)§

Persistent memory§

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)

A recall entry is a map with id, category, content, source, tags.

recall takes 6 args — recall(category, tags, search, mode, limit, from) — all optional, all usable as named args (recall("learning", limit = 10)):

ArgMeaning
categoryone of the fixed categories (below)
tagslist; OR by default
searchfree-text substring match
mode"all" = every tag must match (AND)
limitmax entries, default 200
fromsource namespace to read (see next section)
Categories are a fixed English set: preference, rule, learning, decision, context. Any other string (e.g. "preferencia") raises an error.

remember takes at most 3 arguments — remember(category, content, tags) — and recall at most 6: since v0.6.29 an extra one is an error (remember() takes at most 3 arguments, got 4), not a silent drop.

What recall returns is a value (v0.6.29+)§

Lists and maps are values, so an entry you got from recall is your copy: set e["content"] to … changes it and nothing else. The store changes only through the family — remember a new entry, forget_memory(id) the old one — and so does what other agents, requests and later runs see. The same holds for a task that receives state: it changes its own copy and must give it back (see Multi-agent § Shared state).

Every recall is an input — lineage (v0.6.29+)§

The engine records each recall in lineage(): source recall, what = query sha256-salted:<hex> (a salted commitment to the arguments — a search text such as "session://TOKEN" is data, never published as a host) and the sha256 of the result as canonical_json (encoding: "jcs"), so "0x" + e.sha256 == hex(sha256(canonical_json(rows))). receipt() publishes it without the salt — see Observability § Lineage.

Per-agent namespaces (source / from)§

Each entry records its writer in source: inside agent X a remember writes source = "X"; top-level code (and serve handlers) write "main". Reads are namespaced by default, so agents sharing one memory don't confuse each other's notes:

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

Rules and progress are not namespaced — one rulebook, one plan board per program.

Progress (crash-resumable)§

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

Owner rules§

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

Levels: must (hard block), should (warning), avoid / prefer (preferences). Numeric conditions are evaluated against the context map. get_rules(category?) lists them; memory_summary() prints an overview of everything stored.

Serve state (per-process, in-memory)§

Under serve, state_* builtins (e.g. state_incr("visits")) share in-memory state across requests; to change a shared map from concurrent requests use the atomic state_merge(key, map) and state_cas(key, expected, new) (engine v0.6.45+), not state_get + state_set, which lose writes — see HTTP server. (They're a serve feature, not available in plain run, and they need no memory declaration — they never touch disk.) For durable state, use the declared memory/progress builtins above or a database.