3 Dendrites, 1 Synapsenamespace: rag-cli
rag-nodeorchestratorthe rag Axon
Hosts the one Neuron, and doubles as the CLI's orchestrator. A Dendrite that both originates and hosts work is supported, so there is no fourth node.
RECALL / IMPRINT
TOOL_CALL
engram-hostworkerweb-memory
Hosts the Engram - a BM25 index over page chunks. It answers RECALL and IMPRINT and does nothing else.
web-nodeworkerthe web Effector
Hosts the Effector - search and fetch, backed by an MCP server. It answers TOOL_CALL and does nothing else.
0/17 Signals0 tool calls0 memory ops
cosmo prism --tail -n rag-clihover a Signal
Press Play. Every line that appears is one Signal that actually crossed the bus.
The trace is 17 Signals cold, 5 warm. That difference is the whole demo.
python cli.py
waiting...
// the code that did that

Every primitive is a decorated reaction to a Signal.

Every underlined token is hoverable. This is the whole system - not an excerpt chosen to look small.

sidedoes the workwatches it happen
agent@AXON.before_task@AXON.host.on_agent_output
memory@ENGRAM.on_recall@ENGRAM.host.on_recalled
action@EFFECTOR.on_tool_call@EFFECTOR.host.on_final

The left column runs inside the request and its return value becomes the reply. The right column fires after the reply is already on the wire - it can observe, count and annotate, but it cannot answer. Confusing the two is how you write to a store twice.

neurons/rag.pythe Neuron - a stock model + three decorators
# THE NEURON is a stock chat model. Three decorators make it a RAG.
 
AXON = Axon(neuron_id="rag", neuron_fn=llm()A hosted chat model and nothing else - no retrieval code, no tool code, no protocol code. It never learns Cosmonapse exists. Swap it in config.py and no other file changes., ...)
 
@AXON.before_taskRuns before the model does. The question comes in, a chat prompt goes out - and every Signal this example sends is emitted from in here.
async def situate(input):
d = AXON.dendriteThe Dendrite hosting this Axon. Memory and tools are both reached through it - they are Signals on the bus, not library calls.
 
# 1. What do we already know?
known = await d.recallEmits RECALL, waits for RECALLED. Answered by whichever Dendrite hosts that Engram - which may be another machine.(engram_id="web-memory", query={...})
 
# 2. Not enough -> go and learn.
if not _coversThe entire "do I already know this?" policy: enough passages, covering enough of the question's content words. Two numbers in config.py.(question, passages):
found = await d.call_toolEmits TOOL_CALL, waits for TOOL_RESULT. Serviced by whichever Dendrite hosts that Effector.(effector_id="web-effector", tool="search", ...)
for url in _urls(found.result["response"]):
page = await d.call_tool(effector_id="web-effector", tool="fetch", ...)
for i, chunk in enumerate(_chunksFixed-width overlapping chunks. Deliberately dumb - a smarter splitter is a better splitter, not a different architecture.(page)):
await d.imprintEmits IMPRINT. This one does not wait for the receipt.(engram_id="web-memory", op="upsert",
merge_key=f"{url}#{i}"Re-reading a page upserts its chunks instead of duplicating them. The index is idempotent under repeat runs., await_ack=FalseEmit and move on. Nothing downstream needs the receipt, so this IMPRINT never blocks.)
 
# 3. The same recall as step 1. Both paths converge here.
passages = _passages((await d.recall(...)).hits)
 
return {"messages": [systemThe model is handed finished passages and a question. It has no tools and no memory of its own., user]}
 
@AXON.detects_outputRuns on the model's raw reply. Here the reply IS the answer, so this just re-attaches the sources it was allowed to cite.
def answer(raw): ...
 
@AXON.host.on_agent_outputA DEFERRED HOST decorator. It queues at import time and is replayed onto whichever Dendrite hosts this Axon - so brain.py wires no handlers. Here the chain has one link; in 14-agent this same decorator hands work from the planner to the researcher.(neuron="rag")
async def conclude(sig): # THE CHAIN: emit FINAL
neurons/rag.pythe declaration - what it may touch
# THE DECLARATION. What this Neuron is allowed to touch.
 
