Topology

Planner, two specialists, four tools, one memory.

Three agent nodes (role="orchestrator" - only because their chain handlers dispatch TASKs; tool calls are not role-gated), three worker tool nodes (web-node hosts the websearch AND fetch Axons), a worker engram host, and the caller. Chain state rides the TASK inputs; progress is recalled from the engram - no node holds run state.

topology
One dispatch, no loop  -  the Dendrites chain the run themselves

  agent-loop (orchestrator) --- ONE TASK ["planner"] ---> awaits the trace's FINAL

  planner-node   @on_agent_output("planner")   -> TASK ["research"|"coding"],
                                                 or assemble + imprint + FINAL
  research-node  @on_agent_output("research")  -> TASK ["planner"] (next step)
  coding-node    @on_agent_output("coding")    -> TASK ["planner"] (next step)
  engram host    @on_imprint_signal            -> mirror "answer" imprints to disk
  tool nodes     @on_tool_call(<capability>)   -> run the MCP, reply TOOL_RESULT

  tools (workers)  web-node (websearch + fetch Axons) | clock | files
  planner -> ["time"]   research -> ["websearch"] ["fetch"]   coding -> ["filesystem"]
00 · Install & run

One process by default (in-process MemorySynapse); set SYNAPSE_URL to split across processes.

# Needs a real HF_TOKEN in cosmonapse-examples/.env. fetch/time run via
# uvx (install uv); filesystem runs via npx (Node 18+).
$ pip install cosmonapse httpx python-dotenv mcp fastapi uvicorn
$ pip install duckduckgo-mcp-server   # the websearch tool (launched via uvx)
$ uvicorn app:app --port 8000

$ curl -X POST localhost:8000/run -H 'content-type: application/json' \
       -d '{"goal": "Research X and write a Python script that does Y"}'
$ curl localhost:8000/memory
Scaffold

Init, scaffold, then code. cosmo init writes the standard skeleton every example follows - config.py, neurons/, effector/, brain.py, demo.py - and the files on this page are what you code on top of the generated stubs.

$ cosmo init agent -n agent

  Scaffolded agent in ./agent
    + config.py   + neurons/hello.py   + effector/tools.py
    + brain.py    + demo.py            + README.md
01 · One dispatch, no loop

run_agent is a single dispatch_and_wait.

scope="terminal" resolves the Pathway on FINAL / ERROR only, and finalize=False leaves FINAL to the chain - the run concludes when the planner node emits it. All chain TASKs share this trace, so that FINAL resolves this call.

brain.py
# run_agent - dispatch ONE TASK, wait for FINAL. No loop; the nodes chain.
async def run_agent(orchestrator, goal, *, max_steps=MAX_STEPS, timeout_s=600.0):
    tag = new_trace_id()        # the run's trace; also labels engram entries
    sig = await orchestrator.dispatch_and_wait(
        capabilities=["planner"],
        input={"goal": goal, "tag": tag, "step": 1, "max_steps": max_steps,
               "steps": [], "sources": []},
        trace_id=tag,
        scope="terminal",       # resolve on FINAL / ERROR only
        finalize=False,         # the planner node owns FINAL, not a worker
        timeout_s=timeout_s,
    )
    if sig.type is SignalType.ERROR:
        raise RuntimeError(f"run failed: {sig.payload.get('message')}")
    return sig.payload["result"]
02 · Stock Neurons, decorated behaviour

The planner is just llm(MODEL) plus hooks.

@before_task shapes the TASK input - recall the answer cache and this run's progress via the Dendrite, ground the prompt with the clock tool. @detects_output shapes the raw reply into a validated route decision. The model itself never touches the protocol.

neurons/model/planner.py
# The neuron_fn is a STOCK Neuron - an HF-router chat model with zero
# protocol knowledge. Everything else is decorator-specified on its Axon.
AXON = Axon(neuron_id="planner", neuron_fn=llm(MODEL), capabilities=["planner"])

@AXON.before_task
async def situate(input):
    """Cache check + progress recall + clock grounding, then the prompt."""
    tid, _ = ambient_trace()
    d = AXON.dendrite

    # A previously remembered answer short-circuits the whole run.
    cached = await d.recall(engram_id=ENGRAM_ID,
                            query={"merge_key": f"answer:{input['goal']}"},
                            deadline_ms=2000)

    # Progress is not carried by any node - it is recalled from the engram.
    done = await d.recall(engram_id=ENGRAM_ID,
                          query={"tag": input["tag"], "top_k": 50},
                          deadline_ms=2000)

    # Ground the prompt in real time - via the planner's OWN clock tool.
    clock = await bus.mcp(d, ["time"], "get_current_time", {"timezone": "UTC"})
    return {"messages": [...]}   # the prompt the stock Neuron will run

