Skip to content

In-Memory Storage

The in-memory storage package (@helix-agents/store-memory) provides simple Map-based implementations for development and testing. Data exists only in process memory and is lost when the process exits.

When to Use

Good fit:

  • Local development
  • Unit tests
  • Prototyping
  • Single-process deployments where durability isn't needed

Not ideal for:

  • Production requiring durability
  • Multi-process deployments
  • Long-running agents that may outlive the process

Installation

bash
npm install @helix-agents/store-memory

Or use the SDK which includes it:

bash
npm install @helix-agents/sdk

InMemoryStateStore

Basic Usage

typescript
import { InMemoryStateStore } from '@helix-agents/store-memory';

const stateStore = new InMemoryStateStore();

// Save state
await stateStore.save({
  runId: 'run-123',
  agentType: 'researcher',
  streamId: 'run-123',
  messages: [],
  customState: { count: 0 },
  stepCount: 0,
  status: 'running',
  subAgentRefs: [],
  aborted: false,
});

// Load state
const state = await stateStore.load('run-123');
console.log(state?.status); // 'running'

// Delete state
await stateStore.delete('run-123');

Atomic Operations

typescript
// Append messages (thread-safe within single process)
await stateStore.appendMessages('run-123', [{ role: 'user', content: 'Hello' }]);

// Merge custom state changes
await stateStore.mergeCustomState('run-123', {
  values: { count: 5, items: ['new item'] },
  arrayReplacements: new Set(), // Arrays to replace, not append
  warnings: [],
});

// Update status
await stateStore.updateStatus('run-123', 'completed');

// Increment step count
const newCount = await stateStore.incrementStepCount('run-123');
console.log(newCount); // 1

Message Queries

typescript
// Get paginated messages
const result = await stateStore.getMessages('run-123', {
  offset: 0,
  limit: 10,
  includeThinking: false, // Strip thinking content
});

console.log(result.messages); // Message[]
console.log(result.total); // Total count
console.log(result.hasMore); // More pages available?

// Get message count
const count = await stateStore.getMessageCount('run-123');

Testing Utilities

The in-memory store includes utilities not in the interface:

typescript
// Get all stored run IDs
const runIds = stateStore.getAllRunIds();

// Clear all state (useful between tests)
stateStore.clear();

// Get count of stored states
console.log(stateStore.size);

InMemoryStreamManager

Basic Usage

typescript
import { InMemoryStreamManager } from '@helix-agents/store-memory';

const streamManager = new InMemoryStreamManager();

// Create writer
const writer = await streamManager.createWriter('stream-123', 'run-123', 'researcher');

// Write chunks
await writer.write({
  type: 'text_delta',
  agentId: 'run-123',
  agentType: 'researcher',
  timestamp: Date.now(),
  delta: 'Hello',
});

// Close writer (NOT the stream)
await writer.close();

// End the stream
await streamManager.endStream('stream-123');

Reading Streams

typescript
// Create reader
const reader = await streamManager.createReader('stream-123');

if (reader) {
  try {
    for await (const chunk of reader) {
      console.log(chunk.type, chunk);
    }
  } finally {
    await reader.close();
  }
}

Stream Lifecycle

typescript
// Stream is created implicitly by first writer
const writer = await streamManager.createWriter('stream-123', 'run-123', 'agent');

// Write events...
await writer.write({
  /* chunk */
});
await writer.close();

// Option 1: End stream successfully
await streamManager.endStream('stream-123', { result: 'done' });

// Option 2: Fail stream
await streamManager.failStream('stream-123', 'Something went wrong');

Testing Utilities

typescript
// Clear all streams
streamManager.clear();

// Check stream existence
const exists = streamManager.hasStream('stream-123');

// Get all stream IDs
const streamIds = streamManager.getAllStreamIds();

// Get stream status
const status = streamManager.getStreamStatus('stream-123');
// 'active' | 'ended' | 'failed' | undefined

Complete Example

typescript
import { JSAgentExecutor } from '@helix-agents/runtime-js';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';
import { VercelAIAdapter } from '@helix-agents/llm-vercel';

// Create stores
const stateStore = new InMemoryStateStore();
const streamManager = new InMemoryStreamManager();

// Create executor
const executor = new JSAgentExecutor(stateStore, streamManager, new VercelAIAdapter());

// Execute agent
const handle = await executor.execute(MyAgent, 'Research AI');

// Stream results
const stream = await handle.stream();
if (stream) {
  for await (const chunk of stream) {
    if (chunk.type === 'text_delta') {
      process.stdout.write(chunk.delta);
    }
  }
}

// Get final result
const result = await handle.result();
console.log(result.output);

// Cleanup (optional - for testing)
stateStore.clear();
streamManager.clear();

Unit Testing Pattern

typescript
import { describe, it, beforeEach, expect } from 'vitest';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';

describe('MyAgent', () => {
  let stateStore: InMemoryStateStore;
  let streamManager: InMemoryStreamManager;

  beforeEach(() => {
    // Fresh stores for each test
    stateStore = new InMemoryStateStore();
    streamManager = new InMemoryStreamManager();
  });

  it('should complete successfully', async () => {
    const executor = new JSAgentExecutor(stateStore, streamManager, mockLLMAdapter);

    const handle = await executor.execute(MyAgent, 'Test input');
    const result = await handle.result();

    expect(result.status).toBe('completed');
    expect(result.output).toBeDefined();
  });

  it('should track state changes', async () => {
    // ... run agent ...

    // Verify state was saved
    const runIds = stateStore.getAllRunIds();
    expect(runIds).toHaveLength(1);

    const state = await stateStore.load(runIds[0]);
    expect(state?.status).toBe('completed');
  });
});

Limitations

No Persistence

Data exists only in memory:

typescript
const stateStore = new InMemoryStateStore();
await stateStore.save(state);

// Process restart - all data lost
// After restart:
const loaded = await stateStore.load(state.runId); // null

Single Process Only

Cannot share state across processes:

Process 1: stateStore.save(state)
Process 2: stateStore.load(runId)  // null - different Map instance

No Real-Time Streaming Across Processes

Streams are local to the process:

Process 1: streamManager.createWriter(...)
Process 2: streamManager.createReader(...)  // Different stream instance

Memory Usage

Large numbers of agents or long conversations consume memory:

typescript
// Each run accumulates messages
// No automatic cleanup
// Eventually: OutOfMemoryError

// Solution: Clean up after completion
await handle.result();
await stateStore.delete(handle.runId);

Migration to Production

When moving to production, swap to Redis stores:

typescript
// Development
const stateStore =
  process.env.NODE_ENV === 'production' ? new RedisStateStore(redis) : new InMemoryStateStore();

const streamManager =
  process.env.NODE_ENV === 'production'
    ? new RedisStreamManager(redis)
    : new InMemoryStreamManager();

The interface is identical - your code doesn't change.

Next Steps

Released under the MIT License.