AXON = AxonThe Axon is the agent-side interface: it declares capabilities, validates output, and resolves bindings. The Neuron above never imports it.(
neuron_id="rag", neuron_fn=answer_neuron, capabilities=["rag"What this Neuron advertises. The CLI dispatches to this capability, not to a node - so you can run three replicas and the Synapse load-balances them.],
engrams=[EngramBindingDeclarative wiring. The Neuron says "web"; this says what "web" means on the wire. A name the Axon did not declare is refused at call time.(name="web", directed_id="web-memory")],
effectors=[EffectorBinding(name="web", directed_id="web-effector",
tools=("search", "fetch"))],
tool_standard=THE GATE: effectors= without a tool_standard fails at construction, not silently at runtime. The standard is how an Axon recognises a tool call in a model's raw output - bindings without one would be dead wiring."codex",
)
engram/web_memory.pythe Engram - Engram.serve()
# THE ENGRAM. Two decorators - the memory side of the same idea.
 
ENGRAM = Engram.serveThe memory-side twin of Effector.serve(). No ABC to implement and no reply to publish - the Engram is still attached under its own id, so it REGISTERs normally and an observer draws it as one engram node.(engram_id="web-memory", engram_kind="lexical")
 
@ENGRAM.servesThe can_serve gate. Return False and the hosting Dendrite skips responding - which is how a BM25 memory declines a vector query routed by kind rather than by id.
def only_text(query): return "text" in query
 
@ENGRAM.on_recallRECALL arrives, this runs, and what it RETURNS becomes the RECALLED hits. It executes INSIDE the Dendrite's handling pass - the same position @AXON.before_task occupies for a Neuron - so resolution and attribution keep working.
async def search(query, *, deadline_ms=None, min_confidence=None):
return [HitReturn Hits, or plain {id, entry, score} dicts. Returning None falls through to the next handler, so a quota or ACL can sit in front of the real backend.(id=eid, entry=e, score=bm25) ...]
 
@ENGRAM.on_imprintIMPRINT arrives, this runs, and what it returns becomes the IMPRINTED receipt. The index cap lives in here because this handler owns the write.
async def write(op, entry, *, merge_key=None):
eid = _put(entry, merge_key); _cap()
return eid
 
@ENGRAM.host.on_recalledA pure OBSERVER - it fires AFTER the reply is on the wire, so it cannot filter or rewrite a query. Note which signal carries what: the request had the query, only the reply has the hits.
async def mark_used(sig): # stamps last_used; never writes
effector/web.pythe Effector - Effector.serve()
# THE EFFECTOR. Same shape again, on the action side.
 
EFFECTOR = Effector.serveOne protocol hook. A TOOL_CALL arrives, your handler runs, its return value is emitted as the TOOL_RESULT - no manual publish, no dispatch table in the SDK.(effector_id="web-effector", effector_kind="web")
 
@EFFECTOR.on_tool_callThe tool itself. A raise becomes `error` on the TOOL_RESULT, and a tool error never terminates the parent TASK.
async def handle(tool, args, *, trace_idInjected only because it is declared - so are call_id and deadline_ms if a handler asks for them.=None):
# search | fetch, proxied onto one MCP server
 
@EFFECTOR.host.on_finalThe action-side observer. Drops this trace's fetch memo when the trace ends - without it the dict grows for the life of the process.
async def forget(sig): _SEEN.pop(sig.trace_id, None)
brain.pythe wiring - three Dendrites
# The wiring. Three Dendrites, one Synapse.
 
host = DendriteA Dendrite is the synapse-side participant: it owns routing and exposes the aggregate capabilities of whatever is attached to it.(synapse=synapse, dendrite_id="engram-host", role="worker")
host.attach_engramThat is the entire registration step. No registry, no handler table, no manual subscription - the Dendrite now answers RECALL and IMPRINT for this Engram.(web_memory.ENGRAM)
 
tools = Dendrite(synapse=synapse, dendrite_id="web-node", role="worker")
tools.attach_effectorSame idea for tools. The SDK services this Effector's TOOL_CALL / TOOL_RESULT from here on.(web.EFFECTOR)
 
node = Dendrite(synapse=synapse, dendrite_id="rag-node", role="orchestrator"Only an orchestrator may dispatch TASKs. Workers host Axons, Effectors and Engrams and never originate work.)
node.attach_axon(rag.AXON)

Same three primitives, an order of magnitude more system.

15-claude-harness is a claude-code-style coding agent: 3 Neurons, 4 Effectors, 1 Engram, model-driven tool calls, and a compaction chain. Same file layout. Still no agent loop anywhere.