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
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
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:
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.
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):
- Explicit parameter passed to constructor/factory (e.g.,
createCustomToolMetaTools({ storage })) - Value registered in
StorageRegistry - 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:
// 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:
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
// 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
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:
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
// 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
// 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):
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):
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:
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
// 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
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
~/.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