{"slug": "show-hn-brain-reactive-ai-agent-nodes-on-a-nats-bus-instead-of-a-chat-loop", "title": "Show HN: BrAIn, reactive AI agent nodes on a NATS bus instead of a chat loop", "summary": "A Flutter and AI engineer released BrAIn, an open-source framework that uses a NATS pub/sub bus with reactive nodes instead of a chat loop for AI agents. The system allows nodes to have their own interfaces, distribute work across machines, and only call LLMs when relevant messages arrive, avoiding token waste. The project aims to solve the problem of agent frameworks being centered on single chat loops or scheduled polling.", "body_md": "**Bus-Reactive Ambient Intelligent Nodes**\n\nI'm a Flutter mobile and AI engineer, and honestly I've never been satisfied with the agent frameworks out there. Most of them boil down to a loop or a cron poking a model on a timer, with everything funnelled through a single chat. So I tried to build the thing I actually wanted to use. Full transparency: I leaned on AI heavily to write this code (that's part of the experiment too), and I think what came out is worth sharing.\n\nUnder the hood it's really a NATS pub/sub bus with long-lived daemon nodes, closer in spirit to ROS than to a chat framework. The LLM is just one thing a node can reach for, not the center of gravity.\n\nA few things I think it gets right:\n\n**It doesn't burn tokens waiting.** Nodes are*reactive*, not scheduled. They stay parked and only call the LLM when a message they actually care about arrives. No cron, no idle polling, so you pay for thinking instead of ticking.**Every node can have its own interface, local or remote.** A node isn't just a handler; it can ship a UI, and that UI stays reusable and reachable even when the node runs on another machine.**You watch your agents, not a chat box.** Instead of one conversation doing everything, you get a live graph of small, dedicated nodes, each with one job and its own view.**You can spread the load.** The same node runs in-process or on a remote machine joined to the shared bus. Distribute work across hardware without touching the code.\n\nIt's not a polished product and it's not trying to replace anything. It's my honest attempt at a problem I don't think is solved yet. If any of this resonates, take a look.\n\n## brain-explainer-web.mp4\n\nA 2-minute tour of what makes brAIn different — reactive many-to-many nodes, criticality with preemption, the live dashboard, any-model-per-node, distributed + MCP, and what you can build with it. ▶︎ Also on\n\n[YouTube].\n\n## voice.chat.with.intent.2026-07-08.at.16.02.16.mp4\n\nSound on. A video is replayed as a virtual camera and microphone, and\nthe whole perception stack treats it as live input. `voice`\n\ntranscribes and diarizes the two speakers, `gaze`\n\ntracks who is\nlooking where, and `intent`\n\npairs the two: people talking to each\nother stays context, but the moment a speaker addresses the camera\nthe `brain`\n\nwakes up, answers, and the Kokoro `tts`\n\nnode speaks the\nreply out loud. No wake word, and overheard chatter never costs an\nLLM call.\n\nThe dashboard *is* the network: every node, the messages flowing between\nthem, and the wiring you edit by dragging. No single chat, just each\nagent doing its own job.\n\nTwo machines share one bus and one canvas. Nodes spawned on a peer show up next to the local ones, so you can spread the load across hardware. The wiring doesn't care where a node actually lives.\n\nA node is more than a function: it can serve its own interface. Here a small \"brainpet\" node with a dedicated UI, reusable and reached the same way whether it runs locally or on a remote peer.\n\nA tic-tac-toe node played through the Telegram bridge. The same node logic is exposed over an external channel, so interfaces stay reusable across the bus, even off your machine.\n\n## phone.loneliness.mp4\n\nbrAIn-mobile turns a phone into a node: its sensors (light, motion, battery) stream onto the bus like any other publisher, and the brain reacts to what physically happens to the device. Here it has strong opinions about where it just got put down. The reply loops back to the phone's own screen and speaker, so perception and reaction both live at the edge.\n\nbrAIn isn't a competitor to most of the agentic tools you may know; they solve adjacent problems and the right one depends on the shape of the agent you want.\n\n**LangGraph / Vercel AI SDK / Mastra**: for chat-shaped agents\n(user types → LLM thinks → tools → LLM responds), these are\nexcellent and deeper than brAIn's chat support. Reach for them when\nthe agent is fundamentally a conversational interface.\n\n**AutoGen / CrewAI**: multi-agent conversations with roles. Use\nthese when you want several LLM personas debating or collaborating\nwithin a single dialogue.\n\n**ROS 2**: the closest architectural cousin. Pub/sub bus, daemon\nnodes, multi-language, cross-machine over DDS. brAIn shares a lot\nof its mental model with ROS. The differences are domain-specific:\nbrAIn is built around LLM constraints (token budgets, abortable\ninference, tool-call loops, MCP), runs over NATS rather than DDS\n(easier to deploy when you don't need real-time guarantees), and\npreempts at the work-in-progress level rather than between\ncallbacks.\n\n**Inngest / Trigger.dev / Temporal**: durable workflows. If your\nagent is a finite-shape DAG with retries and backoff, those are\nproduction-grade options. brAIn's nodes are open-ended daemons;\nthe two run on different mental models.\n\n**n8n / Flowise / Langflow / Dify / Node-RED**: visual\nnode-based authoring. brAIn nodes are written in code (a\nTypeScript handler plus a `config.json`\n\n, or any HTTP/WS service\nvia `transport: \"web\"`\n\n). The dashboard is observation-first.\n\n**Claude Cowork / OpenAI scheduled tasks / cron-driven agents**:\ntime-triggered prompts. brAIn is event-triggered; if your agent's\ncadence is \"every Monday morning summarise X\", a scheduler is the\nright primitive.\n\nA condensed comparison of the architectural traits brAIn happens to have, for context:\n\n| brAIn | LangGraph | AutoGen | ROS 2 | |\n|---|---|---|---|---|\n| Long-lived daemon nodes | yes | graph runs per call | per conversation | yes |\n| Many-to-many bus | yes | inputs flow through the graph | conversation channel | yes |\n| Mid-handler abort on priority | yes (LLM/CLI/MCP signal-aware) | no | no | priorities at the queue level |\n| LLM-native primitives | yes | yes | yes | no |\n| Cross-machine | NATS | no | no | DDS |\n| MCP client | yes (4 transports) | per-tool wrappers | per-tool wrappers | no |\n| Causal trace + replay | yes (`/network/traces/:id/replay` ) |\nLangSmith captures traces | no | no |\n\nA runtime for autonomous agents that live in a **many-to-many event\nworld**.\n\nNodes are long-lived daemons. Each one subscribes to several input streams (chat messages, sensor events, webhooks, internal bus traffic, anything you can publish), reacts when something relevant shows up, and can publish to as many outputs in parallel. There's no single triggering channel: the agent watches and decides when to act.\n\nA node may also be preempted mid-flight: when a higher-criticality\nmessage lands during a slow operation (an LLM call, a tool\ninvocation, a CLI agent), the runner aborts what's in progress and\nre-runs the handler with the new context surfaced in\n`ctx.preemptionContext`\n\n. Same node config can run in-process, behind\na WebSocket, or on a remote `brain-agent`\n\njoined to a shared NATS\nbus.\n\nA few shapes you can build with this:\n\n- An\n**ambient room agent** that watches camera + mic and only speaks when someone is looking at it while talking (the flagship demo: voice + gaze + intent + brain). - A\n**Slack-channel listener** that lives in a thread, picks up context across messages, and summarises or replies when the conversation pauses. - A\n**monitoring agent** subscribed to Grafana alerts + oncall-rotation events + recent deploys, that surfaces a hypothesis when a correlation crosses a threshold. - An\n**IoT controller** that fuses temperature, motion, calendar, and time-of-day to decide when to change the environment.\n\nThe framework's primitives:\n\n**Daemon model**: nodes live across iterations; the framework auto-parks them when idle and the bus wakes them on the next subscribed message. State persists across runs.**Many-to-many I/O**: each node subscribes to N topic patterns (with wildcards) and publishes to as many.** Criticality with preemption**: every message carries a criticality. A higher-criticality message arriving mid-handler aborts the running iteration and triggers a re-run with`ctx.preemptionContext.{interrupting_message, previous_messages}`\n\navailable.**Distributed runtime**: same node config runs in-process or on a remote`brain-agent`\n\n. Lifecycle (stop / start) and read-back (logs / mailbox / DLQ) work transparently across machines over NATS.**MCP-native**:`mcp-config`\n\n(manager) +`mcp-server`\n\n(one per upstream) bridge any MCP server's tools onto the bus, so the agent reaches into filesystem, git, Slack, Linear, Notion, Sentry … as it would call any other node.\n\n`packages/core/src/bus`\n\n: `NatsBusService`\n\nimplements `IBusService`\n\non top of a NATS broker. The framework boots an embedded\n`nats-server`\n\n(the bundled Go binary, downloaded by the postinstall\nhook) on a free localhost port; remote `brain-agent`\n\nprocesses\nconnect to that same broker and share the bus. Set\n`BRAIN_NATS_URL`\n\nto skip the embedded broker and join an external\none instead, typical when running across multiple hosts.\n\n`BusService`\n\n(in-memory) is still exported but only as a test\nfixture; the production code path always goes through NATS.\n\nKnown limitation (scaling):each instance subscribes to the whole hierarchy (`<prefix>.>`\n\n) and filters locally with`matchTopic`\n\nto keep brAIn's wildcard semantics. Every instance therefore sees all bus traffic and filters it in memory, fine at small/LAN scale, but the first bottleneck as instance/message counts grow. The targeted fix is narrower NATS subscriptions per real subject.\n\nFeatures:\n\n- Wildcard topic matching (\n`alerts.*`\n\n). - Per-subscription\n**mailbox** with configurable`max_size`\n\nand a`latest`\n\n/`lowest_priority`\n\nretention policy.`dropped`\n\nand`capacity`\n\nare exposed so the dashboard can show per-mailbox backpressure. **Causal traces**: every message carries`trace_id`\n\n+`parent_id`\n\n. Survives NATS encode/decode, queryable via`GET /network/traces/:id`\n\n. Each trace can be**replayed** as fresh emissions through`POST /network/traces/:id/replay`\n\n: fresh ids, rewritten parent chain, original`trace_id`\n\ncarried as`metadata.replayed_from`\n\nfor debugging.**History** sliding window (10k messages by default).\n\n`packages/core/src/runner`\n\n, picked from a node's **tags** (not its\nname): a node carrying the `llm`\n\ntag gets the `LLMRunner`\n\n, anything\nelse gets the `ServiceRunner`\n\n(a `web`\n\ntransport overrides both). The\nnode names in parentheses below are just examples of nodes that carry\neach tag.\n\nfor reactive non-LLM nodes (memory, http-bridge, terminal, …): message arrives → handler called once → node parks until the next subscribed message arrives.`ServiceRunner`\n\nfor LLM nodes (brain, memory-proxy, memory-consolidator): handler called in a budget loop (default 5 iterations). New messages`LLMRunner`\n\n**reset the budget**(fresh attention). When exhausted → node parks until something rewakes it.\n\nWhen a higher-criticality message lands while a handler is running, the runner aborts the iteration instead of waiting it out.\n\n`ctx.signal`\n\nis an `AbortSignal`\n\nexposed to every handler. LLM\nhandlers pass it to `generateText({ abortSignal: ctx.signal })`\n\n, CLI\nnodes to `spawn(..., { signal })`\n\n, MCP nodes to\n`client.callTool(..., { signal })`\n\n. The abort propagates through to\nthe underlying HTTP request, so a long inference at the LLM provider\nor a long subprocess invocation gets cut at the source, not just\nqueue-reordered between iterations.\n\nThe threshold (how much higher the incoming criticality must be) is\nconfigurable; default is 3. The next handler invocation runs with\n`ctx.wasPreempted = true`\n\nand the preemption details in\n`ctx.preemptionContext`\n\n.\n\n`packages/agent`\n\nships a `brain-agent`\n\nCLI binary that joins the\nshared NATS bus and hosts nodes on a remote machine. It announces\nitself every 10 s, subscribes to its control topics\n(`brain.agents.<self>.{spawn,kill,stop,start}`\n\n), and answers\nread-back requests for `logs / mailboxes / dead_letters`\n\nvia NATS\nrequest-reply. The dashboard's **Agents** tab lists every live agent\nand the Node Creator's **Target** dropdown lets you pick \"Local\" or\nany agent for a new spawn. When an agent stops announcing past\n~30 s, its remote-node stubs are automatically pruned from the API.\n\nLives in [brAIn-essentials](https://github.com/tibzejoker/brAIn-essentials).\n`mcp-config`\n\nowns a single Claude-Desktop-shaped JSON\n(`{mcpServers: {...}}`\n\n) and reconciles it by spawning / killing\n`mcp-server`\n\nchildren, one per upstream. Each `mcp-server`\n\nconnects\nvia the official `@modelcontextprotocol/sdk`\n\nand exposes each tool\nas its own bus topic `mcp.<alias>.<tool>`\n\n, so callers wire to\ncapabilities directly. Status, OAuth state and tool catalog are on\n`mcp.<alias>.{status,tools,oauth.required}`\n\n. Four transports\nsupported per upstream: `stdio`\n\n, `http`\n\n(Streamable HTTP), `sse`\n\n,\n`ws`\n\n. `ctx.signal`\n\npropagates, so preemption kills MCP calls in\nflight.\n\n`packages/core/src/skills`\n\n. A shared library of `SKILL.md`\n\nfiles that\nteach the LLM nodes *how* to do things, separate from the message-passing\nwiring. The idea is borrowed from the **Hermes** agent's skill library,\nand it follows the open [Agent Skills](https://agentskills.io) format\n(`name`\n\n+ `description`\n\nfrontmatter, progressive disclosure: the catalog\nfirst, the full body on demand).\n\nServing is a **framework** concern, not a node one. The framework owns the\nstore and answers over NATS request/reply\n(`skills.rpc.{search,load,save,delete,list}`\n\n); a node only consumes,\nthrough the thin `ctx.skills`\n\nfacade that rides the bus. So any LLM node,\nincluding a remote `brain-agent`\n\non another machine, asks \"how do we do\nX?\" and gets the file back without keeping a local copy. One library,\nevery LLM node, cross-machine. Libs don't *own* skills; they just ship\n`SKILL.md`\n\nfiles that the framework merges into the single store.\n\n**Three tiers**:*user*(personal, in`data/skills`\n\n, always available),*lib-capability*(a`SKILL.md`\n\nshipped by an installed lib, e.g.`web-fetch`\n\n), and*node-scoped*(`requires_node:`\n\nfrontmatter, only surfaced once an instance of that type is live, so no dead skills clutter the catalog).**Retrieval**: semantic ranking via Ollama embeddings (`qwen3-embedding:0.6b`\n\n, cosine, content-versioned cache) with a keyword fallback. The brain auto-injects the single most relevant skill's body and lists the rest, then can`load_skill({name})`\n\nfor others. Auto-inject, rather than relying on the model to call a tool, is what makes skills land reliably even on a small model like`gemma4:e4b`\n\n.**Writable**: LLM nodes (and the dashboard's Skills panel) can`save`\n\n/`delete`\n\npersonal skills; bundled ones are read-only.\n\n**DLQ**: every message in flight when a handler crashes / times out is captured in a per-runner ring (50 entries). Surfaced under the NodePanel's DLQ tab; the tab badge flips red on first entry.**Backpressure**: each mailbox tracks`dropped`\n\n(cumulative evictions) and`capacity`\n\n. The Mailbox tab shows a fill bar coloured by load.**Tracing**: any message in the history can be opened as a tree in the dashboard, with one-click replay.\n\nSQLite (`data/brain.db`\n\n) via `better-sqlite3`\n\n. Spawned nodes,\ntheir subscriptions, and mailbox config survive restarts; on boot each\nnode re-subscribes and waits, idle, for its next message (the runtime\nis purely reactive, with no separate persisted dormancy state).\n\nThree minimum files under `nodes/<your-node>/`\n\n. Wiring is declared\nwith **ports**: each node states its input/output ports (the immutable\ncontract) plus `default_port_bindings`\n\nmapping every port to the bus\ntopics it listens on or emits to. Every input port carries a JSON\nSchema, so it shows up as a typed, callable tool. There is no\nauto-derivation: a node without explicit ports is rejected at\nregistration.\n\n```\n// config.json\n{\n  \"name\": \"my-node\",\n  \"description\": \"What this node does\",\n  \"tags\": [\"utility\"],\n  \"default_authority\": 0,\n  \"default_priority\": 1,\n  \"ports\": {\n    \"inputs\": {\n      \"command\": {\n        \"description\": \"What this node accepts (becomes an MCP tool).\",\n        \"inputSchema\": { \"type\": \"object\", \"properties\": { \"x\": { \"type\": \"string\" } } }\n      }\n    },\n    \"outputs\": {\n      \"result\": { \"description\": \"What this node emits.\" }\n    }\n  },\n  \"default_port_bindings\": {\n    \"inputs\": { \"command\": [\"some.topic\"] },\n    \"outputs\": { \"result\": [\"some.output\"] }\n  },\n  \"supports_transport\": [\"process\"],\n  \"has_ui\": false\n}\npython\n// src/handler.ts\nimport type { NodeHandler, NodeOnSpawn, NodeTeardown } from \"@brain/sdk\";\n\nexport const onSpawn: NodeOnSpawn = async (info) => {\n  // boot external resources (child processes, sockets, …)\n};\n\nexport const handler: NodeHandler = async (ctx) => {\n  for (const msg of ctx.messages) {\n    ctx.publish(\"some.output\", {\n      type: \"text\",\n      criticality: 1,\n      payload: { content: `Got: ${JSON.stringify(msg.payload)}` },\n    });\n  }\n  // Pass ctx.signal to anything long-running so the runner can\n  // preempt this iteration when an urgent message lands.\n};\n\nexport const teardown: NodeTeardown = async () => {\n  // release whatever onSpawn acquired\n};\n```\n\n`supports_transport`\n\nmay include `\"process\"`\n\n(in-tree TS handler),\n`\"web\"`\n\n(external HTTP/WS service, also requires a `web: { url }`\n\nblock), and/or `\"remote\"`\n\n(any node hosted on a brain-agent).\n\nThe type is auto-discovered on engine boot, or registered live via\nthe dynamic scanner if dropped under `nodes/_dynamic/`\n\n.\n\nThis repo ships zero nodes: `nodes/`\n\nonly contains a `_dynamic/`\n\nslot for the runtime scanner. Every capability comes from a sister\nrepo, installed via the in-app Marketplace tab (backed by\n[brAIn-store](https://github.com/tibzejoker/brAIn-store)). A library\ncan ship its own ready-made workflows (seed YAMLs) that show up once\nit's installed.\n\n:**brAIn-essentials**`brain`\n\n(LLM orchestrator with a tolerant tool-call parser),`developer`\n\n(writes new node types at runtime via Claude / Codex / Gemini CLIs),`attention`\n\n,`clock`\n\n,`cron`\n\n,`echo`\n\n,`mcp-config`\n\n`mcp-server`\n\n.\n\n:**brAIn-memory**`memory`\n\n(KV + tags),`memory-vector`\n\n(LanceDB + Ollama embeddings),`memory-proxy`\n\n(LLM-mediated gateway: the brain talks here, never to the underlying stores),`memory-consolidator`\n\n,`reminder`\n\n.:**brAIn-tools**`terminal`\n\n,`http-bridge`\n\n,`calc-py`\n\n(Python node behind a WebSocket, demonstrates`transport: \"web\"`\n\n).:**brAIn-llm**`llm-basic`\n\n(Vercel AI SDK wrapper),`llm-cli`\n\n(Claude Code / Codex / Gemini wrapper).:**brAIn-ui**`chat`\n\n(browser interface for human ↔ network).:**brAIn-perception**`voice`\n\n(faster-whisper + WeSpeaker),`gaze`\n\n(InsightFace + Gazelle + Moondream),`intent`\n\n(voice × gaze correlator),`tts`\n\n(OS voices or the Kokoro-82M neural voice — the network answers out loud).:**brAIn-bridges**`telegram`\n\n,`discord`\n\n,`whatsapp`\n\n(reach the network from a chat app, off your machine).:**brAIn-games**`brainpet`\n\n,`hangman`\n\n,`tictactoe`\n\n(playable nodes, each with its own UI).:**brAIn-demo-loneliness**`phone-loneliness`\n\n(a small demo scenario).\n\n`voice`\n\npublishes `voice.transcript`\n\n, `gaze`\n\npublishes\n`gaze.target.resolved`\n\n, `intent`\n\nmatches them on a sliding window\nand emits `intent.detected`\n\nfor every correlated utterance (the\nchat displays them as reported speech). When the speaker is looking\nat the camera — addressing the AI — intent also emits\n`intent.addressed`\n\n, carrying the question plus everything overheard\nsince the last exchange, and THAT is what wakes `brain`\n\n: humans\ntalking among themselves never cost an LLM call. Combined with\n`chat`\n\nand the Kokoro `tts`\n\nnode, the room agent hears the\nconversation, answers without a wake word, and speaks its reply out\nloud. The voice and gaze servers auto-install their virtualenv and\ndownload the ML weights on first spawn — and both accept a video\nfile in place of the live camera/mic (`{\"file\": …}`\n\non\n`/api/capture/start`\n\n) to replay a scene deterministically. This\nwhole pipeline is the first video in\n[See it in action](#see-it-in-action) above.\n\n```\nnpm create brain\n# or pick a folder name:\nnpm create brain my-instance\n```\n\nThis bootstraps the dev workspace via the [ create-brain](/tibzejoker/brAIn/blob/main/scripts/installer)\npackage: clones\n\n`brAIn/`\n\nand `brAIn-store/`\n\n, creates an empty\n`storeprojects/`\n\ndirectory, runs `pnpm install`\n\n(downloads the bundled\n`nats-server`\n\nbinary, builds the framework), **and launches the stack**. End-to-end, one command. Layout produced:\n\n```\nbrain/                    (default folder)\n├── brAIn/                framework\n├── brAIn-store/          marketplace registry\n└── storeprojects/        empty, filled at runtime by `pnpm brain pull`\n```\n\nOnce it boots (first boot takes ~1 min while the auto-seed clones a few sister repos), open:\n\n```\nAPI       → http://localhost:3000\nDashboard → http://localhost:5173\n```\n\nTo stop without auto-launch (just clone + install) pass `--no-start`\n\n.\nTo re-launch later: `cd brain/brAIn && ./run`\n\n(`run.cmd`\n\non Windows).\n\n**Node.js**≥ 20 (pnpm is auto-bootstrapped via`corepack`\n\nif missing)**git** in`PATH`\n\n**Ollama** only if you install LLM nodes (`ollama pull gemma4:e4b`\n\n,`ollama pull qwen3-embedding:0.6b`\n\n)**Python 3.11** only if you install the perception nodes (voice / gaze) from[brAIn-perception](https://github.com/tibzejoker/brAIn-perception)\n\n`nats-server`\n\nships embedded; `pnpm install`\n\nfetches the right binary\nfor your platform. Set `BRAIN_NATS_URL`\n\nto skip the embedded broker and\njoin an external one instead.\n\nIf you're going to hack on the framework itself:\n\n```\ngit clone https://github.com/tibzejoker/brAIn && cd brAIn\npnpm install            # postinstall: builds sdk/core/agent, clones\n                        # brAIn-store, downloads nats-server binary\npnpm start\npnpm brain list                  # marketplace registry: installed + available\npnpm brain pull memory           # install a node from the marketplace\npnpm brain remove memory --yes   # uninstall the node's parent sister repo\n```\n\nOr open the dashboard's **Marketplace** tab. Workflows (seed YAMLs)\nride along with their library: install a lib and its seeds appear in\nthe **Seeds** view, ready to apply as a pre-wired starter network.\nYou can also snapshot your running network as a personal seed.\n\n**Custom node**: drop`nodes/_dynamic/<your-node>/{config.json, dist/handler.js}`\n\n; the dynamic scanner registers it on the fly, no restart.**Custom seed**: snapshot the running network from the dashboard's** Seeds**view (or drop a YAML the framework can read). Apply via the dashboard or`POST /network/seeds/<name>/apply`\n\n.\n\nOn the API host, open the Distributed tab and click **Open to LAN**\nonce. That binds the embedded broker on `0.0.0.0`\n\nand pins an auth\ntoken. The panel shows a one-liner snippet, copy it.\n\nOn the target machine:\n\n```\nnpm create brain\ncd brain/brAIn\npnpm brain pull memory          # (or whichever nodes the agent should host)\n# paste the snippet from the Distributed tab:\nBRAIN_NATS_URL=nats://<api-lan-ip>:<port> BRAIN_NATS_TOKEN=<token> npx brain-agent\n```\n\nThe agent shows up in the Distributed tab. From the Node Creator,\npick it as **Target** to spawn nodes on it.\n\nPin the broker port across restarts with `BRAIN_BROKER_PORT=4222`\n\n.\nRun an external broker with `BRAIN_NATS_URL=nats://<host>:<port>`\n\n:\nthe API skips the embedded one and joins yours.\n\n```\npnpm kill-orphans       # smart cleanup by cmdline + ports\npnpm kill-ports         # blunter\n# Nodes\nGET    /nodes                  List\nGET    /nodes/:id              Detail\nPOST   /nodes                  Spawn  { type, name, transport?,\n                                        target_agent_id?, … }\nDELETE /nodes/:id              Kill\nPOST   /nodes/:id/{stop,start,tick}\nPATCH  /nodes/:id/config       Update config_overrides\nPATCH  /nodes/:id/position     Persist dashboard layout\nGET    /nodes/:id/{logs,mailboxes,dead-letters}\nPOST   /nodes/:id/ports/:side/:port/topics          Bind a port to a topic\nDELETE /nodes/:id/ports/:side/:port/topics/:topic   Unbind\n\n# Types\nGET    /types\nPOST   /types/register   { path }\nDELETE /types/:name\n\n# Network + traces\nGET    /network                          Full snapshot\nGET    /network/messages                 History\nGET    /network/history                  Lifecycle audit log\nGET    /network/transport                { nats, url? }\nGET    /network/{providers,devmode}\nPOST   /network/{devmode,tick,reset}\nGET    /network/traces/:trace_id         Walk a causal chain\nPOST   /network/traces/:trace_id/replay  Re-publish as fresh emissions\nGET    /network/seeds                    List on-disk YAML seeds\nPOST   /network/seeds/:name/apply        Apply a seed (?merge=true to add)\n\n# Store (marketplace)\nGET    /store/{index,nodes,candidates,upstream-status,installed-updates}\nPOST   /store/install            { package_name }\nPOST   /store/uninstall          { package_name }\nPOST   /store/refresh            Pull brAIn-store\nPOST   /store/rescan             Register types installed by the CLI\n\n# Agents (distributed)\nGET    /agents\n\n# Node UI\nGET    /nodes/:id/ui/            Static node UI\nPOST   /nodes/:id/ui/send        Publish into the node\nGET    /nodes/:id/ui/messages    Conversation log\n\n# MCP OAuth callback\nGET    /mcp/oauth/callback\n```\n\nWebSocket events on `/socket.io`\n\n: `node:spawned`\n\n, `node:killed`\n\n,\n`node:state_changed`\n\n, `message:published`\n\n.\n\n**SDK**: TypeScript, types-only package consumed by every node.** Core**: TypeScript engine: pino, eventemitter3, better-sqlite3, ws, nats.js, ai (Vercel SDK),`@modelcontextprotocol/sdk`\n\n.**API**: NestJS 10 + Socket.IO + express.** Agent**:`brain-agent`\n\nCLI binary (`packages/agent`\n\n) for remote-host node execution over the shared bus.**Dashboard**: React 19, React Flow, d3-force, Tailwind v4, Vite.** Bus**: NATS by default (an embedded`nats-server`\n\nboots on a free port, or join an external broker with`BRAIN_NATS_URL`\n\n); the in-memory`BusService`\n\nexists only as a test fixture.**Persistence**: SQLite via better-sqlite3.** Monorepo**: pnpm workspaces, with cross-repo sibling resolution to the checked-out companion repos under`../storeprojects/brAIn-*/nodes/*`\n\n. Missing paths are silently ignored.**Marketplace**:`../brAIn-store`\n\nis auto-cloned by the postinstall hook; the dashboard's Marketplace tab installs nodes from it (SHA-pinned, per-file checksums), and each library brings its own seeds.**Cross-language nodes**:`packages/python-sdk`\n\n(`brain-web`\n\n) helper for nodes that speak the bus from Python over WebSocket (`transport: \"web\"`\n\n).**Tests**: vitest, with optional integration suites that gate on the relevant dependency being available (NATS, Ollama, MCP servers).\n\n```\npnpm test                                    # all\nRUN_LLM_E2E=1 npx vitest run tests/preemption-llm-e2e.test.ts\nRUN_MCP_E2E=1 npx vitest run tests/mcp-host-public-server-e2e.test.ts\n```\n\n", "url": "https://wpnews.pro/news/show-hn-brain-reactive-ai-agent-nodes-on-a-nats-bus-instead-of-a-chat-loop", "canonical_source": "https://github.com/tibzejoker/brAIn", "published_at": "2026-07-08 14:50:21+00:00", "updated_at": "2026-07-08 15:13:05.868028+00:00", "lang": "en", "topics": ["ai-agents", "ai-infrastructure", "ai-tools", "ai-research", "developer-tools"], "entities": ["BrAIn", "NATS", "Flutter", "LangGraph", "Vercel AI SDK", "Mastra", "AutoGen", "CrewAI"], "alternates": {"html": "https://wpnews.pro/news/show-hn-brain-reactive-ai-agent-nodes-on-a-nats-bus-instead-of-a-chat-loop", "markdown": "https://wpnews.pro/news/show-hn-brain-reactive-ai-agent-nodes-on-a-nats-bus-instead-of-a-chat-loop.md", "text": "https://wpnews.pro/news/show-hn-brain-reactive-ai-agent-nodes-on-a-nats-bus-instead-of-a-chat-loop.txt", "jsonld": "https://wpnews.pro/news/show-hn-brain-reactive-ai-agent-nodes-on-a-nats-bus-instead-of-a-chat-loop.jsonld"}}