State Management
Understanding how Runtime manages conversation state.
Overview
Section titled “Overview”Runtime uses session-based state management to maintain conversation history across multiple turns.
Core Concept
Section titled “Core Concept”Every conversation has:
- Session ID: Unique identifier
- Message History: Past conversation turns
- State Store: Persistent storage
User → [Session: abc123] → Pipeline → LLM ↓ ↓ [Redis/Memory Store] Response ↓ Persisted HistoryWhy State Management?
Section titled “Why State Management?”Problem: Stateless LLMs
Section titled “Problem: Stateless LLMs”LLMs don’t remember previous interactions:
// First callresponse1 := llm.Complete("What's the capital of France?")// Response: "Paris"
// Second call - LLM has no memoryresponse2 := llm.Complete("What about Germany?")// Response: "Germany? What about it?" (No context!)Solution: Conversation History
Section titled “Solution: Conversation History”Pass history with each request:
messages := []Message{ {Role: "user", Content: "What's the capital of France?"}, {Role: "assistant", Content: "Paris"}, {Role: "user", Content: "What about Germany?"},}response := llm.Complete(messages)// Response: "The capital of Germany is Berlin"Session-Based Architecture
Section titled “Session-Based Architecture”Session ID
Section titled “Session ID”Each conversation has a unique ID:
sessionID := "user-123-conv-456"
result, err := pipeline.ExecuteWithSession(ctx, sessionID, "user", "Hello")Sessions enable:
- Multi-user support: Separate conversations
- History isolation: Users don’t see each other’s history
- Concurrent access: Multiple requests per session
StateMiddleware
Section titled “StateMiddleware”Manages state automatically:
stateMiddleware := middleware.StateMiddleware(store, &middleware.StateMiddlewareConfig{ MaxMessages: 10, TTL: 24 * time.Hour,})
pipeline := pipeline.NewPipeline( stateMiddleware, // ... other middleware)Before execution:
- Load history for session ID
- Add to ExecutionContext.Messages
After execution:
- Save new messages
- Update store
State Store Interface
Section titled “State Store Interface”All stores implement:
type StateStore interface { Load(sessionID string) ([]Message, error) Save(sessionID string, messages []Message) error Delete(sessionID string) error}In-Memory Store
Section titled “In-Memory Store”Fast, but not persistent:
store := statestore.NewInMemoryStateStore()Characteristics:
- Speed: ~1-10µs per operation
- Persistence: Lost on restart
- Scalability: Single instance only
- Cleanup: Manual eviction needed
Use cases:
- Testing
- Development
- Single-instance deployments
- Short-lived sessions
Redis Store
Section titled “Redis Store”Persistent and scalable:
redisClient := redis.NewClient(&redis.Options{ Addr: "localhost:6379",})store := statestore.NewRedisStateStore(redisClient)Characteristics:
- Speed: ~1-5ms per operation
- Persistence: Survives restarts
- Scalability: Multi-instance support
- Cleanup: TTL-based expiration
Use cases:
- Production deployments
- Multi-instance scaling
- Long-lived sessions
- High availability
Design Decisions
Section titled “Design Decisions”Why Session IDs?
Section titled “Why Session IDs?”Decision: Use session IDs to identify conversations
Rationale:
- Isolation: Separate conversations
- Multi-user: Support concurrent users
- Flexibility: Sessions can be user-scoped, conversation-scoped, or any other scope
Alternative considered: Implicit session based on user ID. Rejected because:
- Users may have multiple conversations
- No way to start new conversation
- Harder to manage session lifecycle
Why Separate Store Interface?
Section titled “Why Separate Store Interface?”Decision: StateStore is separate from middleware
Rationale:
- Pluggable: Easy to swap storage backends
- Testable: Mock stores for testing
- Reusable: Stores can be used outside pipelines
Alternative considered: Embedding storage in StateMiddleware. Rejected as too coupled.
Why Message History?
Section titled “Why Message History?”Decision: Store full messages, not raw text
Rationale:
- Rich context: Preserve roles, tool calls, metadata
- Accurate replay: Reconstruct exact conversation
- Provider compatibility: Messages map to provider formats
Alternative considered: Store only text. Rejected because:
- Loses role information
- Can’t reconstruct tool interactions
- Harder to debug
Why TTL?
Section titled “Why TTL?”Decision: Messages expire after TTL
Rationale:
- Cleanup: Automatic deletion of old sessions
- Privacy: Don’t store conversations forever
- Cost: Reduce storage costs
Trade-off: Active conversations may expire. Acceptable with reasonable TTL (e.g., 24 hours).
Storage Strategies
Section titled “Storage Strategies”Message Limits
Section titled “Message Limits”Limit history size:
StateMiddleware(store, &StateMiddlewareConfig{ MaxMessages: 10, // Keep last 10 messages only})Benefits:
- Performance: Less data to load
- Cost: Fewer tokens sent to LLM
- Relevance: Focus on recent context
Trade-off: Loses older context. Use higher limits for long conversations.
Sliding Window
Section titled “Sliding Window”Keep recent messages:
[Old messages...] [Recent 10 messages] ← Kept ↓ DiscardedTime-Based Expiration
Section titled “Time-Based Expiration”Delete old sessions:
StateMiddleware(store, &StateMiddlewareConfig{ TTL: 24 * time.Hour, // Sessions expire after 24 hours})Manual Cleanup
Section titled “Manual Cleanup”Delete sessions explicitly:
// End conversationstore.Delete(sessionID)Scaling Considerations
Section titled “Scaling Considerations”Single-Instance (In-Memory)
Section titled “Single-Instance (In-Memory)”User → [Instance] → In-Memory StoreLimitations:
- Single point of failure
- No persistence
- Limited to one instance
Multi-Instance (Redis)
Section titled “Multi-Instance (Redis)”User → [Instance 1] ↘ [Redis Store]User → [Instance 2] ↗Benefits:
- High availability
- Persistence
- Horizontal scaling
Session Affinity
Section titled “Session Affinity”Route users to same instance:
User (session-123) → Instance 1User (session-456) → Instance 2Benefits:
- Reduced latency (local cache)
- Lower Redis load
Implementation: Load balancer with session affinity
State Loading Performance
Section titled “State Loading Performance”Load Time
Section titled “Load Time”Typical performance:
- In-Memory: 1-10µs
- Redis (local): 1-2ms
- Redis (remote): 5-10ms
Optimization Strategies
Section titled “Optimization Strategies”1. Message Limits
// Fast: Load last 10 messagesStateMiddleware(store, &StateMiddlewareConfig{ MaxMessages: 10,})
// Slow: Load all messagesStateMiddleware(store, &StateMiddlewareConfig{ MaxMessages: -1, // No limit})2. Lazy Loading
Load on demand:
type LazyStateStore struct { inner StateStore cache map[string][]Message}
func (s *LazyStateStore) Load(sessionID string) ([]Message, error) { if cached, ok := s.cache[sessionID]; ok { return cached, nil // Cache hit }
messages, err := s.inner.Load(sessionID) s.cache[sessionID] = messages return messages, err}3. Compression
Compress stored messages:
type CompressedStore struct { inner StateStore}
func (s *CompressedStore) Save(sessionID string, messages []Message) error { compressed := compress(messages) return s.inner.Save(sessionID, compressed)}Trade-off: CPU for storage. Worth it for large histories.
Concurrency and Consistency
Section titled “Concurrency and Consistency”Race Conditions
Section titled “Race Conditions”Multiple requests per session:
Request 1: Load → Execute → SaveRequest 2: Load → Execute → SaveProblem: Request 2 may overwrite Request 1’s changes.
Solution: Optimistic Locking
Section titled “Solution: Optimistic Locking”type Message struct { // ... fields Version int}
func (s *StateStore) Save(sessionID string, messages []Message, expectedVersion int) error { currentVersion := s.getVersion(sessionID) if currentVersion != expectedVersion { return ErrVersionMismatch }
s.saveWithVersion(sessionID, messages, currentVersion+1) return nil}Retry on version mismatch.
Eventual Consistency
Section titled “Eventual Consistency”With Redis:
- Single-instance Redis: Strongly consistent
- Redis Cluster: Eventually consistent
Impact: Rare edge cases where history may be stale. Acceptable for most applications.
Testing State Management
Section titled “Testing State Management”In-Memory for Tests
Section titled “In-Memory for Tests”Use in-memory store for fast tests:
func TestStateManagement(t *testing.T) { store := statestore.NewInMemoryStateStore()
// Test save messages := []Message err := store.Save("session-1", messages) assert.NoError(t, err)
// Test load loaded, err := store.Load("session-1") assert.NoError(t, err) assert.Equal(t, messages, loaded)}Mock Store
Section titled “Mock Store”For testing middleware:
type MockStateStore struct { messages map[string][]Message}
func (m *MockStateStore) Load(sessionID string) ([]Message, error) { return m.messages[sessionID], nil}
func (m *MockStateStore) Save(sessionID string, messages []Message) error { m.messages[sessionID] = messages return nil}Common Patterns
Section titled “Common Patterns”User Sessions
Section titled “User Sessions”One session per user:
sessionID := fmt.Sprintf("user-%s", userID)result, err := pipeline.ExecuteWithSession(ctx, sessionID, "user", "Hello")Use case: Single ongoing conversation per user
Conversation Sessions
Section titled “Conversation Sessions”Multiple conversations per user:
sessionID := fmt.Sprintf("user-%s-conv-%s", userID, conversationID)result, err := pipeline.ExecuteWithSession(ctx, sessionID, "user", "Hello")Use case: User can start multiple conversations
Temporary Sessions
Section titled “Temporary Sessions”Anonymous sessions:
sessionID := uuid.New().String()result, err := pipeline.ExecuteWithSession(ctx, sessionID, "user", "Hello")Use case: Guest users, temporary chats
Cleanup on Logout
Section titled “Cleanup on Logout”Delete user sessions:
func handleLogout(userID string) { sessionID := fmt.Sprintf("user-%s", userID) store.Delete(sessionID)}Production Considerations
Section titled “Production Considerations”Monitoring
Section titled “Monitoring”Track state metrics:
- Load latency: Time to load history
- Save latency: Time to save messages
- History size: Bytes per session
- Active sessions: Number of sessions
Error Handling
Section titled “Error Handling”Handle store failures gracefully:
messages, err := store.Load(sessionID)if err != nil { log.Printf("Failed to load state: %v", err) // Continue with empty history messages = []Message{}}Backup and Recovery
Section titled “Backup and Recovery”Redis persistence:
- RDB: Periodic snapshots
- AOF: Append-only file
Enable both for best durability.
Privacy Compliance
Section titled “Privacy Compliance”- Data deletion: Implement Delete()
- Data export: Allow users to export history
- Encryption: Encrypt messages at rest
- TTL: Automatically delete old data
Example: Complete Setup
Section titled “Example: Complete Setup”Development (In-Memory)
Section titled “Development (In-Memory)”store := statestore.NewInMemoryStateStore()
stateMiddleware := middleware.StateMiddleware(store, &middleware.StateMiddlewareConfig{ MaxMessages: 10, TTL: 1 * time.Hour,})
pipeline := pipeline.NewPipeline( stateMiddleware, // ... other middleware)Production (Redis)
Section titled “Production (Redis)”redisClient := redis.NewClient(&redis.Options{ Addr: "redis:6379", Password: os.Getenv("REDIS_PASSWORD"), DB: 0, MaxRetries: 3, PoolSize: 10,})
store := statestore.NewRedisStateStore(redisClient)
stateMiddleware := middleware.StateMiddleware(store, &middleware.StateMiddlewareConfig{ MaxMessages: 20, TTL: 24 * time.Hour,})
pipeline := pipeline.NewPipeline( stateMiddleware, // ... other middleware)Summary
Section titled “Summary”State management provides:
✅ Multi-turn conversations: Maintain context across requests
✅ Multi-user support: Isolated sessions per user
✅ Scalability: Redis for distributed deployments
✅ Performance: Configurable history limits
✅ Flexibility: Pluggable storage backends
Related Topics
Section titled “Related Topics”- Pipeline Architecture - How state fits in pipelines
- Middleware Design - StateMiddleware implementation
- StateStore Reference - Complete API
- Multi-turn Conversations Tutorial - Hands-on guide
Further Reading
Section titled “Further Reading”- Redis persistence strategies
- Session management patterns
- Distributed state management
- CAP theorem and consistency