molroo docs
Guides

Plugins

Extend world behavior with the plugin system: compose, createPlugin, withHistory.

Plugins

The molroo SDK has a composable plugin system that lets you extend world behavior without modifying core SDK code. Plugins can add new methods to the world, hook into lifecycle events, or both.

Plugin interface

A plugin is a function that receives a MolrooWorld and returns an enhanced version of it, optionally with additional methods. It can also declare lifecycle hooks.

interface Plugin<TPluginApi extends object = object> {
  (world: MolrooWorld): MolrooWorld & TPluginApi;
  onTick?: (world: MolrooWorld, seconds: number, result: any) => Promise<void>;
  onChat?: (world: MolrooWorld, result: any) => Promise<void>;
  onEvent?: (world: MolrooWorld, result: any) => Promise<void>;
  onPhaseChange?: (world: MolrooWorld, from: string, to: string) => Promise<void>;
}

compose()

The compose() function applies one or more plugins to a world. It collects all hooks and wraps world methods so hooks fire automatically after each operation.

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

const enhanced = compose(world, [withHistory()]);

// Now you can use both standard world methods and plugin methods
const result = await enhanced.chatStateful('Sera', 'Hi!');
console.log(enhanced.getHistory());

You can compose multiple plugins together:

const enhanced = compose(world, [
  withHistory(),
  myLoggerPlugin,
  myAnalyticsPlugin,
]);

compose() wraps the world's chat, tick, timeJump, and event methods to automatically invoke the corresponding hooks on all composed plugins after each call.

createPlugin()

createPlugin() is a helper for building plugins that combines an init function with lifecycle hooks:

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

const logger = createPlugin(
  // Init function: receives world, returns enhanced world (or same world)
  (world) => world,
  // Lifecycle hooks
  {
    onChat: async (world, result) => {
      console.log(
        `[${result.entityName}] ${result.response.emotion.discrete.primary}`
      );
    },
    onTick: async (world, seconds, result) => {
      console.log(`Tick: ${seconds}s elapsed`);
    },
  },
);

Adding methods to the world

The init function can attach new methods by extending the world object:

const statsPlugin = createPlugin(
  (world) => {
    let chatCount = 0;

    return Object.assign(world, {
      getChatCount() {
        return chatCount;
      },
      incrementChatCount() {
        chatCount++;
      },
    });
  },
  {
    onChat: async (world, result) => {
      // Access the methods we added
      (world as any).incrementChatCount();
    },
  },
);

const enhanced = compose(world, [statsPlugin]);
enhanced.getChatCount(); // 0
await enhanced.chat('Sera', 'Hello');
enhanced.getChatCount(); // 1

Lifecycle hooks

Plugins can hook into four lifecycle events:

HookFires afterParameters
onTickworld.tick()(world, seconds, result)
onChatworld.chat()(world, result)
onEventworld.event()(world, result)
onPhaseChangePhase transition(world, fromPhase, toPhase)

Hooks are asynchronous and execute in the order plugins were passed to compose(). They run after the core operation completes, so the result parameter contains the full response.

Built-in: withHistory()

The SDK includes withHistory(), a built-in plugin for in-memory conversation history management. It adds several methods to the world:

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

const enhanced = compose(world, [withHistory()]);

Added methods

MethodDescription
chatStateful(entity, message, options?)Chat and automatically append to history
getHistory()Get the full conversation history array
clearHistory()Remove all history entries
setHistory(entries)Replace history with a specific array
addToHistory(entry)Manually add a history entry
resetHistory()Alias for clearHistory()

Example usage

const enhanced = compose(world, [withHistory()]);

// chatStateful automatically tracks history
await enhanced.chatStateful('Sera', 'Good morning!');
await enhanced.chatStateful('Sera', 'What is the weather like?');

const history = enhanced.getHistory();
console.log(history.length); // 2
console.log(history[0].message); // 'Good morning!'

// Clear and start fresh
enhanced.clearHistory();

chatStateful() is a convenience wrapper around chat(). It calls chat() internally and appends the exchange to the in-memory history. Use chat() directly if you manage history yourself.

Example: emotion tracker plugin

Here is a more complete example -- a plugin that tracks emotion trends over time:

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

interface EmotionEntry {
  entity: string;
  emotion: string;
  vad: { v: number; a: number; d: number };
  timestamp: number;
}

const emotionTracker = createPlugin(
  (world) => {
    const entries: EmotionEntry[] = [];

    return Object.assign(world, {
      getEmotionHistory(entity?: string) {
        if (entity) return entries.filter((e) => e.entity === entity);
        return [...entries];
      },
      getEmotionTrend(entity: string, last = 10) {
        const recent = entries
          .filter((e) => e.entity === entity)
          .slice(-last);
        if (recent.length === 0) return null;
        const avgV =
          recent.reduce((sum, e) => sum + e.vad.v, 0) / recent.length;
        return { averageValence: avgV, sampleSize: recent.length };
      },
    });
  },
  {
    onChat: async (world, result) => {
      const tracker = world as any;
      const entries = tracker.getEmotionHistory() as EmotionEntry[];
      entries.push({
        entity: result.entityName,
        emotion: result.response.emotion.discrete.primary,
        vad: result.response.emotion.vad,
        timestamp: Date.now(),
      });
    },
  },
);

// Usage
const enhanced = compose(world, [emotionTracker, withHistory()]);
await enhanced.chatStateful('Sera', 'I have great news!');
await enhanced.chatStateful('Sera', 'Actually, never mind...');

const trend = enhanced.getEmotionTrend('Sera');
console.log(trend); // { averageValence: 0.15, sampleSize: 2 }

Plugin composition order

When multiple plugins are composed, their hooks execute in the order they appear in the array:

const enhanced = compose(world, [pluginA, pluginB, pluginC]);

// After chat():
// 1. pluginA.onChat()
// 2. pluginB.onChat()
// 3. pluginC.onChat()

If a plugin's init function wraps a method, the wrapping also follows composition order (first plugin's wrapper is innermost).

Next steps

On this page