Skip to content

Memory & Persistence

Memory allows agents to remember past interactions and maintain context across multiple conversations. AgentForge provides utilities for working with LangGraph's checkpointing system.

Related Concepts

What is Memory?

Memory in AgentForge refers to:

  1. Conversation History - Messages exchanged between user and agent
  2. State Persistence - Saving agent state between sessions
  3. Thread Management - Managing multiple conversation threads
  4. Checkpoint History - Accessing past states for debugging or rollback

Checkpointers

Checkpointers save agent state at each step, enabling:

  • Resuming conversations after interruptions
  • Multi-turn interactions with context
  • Time travel to previous states
  • Debugging by inspecting state history

Memory Checkpointer

For development and testing - stores state in memory:

typescript
import { createMemoryCheckpointer } from '@agentforge/core';
import { StateGraph } from '@langchain/langgraph';

const checkpointer = createMemoryCheckpointer();

const app = workflow.compile({ checkpointer });

// First conversation
await app.invoke(
  { messages: ['Hello'] },
  { configurable: { thread_id: 'conversation-1' } }
);

// Continue conversation (remembers context)
await app.invoke(
  { messages: ['What did I just say?'] },
  { configurable: { thread_id: 'conversation-1' } }
);

Memory Checkpointer

Data is lost when the process exits. Use SQLite or Postgres for production.

SQLite Checkpointer

For local persistence - stores state in a SQLite database:

typescript
import { createSqliteCheckpointer } from '@agentforge/core';

const checkpointer = await createSqliteCheckpointer({
  path: './checkpoints.db',
  autoMigrate: true, // Create tables automatically
});

const app = workflow.compile({ checkpointer });

Benefits:

  • ✅ Persists across restarts
  • ✅ Good for local development
  • ✅ Single-machine deployments
  • ✅ No external dependencies

Limitations:

  • ❌ Not suitable for distributed systems
  • ❌ Limited concurrent access
  • ❌ File-based storage

Installation

Requires @langchain/langgraph-checkpoint-sqlite:

bash
npm install @langchain/langgraph-checkpoint-sqlite

Thread Management

Threads represent separate conversation contexts.

Creating Threads

typescript
import { createThreadConfig, generateThreadId } from '@agentforge/core';

// Auto-generate thread ID
const config1 = createThreadConfig();

// Use specific thread ID
const config2 = createThreadConfig('user-123-session-456');

// With additional metadata
const config3 = createThreadConfig('thread-1', {
  userId: 'user-123',
  sessionId: 'session-456',
  createdAt: Date.now(),
});

Using Threads

typescript
const checkpointer = createMemoryCheckpointer();
const app = workflow.compile({ checkpointer });

// User 1's conversation
const thread1 = createThreadConfig('user-1');
await app.invoke({ messages: ['Hello'] }, thread1);
await app.invoke({ messages: ['How are you?'] }, thread1);

// User 2's conversation (separate context)
const thread2 = createThreadConfig('user-2');
await app.invoke({ messages: ['Hi there'] }, thread2);
await app.invoke({ messages: ['What can you do?'] }, thread2);

Multi-User Conversations

typescript
import { createConversationConfig } from '@agentforge/core';

// Create conversation for specific user
const userConfig = createConversationConfig('user-123', {
  sessionId: 'session-456',
  metadata: {
    name: 'Alice',
    preferences: { theme: 'dark' },
  },
});

await app.invoke({ messages: ['Hello'] }, userConfig);

Checkpoint History

Access past states for debugging or rollback.

Getting Checkpoint History

typescript
import { getCheckpointHistory } from '@agentforge/core';

const history = await getCheckpointHistory(app, {
  threadId: 'conversation-1',
  limit: 10, // Last 10 checkpoints
});

for (const checkpoint of history) {
  console.log('Step:', checkpoint.metadata.step);
  console.log('State:', checkpoint.state);
  console.log('Timestamp:', checkpoint.metadata.timestamp);
}

Getting Latest Checkpoint

typescript
import { getLatestCheckpoint } from '@agentforge/core';

const latest = await getLatestCheckpoint(app, 'conversation-1');

if (latest) {
  console.log('Last state:', latest.state);
  console.log('Last step:', latest.metadata.step);
}

Clearing Thread History

typescript
import { clearThread } from '@agentforge/core';

