Skip to content

State Management

Understanding conversation state and persistence in PromptKit.

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

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

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'?"

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"

Each conversation has a session ID:

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

Sessions enable:

  • Multi-user support: Separate conversations
  • History isolation: Users don’t see each other’s messages
  • Concurrent access: Multiple requests per session

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

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

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

Control history size:

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

Benefits:

  • Lower costs (fewer tokens)
  • Faster loading
  • More relevant context

Auto-delete old sessions:

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

Benefits:

  • Automatic cleanup
  • Privacy compliance
  • Cost reduction

One session per user:

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

Use case: Single ongoing conversation per user

Multiple conversations per user:

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

Use case: User can start multiple conversations

Anonymous sessions:

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

Use case: Guest users, no account required

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’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

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

Benefits:

  • High availability
  • Horizontal scaling
  • Shared state

Route user to same instance:

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

Benefits:

  • Local caching
  • Reduced Redis load
  • Lower latency
// Fast: Load 10 messages
MaxMessages: 10
// Slow: Load all messages
MaxMessages: -1 // Unlimited

Load on demand:

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

For large histories:

compressed := gzip.Compress(messages)
store.Save(sessionID, compressed)
type StateMetrics struct {
ActiveSessions int
AvgHistorySize int
LoadLatency time.Duration
StorageUsed int64
}
if metrics.ActiveSessions > 10000 {
alert.Send("High active session count")
}
if metrics.LoadLatency > 100*time.Millisecond {
alert.Send("Slow state loading")
}
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)
}
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")
}

State management provides:

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