Capability routing.
capabilities=[...] publishes on the routed subject and the broker delivers the TASK to exactly one Dendrite whose advertised capabilities cover it. No token needed.Clone the examples repo, install, and run from inside the example's folder.
$ git clone https://github.com/Cosmonapse/cosmonapse-examples $ pip install cosmonapse $ cd cosmonapse-examples/09-capability-routing $ python brain.py demo $ python brain.py summarize "Neurons are plain functions that..." $ python brain.py who translate
-> summarize summarizer <- summary: Cosmonapse is an event-driven substrate ... -> translate translator <- [fr] Hello, world -> summarize summarizer <- summary: Neurons are plain functions that......
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.
09-capability-routing/ neurons/roles.py summarizer + translator, different capabilities receptors/terminal.py route by capability; who lists providers brain.py a node per role + terminal-node
Capabilities are advertised, not configured.
Each Axon's capabilities ride its REGISTER. Nothing on the calling side keeps a list of workers.
from cosmonapse import Axon async def summarize(input, context): return {"result": f"summary: {input['text'][:40]}..."} async def translate(input, context): return {"result": f"[fr] {input['text']}"} # role -> (capabilities advertised in REGISTER, neuron_fn) ROLES = { "summarizer": (["summarize", "text"], summarize), "translator": (["translate", "text"], translate), } def make_axon(role: str) -> Axon: capabilities, fn = ROLES[role] return Axon(neuron_id=role, neuron_fn=fn, capabilities=capabilities)
ask(..., capabilities=[...]).
The terminal holds no ids. Identical capability profiles share a queue group, so two summarizers would load-balance and each TASK is still delivered once. The rendered reply shows sig.directed.id: who actually answered.
RECEPTOR = CliReceptor( input_key="text", prog="routing", description="Route by capability, never by worker id.", timeout_s=30.0, ) @RECEPTOR.on_result def render(sig): who = sig.directed.id if sig.directed else "?" if sig.type is SignalType.ERROR: return f"{who}: ERROR {sig.payload.get('message')}" return f"{who:<10} <- {sig.payload['output']['result']}" @RECEPTOR.command(local=True, help="route text to whoever can summarize") async def summarize(text: str): return await RECEPTOR.ask({"text": text}, capabilities=["summarize"]) @RECEPTOR.command(local=True, help="route text to whoever can translate") async def translate(text: str): return await RECEPTOR.ask({"text": text}, capabilities=["translate"]) @RECEPTOR.command(local=True, help="run three capability-tagged tasks") async def demo(): lines = [] for capability, text in TASKS: reply = await RECEPTOR.ask({"text": text}, capabilities=[capability]) lines.append(f"-> {capability:<9} {reply}") return "\n".join(lines)
Who can do this, right now?
Routing does not need a registry; listing does. A node with a registry_store folds REGISTER, HEARTBEAT and DEREGISTER into a live view that find_neurons reads. REGISTER rides the bus like any Signal, so a fresh process polls briefly.
@RECEPTOR.command(local=True, help="which live Neurons advertise a capability") async def who(capability: str): for _ in range(20): # REGISTER rides the bus - poll briefly found = await RECEPTOR.dendrite.find_neurons(capability=capability) if found: return {capability: [n.neuron_id for n in found]} await asyncio.sleep(0.05) return {capability: []}
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 roles from receptors import terminal def build_worker(synapse, role: str) -> Dendrite: node = Dendrite(synapse=synapse, namespace=NAMESPACE, dendrite_id=role, role="worker") node.attach_axon(roles.make_axon(role)) return node def build_terminal(synapse) -> Dendrite: node = Dendrite(synapse=synapse, namespace=NAMESPACE, dendrite_id="terminal-node", heartbeat_s=0, registry_store=MemoryRegistryStore()) # powers find_neurons() node.attach_receptor(terminal.RECEPTOR) return node
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 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 demo # 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 demo $ SYNAPSE_URL=kafka://127.0.0.1:9092 python brain.py demo