Documentation/Session Persistence
v2.0.0-beta.1Snapshot bebdcd1Markdown ↗

Session Persistence

Session Persistence

Save and resume agent conversations across restarts using AgentContextNextGen and FileContextStorage.

agent.loadSession() and context.load() validate protected evidence/task scope, association and version before restoration, then commit the active save target on success. Rejected loads keep the previous target and scoped state; checkpoint saves wait for restoration. Custom restore hooks retain their extension point and must honor serializable plugin state; saves fail closed if a plugin refuses rollback. Recovery requires successful restoration of every affected plugin instance; a legacy checkpoint missing it does not clear the guard. Trusted host context.restoreState(goodSnapshot) can also repair it. Rollback snapshots follow the existing JSON contract, including toJSON hooks. A synchronous loaded-notification error rolls back state and ID with its original error. Run loads while execution/host edits are idle.

context.save(id?, metadata?, state?) keeps existing serialized overrides and also accepts a snapshot builder as its optional third argument; the builder runs after preceding checkpoint operations. loadRaw() retains its legacy unvalidated session ID selection and is not the managed restore path. Failed shared-context resumes include construction and synchronous notification in the transaction, restoring permission/tool/prompt/callback/role bindings before candidate cleanup. Cleanup attempts all owned resources even if a plugin throws; direct destroy() reports the first teardown error after cleanup. Read the complete restoration boundaries for custom-hook IO, private side effects and shared ownership.

Quick Start

typescript
import { AgentContextNextGen, createFileContextStorage } from 'oneringai';

// Create storage for the agent
const storage = createFileContextStorage('my-assistant');
// Sessions stored at: ~/.oneringai/agents/my-assistant/sessions/

// Create context with storage
const ctx = AgentContextNextGen.create({
  model: 'gpt-4.1',
  features: { workingMemory: true },
  storage,
});

// Build up state
ctx.addUserMessage('Remember: my name is Alice');
await ctx.memory?.store('user_name', 'User name', 'Alice');

// Save session
await ctx.save('session-001', { title: 'User Session' });

// Later... load session
const ctx2 = AgentContextNextGen.create({ model: 'gpt-4.1', storage });
const loaded = await ctx2.load('session-001');

if (loaded) {
  // Full state restored
  const name = await ctx2.memory?.retrieve('user_name');
  console.log(name); // 'Alice'
}

Storage Backend: FileContextStorage

typescript
import { FileContextStorage, createFileContextStorage } from 'oneringai';

// Simple: use helper function
const storage = createFileContextStorage('my-agent');

// Advanced: custom config
const storageVariant2 = new FileContextStorage({
  agentId: 'my-agent',
  baseDirectory: '/custom/path/agents',  // Override default ~/.oneringai/agents
  prettyPrint: true,  // Human-readable JSON
});

Storage Location: ~/.oneringai/agents/<agentId>/sessions/<sessionId>.json

Custom Storage

Implement IContextStorage interface:

typescript
import type { IContextStorage, StoredContextSession, SerializedContextState, ContextSessionMetadata, ContextStorageListOptions, ContextSessionSummary } from 'oneringai';

abstract class DatabaseContextStorage implements IContextStorage {
  abstract save(sessionId: string, state: SerializedContextState, metadata?: ContextSessionMetadata): Promise<void>;
  abstract load(sessionId: string): Promise<StoredContextSession | null>;
  abstract delete(sessionId: string): Promise<void>;
  abstract exists(sessionId: string): Promise<boolean>;
  abstract list(options?: ContextStorageListOptions): Promise<ContextSessionSummary[]>;
  getPath() { return 'database://...'; }
}

Centralized Storage Registry

Instead of configuring each subsystem separately, use StorageRegistry to set storage backends in one call. Custom tools, media, sessions, context plugins, OAuth, routines, correlation records, agent/connector definitions, and permission rules consult the registry. Resolution can happen during construction or lazily at execution time depending on the subsystem; the table below records each actual default.

typescript
import { StorageRegistry } from 'oneringai';

