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>;
}| Hook | Fires After | Parameters |
|---|---|---|
onTick | world.tick() | world, seconds, TickResult |
onChat | world.chat() | world, ChatResult |
onEvent | world.event() | world, EventResult |
onPhaseChange | world.tick() or world.timeJump() when phase changes | world, 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:
- Applies each plugin's init function sequentially, building up the enhanced world
- Collects all lifecycle hooks from all plugins
- Wraps
chat(),tick(),timeJump(), andevent()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 & StatefulChatPluginApiHistoryPluginApi
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 loggerPluginHooks 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.