langchain-hs-core-0.0.6.0: src/Langchain/Memory/Session.hs
{-# LANGUAGE RecordWildCards #-}
{- |
Module : Langchain.Memory.Session
Description : Multi-session conversation memory management with TTL eviction
Copyright : (c) 2025-2026 Tushar Adhatrao
License : MIT
Maintainer : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability : experimental
Thread-safe multi-session conversation store.
Provides per-session memory isolation for web servers, chatbots, and multi-tenant agents
with configurable Time-To-Live (TTL) expiration to avoid unbounded memory leaks.
-}
module Langchain.Memory.Session
( SessionId
, SessionMemoryStore (..)
, newSessionMemoryStore
, newSessionMemoryStoreWithTTL
, getSessionMemory
, listSessions
, hasSession
, deleteSession
, clearAllSessions
, sessionCount
, cleanupExpiredSessions
, exportAllSessions
) where
import Control.Concurrent.STM
( TVar
, atomically
, modifyTVar'
, newTVarIO
, readTVar
, readTVarIO
, writeTVar
)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import Data.Time (NominalDiffTime, UTCTime, diffUTCTime, getCurrentTime)
import Langchain.Model (Message)
-- | Unique identifier for a conversation session (e.g. user ID, session token, or UUID)
type SessionId = Text
-- | Thread-safe multi-session memory repository
data SessionMemoryStore mem = SessionMemoryStore
{ storeVar :: !(TVar (Map SessionId (mem, UTCTime)))
, memFactory :: !(SessionId -> IO mem)
, sessionTTL :: !(Maybe NominalDiffTime)
}
-- | Create a new SessionMemoryStore with no TTL (sessions persist until explicitly deleted)
newSessionMemoryStore ::
MonadIO m =>
-- | Factory creating a fresh memory instance for a given SessionId
(SessionId -> IO mem) ->
m (SessionMemoryStore mem)
newSessionMemoryStore factory = liftIO $ do
var <- newTVarIO Map.empty
pure $ SessionMemoryStore var factory Nothing
-- | Create a new SessionMemoryStore with automatic Time-To-Live expiration
newSessionMemoryStoreWithTTL ::
MonadIO m =>
-- | Maximum idle time before session is considered expired
NominalDiffTime ->
-- | Factory creating a fresh memory instance for a given SessionId
(SessionId -> IO mem) ->
m (SessionMemoryStore mem)
newSessionMemoryStoreWithTTL ttl factory = liftIO $ do
var <- newTVarIO Map.empty
pure $ SessionMemoryStore var factory (Just ttl)
-- | Retrieve (or create) the memory instance for a specific session, updating its last access timestamp
getSessionMemory ::
MonadIO m =>
SessionMemoryStore mem ->
SessionId ->
m mem
getSessionMemory SessionMemoryStore {..} sId = liftIO $ do
now <- getCurrentTime
mExisting <- atomically $ do
m <- readTVar storeVar
case Map.lookup sId m of
Just (mem, lastAccess) ->
case sessionTTL of
Just ttl | diffUTCTime now lastAccess > ttl -> pure Nothing
_ -> do
modifyTVar' storeVar (Map.insert sId (mem, now))
pure (Just mem)
Nothing -> pure Nothing
case mExisting of
Just mem -> pure mem
Nothing -> do
newMem <- memFactory sId
atomically $ modifyTVar' storeVar (Map.insert sId (newMem, now))
pure newMem
-- | List all active session IDs
listSessions :: MonadIO m => SessionMemoryStore mem -> m [SessionId]
listSessions SessionMemoryStore {..} = liftIO $ do
m <- readTVarIO storeVar
pure $ Map.keys m
-- | Check if a session exists and is not expired
hasSession :: MonadIO m => SessionMemoryStore mem -> SessionId -> m Bool
hasSession SessionMemoryStore {..} sId = liftIO $ do
now <- getCurrentTime
m <- readTVarIO storeVar
pure $ case Map.lookup sId m of
Just (_, lastAccess) ->
case sessionTTL of
Just ttl -> diffUTCTime now lastAccess <= ttl
Nothing -> True
Nothing -> False
-- | Delete a specific session from the store
deleteSession :: MonadIO m => SessionMemoryStore mem -> SessionId -> m ()
deleteSession SessionMemoryStore {..} sId = liftIO $ atomically $ do
modifyTVar' storeVar (Map.delete sId)
-- | Remove all sessions from the store
clearAllSessions :: MonadIO m => SessionMemoryStore mem -> m ()
clearAllSessions SessionMemoryStore {..} = liftIO $ atomically $ do
writeTVar storeVar Map.empty
-- | Count active sessions
sessionCount :: MonadIO m => SessionMemoryStore mem -> m Int
sessionCount SessionMemoryStore {..} = liftIO $ do
m <- readTVarIO storeVar
pure $ Map.size m
-- | Purge all sessions that have exceeded the TTL limit. Returns the number of evicted sessions.
cleanupExpiredSessions :: MonadIO m => SessionMemoryStore mem -> m Int
cleanupExpiredSessions SessionMemoryStore {..} = liftIO $ case sessionTTL of
Nothing -> pure 0
Just ttl -> do
now <- getCurrentTime
atomically $ do
m <- readTVar storeVar
let (active, expired) = Map.partition (\(_, lastAccess) -> diffUTCTime now lastAccess <= ttl) m
writeTVar storeVar active
pure (Map.size expired)
-- | Export all sessions' message histories
exportAllSessions ::
MonadIO m =>
(mem -> m [Message]) ->
SessionMemoryStore mem ->
m (Map SessionId [Message])
exportAllSessions getMsgs SessionMemoryStore {..} = do
sMap <- liftIO $ readTVarIO storeVar
Map.traverseWithKey (\_ (mem, _) -> getMsgs mem) sMap