// Configure all storage backends at init time
StorageRegistry.configure({
  // Global singletons
  media: new S3MediaStorage(),
  oauthTokens: new FileTokenStorage({ directory: './tokens' }),

  // Context-aware factories (called with optional StorageContext for multi-tenant)
  customTools: (ctx) => new MongoCustomToolStorage(ctx?.userId),
  sessions: (agentId) => new RedisContextStorage(agentId),
  persistentInstructions: (agentId) => new DBInstructionsStorage(agentId),
  workingMemory: () => new RedisMemoryStorage(),
  userInfo: (ctx) => new MongoUserInfoStorage(ctx?.userId),
  routineDefinitions: (ctx) => new MongoRoutineDefinitionStorage(ctx?.userId),
  routineExecutions: (ctx) => new MongoRoutineExecutionStorage(ctx?.userId),

  // Additional global backends
  correlations: new MongoCorrelationStorage(),
  permissionRules: new MongoPermissionRulesStorage(),
});

// That's it! All agents, tools, and plugins will use these backends automatically.
const agent = Agent.create({ connector: 'openai', model: 'gpt-4.1' });

Resolution order (every subsystem follows this):

  1. Explicit parameter passed to constructor/factory (e.g., createCustomToolMetaTools({ storage }))
  2. Value registered in StorageRegistry
  3. Built-in file-based (or in-memory) default

No breaking changes — setMediaStorage(), Connector.setDefaultStorage(), and all explicit constructor params continue to work exactly as before. They now delegate to the registry internally.

Available storage keys:

Key Type Default Consumed By
customTools (ctx?) => ICustomToolStorage FileCustomToolStorage custom_tool_save/list/load/delete meta-tools
media IMediaStorage FileMediaStorage Image/video/TTS output tools
oauthTokens ITokenStorage MemoryStorage Connector OAuth token persistence
agentDefinitions IAgentDefinitionStorage — Agent.saveDefinition(), Agent.fromStorage()
connectorConfig IConnectorConfigStorage — ConnectorConfigStore.create()
sessions (agentId, ctx?) => IContextStorage — AgentContextNextGen constructor
persistentInstructions (agentId, ctx?) => IPersistentInstructionsStorage FilePersistentInstructionsStorage PersistentInstructionsPluginNextGen
workingMemory (ctx?) => IMemoryStorage InMemoryStorage WorkingMemoryPluginNextGen
userInfo (ctx?) => IUserInfoStorage FileUserInfoStorage UserInfoPluginNextGen
routineDefinitions (ctx?) => IRoutineDefinitionStorage FileRoutineDefinitionStorage Routine definition persistence
routineExecutions (ctx?) => IRoutineExecutionStorage — Routine execution detail/list tools
correlations ICorrelationStorage — Agent correlation/result routing
permissionRules IUserPermissionRulesStorage — Persistent per-user permission rules
evidence (scope, ctx?) => IEvidenceStorage Process-local when evidence is explicitly enabled Scoped source retention; separate from session snapshots
runtimeSessions (trustedContext) => IRuntimeSessionStorage Unconfigured Optional runtime control/recovery; managed drivers require durable storage

Individual access:

typescript
// Set one backend
StorageRegistry.set('media', new S3MediaStorage());

// Get (returns undefined if not configured)
const storage = StorageRegistry.get('customTools');

// Check if configured
if (StorageRegistry.has('sessions')) { /* ... */ }

// Reset all (useful in tests)
StorageRegistry.reset();

Multi-Tenant / Multi-User:

Per-agent factories receive an optional StorageContext — an opaque object (like ConnectorAccessContext) that carries userId, tenantId, or any custom fields:

typescript
import { StorageRegistry } from 'oneringai';

// Configure factories that partition by tenant
StorageRegistry.configure({
  sessions: (agentId, ctx) => new TenantSessionStorage(agentId, ctx?.tenantId as string),
  persistentInstructions: (agentId, ctx) => new TenantInstructionsStorage(agentId, ctx?.userId as string),
  workingMemory: (ctx) => new TenantMemoryStorage(ctx?.tenantId as string),
});

// Single-tenant startup only; do not change global context during overlapping tenant requests
StorageRegistry.setContext({ userId: 'alice', tenantId: 'acme-corp' });

// All agents now get tenant-scoped storage automatically
const agent = Agent.create({ connector: 'openai', model: 'gpt-4.1', userId: 'alice' });

