Reactive Agents, Typed Event Handlers, and Agent Swarms: What's New in Mozaik

When we released Mozaik 3.0, 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:

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
1import {
2 BaseAgentParticipant,
3 UserMessageItem,
4 FunctionCallItem,
5 FunctionCallOutputItem,
6 ReasoningItem,
7 ModelMessageItem,
8 AgenticEnvironment,
9 ModelContext,
10 GenerativeModel,
11 InputStream,
12 InferenceRunner,
13 FunctionCallRunner,
14} from "@mozaik-ai/core"
15
16export class ReactiveAgent extends BaseAgentParticipant {
17 constructor(
18 inputSource: InputStream,
19 inferenceRunner: InferenceRunner,
20 functionCallRunner: FunctionCallRunner,
21 private readonly environment: AgenticEnvironment,
22 private readonly context: ModelContext,
23 private readonly model: GenerativeModel,
24 ) {
25 super(inputSource, inferenceRunner, functionCallRunner)
26 }
27
28 // A message from a human (or any other participant) - record it and think.
29 async onMessage(message: string): Promise<void> {
30 this.context.addContextItem(UserMessageItem.create(message))
31 this.runInference(this.environment, this.context, this.model)
32 }
33
34 // The agent just produced a function call - execute it.
35 async onFunctionCall(item: FunctionCallItem): Promise<void> {
36 this.context.addContextItem(item)
37 this.executeFunctionCall(this.environment, item)
38 }
39
40 // The tool just produced an output - feed it back and run inference again.
41 async onFunctionCallOutput(item: FunctionCallOutputItem): Promise<void> {
42 this.context.addContextItem(item)
43 this.runInference(this.environment, this.context, this.model)
44 }
45
46 // Keep the local context in sync with model-emitted reasoning and replies.
47 async onReasoning(item: ReasoningItem): Promise<void> {
48 this.context.addContextItem(item)
49 }
50
51 async onModelMessage(item: ModelMessageItem): Promise<void> {
52 this.context.addContextItem(item)
53 }
54}

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.

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
1import {
2 Participant,
3 FunctionCallItem,
4 FunctionCallOutputItem,
5 ReasoningItem,
6 ModelMessageItem,
7} from "@mozaik-ai/core"
8
9// A passive observer that listens to external events from other participants.
10// It does not run inference or call tools - it just watches what flows through
11// the environment and logs it.
12export class TranscriptLogger extends Participant {
13 async onMessage(message: string): Promise<void> {
14 console.log("[message]", message)
15 }
16
17 async onExternalFunctionCall(
18 source: Participant,
19 item: FunctionCallItem,
20 ): Promise<void> {
21 console.log(`[${source.constructor.name}] function_call`, item.toJSON())
22 }
23
24 async onExternalFunctionCallOutput(
25 source: Participant,
26 item: FunctionCallOutputItem,
27 ): Promise<void> {
28 console.log(
29 `[${source.constructor.name}] function_call_output`,
30 item.toJSON(),
31 )
32 }
33
34 async onExternalReasoning(
35 source: Participant,
36 item: ReasoningItem,
37 ): Promise<void> {
38 console.log(`[${source.constructor.name}] reasoning`, item.toJSON())
39 }
40
41 async onExternalModelMessage(
42 source: Participant,
43 item: ModelMessageItem,
44 ): Promise<void> {
45 console.log(`[${source.constructor.name}] model_message`, item.toJSON())
46 }
47}

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:

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."

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.

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

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.

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.

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.

Miodrag Vilotijević

Miodrag Vilotijević

Co-founder @ JigJoy

Building the future of agentic systems

To answer the question of what is going to happen next, we need to work out what has already happened; that is, to understand where we will be tomorrow, we need to understand what it was that got us to where we are today.
Paul Geroski
Swarm thumbnail created by 378694390 ©Karina Schultze | Dreamstime.com