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 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/03-round-robin
$ python brain.py haikus              # four prompts, alternating workers
$ python brain.py "haiku: the sun"     # one prompt, next worker in line
$ python brain.py                      # REPL - every line rotates
worker-a -> Morning light breaks through ...
worker-b -> Silver moon ascends ...
worker-a -> Waves crash on the shore ...
worker-b -> Whispers through the trees ...

Exact text varies - the model is stochastic. The alternation does not.

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.

03-round-robin
03-round-robin/
  config.py              WORKERS = ("worker-a", "worker-b")
  neurons/pool.py        make_axon(worker_id) - same Neuron, different id
  receptors/terminal.py  the rotation
  brain.py               one node per worker + terminal-node
01 · The pool

One Neuron, many ids.

A factory rather than a module-level AXON: an Axon attaches to exactly one Dendrite, and the brain builds one per worker id.

neurons/pool.py
from cosmonapse import Axon, Neuron

from config import hf_token


def make_axon(neuron_id: str) -> Axon:
    return Axon(
        neuron_id=neuron_id,
        neuron_fn=Neuron(
            source="huggingface",
            endpoint="https://router.huggingface.co",
            model="meta-llama/Llama-3.1-8B-Instruct",
            api_key=hf_token(),
            use_chat_api=True,
            max_new_tokens=128,
        ),
        capabilities=["text-generation", "chat"],
    )
02 · The rotation

Pick the next id, ask with it.

A Receptor's target is usually fixed at construction, but ask() takes neuron= or capabilities= per call. So the command picks the next worker from itertools.cycle and asks it. ask() opens the Pathway on the trace and resolves on that worker's AGENT_OUTPUT, which is everything the old hand-rolled futures did. The commands are local=True because they choose their own target instead of handing an input to the Receptor.

receptors/terminal.py
import itertools

from cosmonapse import CliReceptor, SignalType

from config import WORKERS

RECEPTOR = CliReceptor(
    prog="round-robin",
    description="Cycle prompts across a pool of identical workers.",
    timeout_s=60.0,
)

_rotation = itertools.cycle(WORKERS)


@RECEPTOR.on_result
def render(sig):
    if sig.type is SignalType.ERROR:
        return f"ERROR {sig.payload.get('message')}"
    return sig.payload["output"]["response"].strip()


@RECEPTOR.command(local=True, default=True, help="send a prompt to the next worker")
async def ask(prompt: str):
    target = next(_rotation)                          # round-robin pick
    reply = await RECEPTOR.ask({"prompt": prompt}, neuron=target)
    return f"{target} -> {reply}"


@RECEPTOR.command("haikus", local=True, help="four prompts, alternating workers")
async def haikus():
    topics = ("the sun", "the moon", "the sea", "the wind")
    return "\n".join([await ask(f"haiku: {t}") for t in topics])
03 · The brain

A node per worker.

Each worker gets a Dendrite with dendrite_id set to its worker id, so it is separately addressable and separately visible in Prism. Moving worker-b to another machine is running its builder there against the same SYNAPSE_URL.

brain.py
import asyncio
import contextlib
import signal

from cosmonapse import Dendrite, MemorySynapse, connect_synapse, run_brain

from config import NAMESPACE, SYNAPSE_URL, WORKERS
from neurons import pool
from receptors import terminal


def build_worker(synapse, worker_id: str) -> Dendrite:
    """One pool member. Same Neuron, different id."""
    node = Dendrite(synapse=synapse, namespace=NAMESPACE,
                    dendrite_id=worker_id, role="worker")
    node.attach_axon(pool.make_axon(worker_id))
    return node


def build_terminal(synapse) -> Dendrite:
    node = Dendrite(synapse=synapse, namespace=NAMESPACE,
                    dendrite_id="terminal-node", heartbeat_s=0)
    node.attach_receptor(terminal.RECEPTOR)
    return node

No fixed order needed? Give the workers identical capabilities and dispatch with capabilities=["chat"]. Dendrites with the same capability profile share a queue group and the broker load-balances, as in capability routing.

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=quickstart

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

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

# production transports - only the URL changes
$ SYNAPSE_URL=nats://127.0.0.1:4222 python brain.py haikus
$ SYNAPSE_URL=kafka://127.0.0.1:9092 python brain.py haikus
http://127.0.0.1:7071 · -n quickstart
Prism renders every Signal on the bus as it fires - REGISTER, TASK, AGENT_OUTPUT, FINAL.