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(); // 1Lifecycle hooks
Plugins can hook into four lifecycle events:
| Hook | Fires after | Parameters |
|---|---|---|
onTick | world.tick() | (world, seconds, result) |
onChat | world.chat() | (world, result) |
onEvent | world.event() | (world, result) |
onPhaseChange | Phase 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
| Method | Description |
|---|---|
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).