State Management

Understanding conversation state and persistence in PromptKit.

What is State Management?

State management maintains conversation history across multiple turns. It allows LLMs to remember previous interactions.

Why Manage State?

Context: LLMs need history to understand conversations
Continuity: Users expect the AI to remember
Multi-turn: Enable back-and-forth dialogue
Personalization: Remember user preferences

The Problem

LLMs are stateless:

// First message
response1 := llm.Complete("What's the capital of France?")
// "Paris"

// Second message - no memory!
response2 := llm.Complete("What about Germany?")
// "What do you mean 'what about Germany'?"

The Solution

Pass conversation history:

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)
// "The capital of Germany is Berlin"

State in PromptKit

Session-Based Architecture

Each conversation has a session ID:

sessionID := "user-123"
result, _ := pipeline.ExecuteWithSession(ctx, sessionID, "user", "Hello")

Sessions enable:

StateMiddleware

Manages state automatically:

store := statestore.NewRedisStateStore(redisClient)

stateMiddleware := middleware.StateMiddleware(store, &middleware.StateMiddlewareConfig{
    MaxMessages: 10,
    TTL:         24 * time.Hour,
})

pipe := pipeline.NewPipeline(
    stateMiddleware,  // Loads history before, saves after
    middleware.ProviderMiddleware(provider, nil, nil, nil),
)

Before execution: Loads history
After execution: Saves new messages

State Stores

In-Memory Store

Fast, but not persistent:

store := statestore.NewInMemoryStateStore()

Pros: Very fast (~1-10µs)
Cons: Lost on restart, single-instance only
Use for: Development, testing, demos

Redis Store

Persistent and scalable:

redisClient := redis.NewClient(&redis.Options{
    Addr: "localhost:6379",
})
store := statestore.NewRedisStateStore(redisClient)

Pros: Persistent, multi-instance, TTL support
Cons: Slower (~1-5ms), requires Redis
Use for: Production, distributed systems

Configuration Options

Message Limits

Control history size:

config := &middleware.StateMiddlewareConfig{
    MaxMessages: 20,  // Keep last 20 messages
}

Benefits:

Time-To-Live (TTL)

Auto-delete old sessions:

config := &middleware.StateMiddlewareConfig{
    TTL: 24 * time.Hour,  // Delete after 24h
}

Benefits:

Session Patterns

User Sessions

One session per user:

sessionID := fmt.Sprintf("user-%s", userID)

Use case: Single ongoing conversation per user

Conversation Sessions

Multiple conversations per user:

sessionID := fmt.Sprintf("user-%s-conv-%s", userID, conversationID)

Use case: User can start multiple conversations

Temporary Sessions

Anonymous sessions:

sessionID := uuid.New().String()

Use case: Guest users, no account required

Best Practices

Do’s

Use Redis in production

// Production
store := statestore.NewRedisStateStore(redisClient)

// Development
store := statestore.NewInMemoryStateStore()

Set appropriate limits

// Balance context vs cost
MaxMessages: 10-20  // Good for most cases

Set TTL for privacy

TTL: 24 * time.Hour  // Delete old conversations

Handle errors gracefully

messages, err := store.Load(sessionID)
if err != nil {
    // Continue with empty history
    messages = []Message{}
}

Don’ts

Don’t store infinite history - Cost and performance
Don’t use in-memory in production - Not persistent
Don’t forget to clean up - Privacy and storage
Don’t ignore errors - Handle store failures

Multi-Instance Scaling

With Redis

User A → [Instance 1] ↘
                        [Redis Store]
User B → [Instance 2] ↗

Benefits:

Session Affinity

Route user to same instance:

User (session-123) → Instance 1  (every time)
User (session-456) → Instance 2  (every time)

Benefits:

Performance Optimization

Limit History Size

// Fast: Load 10 messages
MaxMessages: 10

// Slow: Load all messages
MaxMessages: -1  // Unlimited

Lazy Loading

Load on demand:

// Only load when needed
if requiresHistory {
    messages, _ := store.Load(sessionID)
}

Compression

For large histories:

compressed := gzip.Compress(messages)
store.Save(sessionID, compressed)

Monitoring State

Track Metrics

type StateMetrics struct {
    ActiveSessions   int
    AvgHistorySize   int
    LoadLatency      time.Duration
    StorageUsed      int64
}

Set Alerts

if metrics.ActiveSessions > 10000 {
    alert.Send("High active session count")
}

if metrics.LoadLatency > 100*time.Millisecond {
    alert.Send("Slow state loading")
}

Testing State Management

Unit Tests

func TestStateStore(t *testing.T) {
    store := statestore.NewInMemoryStateStore()
    
    // Save messages
    messages := []types.Message{
        {Role: "user", Content: "Hello"},
    }
    err := store.Save("session-1", messages)
    assert.NoError(t, err)
    
    // Load messages
    loaded, err := store.Load("session-1")
    assert.NoError(t, err)
    assert.Equal(t, messages, loaded)
}

Integration Tests

func TestConversationFlow(t *testing.T) {
    pipe := createPipelineWithState()
    sessionID := "test-session"
    
    // First message
    result1, _ := pipe.ExecuteWithSession(ctx, sessionID, "user", "My name is Alice")
    
    // Second message - should remember
    result2, _ := pipe.ExecuteWithSession(ctx, sessionID, "user", "What's my name?")
    
    assert.Contains(t, result2.Response.Content, "Alice")
}

Summary

State management provides:

Context - LLMs remember conversations
Continuity - Multi-turn dialogue
Scalability - Redis for distributed systems
Performance - Configurable limits
Privacy - TTL-based cleanup