molroo docs
SDK Reference

Memory

MemoryAdapter interface for advanced memory features — episodic recall, semantic search, and reflections.

Memory

The SDK offers two modes for managing conversation context:

  1. Simple mode (external history) — Manage conversation history yourself using the updatedHistory returned from chat(). No adapter needed.
  2. Advanced mode (MemoryAdapter) — Implement the MemoryAdapter interface for episodic recall, semantic search, and reflections.
import type {
  MemoryAdapter,
  RecallQuery,
  SemanticRecallOptions,
  RecallLimits,
  Reflection,
} from '@molroo-io/sdk';

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.

On this page