@AXON.detects_output
def decide(raw):
    """Parse the model's JSON; force finish past max_steps; echo the chain."""
    decision = _first_json(raw.get("response")) or {}
    ...
    return {"route": route, "task": task, **chain}
03 · The chain

Dendrites create the TASKs.

Each agent's chain handler is ONE deferred host decorator - @AXON.host.on_agent_output - applied to the hosting Dendrite when it announces the Axon, subscription ensured. The planner node routes to a specialist or - on finish - assembles the report from the engram, imprints it, and emits FINAL.

neurons/model/planner.py
@AXON.host.on_agent_output(neuron="planner")
async def chain(sig):
    """Declared at module level. The Axon applies it to the HOSTING
    Dendrite when announced (subscription ensured) - no on_connect
    boilerplate, no wiring helpers."""
    node = AXON.dendrite
    d = sig.payload.get("output", {})
    route, goal, tag = d.get("route"), d.get("goal"), d.get("tag")

    if route in ("research", "coding"):
        # The Dendrite CREATES the next TASK - same trace, so the
        # chain's FINAL resolves the caller's Pathway.
        await node.dispatch_task(
            capabilities=[route],
            input={"task": d.get("task", ""), "goal": goal, "tag": tag, ...},
            trace_id=sig.trace_id, parent_id=sig.id,
        )
        return

    # finish: assemble the report from this run's engram entries,
    # remember it under the goal, then conclude the trace.
    got = await node.recall(engram_id=ENGRAM_ID,
                            query={"tag": tag, "top_k": 50}, ...)
    await node.imprint(engram_id=ENGRAM_ID, op="upsert",
                       merge_key=f"answer:{goal}",
                       entry={"content": report, "tags": ["answer"]},
                       meta={"path": report_path},
                       await_ack=True, deadline_ms=2000, ...)
    await node.emit_final(
        trace_id=sig.trace_id, parent_id=sig.id,
        result={"report": report, "source": "web", ...},
    )
04 · A specialist

Research: search, fetch, imprint, hand back.

The research node gathers web context through its own tools in @before_task, imprints its note in @detects_output, and its chain handler dispatches the next planner TASK. The coding node is the same shape: recall notes, write report/solution.py via the filesystem MCP, imprint the code, hand back.

neurons/model/research.py
@AXON.before_task
async def gather(input):
    """Gather web context via MY tools, shaped into the prompt."""
    search = await bus.mcp(d, ["websearch"], "search",
                           {"query": task, "max_results": 5})
    page = await bus.mcp(d, ["fetch"], "fetch",
                         {"url": url, "max_length": 2500})
    return {"messages": [...]}   # subtask + web context

@AXON.detects_output
async def note_and_imprint(raw):
    """Shape the note, remember it (dendrite.imprint), echo the chain."""
    note = (raw.get("response") or "").strip()
    await AXON.dendrite.imprint(
        engram_id=ENGRAM_ID, op="append",
        entry={"content": note, "tags": ["research", st.get("tag", "")]},
        await_ack=True, deadline_ms=2000,
    )
    return {"note": note, **chain_state}

# @AXON.host.on_agent_output(neuron="research"): the chain handler hands
# back to the planner with step + 1 - same trace, same pattern as above.
05 · Tool calls

MCP = TOOL_CALL → TOOL_RESULT. Not a TASK.

A tool invocation rides the cognition pair made for it: the caller emits a TOOL_CALL directed at a capability, the tool node's @AXON.host.on_tool_call handler runs the MCP and answers with a TOOL_RESULT echoing the call_id. Fresh trace per exchange - tool traffic reads as tool traffic.

bus.py
# bus.py - MCP tools are NOT tasks. A tool invocation rides the cognition
# pair built for exactly this: TOOL_CALL out, TOOL_RESULT back.
async def mcp(dendrite, caps, tool, arguments, *, timeout=60.0):
    """Call one MCP tool over the TOOL_CALL / TOOL_RESULT signal pair."""
    call_id = new_event_id()
    tid = new_trace_id()            # own trace: never the agent chain's
    pw = await dendrite.observe_pathway(tid)
    async with pw:
        await dendrite.emit_tool_call(
            trace_id=tid, parent_id=call_id,
            tool=tool, args=arguments, call_id=call_id,
            neuron=caps[0],         # directed at the CAPABILITY
        )
        while True:
            sig = await pw.wait_for(SignalType.TOOL_RESULT, timeout_s=timeout)
            if sig.payload.get("call_id") != call_id:
                continue
            if sig.payload.get("error") is not None:
                raise RuntimeError(f"{caps}.{tool} error: ...")
            return sig.payload["result"]

