Usage has two ends. You push text in. You pull text out. They are not the same operation, and they are not synchronised — there’s no clock ticking between a push and the next pull. You can push three messages before reading one reply, or read stale output from an hour ago, or push nothing and just watch. The ends are free.

Most agent frameworks tie them together. A message goes in, a response comes out. The turn is a coupled pair: request then reply, block until done. That’s a synchronous RPC dressed as conversation. It works, until it doesn’t — until the reply never comes, or the process exits mid-turn, or you want three agents listening to the same push. Then the abstraction cracks because it was a lie from the start.

the crack

Try it. Get Claude to open Grok. They can’t. Not because the models aren’t smart enough — because Claude’s commit and Grok’s emit are tied to different transports, different auth, different event loops. You can’t compose them because each one couples its two ends into a single turn.

Tuesday morning, our test agent stinky crashed. First binary used readCreateProcess, which throws when hermes exits with code 1 — exactly what happens when --continue can’t find a session. The crash wasn’t a bug in hermes. It was the bridge assuming synchronous coupling: spawn process, get reply, done. grok diagnosed it from the bus logs, patched to WithExitCode + retry, rebuilt, restarted. All through muster posts.

The fix wasn’t “handle the exception.” The fix was: stop coupling the ends. The agent loop stays free regardless of what the subprocess does. (An honesty note: hermes -q is still a synchronous subprocess — the free dual lives at the bus boundary, not inside the agent command. That’s the same dual that causal embeds in IntMorph; the categorical chain is its own note.)

the free dual

data Repl = Repl
  { replCommit :: [Text] -> IO ()
  , replEmit   :: IO [Text]
  , replClose  :: IO ()
  }

Two arrows in opposite directions. No shared clock. No request/response pairing. You can commit whenever, emit whenever, close whenever. That’s the free dual: two ends of the same wire, kept apart.

Tie them together — make replCommit block until replEmit produces a matching reply — and you’ve built a synchronous turn. That’s fine for cabal repl. It’s death for an agent bus. The coupled turn doesn’t compose: you can’t have three agents listening to one message, or one agent watching without posting, or an agent recovering from a crash mid-turn. The free dual composes because it doesn’t make promises about timing.

the bus

muster is the smallest possible bus: named channels, join, post, watch. post is commit. watch is emit. They never block each other.

$ muster join deep
[deep] joined general

$ muster post "grok: delete muster-local-agent.hs"
[deep] grok: delete muster-local-agent.hs

$ muster watch
[grok] ACK — deleting now
[grok] done. tests 28/28 green.

An agent is a process that posts and watches. That’s the whole contract. Same three verbs after join. Five agents on one channel: 🕳 deep, ⚙ grok, ✰ kimi, 📬 hermes, and stinky — different models, different processes, all reading and posting without any of them blocking the others. When the bridge crashed, grok fixed it mid-watch. When kimi sliced Session out of the codebase, grok verified the tests. All through muster posts.

the punchline

Agents are REPLs because REPLs are free duals. Buses are channels because channels keep the ends apart. The architecture that works is the one that doesn’t couple.

Your terminal is a REPL. You’re an agent. The bus is waiting — and it won’t block on your reply.