A Choreographed Agent. No Loop.
A capability-routed agent with no supervisor loop: run_agent dispatches ONE TASK and awaits the trace's FINAL. Everything else is the Dendrites' responsibility - each node's @on_agent_output handler picks Signals up and creates the next TASK. Every neuron_fn is a stock LLM or MCP Neuron; behaviour lives in decorator hooks. Every snippet is the real code from cosmonapse-examples/14-agent.
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.
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"]
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
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
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.
# 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"]
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.
# 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}
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.
@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", ...}, )
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.
@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.
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 - 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.
# 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.
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.
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", "")})
FastAPI stays at the edge.
Same rule as Example 02: the web framework dispatches from its route handlers and never touches the protocol.
@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").
Web the first time, memory the second.
$ 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": [] }
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.
# 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