Documentation
¶
Overview ¶
Package statestore provides conversation state persistence and management.
Index ¶
- Constants
- Variables
- type ConversationState
- type InMemoryIndex
- type InMemoryIndexOption
- type IndexResult
- type LLMSummarizer
- type ListOptions
- type MemoryStore
- func (s *MemoryStore) AppendMessages(ctx context.Context, id string, messages []types.Message) error
- func (s *MemoryStore) Close()
- func (s *MemoryStore) Delete(ctx context.Context, id string) error
- func (s *MemoryStore) Fork(ctx context.Context, sourceID, newID string) error
- func (s *MemoryStore) Len() int
- func (s *MemoryStore) List(ctx context.Context, opts ListOptions) ([]string, error)
- func (s *MemoryStore) Load(ctx context.Context, id string) (*ConversationState, error)
- func (s *MemoryStore) LoadMetadata(ctx context.Context, id string) (map[string]interface{}, error)
- func (s *MemoryStore) LoadRecentMessages(ctx context.Context, id string, n int) ([]types.Message, error)
- func (s *MemoryStore) LoadSummaries(ctx context.Context, id string) ([]Summary, error)
- func (s *MemoryStore) MessageCount(ctx context.Context, id string) (int, error)
- func (s *MemoryStore) Save(ctx context.Context, state *ConversationState) error
- func (s *MemoryStore) SaveSummary(ctx context.Context, id string, summary Summary) error
- type MemoryStoreOption
- type MessageAppender
- type MessageIndex
- type MessageReader
- type MetadataAccessor
- type RedisOption
- type RedisStore
- func (s *RedisStore) AppendMessages(ctx context.Context, id string, messages []types.Message) error
- func (s *RedisStore) Delete(ctx context.Context, id string) error
- func (s *RedisStore) Fork(ctx context.Context, sourceID, newID string) error
- func (s *RedisStore) List(ctx context.Context, opts ListOptions) ([]string, error)
- func (s *RedisStore) Load(ctx context.Context, id string) (*ConversationState, error)
- func (s *RedisStore) LoadMetadata(ctx context.Context, id string) (map[string]interface{}, error)
- func (s *RedisStore) LoadRecentMessages(ctx context.Context, id string, n int) ([]types.Message, error)
- func (s *RedisStore) LoadSummaries(ctx context.Context, id string) ([]Summary, error)
- func (s *RedisStore) MessageCount(ctx context.Context, id string) (int, error)
- func (s *RedisStore) Save(ctx context.Context, state *ConversationState) error
- func (s *RedisStore) SaveSummary(ctx context.Context, id string, summary Summary) error
- type Store
- type Summarizer
- type Summary
- type SummaryAccessor
Constants ¶
const ( SortByCreatedAt = "created_at" SortByUpdatedAt = "updated_at" )
Sort field constants for ListOptions.SortBy.
const DefaultMaxEntries = 10000
DefaultMaxEntries is the default maximum number of entries a MemoryStore will hold. When the limit is reached, the least-recently-accessed entry is evicted. Use WithNoMaxEntries() to explicitly disable the entry limit.
const DefaultMaxIndexedConversations = 1000
DefaultMaxIndexedConversations is the default maximum number of conversations that can be indexed in memory before LRU eviction of the least recently used.
const DefaultTTL = 1 * time.Hour
DefaultTTL is the default time-to-live for conversation states in a MemoryStore. Entries that have not been accessed within the TTL are considered expired and eligible for eviction. Use WithNoTTL() to explicitly disable TTL expiration.
Variables ¶
var ErrInvalidID = errors.New("invalid conversation ID")
ErrInvalidID is returned when an invalid conversation ID is provided.
var ErrInvalidState = errors.New("invalid conversation state")
ErrInvalidState is returned when a conversation state is invalid.
var ErrNotFound = errors.New("conversation not found")
ErrNotFound is returned when a conversation doesn't exist in the store.
Functions ¶
This section is empty.
Types ¶
type ConversationState ¶
type ConversationState struct {
ID string // Unique conversation identifier
UserID string // User who owns this conversation
Messages []types.Message // Message history (using unified types.Message)
SystemPrompt string // System prompt for this conversation
Summaries []Summary // Compressed summaries of old turns
TokenCount int // Total tokens in messages
LastAccessedAt time.Time // Last time conversation was accessed
Metadata map[string]interface{} // Arbitrary metadata (e.g., extracted context)
}
ConversationState represents stored conversation state in the state store. This is the primary data structure for persisting and loading conversation history.
type InMemoryIndex ¶ added in v1.3.1
type InMemoryIndex struct {
// contains filtered or unexported fields
}
InMemoryIndex provides an in-memory implementation of MessageIndex using brute-force cosine similarity search. Suitable for development, testing, and conversations with up to ~10K messages.
The number of indexed conversations is bounded by maxConversations with LRU eviction to prevent unbounded memory growth.
func NewInMemoryIndex ¶ added in v1.3.1
func NewInMemoryIndex(provider providers.EmbeddingProvider, opts ...InMemoryIndexOption) *InMemoryIndex
NewInMemoryIndex creates a new in-memory message index.
func (*InMemoryIndex) Delete ¶ added in v1.3.1
func (idx *InMemoryIndex) Delete(_ context.Context, conversationID string) error
Delete removes all indexed messages for a conversation.
func (*InMemoryIndex) Index ¶ added in v1.3.1
func (idx *InMemoryIndex) Index( ctx context.Context, conversationID string, turnIndex int, message types.Message, ) error
Index adds a message to the search index by computing its embedding.
func (*InMemoryIndex) Search ¶ added in v1.3.1
func (idx *InMemoryIndex) Search(ctx context.Context, conversationID, query string, k int) ([]IndexResult, error)
Search finds the top-k messages most relevant to the query string.
type InMemoryIndexOption ¶ added in v1.3.10
type InMemoryIndexOption func(*InMemoryIndex)
InMemoryIndexOption configures an InMemoryIndex.
func WithMaxIndexedConversations ¶ added in v1.3.10
func WithMaxIndexedConversations(maxConv int) InMemoryIndexOption
WithMaxIndexedConversations sets the maximum number of conversations to index. Default is DefaultMaxIndexedConversations (1000).
type IndexResult ¶ added in v1.3.1
type IndexResult struct {
// TurnIndex is the position of the message in the conversation history.
TurnIndex int
// Message is the full message content.
Message types.Message
// Score is the relevance score (higher is more relevant, typically 0.0-1.0).
Score float64
}
IndexResult represents a single search result from the message index.
type LLMSummarizer ¶ added in v1.3.1
type LLMSummarizer struct {
// contains filtered or unexported fields
}
LLMSummarizer uses an LLM provider to compress messages into summaries.
func NewLLMSummarizer ¶ added in v1.3.1
func NewLLMSummarizer(provider providers.Provider) *LLMSummarizer
NewLLMSummarizer creates a new LLM-based summarizer. A cheaper/faster model is recommended (e.g., GPT-3.5, Claude Haiku).
type ListOptions ¶
type ListOptions struct {
// UserID filters conversations by the user who owns them.
// If empty, all conversations are returned (subject to pagination).
UserID string
// Limit is the maximum number of conversation IDs to return.
// If 0, a default limit (e.g., 100) should be applied.
Limit int
// Offset is the number of conversations to skip (for pagination).
Offset int
// SortBy specifies the field to sort by (e.g., "created_at", "updated_at").
// If empty, implementation-specific default sorting is used.
SortBy string
// SortOrder specifies sort direction: "asc" or "desc".
// If empty, defaults to "desc" (newest first).
SortOrder string
}
ListOptions provides filtering and pagination options for listing conversations.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore provides an in-memory implementation of the Store interface. It is thread-safe and suitable for development, testing, and single-instance deployments. For distributed systems, use RedisStore or a database-backed implementation.
func NewMemoryStore ¶
func NewMemoryStore(opts ...MemoryStoreOption) *MemoryStore
NewMemoryStore creates a new in-memory state store. By default, DefaultTTL and DefaultMaxEntries are applied to prevent unbounded memory growth in server scenarios. Options can be provided to override these defaults. Use WithNoTTL() and/or WithNoMaxEntries() to explicitly disable limits.
func (*MemoryStore) AppendMessages ¶ added in v1.3.1
func (s *MemoryStore) AppendMessages(ctx context.Context, id string, messages []types.Message) error
AppendMessages appends messages to the conversation's message history. If the entry is expired, it is treated as non-existent and a new state is created.
func (*MemoryStore) Close ¶ added in v1.3.2
func (s *MemoryStore) Close()
Close stops the background eviction goroutine, if running. It is safe to call Close multiple times.
func (*MemoryStore) Delete ¶
func (s *MemoryStore) Delete(ctx context.Context, id string) error
Delete removes a conversation state by ID.
func (*MemoryStore) Fork ¶ added in v1.1.6
func (s *MemoryStore) Fork(ctx context.Context, sourceID, newID string) error
Fork creates a copy of an existing conversation state with a new ID.
func (*MemoryStore) Len ¶ added in v1.3.2
func (s *MemoryStore) Len() int
Len returns the number of entries currently in the store, including expired entries that have not yet been evicted. This is primarily useful for testing.
func (*MemoryStore) List ¶
func (s *MemoryStore) List(ctx context.Context, opts ListOptions) ([]string, error)
List returns conversation IDs matching the given criteria. Expired entries are excluded from results.
func (*MemoryStore) Load ¶
func (s *MemoryStore) Load(ctx context.Context, id string) (*ConversationState, error)
Load retrieves a conversation state by ID. Returns a deep copy to prevent external mutations. Expired entries are lazily evicted on access and return ErrNotFound.
func (*MemoryStore) LoadMetadata ¶ added in v1.3.10
LoadMetadata returns just the metadata map for the given conversation. This avoids the cost of deep-copying the entire message history, making it significantly cheaper than Load() for callers that only need metadata. Expired entries are lazily evicted and return ErrNotFound.
func (*MemoryStore) LoadRecentMessages ¶ added in v1.3.1
func (s *MemoryStore) LoadRecentMessages(ctx context.Context, id string, n int) ([]types.Message, error)
LoadRecentMessages returns the last n messages for the given conversation. Expired entries return ErrNotFound.
func (*MemoryStore) LoadSummaries ¶ added in v1.3.1
LoadSummaries returns all summaries for the given conversation. Uses a read lock since summaries contain only value types and cloning is a simple slice copy that doesn't benefit from write-lock protection. Expired entries return nil but are not eagerly evicted under RLock.
func (*MemoryStore) MessageCount ¶ added in v1.3.1
MessageCount returns the total number of messages in the conversation. Uses a read lock since it only needs the message count, not a mutable reference. Expired entries return ErrNotFound but are not eagerly evicted under RLock; they will be cleaned up on the next write operation or background eviction.
func (*MemoryStore) Save ¶
func (s *MemoryStore) Save(ctx context.Context, state *ConversationState) error
Save persists a conversation state. If it already exists, it will be updated. When a max entries limit is set and the store is full, the least-recently-accessed entry is evicted to make room.
func (*MemoryStore) SaveSummary ¶ added in v1.3.1
SaveSummary appends a summary to the conversation's summary list. Expired entries return ErrNotFound.
type MemoryStoreOption ¶ added in v1.3.2
type MemoryStoreOption func(*MemoryStore)
MemoryStoreOption configures optional behavior for MemoryStore.
func WithMemoryEvictionInterval ¶ added in v1.3.2
func WithMemoryEvictionInterval(d time.Duration) MemoryStoreOption
WithMemoryEvictionInterval sets the interval for the background cleanup goroutine that removes expired entries. If zero, no background cleanup runs and eviction happens only lazily on access. Requires a non-zero TTL to have any effect.
func WithMemoryMaxEntries ¶ added in v1.3.2
func WithMemoryMaxEntries(n int) MemoryStoreOption
WithMemoryMaxEntries sets the maximum number of entries the store will hold. When the limit is reached, the least-recently-accessed entry is evicted. A zero or negative value disables the limit. By default, DefaultMaxEntries is applied. Use WithNoMaxEntries() as an explicit, self-documenting way to disable the entry limit.
func WithMemoryTTL ¶ added in v1.3.2
func WithMemoryTTL(ttl time.Duration) MemoryStoreOption
WithMemoryTTL sets the time-to-live for conversation states. Entries that have not been accessed within the TTL are considered expired and eligible for eviction. A zero or negative TTL disables expiration. By default, DefaultTTL is applied. Use WithNoTTL() as an explicit, self-documenting way to disable TTL expiration.
func WithNoMaxEntries ¶ added in v1.3.10
func WithNoMaxEntries() MemoryStoreOption
WithNoMaxEntries explicitly disables the max entries limit so the store can grow unbounded. This overrides the DefaultMaxEntries that is otherwise applied automatically.
func WithNoTTL ¶ added in v1.3.10
func WithNoTTL() MemoryStoreOption
WithNoTTL explicitly disables TTL expiration so entries never expire. This overrides the DefaultTTL that is otherwise applied automatically.
type MessageAppender ¶ added in v1.3.1
type MessageAppender interface {
// AppendMessages appends messages to the conversation's message history.
// Creates the conversation if it doesn't exist.
AppendMessages(ctx context.Context, id string, messages []types.Message) error
}
MessageAppender allows appending messages without a full load+replace+save cycle. This is an optional interface — stores that implement it enable incremental saves. Pipeline stages type-assert for this interface and fall back to Store.Save when unavailable.
type MessageIndex ¶ added in v1.3.1
type MessageIndex interface {
// Index adds a message to the search index for the given conversation.
// turnIndex is the position of the message in the conversation history.
Index(ctx context.Context, conversationID string, turnIndex int, message types.Message) error
// Search finds the top-k messages most relevant to the query string.
// Results are ordered by descending relevance score.
Search(ctx context.Context, conversationID string, query string, k int) ([]IndexResult, error)
// Delete removes all indexed messages for a conversation.
Delete(ctx context.Context, conversationID string) error
}
MessageIndex provides semantic search over conversation messages. Implementations can use embedding-based vector search or other similarity methods to find messages relevant to a given query.
type MessageReader ¶ added in v1.3.1
type MessageReader interface {
// LoadRecentMessages returns the last n messages for the given conversation.
// Returns ErrNotFound if the conversation doesn't exist.
LoadRecentMessages(ctx context.Context, id string, n int) ([]types.Message, error)
// MessageCount returns the total number of messages in the conversation.
// Returns ErrNotFound if the conversation doesn't exist.
MessageCount(ctx context.Context, id string) (int, error)
}
MessageReader allows loading a subset of messages without full state deserialization. This is an optional interface — stores that implement it enable efficient partial reads. Pipeline stages type-assert for this interface and fall back to Store.Load when unavailable.
type MetadataAccessor ¶ added in v1.3.10
type MetadataAccessor interface {
// LoadMetadata returns just the metadata map for the given conversation.
// Returns ErrNotFound if the conversation doesn't exist.
// The returned map is a deep copy safe for mutation by the caller.
LoadMetadata(ctx context.Context, id string) (map[string]interface{}, error)
}
MetadataAccessor allows reading metadata without loading the full state. This is an optional interface — stores that implement it enable efficient metadata reads without the cost of deep-copying the message history. Pipeline stages type-assert for this interface and fall back to Store.Load when unavailable.
type RedisOption ¶
type RedisOption func(*RedisStore)
RedisOption configures a RedisStore.
func WithPrefix ¶
func WithPrefix(prefix string) RedisOption
WithPrefix sets the key prefix for Redis keys. Default is "promptkit".
func WithTTL ¶
func WithTTL(ttl time.Duration) RedisOption
WithTTL sets the time-to-live for conversation states. After this duration, conversations will be automatically deleted. Default is 24 hours. Set to 0 for no expiration.
type RedisStore ¶
type RedisStore struct {
// contains filtered or unexported fields
}
RedisStore provides a Redis-backed implementation of the Store interface. It uses JSON serialization for state storage and supports automatic TTL-based cleanup. This implementation is suitable for distributed systems and production deployments.
func NewRedisStore ¶
func NewRedisStore(client *redis.Client, opts ...RedisOption) *RedisStore
NewRedisStore creates a new Redis-backed state store.
Example:
store := NewRedisStore(
redis.NewClient(&redis.Options{Addr: "localhost:6379"}),
WithTTL(24 * time.Hour),
WithPrefix("myapp"),
)
func (*RedisStore) AppendMessages ¶ added in v1.3.1
AppendMessages appends messages to the conversation's message list using RPUSH. Uses Redis pipelining to batch the RPUSH, EXPIRE, and meta update in a single round-trip.
func (*RedisStore) Delete ¶
func (s *RedisStore) Delete(ctx context.Context, id string) error
Delete removes a conversation state from Redis. Removes both decomposed keys (meta, messages, summaries) and legacy monolithic key. Uses a pipeline to batch all DEL commands and optional user index cleanup.
func (*RedisStore) Fork ¶ added in v1.1.6
func (s *RedisStore) Fork(ctx context.Context, sourceID, newID string) error
Fork creates a copy of an existing conversation state with a new ID.
func (*RedisStore) List ¶
func (s *RedisStore) List(ctx context.Context, opts ListOptions) ([]string, error)
List returns conversation IDs matching the given criteria.
func (*RedisStore) Load ¶
func (s *RedisStore) Load(ctx context.Context, id string) (*ConversationState, error)
Load retrieves a conversation state by ID from Redis. Tries the decomposed format (meta key + messages list + summaries list) first, then falls back to the legacy monolithic JSON string for backward compatibility.
func (*RedisStore) LoadMetadata ¶ added in v1.3.10
LoadMetadata returns just the metadata map for the given conversation. This only loads the meta key, avoiding deserialization of the messages and summaries lists.
func (*RedisStore) LoadRecentMessages ¶ added in v1.3.1
func (s *RedisStore) LoadRecentMessages(ctx context.Context, id string, n int) ([]types.Message, error)
LoadRecentMessages returns the last n messages using LRANGE on the messages list. Falls back to loading from the monolithic key if the list doesn't exist.
func (*RedisStore) LoadSummaries ¶ added in v1.3.1
LoadSummaries returns all summaries for the conversation.
func (*RedisStore) MessageCount ¶ added in v1.3.1
MessageCount returns the total number of messages. Falls back to loading from the monolithic key if the list doesn't exist.
func (*RedisStore) Save ¶
func (s *RedisStore) Save(ctx context.Context, state *ConversationState) error
Save persists a conversation state to Redis using decomposed keys. Metadata is stored in a meta key, messages in a Redis list, and summaries in a separate list. Messages use append-only delta writes: only new messages (beyond what is already stored) are RPUSHed, avoiding the cost of DEL+RPUSH for the entire list on every save. If the message count in Redis exceeds the local count (e.g., external truncation), a full rewrite is performed.
func (*RedisStore) SaveSummary ¶ added in v1.3.1
SaveSummary appends a summary to the conversation's summary list. Uses a pipeline to batch RPUSH and EXPIRE into a single round-trip.
type Store ¶
type Store interface {
// Load retrieves conversation state by ID
Load(ctx context.Context, id string) (*ConversationState, error)
// Save persists conversation state
Save(ctx context.Context, state *ConversationState) error
// Fork creates a copy of an existing conversation state with a new ID
// The original conversation is left unchanged. Returns ErrNotFound if sourceID doesn't exist.
Fork(ctx context.Context, sourceID, newID string) error
}
Store defines the interface for persistent conversation state storage.
type Summarizer ¶ added in v1.3.1
type Summarizer interface {
// Summarize compresses the given messages into a concise summary.
Summarize(ctx context.Context, messages []types.Message) (string, error)
}
Summarizer compresses a batch of messages into a summary string. Implementations may use LLM providers, extractive methods, or other compression strategies.
type Summary ¶
type Summary struct {
StartTurn int // First turn included in this summary
EndTurn int // Last turn included in this summary
Content string // Summarized content
TokenCount int // Token count of the summary
CreatedAt time.Time // When this summary was created
}
Summary represents a compressed version of conversation turns. Used to maintain context while reducing token count for older conversations.
type SummaryAccessor ¶ added in v1.3.1
type SummaryAccessor interface {
// LoadSummaries returns all summaries for the given conversation.
// Returns nil (not an error) if no summaries exist.
LoadSummaries(ctx context.Context, id string) ([]Summary, error)
// SaveSummary appends a summary to the conversation's summary list.
SaveSummary(ctx context.Context, id string, summary Summary) error
}
SummaryAccessor allows reading and writing summaries independently of the full state. This is an optional interface for stores that support efficient summary operations.