molroo docs
SDK Reference

Plugin System

Plugin, compose, createPlugin, withHistory — extend world behavior.

Plugin System

The molroo SDK provides a lightweight plugin system for extending MolrooWorld with custom behavior and lifecycle hooks. Plugins can add new methods to the world instance and react to chat, tick, event, and phase change operations.

import { compose, createPlugin, withHistory } from '@molroo-ai/sdk';
import type { Plugin, PluginHooks } from '@molroo-ai/sdk';

Plugin Interface

A plugin is a callable function that takes a MolrooWorld and returns an enhanced world, optionally with lifecycle hooks attached.

interface Plugin<TPluginApi extends object = object> {
  /** Transform the world instance, adding custom methods/properties */
  (world: MolrooWorld): MolrooWorld & TPluginApi;

  /** Called after every tick() */
  onTick?: (world: MolrooWorld, seconds: number, result: TickResult) => Promise<void>;

  /** Called after every chat() */
  onChat?: (world: MolrooWorld, result: ChatResult) => Promise<void>;

  /** Called after every event() */
  onEvent?: (world: MolrooWorld, result: EventResult) => Promise<void>;

  /** Called when a phase transition occurs (during tick or timeJump) */
  onPhaseChange?: (world: MolrooWorld, from: string, to: string) => Promise<void>;
}

PluginHooks

The lifecycle hooks interface:

interface PluginHooks {
  onTick?: (world: MolrooWorld, seconds: number, result: TickResult) => Promise<void>;
  onChat?: (world: MolrooWorld, result: ChatResult) => Promise<void>;
  onEvent?: (world: MolrooWorld, result: EventResult) => Promise<void>;
  onPhaseChange?: (world: MolrooWorld, from: string, to: string) => Promise<void>;
}
HookFires AfterParameters
onTickworld.tick()world, seconds, TickResult
onChatworld.chat()world, ChatResult
onEventworld.event()world, EventResult
onPhaseChangeworld.tick() or world.timeJump() when phase changesworld, fromPhase, toPhase

createPlugin(init, hooks?)

Helper function to create a plugin with lifecycle hooks. Attaches the hook functions directly to the init function.

function createPlugin<T extends object = object>(
  init: (world: MolrooWorld) => MolrooWorld & T,
  hooks?: PluginHooks,
): Plugin<T>

Example:

interface LoggerApi {
  getLogs(): string[];
}

const loggerPlugin = createPlugin<LoggerApi>(
  (world) => {
    const logs: string[] = [];
    return Object.assign(world, {
      getLogs: () => [...logs],
    });
  },
  {
    onChat: async (world, result) => {
      console.log(`[chat] ${result.entityName}: ${result.response.emotion.discrete.primary}`);
    },
    onTick: async (world, seconds, result) => {
      console.log(`[tick] ${seconds}s elapsed, ${result.pendingEventsProcessed} events processed`);
    },
    onPhaseChange: async (world, from, to) => {
      console.log(`[phase] ${from} -> ${to}`);
    },
  },
);

compose(baseWorld, plugins)

Apply multiple plugins to a world instance and wire up all lifecycle hooks. Returns the enhanced world with all plugin APIs merged.

function compose<TPlugins extends Plugin[]>(
  baseWorld: MolrooWorld,
  plugins: TPlugins,
): MolrooWorld & UnionToIntersection<PluginApi<TPlugins[number]>>

The compose function:

  1. Applies each plugin's init function sequentially, building up the enhanced world
  2. Collects all lifecycle hooks from all plugins
  3. Wraps chat(), tick(), timeJump(), and event() to invoke the corresponding hooks after each operation

Example:

const baseWorld = await MolrooWorld.create(config, setup);

const enhanced = compose(baseWorld, [
  withHistory(),
  loggerPlugin,
]);

// Now enhanced has both HistoryPluginApi and LoggerApi methods
const result = await enhanced.chatStateful('Sera', 'Hello!');
const history = enhanced.getHistory();
const logs = enhanced.getLogs();

withHistory()

Built-in plugin that adds in-memory conversation history management to the world. Provides both manual history management (HistoryPluginApi) and stateful chat (StatefulChatPluginApi).

function withHistory(): (
  world: MolrooWorld,
) => MolrooWorld & HistoryPluginApi & StatefulChatPluginApi

HistoryPluginApi

Manual history management:

interface HistoryPluginApi {
  /** Get a copy of the current history */
  getHistory(): ChatMessage[];

  /** Clear all history */
  clearHistory(): void;

  /** Replace the entire history */
  setHistory(history: ChatMessage[]): void;

  /** Append messages to history */
  addToHistory(...messages: ChatMessage[]): void;
}

StatefulChatPluginApi

Chat with automatic history tracking:

interface StatefulChatPluginApi {
  /**
   * Chat with automatic history management.
   * Appends user message and assistant response to history after each call.
   */
  chatStateful(to: string, message: string, options?: ChatOptions): Promise<ChatResult>;

  /** Get a copy of the current history */
  getHistory(): ChatMessage[];

  /** Reset (clear) all history */
  resetHistory(): void;
}

Usage Example

import { MolrooWorld, compose, withHistory } from '@molroo-ai/sdk';

const baseWorld = await MolrooWorld.create(config, setup);
const world = compose(baseWorld, [withHistory()]);

// chatStateful automatically tracks conversation history
await world.chatStateful('Sera', 'Hi there!');
await world.chatStateful('Sera', 'What do you recommend?');

console.log(world.getHistory());
// [
//   { role: 'user', content: 'Hi there!' },
//   { role: 'assistant', content: "Hey! Welcome to the cafe!" },
//   { role: 'user', content: 'What do you recommend?' },
//   { role: 'assistant', content: "Try our lavender latte!" },
// ]

// Manual history management
world.clearHistory();
world.addToHistory(
  { role: 'user', content: 'Previous context...' },
  { role: 'assistant', content: 'I remember that!' },
);

Writing a Custom Plugin

A complete example of a custom plugin that tracks emotion trends:

import { createPlugin } from '@molroo-ai/sdk';
import type { VAD } from '@molroo-ai/sdk';

interface EmotionTrackerApi {
  getEmotionHistory(entityName: string): VAD[];
  getAverageValence(entityName: string): number;
}

const emotionTracker = createPlugin<EmotionTrackerApi>(
  (world) => {
    const history: Record<string, VAD[]> = {};

    return Object.assign(world, {
      getEmotionHistory(entityName: string) {
        return [...(history[entityName] ?? [])];
      },
      getAverageValence(entityName: string) {
        const entries = history[entityName];
        if (!entries?.length) return 0;
        return entries.reduce((sum, v) => sum + v.V, 0) / entries.length;
      },
    });
  },
  {
    onChat: async (_world, result) => {
      const name = result.entityName;
      const vad = result.response.emotion.vad;
      // Access closure from init via the plugin instance
      // (In practice, store history in the world or a WeakMap)
    },
  },
);

// Use it
const world = compose(baseWorld, [emotionTracker]);
const history = world.getEmotionHistory('Sera');

Composing Multiple Plugins

Plugins compose cleanly. Each plugin's API methods and hooks are merged:

const world = compose(baseWorld, [
  withHistory(),
  emotionTracker,
  loggerPlugin,
]);

// All APIs are available
world.chatStateful('Sera', 'Hello');  // from withHistory
world.getEmotionHistory('Sera');      // from emotionTracker
world.getLogs();                       // from loggerPlugin

Hooks from all plugins fire in the order plugins are listed. For example, after a chat() call, onChat fires for withHistory first, then emotionTracker, then loggerPlugin.

On this page