If no global context is set, AgentContextNextGen derives { userId } for the sessions factory. Custom-tool, user-info, and routine tools can derive { userId } from their per-call ToolContext. The workingMemory and persistentInstructions factories receive only StorageRegistry.getContext() and have no per-instance fallback, so call StorageRegistry.setContext({ userId, tenantId }) when those backends must be partitioned. Because the registry context is process-global, request-concurrent multi-tenant servers should prefer explicitly scoped factories/instances over mutating global context during overlapping requests.

Session Management APIs

typescript
// Check if session exists
const exists = await ctx.sessionExists('session-001');

// Delete session
await ctx.deleteSession('session-001');

// Get current session ID
console.log(ctx.sessionId);  // 'session-001' or null

// List all sessions for this agent
const sessions = await storage.list();
for (const s of sessions) {
  console.log(`${s.sessionId}: ${s.metadata?.title} (${s.messageCount} messages)`);
}

Using with Agent

typescript
import { Agent, createFileContextStorage } from 'oneringai';

const storage = createFileContextStorage('my-agent');

const agent = Agent.create({
  connector: 'openai',
  model: 'gpt-4.1',
  context: {
    model: 'gpt-4.1',
    agentId: 'my-agent',
    features: { workingMemory: true },
    storage,
  },
});

// Run agent
await agent.run('Remember: my favorite color is blue');

// Save session
await agent.context.save('session-001');

// Later... load session
await agent.context.load('session-001');
await agent.run('What is my favorite color?');
// Output: "Your favorite color is blue."

What Gets Persisted

Component Persisted? Notes
Conversation history ✅ All messages with timestamps
WorkingMemory entries ✅ Full values, not just index
InContextMemory entries ✅ Via plugin state
Tool enable/disable state ❌ ToolManager.getState()/loadState() exist but are not wired into ctx.save()/ctx.load() — persist/restore separately if needed
System prompt ✅

AgentContextNextGen Session Persistence

The recommended approach to session persistence using AgentContextNextGen:

typescript
import { AgentContextNextGen, createFileContextStorage, MessageRole, ContentType } from 'oneringai';

// Create storage for the agent
const storage = createFileContextStorage('my-assistant');
// Sessions stored at: ~/.oneringai/agents/my-assistant/sessions/

// Create context with storage
const ctx = AgentContextNextGen.create({
  model: 'gpt-4.1',
  features: { workingMemory: true, inContextMemory: true },
  storage,
});

// Build up state
ctx.addUserMessage('My name is Alice and I prefer dark mode.');
ctx.addAssistantResponse([{ type: 'message', role: MessageRole.ASSISTANT, content: [
  { type: ContentType.OUTPUT_TEXT, text: 'Nice to meet you, Alice!' },
] }]);
await ctx.memory?.store('user_name', 'User name', 'Alice');
await ctx.memory?.store('user_pref', 'User preferences', { theme: 'dark' });

// Save session with metadata
// Note: this metadata is merged into the serialized state's internal
// `metadata` field, NOT into StoredContextSession.metadata (the field
// storage.list() uses for s.metadata?.title / tags filtering below).
// To make title/tags queryable via storage.list(), call
// storage.updateMetadata(sessionId, { title, tags }) as well.
await ctx.save('session-001', {
  title: 'Alice Support Chat',
  tags: ['support', 'vip'],
});

console.log(ctx.sessionId);  // 'session-001'

Loading Sessions

typescript
// Create new context and load
const ctx2 = AgentContextNextGen.create({
  model: 'gpt-4.1',
  features: { workingMemory: true, inContextMemory: true },
  storage,
});

const loaded = await ctx2.load('session-001');

if (loaded) {
  // Everything is restored:
  const conversation = ctx2.getConversation();
  console.log(conversation[0]);  // User message about Alice

  const name = await ctx2.memory?.retrieve('user_name');
  console.log(name);  // 'Alice'

  const prefs = await ctx2.memory?.retrieve('user_pref');
  console.log(prefs);  // { theme: 'dark' }
}

What Gets Persisted

