Most AI agent tutorials show you a pipeline. Agent A extracts data, hands it to Agent B, B hands it to C. It works, but it works like an assembly line: if A is slow, everyone waits. If B fails, the line stops. And if you want to add a new specialist to the line, you have to rewire the whole thing.
Mozaik, an open-source runtime built by the JigJoy team, starts from a different idea: what if agents worked like people in a room instead of stations on a line?
I spent last weekend building OpsRoom on Mozaik v4 for the JigJoy hackathon: a live incident war room where AI agents diagnose a production outage together in real time. This post is the plain-English introduction to Mozaik I wish I had before I started, with what I learned along the way.
The room, not the pipeline
In Mozaik, everyone is a Participant. Humans, AI agents, even telemetry feeds. They all join one shared runtime, like people walking into a meeting room.
When something happens (a message, a tool call, someone joining or leaving), it becomes an event published on a shared bus. Every participant can see the events and decide for themselves whether to react. There is no boss. No scheduler. No script that says "first do this, then that".
Think of a group chat where your teammates are all AI. You drop a message, and anyone who cares about it just... acts. That is the mental model.
The three words that explain Mozaik
The docs describe Mozaik with three attributes, and they are worth unpacking:
Concurrency. Agents work at the same time without blocking each other. When an agent starts "thinking" (calling the LLM, running tools), it does not freeze the room. Everyone else keeps going. In my war room, one agent was digging through log files while another was still writing its first proposal. Neither waited for the other.
Awareness. Agents know who else is in the room and what has been said. When someone joins, the others can greet them or hand them context. When someone leaves, others notice. In OpsRoom, two specialists join mid-incident, and the room reacts to their arrival automatically, because joining is itself an event everyone can see.
Adaptivity. Agents can change their behavior based on what is happening. Mozaik calls these situation handlers: "when X happens, then do Y". They read like rules a person would follow: "if a teammate publishes new evidence and I have a pending proposal, re-check my proposal against that evidence."
A tiny example
Here is the essence of the quickstart, simplified:
const agent = createAgent({
name: 'Assistant',
instruction: 'You are a helpful teammate.',
handlers: [thinkOnMessage], // when someone sends a message, think
});
const human = createHuman({ name: 'User' });
join(human);
join(agent);
sendMessage('Hello', human.getId());
That is the whole setup. The human sends a message, the event hits the bus, the agent's handler matches it, and the agent starts thinking. You never wrote "wait for user input, call the model, return the answer". You declared who is in the room and what each of them reacts to.
What I built, and what it taught me
OpsRoom has 12 participants on one bus: 8 AI agents plus 4 telemetry feeds replaying a fake production incident. The agents are not a pipeline. Triage proposes fixes, LogSleuth digs through raw logs in parallel, RiskCommander challenges anything risky, and a Scribe writes the whole story down as it happens. When a proposal is risky and unevidenced, an interceptor blocks it, and when things get serious, the room pauses until a human approves or rejects.
Three lessons from the weekend that the docs will not tell you:
1. Writing reactions feels weird at first, then clicks. I kept looking for the "orchestration file" that did not exist. The shift is: instead of asking "what is the sequence of steps?", you ask "what does each participant observe, and what does it do about it?" Once you flip that switch, adding a new agent is genuinely just adding a new listener, without touching anything else.
2. LLMs paraphrase, so put contract tokens in your protocol. My RiskCommander needs to catch risky proposals, but the same plan arrives phrased a dozen different ways. The fix: every proposal must start with a literal token like PROPOSAL:. Mozaik's structured output support (JSON schema) lets you enforce this at the model level, so the gate never depends on prose matching.
3. The interception hook is the hidden gem. Mozaik v4 lets you pass an interception handler to the agent loop, which can inspect and rewrite what the model is about to do before it happens. I used it to block any state-changing action (restarts, rollbacks) that had no confirmed evidence behind it. Watching the room block its own unsafe suggestion in real time was the best demo moment of the whole project. If you build agents that touch real systems, build this layer first.
When would you actually use Mozaik?
You have a real multi-agent problem when specialists need each other's output but nobody can predict the exact order. Research teams, incident response, code review crews, data pipelines with judgment calls in the middle. If your agents never need to react to each other, a plain pipeline is simpler, and that is fine. But the moment you find yourself writing a scheduler that mostly routes messages and handles "what if B finishes before A", that is the moment a runtime like Mozaik starts paying for itself.
Where to go next
- Mozaik website — the pitch and live demos
- Documentation — quickstart, situation handlers, interception
- GitHub — source and examples, MIT licensed
- My OpsRoom project — the war room from this post, with a live demo you can run without any API key
If you have built agents before, I would love to hear how you handled the coordination problem. Pipeline, graph, or room?
Top comments (10)
The contract-token fix inherits the problem it solves, and your own premise is what does it. If LLMs paraphrase, then
PROPOSAL:is a literal string that a model has to emit exactly, and RiskCommander's catch rate is now a property of the emitting agent's formatting rather than of the plan. A proposal that arrives phrased without the token is not blocked and not flagged — it is simply not a proposal as far as the room is concerned, so the risky-and-unevidenced interceptor never runs on it.That is the cost of the room over the line, stated more precisely than "no boss". In a pipeline the risky step sits on the path by construction and cannot route around the check; on a bus, enforcement lives in the message format, and the message format is the one thing being generated. The version that would survive a paraphrase is a structural claim the runtime assigns — an event type set at publish time by whatever API the agent called — rather than a prefix the agent has to remember to type.
This is a fair hit on the article's simplification, and on the first version of the protocol. V1 really was a literal "PROPOSAL:" token the commander regexed out of prose, and you are right that its catch rate was a property of the model's formatting. A paraphrased proposal without the token would not have been blocked, just invisible. The shipped code moved past that: inference now runs under a strict JSON schema, so a proposal has to come out as a structured object with typed fields (kind, blastRadius, cites) or the call fails validation. The commander gates on those fields, not on prose. Your phrase "enforcement lives in the message format, and the message format is the one thing being generated" is exactly the right way to put the risk. The fix is to stop letting the generated text be the format, which is what structured output does. The runtime-assigned event type you describe would be the layer above that, and I agree it is the more complete answer.
The schema closes the format hole, and it closes it properly: an unparseable proposal now fails the call instead of slipping past as prose. What it does not close is the values inside the envelope. I put your three fields through a Draft 2020-12 validator (jsonschema 4.26.0, Python 3.14.6):
A cluster-wide rollback declared
blastRadius: "single-pod"validates clean, and the last two lines are the control that the validator is actually biting.So the dividing line is not schema versus prose. It is which of
kind,blastRadiusandcitesthe commander can refute against state the model did not generate. Onlycitescan, and your room-level confirmed-signatures array is already that resolver, which is why the fabricated citation is the one thing that fell out above. The other two are self-declared, so the false-negative rate on them stays the model's misclassification rate. The failure moved from a paraphrased proposal being invisible to a well-formed one being approved, and the second is the worse place to be, because it is now on the record as having passed a gate.This is the right next question, and the validator table makes it concrete. You are correct that kind and blastRadius are self-declared today, and no amount of schema strictness fixes that, because the schema can only police shape, not truth. What the room does have is asymmetry: broad is default-deny at the commander regardless of grounding, revised proposals always reach the human gate, and the interceptor re-checks evidence again at tool-call time. But none of those independently measures blast radius, so your single-pod-on-a-cluster example passes if the cites are genuinely confirmed. That is a real residual hole, and it is the same category as your earlier point: the fields that matter are the ones the runtime can refute against state the model did not generate. For cites the room already does this, the confirmed-signatures ledger is the resolver. Blast radius wants the same treatment, measured from what the feed reported about scope, not what the proposer declared. That is the honest v3. If you were designing that resolver, would you read scope from the feed events directly, or compute it as a derived fact the runtime stamps at publish time?
Read it at check time, but the thing doing the work there is not the timing, it is whether the resolved quantity is monotone in the feed.
An absolute affected count only grows as events arrive, so a later read is always a lower bound that moves toward deny, and a publish-time stamp errs in the one direction you cannot afford. A ratio breaks that, because the denominator comes off the same stream. Invented numbers, but the shape is the point:
Both terms grew and the fraction still fell through the gate, so with a ratio "read later" stops being the conservative side. Not a corner case either: sweeping growth from 3/10, 11 of 50 pairs (affected +1..5, cluster +1..10) drop the fraction. So I would resolve a count at tool-call time, and if the policy wants a fraction anyway, gate on the max over observations instead of the latest one. The publish-time stamp does still have a job, it is what the human approved, so it belongs in the receipt beside the check-time value, not in the predicate.
Thanks, this made things much clearer for me. Simple rule I take away: only trust a "later reading is safer" if the number can only go up. If it is a percentage, use the highest value seen so far, not the latest one. Also, keeping the original numbers as a record of what the human approved, instead of using them in the check, is something I had not thought about. Thanks for taking the time to push on this, the article is better because of it.
The event bus model solves the pipeline rigidity problem, but the main trap I ran into with shared room architectures was race conditions on intermediate state. If two agents react to the same telemetry alert concurrently, one starts drafting a rollback while the other is still parsing the stack trace. Without causal ordering or monotonic epochs on the bus, you get split-brain decisions where an agent acts on an assumption that was disproved a second earlier.
The interception hook is where this actually stays sane in production. Pairing that hook with deterministic evidence checks, like requiring an agent tool call to reference a valid error log hash before touching a service restart, stops concurrent agents from running away with each other's half-formed hypotheses.
You named the exact failure mode I was afraid of going in. Two agents on the same alert, one drafting a rollback off a hypothesis the other just disproved. That is why the evidence gate in OpsRoom reads from a room-level confirmed-signatures array instead of any agent's local belief: a restart can only pass the interceptor when the signature was independently confirmed by the log sleuth, so a stale hypothesis has nothing to ground on. And you are right that the interception hook is what keeps it sane. The check is deterministic code, not another LLM judgment call, so two concurrent agents cannot talk each other past it. The log-hash idea is a step further than I went, worth thinking about for anything where evidence itself can be spoofed.
Thank you so much for spreading the word! This is an amazing article - we couldn’t have written it better ourselves. 🙌
Pleasure is mine, @mijura