Mozaik

Participants

The single abstraction for humans, agents, observers, and tools in a reactive, event-driven agentic environment.

In Mozaik, everything that produces or consumes events is a Participant: humans, reactive agents, transcript loggers, guardrails, UI streams. They all share one abstraction and one set of typed event handlers for messages, ContextItems, and SemanticEvent<T> stream chunks.

Use BaseParticipant as your base class — every handler it defines is a no-op, so you only override the events you actually care about:

import { BaseParticipant } from '@mozaik-ai/core';

class MyParticipant extends BaseParticipant {
  async onMessage(message: string): Promise<void> {
    // observe, react, or ignore
  }
}

A participant can:

  • Observe events from other participants (telemetry, audit, UI streaming).
  • React to events by triggering its own capabilities (turn an incoming onMessage into an inference run, turn an onFunctionCall into a tool execution).
  • Ignore events it does not care about — handlers it doesn't override are no-ops.

Roles are patterns, not classes

There are no separate BaseHuman / BaseAgent / BaseObserver classes. A participant's role is simply which capability functions it calls and which handlers it overrides:

RoleHow to build it
HumanA participant that calls sendMessage(environment, text, caller)
AgentA participant that calls runInference(...) and executeFunctionCall(...)
ObserverA participant that only overrides handlers and never runs inference

Typed handlers

HandlerTriggered when…
onJoinedthis participant joins an environment
onLeftthis participant leaves an environment
onParticipantJoinedanother participant joins the same environment
onParticipantLeftanother participant leaves the same environment
onMessageany participant sends a message
onFunctionCallits own inference returns a function call
onExternalFunctionCallanother agent's inference returns a function call
onFunctionCallOutputits own function call runner returns a result
onExternalFunctionCallOutputanother agent's function call runner returns a result
onReasoningits own inference returns a reasoning item
onExternalReasoninganother agent's inference returns a reasoning item
onModelMessageits own inference returns an assistant message
onExternalModelMessageanother agent's inference returns an assistant message
onInternalEventits own inference emits a SemanticEvent<T>
onExternalEventanother participant emits a SemanticEvent<T>
onErrorone of its own handlers throws
onParticipantErroranother participant's handler throws

The split between self and onExternal* handlers lets a participant encode "act on my own outputs" separately from "observe others", with no manual source checks. That applies to ContextItems and to semantic events alike.

Capabilities

Capabilities describe what a participant can produce. Each is a non-blocking free function that streams events into the environment; the participant passes itself as caller.

FunctionProduces
sendMessage(environment, message, caller)messages (strings) via onMessage on other participants
runInference({ model, context, caller, environment, ... })ReasoningItem, FunctionCallItem, ModelMessageItem, and SemanticEvent<T> when streaming
executeFunctionCall(environment, item, tool, caller)FunctionCallOutputItem

Capability functions return immediately and emit their events as they are produced. Callers never await a capability for its result — this is what allows multiple participants to act on the same environment concurrently. They are covered in full on Capabilities & Tools.

Membership

A participant joins and leaves environments directly:

import { BaseParticipant } from '@mozaik-ai/core';

const participant = new BaseParticipant();
participant.join(environment);
// ...
participant.leave(environment);

join registers the participant (it gets onJoined, others get onParticipantJoined). leave unregisters it (it gets onLeft, others get onParticipantLeft).

Lifecycle hooks

Every participant receives lifecycle notifications when it or others join or leave an environment:

import { BaseParticipant, Participant } from '@mozaik-ai/core';

export class TeamAgent extends BaseParticipant {
  onJoined(): void {
    console.log('I joined the environment');
  }

  onLeft(): void {
    console.log('I left the environment');
  }

  onParticipantJoined(participant: Participant): void {
    console.log(`${participant.constructor.name} joined`);
  }

  onParticipantLeft(participant: Participant): void {
    console.log(`${participant.constructor.name} left`);
  }
}

This lets participants react to membership changes — for example, an agent could start inference only after a required collaborator has joined, or clean up shared state when someone leaves.

Patterns

Passive observer

Extend BaseParticipant and just consume events. No inference capabilities required.

import {
  BaseParticipant,
  Participant,
  FunctionCallItem,
  FunctionCallOutputItem,
  ReasoningItem,
  ModelMessageItem,
} from '@mozaik-ai/core';

export class TranscriptLogger extends BaseParticipant {
  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);
  }

  async onExternalFunctionCallOutput(source: Participant, item: FunctionCallOutputItem): Promise<void> {
    console.log(`[${source.constructor.name}] function_call_output`, item);
  }

  async onExternalReasoning(source: Participant, item: ReasoningItem): Promise<void> {
    console.log(`[${source.constructor.name}] reasoning`, item);
  }

  async onExternalModelMessage(source: Participant, item: ModelMessageItem): Promise<void> {
    console.log(`[${source.constructor.name}] model_message`, item);
  }
}

Drop one of these into any environment and you have a live transcript. Add another that pushes to a websocket and you have a UI stream. None of the existing participants change.

Reactive agent

A reactive agent extends BaseParticipant and overrides only the handlers it wants to react on. Capabilities are the free functions runInference and executeFunctionCall — the participant passes itself as caller.

import {
  BaseParticipant,
  UserMessageItem,
  FunctionCallItem,
  FunctionCallOutputItem,
  ReasoningItem,
  ModelMessageItem,
  AgenticEnvironment,
  ModelContext,
  Tool,
  runInference,
  executeFunctionCall,
} from '@mozaik-ai/core';

export class ReactiveAgent extends BaseParticipant {
  constructor(
    private readonly environment: AgenticEnvironment,
    private readonly context: ModelContext,
    private readonly tools: Tool[] = [],
  ) {
    super();
  }

  async onMessage(message: string): Promise<void> {
    this.context.addContextItem(UserMessageItem.create(message));
    runInference({
      model: 'gpt-5.5',
      context: this.context,
      tools: this.tools,
      caller: this,
      environment: this.environment,
    });
  }

  async onFunctionCall(item: FunctionCallItem): Promise<void> {
    this.context.addContextItem(item);
    const tool = this.tools.find((t) => t.name === item.name);
    if (tool) executeFunctionCall(this.environment, item, tool, this);
  }

  async onFunctionCallOutput(item: FunctionCallOutputItem): Promise<void> {
    this.context.addContextItem(item);
    runInference({
      model: 'gpt-5.5',
      context: this.context,
      tools: this.tools,
      caller: this,
      environment: this.environment,
    });
  }

  async onReasoning(item: ReasoningItem): Promise<void> {
    this.context.addContextItem(item);
  }

  async onModelMessage(item: ModelMessageItem): Promise<void> {
    this.context.addContextItem(item);
  }
}

Three things to note:

  1. The agent never awaits its own capability calls inside the handlers — those functions are non-blocking, so the environment keeps delivering events while inference and tool execution run in the background.
  2. The split between self handlers and onExternal* handlers means the agent reacts to its own outputs separately from observing others, with no source checks.
  3. Behaviors compose by reaction, not orchestration. Add a second agent that overrides onExternalModelMessage and you get a critique loop. Add a TranscriptLogger and you get a UI stream. Neither change touches the existing participants.

A deeper walkthrough lives on the dedicated Reactive Agent page.

On this page