Persistent Storage
Storage enables Triks to persist data across sessions and restarts.
The Problem
Without storage, every session starts fresh:
User: Set my preferred theme to dark
Agent: Done! Your theme is now dark.
[Later, new session]
User: What's my preferred theme?
Agent: I don't know - I have no memory of previous sessions.With storage:
User: Set my preferred theme to dark
Agent: Done! Your theme is now dark.
[Later, new session]
User: What's my preferred theme?
Agent: Your preferred theme is dark.How Storage Works
1. Enable in Manifest
{
"capabilities": {
"storage": {
"enabled": true,
"maxSizeBytes": 10485760
}
}
}| Field | Description |
|---|---|
enabled | Turn storage on/off |
maxSizeBytes | Quota limit (default: 100MB) |
2. Access via TrikContext
When storage is enabled, the gateway passes a TrikStorageContext to your trik through TrikContext.storage. How you access it depends on your trik’s mode.
Conversational Mode
In a conversational trik, access storage through the context passed to your agent factory or through tools:
import { wrapAgent } from '@trikhub/sdk';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { transferBackTool } from '@trikhub/sdk';
export default wrapAgent(async (context) => {
const setPreference = tool(
async ({ key, value }) => {
await context.storage.set(`pref:${key}`, value);
return JSON.stringify({ success: true });
},
{
name: 'setPreference',
description: 'Store a user preference',
schema: z.object({ key: z.string(), value: z.string() }),
}
);
const getPreference = tool(
async ({ key }) => {
const value = await context.storage.get(`pref:${key}`);
return JSON.stringify({ value: value ?? 'not set' });
},
{
name: 'getPreference',
description: 'Retrieve a user preference',
schema: z.object({ key: z.string() }),
}
);
return createReactAgent({
llm,
tools: [setPreference, getPreference, transferBackTool],
});
});Tool Mode
In a tool-mode trik, access storage through the context passed to each handler:
import { wrapToolHandlers } from '@trikhub/sdk';
export default wrapToolHandlers({
setPreference: async (input, context) => {
const { key, value } = input as { key: string; value: string };
await context.storage.set(`pref:${key}`, value);
return { status: 'saved', key };
},
getPreference: async (input, context) => {
const { key } = input as { key: string };
const value = await context.storage.get(`pref:${key}`);
return { key, value: value ?? 'not set' };
},
});Storage API
All methods are async and return Promises.
get(key: string): Promise<unknown | null>
Retrieve a value by key. Returns null if the key doesn’t exist.
const theme = await context.storage.get('user-theme');
// Returns: "dark" or nullset(key: string, value: unknown, ttl?: number): Promise<void>
Store a value. Optionally set a TTL (time-to-live) in milliseconds.
// Store permanently
await context.storage.set('user-theme', 'dark');
// Store for 1 hour
await context.storage.set('cache-key', data, 3600000);delete(key: string): Promise<boolean>
Delete a key. Returns true if the key existed.
const wasDeleted = await context.storage.delete('old-key');list(prefix?: string): Promise<string[]>
List all keys, optionally filtered by prefix.
const allKeys = await context.storage.list();
// Returns: ["theme", "last-run", "counter"]
const prefixedKeys = await context.storage.list('user:');
// Returns: ["user:settings", "user:history"]getMany(keys: string[]): Promise<Map<string, unknown>>
Get multiple values at once.
const values = await context.storage.getMany(['theme', 'language']);
// Returns: Map { "theme" => "dark", "language" => "en" }setMany(entries: Record<string, unknown>): Promise<void>
Set multiple values at once.
await context.storage.setMany({
theme: 'dark',
language: 'en',
lastLogin: Date.now()
});Where Data is Stored
Storage persists to a SQLite database on the local filesystem:
~/.trikhub/storage/storage.dbThe database uses a single storage table with composite primary key (trik_id, key), where trik_id is the scoped trik name (e.g., @alice/weather). Each Trik has isolated storage - it cannot access other Triks’ data.
Both runtimes use built-in SQLite with no external dependencies:
- JavaScript gateway: Uses Node.js built-in
node:sqlite(requires Node.js 22.5+) - Python gateway: Uses Python stdlib
sqlite3
Quotas and Limits
| Limit | Default | Description |
|---|---|---|
maxSizeBytes | 104857600 (100MB) | Total storage size per Trik |
| Key length | 256 chars | Maximum key length |
| Value size | 10MB | Maximum single value size |
When a Trik exceeds its quota, set operations will throw an error.
TTL (Time-To-Live)
Set expiration times for cache-like behavior:
// Cache API response for 5 minutes
await context.storage.set('api-cache', response, 300000);
// Later: returns null if expired
const cached = await context.storage.get('api-cache');Expired keys are automatically cleaned up on access.
Example: User Preferences Trik
A complete conversational trik that manages user preferences:
import { wrapAgent } from '@trikhub/sdk';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { ChatAnthropic } from '@langchain/anthropic';
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { transferBackTool } from '@trikhub/sdk';
export default wrapAgent(async (context) => {
const llm = new ChatAnthropic({
apiKey: context.config.get('ANTHROPIC_API_KEY'),
model: 'claude-sonnet-4-20250514',
});
const setPreferences = tool(
async ({ theme, language, notifications }) => {
await context.storage.setMany({
theme,
language,
notifications
});
return JSON.stringify({ success: true, message: 'Preferences saved' });
},
{
name: 'setPreferences',
description: 'Save user preferences',
schema: z.object({
theme: z.string(),
language: z.string(),
notifications: z.boolean(),
}),
}
);
const getPreferences = tool(
async () => {
const prefs = await context.storage.getMany([
'theme', 'language', 'notifications'
]);
return JSON.stringify({
theme: prefs.get('theme') ?? 'light',
language: prefs.get('language') ?? 'en',
notifications: prefs.get('notifications') ?? true
});
},
{
name: 'getPreferences',
description: 'Get current user preferences',
schema: z.object({}),
}
);
const resetPreferences = tool(
async () => {
const keys = await context.storage.list();
for (const key of keys) {
await context.storage.delete(key);
}
return JSON.stringify({ success: true, message: 'Preferences reset' });
},
{
name: 'resetPreferences',
description: 'Reset all preferences to defaults',
schema: z.object({}),
}
);
return createReactAgent({
llm,
tools: [setPreferences, getPreferences, resetPreferences, transferBackTool],
});
});Storage Patterns
Pattern 1: Simple Key-Value Cache
async function getCached(
storage: TrikStorageContext,
key: string,
fetcher: () => Promise<unknown>
) {
const cached = await storage.get(`cache:${key}`);
if (cached !== null) return cached;
const fresh = await fetcher();
await storage.set(`cache:${key}`, fresh, 3600000); // 1 hour TTL
return fresh;
}Pattern 2: Counters and Metrics
async function incrementCounter(
storage: TrikStorageContext,
name: string
) {
const current = (await storage.get(`counter:${name}`)) ?? 0;
await storage.set(`counter:${name}`, (current as number) + 1);
return (current as number) + 1;
}Pattern 3: State Machine
type State = 'idle' | 'processing' | 'complete';
async function transition(
storage: TrikStorageContext,
newState: State
) {
const current = await storage.get('state') as State ?? 'idle';
// Validate transition
if (current === 'complete' && newState !== 'idle') {
throw new Error('Cannot transition from complete');
}
await storage.set('state', newState);
await storage.set('stateChangedAt', Date.now());
}Testing Storage
Use an in-memory storage provider for tests:
import { InMemoryStorageProvider } from '@trikhub/gateway';
const storage = new InMemoryStorageProvider();
const context = storage.forTrik('@test/my-trik');
// Test your storage logic
await context.set('key', 'value');
expect(await context.get('key')).toBe('value');For integration tests, use a temporary directory:
import { SqliteStorageProvider } from '@trikhub/gateway';
import { mkdtemp, rm } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
let tempDir: string;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), 'storage-test-'));
});
afterEach(async () => {
await rm(tempDir, { recursive: true });
});
it('persists data', async () => {
const storage = new SqliteStorageProvider(tempDir);
const context = storage.forTrik('@test/trik');
await context.set('key', 'value');
expect(await context.get('key')).toBe('value');
});Next: Learn about the Security Model.