Component Persisted? Notes
Conversation history ✅ All messages with timestamps
WorkingMemory entries ✅ Full values, not just index
Tool enable/disable state ❌ ToolManager.getState()/loadState() exist but are not wired into ctx.save()/ctx.load() — persist/restore separately if needed
Permission approvals ❌ ToolPermissionManager.getState()/loadState() exist but are not wired into ctx.save()/ctx.load() — persist/restore separately if needed
InContextMemory entries ✅ Via plugin state
System prompt ✅
Instructions ✅

Session Management APIs

typescript
// Check if session exists
const exists = await ctx.sessionExists('session-001');

// Delete session
await ctx.deleteSession('session-001');

// Delete current session
await ctx.deleteSession();  // Uses ctx.sessionId

// List all sessions for this agent
const sessions = await storage.list();
for (const s of sessions) {
  console.log(`${s.sessionId}: ${s.metadata?.title} (${s.messageCount} messages)`);
}

// List with filtering
const recentSessions = await storage.list({
  savedAfter: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),  // Last week
  tags: ['support'],
  limit: 10,
});

Storage Backends

FileContextStorage (default):

typescript
import { FileContextStorage, createFileContextStorage } from 'oneringai';

// Simple: use helper
const storage = createFileContextStorage('my-agent');

// Advanced: custom config
const storageVariant2 = new FileContextStorage({
  agentId: 'my-agent',
  baseDirectory: '/custom/path/agents',  // Override default ~/.oneringai/agents
  prettyPrint: true,  // Human-readable JSON
});

Custom Storage (implement IContextStorage):

typescript
import type { IContextStorage, StoredContextSession, SerializedContextState, ContextSessionMetadata, ContextStorageListOptions, ContextSessionSummary } from 'oneringai';

abstract class RedisContextStorage implements IContextStorage {
  abstract save(sessionId: string, state: SerializedContextState, metadata?: ContextSessionMetadata): Promise<void>;
  abstract load(sessionId: string): Promise<StoredContextSession | null>;
  abstract delete(sessionId: string): Promise<void>;
  abstract exists(sessionId: string): Promise<boolean>;
  abstract list(options?: ContextStorageListOptions): Promise<ContextSessionSummary[]>;
  getPath() { return 'redis://...'; }
}

Agent Definition Persistence

Store agent configuration separately from sessions for easy instantiation:

typescript
import { Agent, createFileAgentDefinitionStorage } from 'oneringai';

const defStorage = createFileAgentDefinitionStorage();
// Stores at: ~/.oneringai/agents/<agentId>/definition.json

// Create and configure agent
const agent = Agent.create({
  connector: 'openai',
  model: 'gpt-4.1',
  instructions: 'You are a helpful support assistant.',
  context: {
    model: 'gpt-4.1',
    agentId: 'support-bot',
    features: { workingMemory: true, persistentInstructions: true }
  }
});

// Save definition with metadata
await agent.saveDefinition(defStorage, {
  description: 'Customer support chatbot',
  tags: ['support', 'production'],
  author: 'Team A'
});

Loading Agents from Definitions

typescript
// Later: recreate agent from stored definition
const restored = await Agent.fromStorage('support-bot', defStorage);

if (restored) {
  // Agent has same model, instructions, features as when saved
  const response = await restored.run('Hello!');
}

// With config overrides
const devAgent = await Agent.fromStorage('support-bot', defStorage, {
  model: 'gpt-5.6-luna',  // Override with an economical current model
});

Listing Agent Definitions

typescript
const definitions = await defStorage.list();

for (const def of definitions) {
  console.log(`${def.agentId}: ${def.name}`);
  console.log(`  Type: ${def.agentType}, Model: ${def.model}`);
  console.log(`  Created: ${def.createdAt}`);
}

// Filter by type
const taskAgents = await defStorage.list({ agentType: 'task-agent' });

Storage Structure

text
~/.oneringai/agents/
├── support-bot/
│   ├── definition.json          # Agent configuration
│   ├── custom_instructions.json  # Persistent instructions (if enabled)
│   └── sessions/
│       ├── _index.json          # Session index for fast listing
│       ├── session-001.json     # Full session state
│       └── session-002.json
├── research-bot/
│   ├── definition.json
│   └── sessions/
│       └── ...
└── _agents_index.json           # Agent definitions index
Documentation snapshot · 2026-11-02Suggest a correction ↗

Search documentation

Type to search the v2 documentation.