Memory
MemoryAdapter interface for advanced memory features — episodic recall, semantic search, and reflections.
Memory
The SDK offers two modes for managing conversation context:
- Simple mode (external history) — Manage conversation history yourself using the
updatedHistoryreturned fromchat(). No adapter needed. - Advanced mode (MemoryAdapter) — Implement the
MemoryAdapterinterface for episodic recall, semantic search, and reflections.
import type {
MemoryAdapter,
RecallQuery,
SemanticRecallOptions,
RecallLimits,
Reflection,
} from '@molroo-io/sdk';Simple mode (recommended for most use cases)
For most applications, external history management is sufficient. The chat() method returns updatedHistory which you store and pass back on the next call:
import { Molroo } from '@molroo-io/sdk';
import { createOpenAI } from '@ai-sdk/openai';
const molroo = new Molroo({ apiKey: 'mk_live_...' });
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const sera = await molroo.createPersona('A kind barista named Sera', { llm: openai });
let history: Message[] = [];
const result = await sera.chat('Hello!', { history });
history = result.updatedHistory; // Save this for the next call
const result2 = await sera.chat('What did I just say?', { history });
history = result2.updatedHistory;No memory adapter is needed. You are responsible for persisting history between sessions (e.g., in a database, localStorage, or session store).
MemoryAdapter interface
For advanced features like semantic search, emotion-aware storage, and reflections, implement the MemoryAdapter interface:
interface MemoryAdapter {
/** Save an episode with emotion metadata. */
saveEpisode(episode: Episode): Promise<void>;
/** Recall episodes by structured query (filters). */
recall(query: RecallQuery): Promise<Episode[]>;
/** Semantic similarity search (optional). Requires vector DB. */
semanticRecall?(query: string, options?: SemanticRecallOptions): Promise<Episode[]>;
/** Save a reflection (optional). */
saveReflection?(reflection: Reflection): Promise<void>;
/** Get reflections for a source (optional). */
getReflections?(sourceEntity?: string): Promise<Reflection[]>;
/** Retrieve episodes by IDs (optional). */
getByIds?(ids: string[]): Promise<Episode[]>;
}Only saveEpisode and recall are required. All other methods are optional and enable progressively richer memory features.
RecallQuery
interface RecallQuery {
/** Filter by source entity name. */
sourceEntity?: string;
/** Search keyword or context text. */
context?: string;
/** Max episodes to return. */
limit?: number;
/** Minimum importance score (0-1). */
minImportance?: number;
/** Epoch ms range [from, to]. */
timeRange?: [number, number];
/** Filter by episode type. */
type?: string | string[];
/** Valence range [min, max]. */
valenceRange?: [number, number];
}Episode
interface Episode {
id: string;
timestamp: number; // Epoch ms
sourceEntity?: string; // Who triggered ("alice", "bob")
context?: string; // Semantic context / topic
appraisal?: AppraisalVector; // The appraisal that caused this episode
emotionSnapshot: VAD; // VAD state at time of recording
intensity?: number; // Peak emotional intensity
importance: number; // Retention weight [0-1]
}Reflection
interface Reflection {
id: string;
timestamp: number;
sourceEntity?: string;
content: string; // Natural language self-reflection
trigger: string; // What triggered the reflection
emotionSnapshot: VAD;
}SemanticRecallOptions
interface SemanticRecallOptions {
/** Number of results. Default: 5. */
topK?: number;
/** Minimum similarity score (0-1). */
minScore?: number;
/** Minimum importance threshold. */
minImportance?: number;
}RecallLimits
Tune how many memories are recalled per chat() call:
interface RecallLimits {
/** Max episodic memories. Default: 5. */
episodicLimit?: number;
/** Max semantic results. Default: 3. */
semanticLimit?: number;
/** Max reflections. Default: 3. */
reflectionLimit?: number;
/** Minimum importance threshold. Default: 0. */
minImportance?: number;
}Implementing your own adapter
Here is an example implementing MemoryAdapter with PostgreSQL:
import type { MemoryAdapter, RecallQuery, SemanticRecallOptions, Reflection } from '@molroo-io/sdk';
import type { Episode, VAD } from '@molroo-io/sdk';
class PostgresMemoryAdapter implements MemoryAdapter {
constructor(private db: Pool) {}
async saveEpisode(episode: Episode): Promise<void> {
await this.db.query(
'INSERT INTO episodes (id, timestamp, source_entity, context, emotion_v, emotion_a, emotion_d, importance) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)',
[episode.id, episode.timestamp, episode.sourceEntity, episode.context,
episode.emotionSnapshot.V, episode.emotionSnapshot.A, episode.emotionSnapshot.D,
episode.importance],
);
}
async recall(query: RecallQuery): Promise<Episode[]> {
const conditions: string[] = [];
const params: unknown[] = [];
if (query.sourceEntity) {
params.push(query.sourceEntity);
conditions.push(`source_entity = $${params.length}`);
}
if (query.minImportance) {
params.push(query.minImportance);
conditions.push(`importance >= $${params.length}`);
}
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const limit = query.limit ?? 10;
const result = await this.db.query(
`SELECT * FROM episodes ${where} ORDER BY timestamp DESC LIMIT ${limit}`,
params,
);
return result.rows.map(this.toEpisode);
}
// Optional: semantic search with pgvector
async semanticRecall(query: string, options?: SemanticRecallOptions): Promise<Episode[]> {
const embedding = await this.embed(query);
const topK = options?.topK ?? 5;
const result = await this.db.query(
`SELECT * FROM episodes ORDER BY embedding <-> $1 LIMIT $2`,
[JSON.stringify(embedding), topK],
);
return result.rows.map(this.toEpisode);
}
// Optional: reflections
async saveReflection(reflection: Reflection): Promise<void> {
await this.db.query(
'INSERT INTO reflections (id, timestamp, source_entity, content, trigger, emotion_v, emotion_a, emotion_d) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)',
[reflection.id, reflection.timestamp, reflection.sourceEntity, reflection.content,
reflection.trigger, reflection.emotionSnapshot.V, reflection.emotionSnapshot.A, reflection.emotionSnapshot.D],
);
}
async getReflections(sourceEntity?: string): Promise<Reflection[]> {
const result = sourceEntity
? await this.db.query('SELECT * FROM reflections WHERE source_entity = $1 ORDER BY timestamp DESC', [sourceEntity])
: await this.db.query('SELECT * FROM reflections ORDER BY timestamp DESC');
return result.rows.map(this.toReflection);
}
private toEpisode(row: any): Episode { /* map row to Episode */ }
private toReflection(row: any): Reflection { /* map row to Reflection */ }
private async embed(text: string): Promise<number[]> { /* your embedding logic */ }
}Usage
const molroo = new Molroo({ apiKey: 'mk_live_...' });
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! });
const sera = await molroo.createPersona('Barista Sera', {
llm: openai,
memory: new PostgresMemoryAdapter(pool),
recall: {
episodicLimit: 5,
semanticLimit: 3,
reflectionLimit: 3,
},
});Chat flow with memory
Here is the complete memory lifecycle during a single chat() call:
1. Recall (parallel, before LLM)
+-- adapter.recall({ sourceEntity, limit })
+-- adapter.semanticRecall(message) (if implemented)
+-- adapter.getReflections(sourceEntity) (if implemented)
2. Build memory block -> inject into system prompt
3. LLM generates response + appraisal
4. API perceive() -> returns AgentResponse with memoryEpisode
5. Post-pipeline (fire-and-forget)
+-- adapter.saveEpisode(memoryEpisode)
+-- adapter.saveReflection(reflection) (if triggered)Step 5 runs in the background and does not block the return of PersonaChatResult.