// The smallest useful agent
One Neuron. One Engram. One Effector.
A RAG that goes and gets what it does not know. Below is a real run of cosmonapse-examples/16-rag-cli, replayed Signal by Signal - then the code that produced it, with every primitive annotated. Hover anything.
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_outputmemory
@ENGRAM.on_recall@ENGRAM.host.on_recalledaction
@EFFECTOR.on_tool_call@EFFECTOR.host.on_finalThe 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.