// Clear all checkpoints for a thread
await clearThread(app, 'conversation-1');

Conversation Patterns

Pattern 1: Simple Chat

typescript
import { createMemoryCheckpointer, createThreadConfig } from '@agentforge/core';

const checkpointer = createMemoryCheckpointer();
const app = workflow.compile({ checkpointer });

async function chat(userId: string, message: string) {
  const config = createThreadConfig(userId);
  
  const result = await app.invoke(
    { messages: [message] },
    config
  );
  
  return result.messages[result.messages.length - 1];
}

// Usage
await chat('user-1', 'Hello');
await chat('user-1', 'What is 2+2?');
await chat('user-1', 'Thanks!');

Pattern 2: Session-Based Chat

typescript
async function sessionChat(userId: string, sessionId: string, message: string) {
  const config = createConversationConfig(userId, {
    sessionId,
    metadata: { startedAt: Date.now() },
  });
  
  const result = await app.invoke(
    { messages: [message] },
    config
  );
  
  return result.messages[result.messages.length - 1];
}

// Different sessions for same user
await sessionChat('user-1', 'session-morning', 'Good morning');
await sessionChat('user-1', 'session-afternoon', 'Good afternoon');

Pattern 3: Context Window Management

Manage conversation length to stay within LLM context limits:

typescript
const ChatState = createStateAnnotation({
  messages: {
    schema: z.array(MessageSchema),
    reducer: (left, right) => {
      const combined = [...left, ...right];
      // Keep only last 20 messages
      return combined.slice(-20);
    },
    default: () => [],
  },
});

Pattern 4: Conversation Summarization

Summarize old messages to save context:

typescript
const SummarizedState = createStateAnnotation({
  messages: {
    schema: z.array(MessageSchema),
    reducer: (left, right) => [...left, ...right],
    default: () => [],
  },
  summary: {
    schema: z.string(),
    default: () => '',
  },
});

const summarizeNode = async (state: State) => {
  if (state.messages.length > 50) {
    // Summarize old messages
    const oldMessages = state.messages.slice(0, -20);
    const summary = await llm.invoke(
      `Summarize this conversation: ${JSON.stringify(oldMessages)}`
    );

    return {
      messages: state.messages.slice(-20), // Keep recent messages
      summary: state.summary + '\n' + summary,
    };
  }

  return {};
};

Best Practices

1. Choose the Right Checkpointer

typescript
// ✅ Development - use memory
if (process.env.NODE_ENV === 'development') {
  checkpointer = createMemoryCheckpointer();
}

// ✅ Production - use persistent storage
if (process.env.NODE_ENV === 'production') {
  checkpointer = await createSqliteCheckpointer({
    path: process.env.CHECKPOINT_DB_PATH,
  });
}

2. Use Meaningful Thread IDs

typescript
// ✅ Good - descriptive and unique
const threadId = `user-${userId}-session-${sessionId}`;

// ✅ Good - includes context
const threadId = `support-ticket-${ticketId}`;

// ❌ Bad - not unique enough
const threadId = 'conversation';

// ❌ Bad - hard to debug
const threadId = generateThreadId(); // Random UUID

3. Clean Up Old Threads

typescript
import { clearThread } from '@agentforge/core';

async function cleanupOldThreads(app, maxAge: number) {
  const threads = await getAllThreads(app);

  for (const thread of threads) {
    const age = Date.now() - thread.createdAt;

    if (age > maxAge) {
      await clearThread(app, thread.id);
      console.log(`Cleaned up thread: ${thread.id}`);
    }
  }
}

// Run daily
setInterval(() => {
  cleanupOldThreads(app, 30 * 24 * 60 * 60 * 1000); // 30 days
}, 24 * 60 * 60 * 1000);

4. Handle Checkpoint Errors

typescript
async function safeInvoke(app, input, config) {
  try {
    return await app.invoke(input, config);
  } catch (error) {
    if (error.message.includes('checkpoint')) {
      console.error('Checkpoint error, clearing thread:', config.configurable.thread_id);
      await clearThread(app, config.configurable.thread_id);

      // Retry without history
      return await app.invoke(input, config);
    }
    throw error;
  }
}

5. Monitor Memory Usage

typescript
import { MemoryManager } from '@agentforge/core';

