@bernierllc/agent-conversation-store
v0.2.0
Published
Storage-agnostic persistence contract for agent conversations and messages, with bundled InMemoryConversationStore
Readme
@bernierllc/agent-conversation-store
Storage-agnostic conversation and message persistence contract for LLM agent sessions, with a bundled InMemoryConversationStore reference implementation.
Defines the ConversationStore interface that agent services depend on. Database adapters (Prisma, Supabase, Redis, etc.) are separate packages that satisfy this interface; they can verify compliance via the exported testConversationStore() helper.
Installation
npm install @bernierllc/agent-conversation-storeUsage
import {
InMemoryConversationStore,
ConversationStore,
} from '@bernierllc/agent-conversation-store';
// Development / testing — zero config
const store: ConversationStore = new InMemoryConversationStore();
// Production — inject a database adapter:
// import { PrismaConversationStore } from '@bernierllc/agent-conversation-store-prisma';
// const store: ConversationStore = new PrismaConversationStore(prismaClient);Core Concepts
Context + Subject Pattern
Every conversation belongs to a context (the agent use-case, e.g. 'admin_assistant') and a subjectId (the entity it belongs to — orgId, userId, tenantId). Together these enable findLatest() to resume the most recent session for a given subject.
// Create a conversation
const conversation = await store.createConversation({
context: 'admin_assistant',
subjectId: orgId,
metadata: { userId, orgName },
});
// Resume the latest session on subsequent visits
const existing = await store.findLatest('admin_assistant', orgId);
if (existing) {
const history = await store.loadMessages(existing.id);
// pass history to LLM
}Appending Messages
// User turn
await store.appendMessages(conversation.id, [{
conversationId: conversation.id,
role: 'user',
content: 'Show me bookings for tomorrow',
}]);
// Assistant turn with tool calls
await store.appendMessages(conversation.id, [{
conversationId: conversation.id,
role: 'assistant',
content: 'Let me look that up for you.',
toolCalls: [{
toolName: 'getBookings',
toolCallId: 'tc-001',
state: 'success',
input: { date: '2026-06-13' },
output: { success: true, message: 'Found 12 bookings', data: [] },
}],
}]);
// Load full history for next LLM turn
const messages = await store.loadMessages(conversation.id);Rating a Conversation
await store.rate(conversation.id, 5, 'Very helpful!');API Reference
ConversationStore Interface
| Method | Description |
|--------|-------------|
| createConversation(params) | Create a new conversation for context + subjectId |
| findLatest(context, subjectId) | Most recent conversation for context + subject, or null |
| findById(id) | Conversation by ID, or null |
| appendMessages(conversationId, messages) | Append messages; throws ConversationNotFoundError if missing |
| loadMessages(conversationId) | All messages in creation order |
| rate(conversationId, rating, comment?) | Rate 1–5 stars with optional comment |
Types
interface Conversation {
id: string;
context: string; // agent use-case identifier
subjectId: string; // owner (orgId, userId, etc.)
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
metadata?: Record<string, unknown>;
rating?: 1 | 2 | 3 | 4 | 5;
ratingComment?: string;
}
interface Message {
id: string;
conversationId: string;
role: 'user' | 'assistant' | 'tool';
content: string;
toolCalls?: ToolCallRecord[]; // assistant messages with tool use
createdAt: string; // ISO 8601
}
interface ToolCallRecord {
toolName: string;
toolCallId: string;
state: 'pending' | 'success' | 'error';
input: unknown;
output: { success: boolean; message: string; data?: unknown; error?: string } | null;
}Errors
import {
ConversationStoreError, // base class
ConversationNotFoundError, // conversation ID not found; thrown by appendMessages, rate
MessageAppendError, // unexpected append failure; always includes Error.cause
} from '@bernierllc/agent-conversation-store';
try {
await store.appendMessages('missing-id', [...]);
} catch (err) {
if (err instanceof ConversationNotFoundError) {
// err.context.id contains the missing ID
}
}Writing an Adapter
Implement the ConversationStore interface and verify compliance with the exported test helper:
// In your adapter package's test file:
import { testConversationStore } from '@bernierllc/agent-conversation-store';
import { PrismaConversationStore } from '../src';
describe('PrismaConversationStore compliance', () => {
testConversationStore(() => new PrismaConversationStore(testPrismaClient));
});testConversationStore(factory) accepts a factory function that returns a fresh store instance (called before each test group to ensure isolation). It runs the full compliance suite: create, findLatest, findById, appendMessages, loadMessages, rate — including error cases.
Integration Pattern
agent-service.chat(input, authContext)
→ findLatest(context, subjectId) // check for existing session
→ createConversation() if none
→ loadMessages(conversationId) // prior history for LLM
→ LLM loop runs
→ appendMessages() per turn // persist messages + toolCalls
→ rate() on user feedbackIntegration Documentation
Logger Integration
This package uses @bernierllc/logger for structured debug-level logging on createConversation and appendMessages operations. Logger output is silent unless a transport is configured. This is internal and requires no configuration by consumers.
NeverHub Integration
Status: not-applicable — This package is a core storage contract utility. It does not register with NeverHub. Service and suite packages that consume this package (e.g. @bernierllc/admin-agent-service) handle NeverHub registration.
Graceful Degradation
InMemoryConversationStore has no external dependencies and operates without any network or database connectivity. It is suitable for local development and integration testing without needing a running database.
License
Bernier LLC limited-use license. See LICENSE.
