Install & run

Clone the examples repo, install, and run from inside the example's folder.

terminal
$ git clone https://github.com/Cosmonapse/cosmonapse-examples
$ pip install 'cosmonapse[receptor]' httpx python-dotenv
# a Hugging Face token with read scope, in cosmonapse-examples/.env
$ echo "HF_TOKEN=hf_xxxxxxxxxxxxxxxx" >> cosmonapse-examples/.env

$ cd cosmonapse-examples/17-receptors
$ python smoke_test.py                    # offline - all edges, no token
$ python brain.py "what is a synapse"      # one-shot   -> dispatch_and_wait
$ python brain.py --stream "..."           # one-shot   -> dispatch_and_subscribe
$ python brain.py --send "..."             # one-shot   -> dispatch_task
$ python brain.py ping                     # local command, nothing dispatched
$ python brain.py                          # REPL + http://127.0.0.1:8000

$ curl -s  localhost:8000/run -H 'content-type: application/json' -d '{"input": "what is a synapse"}'
$ curl -sN localhost:8000/run -H 'content-type: application/json' -d '{"input": "...", "mode": "stream"}'
cli    : ['echo(0 prior): hello from the cli']
send   : TASK trc_01J...
stream : ['AGENT_OUTPUT']
chat   : 'echo(0 prior): one' then 'echo(2 prior): two'
api    : wait='echo(0 prior): http' send=True stream=True

OK

That is smoke_test.py: the Neuron swapped for an echo, every edge exercised offline.

Layout

One component per file, one Dendrite per node.

The skeleton cosmo init scaffolds. Modules under neurons/, engram/, effector/ and receptors/ declare behaviour; brain.py owns deployment - which node hosts what.

17-receptors
17-receptors/
  neurons/assistant.py   the Neuron - {prompt | message, history?} -> {reply}
  receptors/terminal.py  CliReceptor
  receptors/api.py       ApiReceptor - one endpoint, send / wait / stream
  receptors/chat.py      ChatReceptor - one turn, one dispatch, voice optional
  brain.py               a worker node + a node per interface
  smoke_test.py          offline proof that every edge reaches the Neuron
  RECIPES.md             copy-paste setups for every backend and hook
01 · The concept

One funnel over the dispatch trio.

A Receptor adds no Signal types and no wire format. It emits the TASK an orchestrator always emitted, tagged with meta.receptor so a trace is attributable to its edge.

the trio
shape    Receptor        Dendrite                  you get
send     rx.send(x)      dispatch_task             the emitted TASK
wait     rx.ask(x)       dispatch + Pathway.wait   the rendered result
stream   rx.stream(x)    dispatch_and_subscribe    a live Pathway
02 · CLI

The command returns the TASK input.

Parameters become arguments by signature: no default is positional, a default is a --flag, a bool default is a switch. local=True answers on the spot. on_signal is a progress channel that never changes the trace. Compare it with the 232-line cli.py in Example 16.

receptors/terminal.py
import asyncio

from cosmonapse import CliReceptor, SignalType

RECEPTOR = CliReceptor(
    neuron="assistant",
    prog="assistant",
    description="Talk to a Cosmonapse Neuron from a terminal.",
    timeout_s=120.0,
)


@RECEPTOR.command(help="ask the assistant")
def ask(prompt: str, tokens: int = 400):
    """The return value IS the TASK input - that is the whole contract."""
    return {"prompt": prompt, "max_new_tokens": tokens}


@RECEPTOR.command("ping", local=True, help="is the worker registered?")
async def ping():
    """local=True: answered here, nothing crosses the wire. REGISTER rides
    the bus like every other Signal, so poll briefly in a fresh process."""
    for _ in range(20):
        found = await RECEPTOR.dendrite.find_neurons()
        if found:
            return {"neurons": [n.neuron_id for n in found]}
        await asyncio.sleep(0.05)
    return {"neurons": []}


@RECEPTOR.on_result
def render(sig):
    return sig.payload["output"]["reply"]


@RECEPTOR.on_print
def show(reply: str):
    print(f"\n{reply}\n")


@RECEPTOR.on_signal(SignalType.TOOL_CALL, SignalType.PLAN)
async def progress(sig):
    # Observation only - delete this and the answers are identical.
    print(f"  . {sig.type.value.lower()}")
03 · API

One endpoint, the caller picks the shape.

