When we released Mozaik v3, we introduced an event-based architecture where participants emit, observe, and react to typed context items inside a shared agentic environment. Since then, the framework has kept moving — and the way we think and talk about it has moved with it.
This post is a tour of what's new. The headline shift is conceptual: Mozaik is now framed around reactive agents — collaborative agents that adapt to their environment and to each other in real time. Underneath that frame, the API has gotten smaller, clearer, and easier to read.
From Non-Blocking to Reactive
Mozaik agents have always been non-blocking. That's still true — capability methods stream events as they're produced, so a slow inference or tool call never holds up other participants. But "non-blocking" describes a property, not a way of thinking.
The way we now think about Mozaik agents is reactive: an agent declares which events it cares about and reacts to them as they arrive — messages from humans, function calls from its own model, reasoning traces from a peer agent, or tool outputs from somewhere else in the environment. Behavior emerges from how agents react, not from a central controller deciding whose turn it is.
A reactive agent is event-driven, collaborative, and adapts to its environment and to other agents in real time.
This shift in framing pays off in design. Once an agent is a thing that reacts, adding behavior to a system stops being about editing a pipeline and becomes about joining another participant.
Typed Event Handlers
The biggest API change since 3.0 is on the participant side. In the original release, a participant exposed a single intercept point: onContextItem(source, item). You looked at the item, decided whether it came from you or someone else, and branched accordingly.
That worked, but it pushed a lot of dispatch logic into every agent. So we replaced the single intercept with a set of typed handlers, one per event kind:
onMessageonFunctionCallonFunctionCallOutputonReasoningonModelMessage
Each handler defaults to a no-op in the base class, so a reactive agent only writes the ones it actually cares about. The result is shorter, more declarative code — a reactive agent reads like a list of "when this happens, I do that," not like a switch statement.
Here is a full reactive agent built on top of BaseAgentParticipant. It records incoming messages, runs inference, executes its own tool calls, and keeps its local context in sync with what the model produces — all by overriding the handlers it cares about:
// reactive-agent.ts
import {
BaseAgentParticipant,
UserMessageItem,
FunctionCallItem,
FunctionCallOutputItem,
ReasoningItem,
ModelMessageItem,
AgenticEnvironment,
ModelContext,
GenerativeModel,
InputStream,
InferenceRunner,
FunctionCallRunner,
} from "@mozaik-ai/core"
export class ReactiveAgent extends BaseAgentParticipant {
constructor(
inputSource: InputStream,
inferenceRunner: InferenceRunner,
functionCallRunner: FunctionCallRunner,
private readonly environment: AgenticEnvironment,
private readonly context: ModelContext,
private readonly model: GenerativeModel,
) {
super(inputSource, inferenceRunner, functionCallRunner)
}
// A message from a human (or any other participant) — record it and think.
async onMessage(message: string): Promise<void> {
this.context.addContextItem(UserMessageItem.create(message))
this.runInference(this.environment, this.context, this.model)
}
// The agent just produced a function call — execute it.
async onFunctionCall(item: FunctionCallItem): Promise<void> {
this.context.addContextItem(item)
this.executeFunctionCall(this.environment, item)
}
// The tool just produced an output — feed it back and run inference again.
async onFunctionCallOutput(item: FunctionCallOutputItem): Promise<void> {
this.context.addContextItem(item)
this.runInference(this.environment, this.context, this.model)
}
// Keep the local context in sync with model-emitted reasoning and replies.
async onReasoning(item: ReasoningItem): Promise<void> {
this.context.addContextItem(item)
}
async onModelMessage(item: ModelMessageItem): Promise<void> {
this.context.addContextItem(item)
}
}
For comparison, the old style looked like this:
// Before: one handler, manual dispatch
async onContextItem(source: Participant, item: ContextItem): Promise<void> {
if (source === this) return
this.context.addContextItem(item)
if (item instanceof UserMessageItem) {
this.runInference(this.environment, this.context, this.model)
return
}
if (item instanceof FunctionCallItem) {
this.executeFunctionCall(this.environment, item)
return
}
}
The Self vs. External Split
For every typed handler, there are two variants: a self handler that fires when this participant emits the event, and an external handler (prefixed with onExternal) that fires when another participant emits it.
| Self (your emissions) | External (everyone else) |
|---|---|
onFunctionCall |
onExternalFunctionCall |
onFunctionCallOutput |
onExternalFunctionCallOutput |
onReasoning |
onExternalReasoning |
onModelMessage |
onExternalModelMessage |
The practical effect: a reactive agent can encode "act on my own outputs" separately from "observe others" — no more manual source === this checks.
- Want to build a critic that reviews a peer agent's answers? Override
onExternalModelMessage. - Want to feed your model's own tool calls back into a runner? Override
onFunctionCall.
The two concerns stop bleeding into each other.
As a concrete example, here is a passive participant that only listens to external events. It does not run inference and does not execute tools — it just watches what other participants emit and logs it. Drop it into any environment and you have a live transcript:
// transcript-logger.ts
import {
Participant,
FunctionCallItem,
FunctionCallOutputItem,
ReasoningItem,
ModelMessageItem,
} from "@mozaik-ai/core"
export class TranscriptLogger extends Participant {
async onMessage(message: string): Promise<void> {
console.log("[message]", message)
}
async onExternalFunctionCall(
source: Participant,
item: FunctionCallItem,
): Promise<void> {
console.log(`[${source.constructor.name}] function_call`, item.toJSON())
}
async onExternalFunctionCallOutput(
source: Participant,
item: FunctionCallOutputItem,
): Promise<void> {
console.log(
`[${source.constructor.name}] function_call_output`,
item.toJSON(),
)
}
async onExternalReasoning(
source: Participant,
item: ReasoningItem,
): Promise<void> {
console.log(`[${source.constructor.name}] reasoning`, item.toJSON())
}
async onExternalModelMessage(
source: Participant,
item: ModelMessageItem,
): Promise<void> {
console.log(`[${source.constructor.name}] model_message`, item.toJSON())
}
}
Plain Messages on the Bus
Conversational text used to flow through the same context-item pipeline as everything else. It worked, but it was heavier than it needed to be. Now there are two clean lanes:
-
Messages — plain strings, delivered via
onMessage(message: string). Use them for what a human types, what one agent says to another, and any other free-form text. - ContextItems — typed model internals (function calls, function call outputs, reasoning items, model messages), delivered via their dedicated handlers.
The participant side mirrors this: the old InputItemSource has become InputStream, an async iterable of strings. Each yielded string fans out via onMessage to every other participant. Inference and tool runners still produce typed ContextItems, exactly as before.
The benefit is mostly clarity. When you read a reactive agent now, you can tell at a glance whether a handler is reacting to "someone said something" or to "the model produced a tool call."
import { InputStream } from "@mozaik-ai/core"
// Async iterable of plain strings — each yield becomes onMessage for peers.
async function* humanInput(): InputStream {
yield "Add rate limiting to all API endpoints"
yield "Focus on the NestJS backend first"
}
A Bigger Model Lineup
The default OpenAI provider now ships a wider model lineup. In addition to Gpt54, Gpt54Mini, and Gpt54Nano, there is now Gpt55. Same OpenAIResponses runtime, same OpenResponses-aligned types — pick the model that fits the job.
import { Gpt55, Gpt54Mini } from "@mozaik-ai/core"
// Heavier reasoning for planning; lighter model for review passes.
const planner = new Gpt55()
const reviewer = new Gpt54Mini()
From One Agent to a Swarm
The reactive frame really earns its keep when more than one agent shares an environment. We've started calling these collaborative groups agent swarms: a handful of focused reactive agents — planner, executors, reviewer, and observers — all joining one AgenticEnvironment and reacting to each other's events.
Agent swarms — multiple reactive agents collaborating in one agentic environment
The clearest production example is baro, a Claude agent orchestrator built on Mozaik. Ten specialized participants — planner, executors, reviewer, fixer, librarian, auditor, and more — work fully concurrently on the same goal, like a team collaborating in real time instead of a single agent doing everything alone.
Adding a new role to a swarm doesn't mean editing a central controller. It means writing one more reactive agent and join()ing it to the environment. Everything else keeps working.
import { AgenticEnvironment } from "@mozaik-ai/core"
const environment = new AgenticEnvironment()
planner.join(environment)
executor.join(environment)
critic.join(environment)
new TranscriptLogger().join(environment)
environment.start()
Composing Intelligence
Each of these changes is small on its own. Together, they shift Mozaik further toward what we wanted from the start: a framework where intelligent behavior is something you compose, not something you orchestrate.
| Change | What it unlocks |
|---|---|
| Reactive agents | Behavior from event handlers, not a central run() loop |
| Typed handlers | Readable "when X, do Y" agents without dispatch boilerplate |
| Self vs. external | Critics, observers, and actors coexist without source === this
|
| Plain messages | Conversational text separate from model-internal ContextItems |
| Agent swarms | Many focused agents on one bus — see baro |
Reactive agents make the building block explicit. Typed handlers make each agent easier to read. The self vs. external split keeps intent obvious. Plain messages keep the conversational lane clean. And the swarm pattern shows what falls out the other side: groups of agents that adapt to their environment and to each other in real time.
Get started
npm install @mozaik-ai/core
Star on GitHub: github.com/jigjoy-ai/mozaik
Related: Mozaik 3.0 — Structured Context & the Agentic Environment ·
Read the full article on JigJoy: jigjoy.ai/blog/mozaik-reactive-agents

Top comments (0)