Overview
The ConversationStore is the core abstraction in ff-ai for managing conversation persistence. It provides a provider-agnostic interface that can be implemented with any storage backend.
Interface
The ConversationStore is defined as an Effect Context.Tag with two methods:
import { ConversationStore } from 'ff-ai' ;
import type { ConversationMessage } from 'ff-ai' ;
// The ConversationStore is an Effect Context.Tag with two methods:
// - getMessages: retrieves messages from a thread
// - saveMessages: persists messages to a thread
//
// Both methods require a ThreadIdentifier with resourceId and threadId
Methods
getMessages
Retrieve messages from a conversation thread.
Parameters for retrieving messages The resource identifier (e.g., user ID, project ID)
The conversation thread identifier
Number of recent user messages to include. The store returns all messages (including assistant and tool messages) from the oldest user message in the window.
return
Effect<ConversationMessage[], StoreError>
An Effect that resolves to an array of messages, ordered chronologically (oldest first)
Window Size Behavior
The windowSize parameter controls conversation context:
Counts only user messages (not assistant or tool messages)
Returns all messages from the Nth most recent user message onward
Default is 10 user messages
Set to 0 to retrieve no messages
The implementation handles cases where fewer messages exist than the window size
Example:
import { ConversationStore } from 'ff-ai' ;
import { Effect } from 'effect' ;
const program = Effect . gen ( function* () {
const store = yield * ConversationStore ;
// Get last 5 user messages and all associated messages
const messages = yield * store . getMessages ({
resourceId: 'user-123' ,
threadId: 'thread-456' ,
windowSize: 5
});
console . log ( `Retrieved ${ messages . length } messages` );
return messages ;
});
saveMessages
Persist messages to a conversation thread.
Parameters for saving messages The conversation thread identifier
messages
ConversationMessage[]
required
Array of messages to save. Messages should include id and createdAt properties.
An Effect that resolves when messages are saved successfully
Example:
import { ConversationStore , ConversationMessage } from 'ff-ai' ;
import { Effect } from 'effect' ;
const program = Effect . gen ( function* () {
const store = yield * ConversationStore ;
const messages = [
ConversationMessage . fromModelMessage ({
role: 'user' ,
content: 'Hello!'
}),
ConversationMessage . fromModelMessage ({
role: 'assistant' ,
content: 'Hi there! How can I help you today?'
})
];
yield * store . saveMessages ({
resourceId: 'user-123' ,
threadId: 'thread-456' ,
messages
});
});
Thread Identifier
Both methods accept a ThreadIdentifier which consists of:
export type ThreadIdentifier = {
resourceId : ResourceId ; // string
threadId : ThreadId ; // string
};
This two-level identification allows you to:
Organize conversations by resource (user, project, workspace)
Have multiple conversation threads per resource
Easily query all conversations for a resource
Error Handling
Both methods return an Effect that may fail with StoreError:
import { Data } from 'effect' ;
export class StoreError extends Data . TaggedError ( 'ff-ai/StoreError' )<{
message : string ;
cause ?: unknown ;
}> {
constructor ( message : string , opts ?: { cause ?: unknown }) {
super ({ message , ... opts });
}
}
Handle errors using Effect operators:
const program = Effect . gen ( function* () {
const store = yield * ConversationStore ;
const messages = yield * store . getMessages ({
resourceId: 'user-123' ,
threadId: 'thread-456'
}). pipe (
Effect . catchTag ( 'StoreError' , ( error ) => {
console . error ( 'Failed to get messages:' , error . message );
return Effect . succeed ([]); // Return empty array on error
})
);
return messages ;
});
Implementing a Custom Provider
You can implement a custom storage provider by creating a Layer that provides the ConversationStore service:
import { ConversationStore } from 'ff-ai' ;
import { Data , Effect , Layer } from 'effect' ;
// Define StoreError inline (not exported from ff-ai)
class StoreError extends Data . TaggedError ( 'StoreError' )<{
message : string ;
cause ?: unknown ;
}> {}
export const MyCustomStoreLayer = Layer . succeed (
ConversationStore ,
{
getMessages: Effect . fn ( function* ( params ) {
// Your implementation here
// Must return Effect<ConversationMessage[], StoreError>
try {
const messages = yield * Effect . tryPromise ({
try : () => myDatabase . getMessages ( params ),
catch : ( error ) => new StoreError (
'Failed to get messages' ,
{ cause: error }
)
});
return messages ;
} catch ( error ) {
return yield * Effect . fail (
new StoreError ( 'Failed to get messages' , { cause: error })
);
}
}),
saveMessages: Effect . fn ( function* ( params ) {
// Your implementation here
// Must return Effect<void, StoreError>
yield * Effect . tryPromise ({
try : () => myDatabase . saveMessages ( params ),
catch : ( error ) => new StoreError (
'Failed to save messages' ,
{ cause: error }
)
});
})
}
);
Built-in Providers
Best Practices
Choose appropriate window sizes
Larger window sizes provide more context but increase token costs and latency. Start with the default of 10 user messages and adjust based on your use case.
Always handle StoreError in your application logic. Consider fallbacks like returning empty arrays or cached data.
Use consistent identifiers
Establish a naming convention for resourceId and threadId early. For example: user-{uuid} and thread-{uuid}.
Next Steps