mode in the body selects send, wait or stream (SSE). GET /run/<trace_id> is a second screen on a trace someone else started. Mount it on an existing app with include_router(RECEPTOR.router).

receptors/api.py
from cosmonapse import ApiReceptor

RECEPTOR = ApiReceptor(
    neuron="assistant",
    path="/run",
    input_key="prompt",
    timeout_s=120.0,
)


@RECEPTOR.on_result
def render(sig):
    return {"reply": sig.payload["output"]["reply"], "trace_id": sig.trace_id}


@RECEPTOR.route("/neurons")
async def neurons():
    """An ordinary route on the same router."""
    found = await RECEPTOR.dendrite.find_neurons()
    return {"neurons": [n.neuron_id for n in found]}
04 · Chat and voice

Voice is client-side.

The page streams each turn over SSE and keeps per-session history, which rides into the TASK as history. With voice=True it adds a mic button and read-back through the browser's Web Speech API: no audio dependency in Python, no audio on the wire.

receptors/chat.py
from cosmonapse import ChatReceptor

RECEPTOR = ChatReceptor(
    neuron="assistant",
    title="Cosmonapse Assistant",
    greeting="Ask me anything - or press mic and talk.",
    voice=True,
    history_turns=8,
    timeout_s=120.0,
)
neurons/assistant.py
from cosmonapse import Axon, Neuron

from config import MODEL, hf_token

_chat = Neuron(
    source="huggingface",
    endpoint="https://router.huggingface.co",
    model=MODEL,
    api_key=hf_token(),
    use_chat_api=True,
    max_new_tokens=400,
)


async def assistant(payload: dict, context: dict | None = None) -> dict:
    """{prompt|message, history?} -> {reply}"""
    text = payload.get("prompt") or payload.get("message") or ""
    turns = payload.get("history") or []
    if turns:
        transcript = "\n".join(f"{t['role']}: {t['content']}" for t in turns)
        text = f"{transcript}\nuser: {text}"
    out = await _chat({"prompt": text}, context)
    reply = out.get("response") if isinstance(out, dict) else str(out)
    return {"reply": reply}


AXON = Axon(
    neuron_id="assistant",
    neuron_fn=assistant,
    capabilities=["chat", "text-generation"],
)
05 · The brain

Interfaces are components.

Each Receptor mounts on its own orchestrator node. run_brain serves all of them: the api and chat share 127.0.0.1:8000 and are merged onto one app. :quit closes the REPL and the HTTP edges keep serving; Ctrl-C stops the brain.

brain.py
import asyncio
import contextlib
import signal

from cosmonapse import (Dendrite, MemoryRegistryStore, MemorySynapse,
                        connect_synapse, run_brain)

from config import NAMESPACE, SYNAPSE_URL
from neurons import assistant
from receptors import api, chat, terminal


def build_worker(synapse) -> Dendrite:
    node = Dendrite(synapse=synapse, namespace=NAMESPACE,
                    dendrite_id="worker", role="worker")
    node.attach_axon(assistant.AXON)
    return node


def build_interface(synapse, name: str, receptor) -> Dendrite:
    """One node per interface. registry_store powers find_neurons() - the
    terminal's `ping` and the api's /neurons."""
    node = Dendrite(synapse=synapse, namespace=NAMESPACE,
                    dendrite_id=f"{name}-node", heartbeat_s=0,
                    registry_store=MemoryRegistryStore())
    node.attach_receptor(receptor)
    return node
Over a real synapse

Same code, different transport.

With SYNAPSE_URL unset the brain runs on an in-process bus. Point it at a synapse and nothing in the modules changes. Prism attaches to any shared synapse and animates every Signal as it crosses.

terminal
# terminal 1 - a dev synapse (TCP, single host)
$ cosmo synapse start memory --namespace=receptors

# terminal 2 - the same brain, over that synapse
$ SYNAPSE_URL=cosmo://127.0.0.1:7070 python brain.py "what is a synapse"

# terminal 3 - Prism, the live view (http://127.0.0.1:7071)
$ cosmo prism --url=cosmo://127.0.0.1:7070 -n receptors

# production transports - only the URL changes
$ SYNAPSE_URL=nats://127.0.0.1:4222 python brain.py "what is a synapse"
$ SYNAPSE_URL=kafka://127.0.0.1:9092 python brain.py "what is a synapse"