Mozaik

Situation handlers

Participants react by pairing a SituationSpecification with a SituationProcessor.

Participants react by registering situation handlers. The runtime publishes events; a handler is a pair of “when” and “then”:

  • SituationSpecification — isSatisfiedBy({ event, participant }). Compose with .and(), .or(), and .not().
  • SituationProcessor — apply(context) is the reaction: runLoop, sendMessage, mutate runtime state, log, persist, …
flowchart LR
  Human[Participant] -->|"sendMessage(text, senderId)"| Runtime(("Runtime"))
  Agent[Participant] -->|"runLoop"| Runtime
  Observer[Participant] -->|join| Runtime
  Runtime -->|"SemanticEvent"| Human
  Runtime -->|"SemanticEvent"| Agent
  Runtime -->|"SemanticEvent"| Observer

Fan-out is synchronous and does not await processors, so a slow listener never blocks producers or other listeners. Participants start receiving events as soon as they join().

There are no built-in specifications — write the ones you need. Self versus others is a filter on producerId, not a second handler API.

import {
  defineRuntime,
  RuntimeState,
  createAgent,
  createHuman,
  SituationSpecification,
  Agent,
  type SituationHandler,
  type SituationContext,
} from '@mozaik-ai/core';

class AppState extends RuntimeState {}

const { initializeRuntime, join, sendMessage, runLoop } = defineRuntime<AppState>();

initializeRuntime({ state: new AppState() });

class WhenOthersSendAMessage extends SituationSpecification {
  isSatisfiedBy({ event, participant }: SituationContext): boolean {
    return event.type === 'message.sent' && event.producerId !== participant.getId();
  }
}

const thinkOnMessage: SituationHandler = {
  specification: new WhenOthersSendAMessage(),
  processor: {
    apply({ event, participant }) {
      if (!(participant instanceof Agent)) return;
      const { message } = event.payload as { message: string };
      runLoop(participant.getId(), message, {
        model: 'gpt-5.5',
        context: participant.getMemory().getContext(),
        tools: participant.getTools(),
      });
    },
  },
};

const agent = createAgent({
  name: 'Assistant',
  capabilities: ['inference'],
  instruction: 'You are a helpful teammate.',
  tools: [],
  handlers: [thinkOnMessage],
});

const human = createHuman({ name: 'User', capabilities: [], handlers: [] });

join(human);
join(agent);

sendMessage('Hello', human.getId());

An observer is the same pattern with processors that only take side actions:

class WhenModelAnswers extends SituationSpecification {
  isSatisfiedBy({ event }: SituationContext): boolean {
    return event.type === 'model.answer';
  }
}

const transcript: SituationHandler = {
  specification: new WhenModelAnswers(),
  processor: {
    apply({ event }) {
      console.log('[model.answer]', event.producerId, event.payload);
    },
  },
};

const observer = createHuman({ name: 'Transcript', capabilities: [], handlers: [transcript] });
join(observer);

Composition

SituationSpecification is an abstract class. Implement isSatisfiedBy, then combine instances:

const whenPeerAnswers = new WhenModelAnswers().and(
  new (class extends SituationSpecification {
    isSatisfiedBy({ event, participant }: SituationContext): boolean {
      return event.producerId !== participant.getId();
    }
  })(),
);

.and(other), .or(other), and .not() return new specifications.

Three things to note

  1. “Act on my own outputs” versus “observe others” is a specification filter on event.producerId versus participant.getId() — not a separate handler API.
  2. Do not await runLoop or sendMessage inside a processor. They return void and keep running in the background while the runtime keeps delivering events.
  3. Behaviors compose by reaction, not orchestration. Add a second agent whose spec matches model.answer and you get a critique loop. Add a transcript observer and you get a UI stream. Neither change touches the existing participants.

processor.apply may return void or Promise<void>. The runtime still does not await it, so a thrown error inside an async processor is not routed onto the bus.

On this page