const memoryManager = new MemoryManager({
  maxMemory: 1024 * 1024 * 1024, // 1GB
  checkInterval: 10000, // Check every 10s
  thresholdPercentage: 80, // Warn at 80%
  onThreshold: (stats) => {
    console.warn('Memory usage high:', stats.percentage + '%');
  },
  onLimit: async (stats) => {
    console.error('Memory limit reached, cleaning up...');
    // Clear old checkpoints
    await cleanupOldThreads(app, 7 * 24 * 60 * 60 * 1000); // 7 days
  },
});

memoryManager.start();

Advanced Features

Time Travel

Go back to a previous state:

typescript
const history = await getCheckpointHistory(app, {
  threadId: 'conversation-1',
});

// Get state from 5 steps ago
const previousState = history[4].state;

// Resume from that state
const result = await app.invoke(
  { messages: ['Let me try again'] },
  {
    configurable: {
      thread_id: 'conversation-1',
      checkpoint_id: history[4].id,
    },
  }
);

Branching Conversations

Create alternate conversation paths:

typescript
// Main conversation
const mainConfig = createThreadConfig('main-thread');
await app.invoke({ messages: ['Hello'] }, mainConfig);

// Branch from current state
const branchConfig = createThreadConfig('branch-thread');
const currentState = await getLatestCheckpoint(app, 'main-thread');

// Start branch with same state
await app.invoke(
  currentState.state,
  branchConfig
);

// Now you have two independent threads
await app.invoke({ messages: ['Path A'] }, mainConfig);
await app.invoke({ messages: ['Path B'] }, branchConfig);

Checkpoint Metadata

Add custom metadata to checkpoints:

typescript
const result = await app.invoke(
  { messages: ['Hello'] },
  {
    configurable: {
      thread_id: 'conversation-1',
    },
    metadata: {
      userId: 'user-123',
      source: 'web-app',
      timestamp: Date.now(),
      tags: ['support', 'billing'],
    },
  }
);

// Query by metadata later
const history = await getCheckpointHistory(app, {
  threadId: 'conversation-1',
  filter: (checkpoint) => {
    return checkpoint.metadata.tags?.includes('billing');
  },
});

Debugging with Memory

Inspect State History

typescript
async function debugConversation(threadId: string) {
  const history = await getCheckpointHistory(app, { threadId });

  console.log(`\n=== Conversation: ${threadId} ===`);

  for (const [index, checkpoint] of history.entries()) {
    console.log(`\nStep ${index + 1}:`);
    console.log('Messages:', checkpoint.state.messages);
    console.log('Metadata:', checkpoint.metadata);
  }
}

await debugConversation('conversation-1');

Compare States

typescript
async function compareStates(threadId: string, step1: number, step2: number) {
  const history = await getCheckpointHistory(app, { threadId });

  const state1 = history[step1].state;
  const state2 = history[step2].state;

  console.log('Differences:');
  console.log('Messages added:',
    state2.messages.length - state1.messages.length
  );
  console.log('Context changed:',
    JSON.stringify(state1.context) !== JSON.stringify(state2.context)
  );
}

Production Considerations

1. Database Backups

typescript
// Backup SQLite database regularly
import { copyFile } from 'fs/promises';

async function backupCheckpoints() {
  const timestamp = Date.now();
  await copyFile(
    './checkpoints.db',
    `./backups/checkpoints-${timestamp}.db`
  );
}

// Run daily
setInterval(backupCheckpoints, 24 * 60 * 60 * 1000);

2. Checkpoint Compression

For long conversations, compress old checkpoints:

typescript
const CompressedState = createStateAnnotation({
  recentMessages: {
    schema: z.array(MessageSchema),
    reducer: (left, right) => [...left, ...right].slice(-10),
    default: () => [],
  },
  archivedSummary: {
    schema: z.string(),
    default: () => '',
  },
});

3. Multi-Tenant Isolation

Ensure thread isolation between tenants:

typescript
function createTenantThreadId(tenantId: string, userId: string, sessionId: string) {
  return `tenant-${tenantId}-user-${userId}-session-${sessionId}`;
}

// Each tenant has isolated threads
const thread1 = createTenantThreadId('tenant-a', 'user-1', 'session-1');
const thread2 = createTenantThreadId('tenant-b', 'user-1', 'session-1');

Next Steps

Released under the MIT License.