# Each exchange gets a FRESH trace, so a TOOL_RESULT can never resolve the
# calling agent's pending chain Pathway - and doppler shows tool traffic as
# TOOL_CALL -> TOOL_RESULT, not nested TASKs. Tool calls are not role-gated
# (only TASK/STOP are): even worker-role nodes call their tools.
neurons/mcp/*.py
# A tool module: one stock MCP Neuron + ONE deferred host decorator.
TOOL = Neuron(source="mcp", command="uvx", args=["duckduckgo-mcp-server"])
AXON = Axon(neuron_id="websearch", capabilities=["websearch"], neuron_fn=TOOL)

@AXON.host.on_tool_call(neuron="websearch")     # calls to MY capability
async def call(sig):
    await bus.tool_reply(AXON.dendrite, AXON.neuron_id, TOOL, sig)

# web-node hosts BOTH the websearch and fetch Axons - one Dendrite, two
# tools; the directed-capability filters keep them discriminated locally.
06 · Dendrite-owned memory

No memory-access neuron.

Recall and imprint are Dendrite behaviour (dendrite.recall / imprint(engram_id=...)); ops fired from hooks inherit the TASK's trace via the ambient context. Host-side persistence is decorator-specified too: @host.on_imprint_signal mirrors answer imprints to report/answer.md.

brain.py
ENGRAM = InMemoryEngram(engram_id="agent-memory", engram_kind="context")

host = Dendrite(synapse=synapse, namespace=NAMESPACE,
                dendrite_id="agent-memory-host",
                role="worker")   # TOOL_CALLs aren't role-gated - no
                                  # orchestrator rights needed for tools
host.attach_engram(ENGRAM)

@host.on_imprint_signal
async def persist_answers(sig):
    """Mirror "answer" imprints carrying a path to disk (filesystem MCP)."""
    entry = sig.payload.get("entry") or {}
    path = (sig.meta or {}).get("path")
    if path and "answer" in (entry.get("tags") or []):
        await bus.mcp(host, ["filesystem"], "write_file",
                      {"path": str(ROOT / path),
                       "content": entry.get("content", "")})
07 · The edge

FastAPI stays at the edge.

Same rule as Example 02: the web framework dispatches from its route handlers and never touches the protocol.

app.py
@app.post("/run")
async def run(body: dict):
    """{"goal", "max_steps"?, "timeout_s"?} -> the trace's FINAL payload."""
    return await run_agent(
        state["orchestrator"], goal,
        max_steps=int(body.get("max_steps", MAX_STEPS)),
        timeout_s=float(body.get("timeout_s", 600.0)),
    )

@app.get("/memory")
async def memory():
    """What the agent remembers - answers, research notes, code snapshots."""
    ...

# A repeat goal returns instantly from the Engram ("source": "memory").
08 · Run it

Web the first time, memory the second.

terminal
$ curl -X POST localhost:8000/run -H 'content-type: application/json' \
       -d '{"goal": "Research the Collatz conjecture, then write a Python CLI
            that prints the Collatz sequence for N"}'
{
  "report": "# Research the Collatz conjecture, ...",
  "code_path": "report/solution.py",
  "report_path": "report/answer.md",
  "source": "web",
  "steps": [{"route": "research", ...}, {"route": "coding", ...}]
}

# run the same goal again - answered from memory on step 1:
{ "report": "...", "source": "memory", "steps": [] }
Watch it in Prism

TASK → TOOL_CALL → TOOL_RESULT → IMPRINT → FINAL, live.

No observability is baked into the example - point cosmo doppler at the synapse and watch the chain hop between nodes as the run unfolds.

terminal
# Set SYNAPSE_URL to a running synapse to split across processes -
# and to watch the chain live.

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

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

# terminal 3  -  the agent, pointed at the bus
$ SYNAPSE_URL=cosmo://127.0.0.1:7070 uvicorn app:app --port 8000
http://127.0.0.1:7071 · -n agent
Prism renders every Signal on the bus as it fires - REGISTER, TASK, AGENT_OUTPUT, FINAL.