00 · Install

InMemoryEngram and SqliteEngram ship with the core package. PostgresEngram needs the [postgres] extra (which pulls in asyncpg).

# Python 3.11+. The default InMemoryEngram needs no extras.
$ pip install cosmonapse

# For durable storage:
$ pip install "cosmonapse"   # PostgresEngram
# (SqliteEngram is in the stdlib  -  no extra needed.)
01 · The Neuron

Pure function - plus two helpers.

The Neuron is still a plain async function. The only change is the signature: recall and imprint arrive as keyword-only parameters injected by the Axon. The Neuron addresses memory by the local binding name ("ctx"), not the wire-level engram_id.

researcher.py
# The Neuron gains two keyword-only parameters: recall and imprint.
# The Axon injects them at call time because the Axon was constructed
# with engrams=[EngramBinding(name="ctx", ...)] (see step 2).
#
# Under the hood, recall("ctx", ...) emits a RECALL Signal under the
# current trace_id and awaits the matching RECALLED reply. The Neuron
# stays pure  -  it never imports the protocol or touches the Synapse.
async def researcher(input, context, *, recall, imprint):
    question = input["question"]

    # 1. Look in shared memory for a prior answer to this exact question.
    prior = await recall("ctx", query={"text": question})
    if prior.hits:
        cached = prior.hits[0].content["answer"]
        return {"answer": cached, "source": "cache"}

    # 2. Compute a "fresh" answer (stubbed for the demo).
    answer = f"Answer to {question!r}: 42"

    # 3. Write it back so the next call hits the cache.
    #    merge_key dedupes by question text so repeated imprints upsert
    #    a single entry per question.
    await imprint(
        "ctx",
        op="upsert",
        entry={"question": question, "answer": answer, "tags": ["qa"]},
        merge_key=f"q:{question}",
        await_ack=True,
        deadline_ms=500,
    )
    return {"answer": answer, "source": "computed"}
02 · The wiring

Engram host · worker · orchestrator.

One Dendrite hosts the Engram, one hosts the Neuron with a declarative EngramBinding, and one dispatches TASKs. The host could be the same process as the worker - we split them here to make the routing explicit.

wiring.py
# Three Dendrites, one shared Synapse:
#
#   host           -  owns the Engram backend (answers RECALL/IMPRINT)
#   worker         -  hosts the Neuron, declares the EngramBinding
#   orchestrator   -  dispatches TASKs
from cosmonapse import (
    Axon, Dendrite, EngramBinding, InMemoryEngram, MemorySynapse,
)

synapse = MemorySynapse()
await synapse.connect()

# 1. Engram host. engram_id="ctx" is the wire address.
host = Dendrite(synapse=synapse, namespace="demo",
                  dendrite_id="engram-host", role="worker")
host.attach_engram(
    InMemoryEngram(engram_id="ctx", engram_kind="context")
)

# 2. Worker. The binding maps a local name ("ctx") to the wire
#    engram_id, so the Neuron addresses memory by a stable local
#    name  -  operations repoint the backend without editing Neuron code.
worker = Dendrite(synapse=synapse, namespace="demo",
                    dendrite_id="worker", role="worker")
worker.attach_axon(
    Axon(
        neuron_id="researcher",
        neuron_fn=researcher,
        capabilities=["research"],
        engrams=[EngramBinding(name="ctx", directed_id="ctx")],
    )
)

orchestrator = Dendrite(synapse=synapse, namespace="demo")
03 · Run

First call computes, second call recalls.

Two back-to-back dispatches with the same question. The first sees an empty Engram and imprints the answer. The second finds the cached entry and short-circuits - proof the imprint landed.

dispatch.py
# Call twice with the same input. The first call computes + imprints;
# the second call recalls and short-circuits.
async with host, worker, orchestrator:
    for label in ("first call ", "second call"):
        reply = await orchestrator.dispatch_and_wait(
            neuron="researcher",
            input={"question": "what is the meaning of life?"},
            timeout_s=5.0,
        )
        out = reply.payload["output"]
        print(f"[{label}] {out['source']:>8s}  →  {out['answer']}")
$ python main.py
[first call ] computed  →  Answer to 'what is the meaning of life?': 42
[second call]    cache  →  Answer to 'what is the meaning of life?': 42
04 · Swap the backend

Three Engram backends, one API.

The Engram interface is uniform. Mount whichever backend fits your deployment; the Neuron and the binding never change.

backends.py
# Same Engram API, three backends. Swap the line where you mount it;
# the Neuron and the binding never change.
from cosmonapse import InMemoryEngram, SqliteEngram, PostgresEngram

# 1 · In-process  -  for tests & single-process apps.
host.attach_engram(InMemoryEngram(engram_id="ctx", engram_kind="context"))

# 2 · SQLite  -  durable, single file, no server.
host.attach_engram(SqliteEngram(
    engram_id="ctx", engram_kind="context",
    path="./engram.db",
))

# 3 · Postgres  -  for production. Requires the [postgres] extra.
host.attach_engram(PostgresEngram(
    engram_id="ctx", engram_kind="context",
    dsn="postgresql://user:pass@localhost/cosmo",
))
05 · Operations & modes

Five imprint ops, three recall modes.

imprint covers add / append / merge / upsert / delete. recall returns one entry, a merged view, or the full set - pick the mode per call, or set a default on the binding.

ops.py
# imprint operations
await imprint("ctx", op="add",    entry={...})                   # fail if exists
await imprint("ctx", op="append", entry={...})                   # always grow
await imprint("ctx", op="merge",  entry={...}, merge_key="q:42")  # combine
await imprint("ctx", op="upsert", entry={...}, merge_key="q:42")  # insert or replace
await imprint("ctx", op="delete", entry={...})                   # remove

# recall modes (configure default on the binding)
EngramBinding(name="ctx", directed_id="ctx", default_recall_mode="merge")
#   "first"   -  return the best single match (default)
#   "merge"   -  combine matching entries across backends
#   "all"     -  return every match, partial flag if any backend timed out
06 · Watch it in Prism

RECALL and IMPRINT, live on the bus.

cosmo doppler --prism shows the full memory round-trip - RECALL, RECALLED, IMPRINT, IMPRINTED - threaded through the same trace as the TASK that caused it.

terminal
# This example runs in-process on MemorySynapse, which Prism cannot
# attach to. To watch it live, run a dev synapse and point the code at it:

# terminal 1  -  the bus
$ cosmo synapse start memory --namespace=demo

# terminal 2  -  Prism, the live browser view (http://127.0.0.1:7071)
$ cosmo doppler --prism --url=cosmo://127.0.0.1:7070 -n demo

# in the code  -  swap one line:
# synapse = MemorySynapse()
synapse = await connect_synapse("cosmo://127.0.0.1:7070")
http://127.0.0.1:7071 · -n demo
Prism showing Signals animating in the demo namespace
Prism renders every Signal on the bus as it fires — REGISTER, TASK, AGENT_OUTPUT, FINAL.