Agent memory & storage#
Conversation state for the built-in copilot: threads, messages and a history window, on a storage adapter you choose. Guide: The agent and your copilot.
import { createMemory, createMemoryStorage, createPgStorage } from '@janux/agent';createMemory(options)#
const memory = createMemory({
storage: createMemoryStorage(),
lastMessages: 20,
generateTitle: (first) => first.slice(0, 60),
});MemoryOptions |
Default | What it does |
|---|---|---|
storage |
(required) | The HarnessStorage adapter |
lastMessages |
20 |
History window handed to the model per turn |
generateTitle |
— | (firstMessage) => string | Promise<string> — names a thread from its first user message |
now |
Date.now |
Injectable clock, for deterministic tests |
The three methods#
| Method | Signature | Notes |
|---|---|---|
ensureThread |
(threadId | undefined, resourceId) => Promise<ThreadRecord> |
Creates the thread when absent. Fails closed: an existing thread whose resourceId differs throws thread_forbidden, so a guessed thread id can't leak another user's conversation |
remember |
(thread, role, content) => Promise<void> |
Appends a message (user / assistant / system / tool) and bumps the thread's updatedAt |
history |
(threadId) => Promise<ChatMessage[]> |
The last lastMessages messages, oldest-first, as provider-shaped messages |
resourceId is the ownership key — a user id, an org id, whatever your identityFor returns. Threads are always scoped by it.
The storage contract#
HarnessStorage is nine methods, so a custom backend (Redis, SQLite, DynamoDB, your own API) is a small object:
interface HarnessStorage {
getThread(id): Promise<ThreadRecord | undefined>;
listThreads(resourceId): Promise<ThreadRecord[]>; // newest first
saveThread(thread): Promise<void>;
deleteThread(id): Promise<void>;
appendMessage(message): Promise<void>;
listMessages(threadId, limit?): Promise<MessageRecord[]>;
saveSnapshot(runId, snapshot): Promise<void>; // durable workflows
loadSnapshot(runId): Promise<unknown | undefined>;
deleteSnapshot(runId): Promise<void>;
}ThreadRecord is { id, resourceId, title, createdAt, updatedAt }; MessageRecord is { id, threadId, role, content, createdAt }. The snapshot trio is what makes durable workflows survive a restart — the same adapter serves both.
createMemoryStorage()#
In-memory adapter: the zero-config default and the right choice for tests. listThreads returns newest-first. State dies with the process, so don't ship it as the only store for a multi-instance deployment.
createPgStorage(options)#
const storage = await createPgStorage({ connectionString: process.env.DATABASE_URL!, max: 20 });Postgres adapter. It creates its own tables on first use (janux_threads, janux_messages, janux_snapshots), so there's no migration step to forget. max is the pool size (default 20). pg is imported dynamically — apps that never call this don't pay for the dependency.
The returned object is a HarnessStorage plus close(): Promise<void>; call it on shutdown (and in tests) to drain the pool.
Wiring it into an agent#
export default defineAgent({
instructions: 'You are the shop copilot.',
harness: { memory: { storage: await createPgStorage({ connectionString: process.env.DATABASE_URL! }) } },
});Related: The agent and your copilot · Agent guardrails · Durable workflows