Sam
08/13/2025, 2:16 PMgetCheckpoints()
and getLatestCheckpoint()
should really only be scoped to a per-user basis, which means if my service gets 100 concurrent requests, there'll be 100 instances of my storage provider object stored in memory. Can't be very scalable. What's the best practice around this?Sam
08/13/2025, 2:18 PMgetCheckpoints(userId: String)
, but doesn't seem as straight forward with the way the PersistencyStorageProvider
has been designedMark Tkachenko
08/14/2025, 10:03 AMuser
entity inside the agent.
So, it’s kinda hard to introduce such discriminator and stay general-purpose 🙂
So, what’s your scenario? You create the agent per user request? Because that’s the only case when persistent provider will be created.
And do you use continues persistency? Because it’s also possible to create and save snapshot manually and also get snapshot by idSam
08/14/2025, 10:49 AMPersistencyStorageProvider
interface should have functions that accept only a userId
, but I think it might be good design for people to decide which filtering metrics they want so it stays generic. So something like this:
public interface PersistencyStorageProvider {
public suspend fun getCheckpoints(filters: Map<String, Any>? = null): List<AgentCheckpointData>
public suspend fun saveCheckpoint(agentCheckpointData: AgentCheckpointData, filters: Map<String, Any>? = null)
public suspend fun getLatestCheckpoint(filters: Map<String, Any>? = null): AgentCheckpointData?
}
class ExampleUsage(private val persistencyStorageProvider: PersistencyStorageProvider) {
suspend fun demonstrateUsage() {
// Example of calling getCheckpoints with filters
val filtersForGet = mapOf(
"date" to "2023-10-01",
"status" to "completed",
"priority" to 1
)
val checkpoints = persistencyStorageProvider.getCheckpoints(filtersForGet)
println("Filtered Checkpoints: $checkpoints")
// Example of calling saveCheckpoint with filters
val agentCheckpointData = AgentCheckpointData(/* initialization */)
val filtersForSave = mapOf("overwrite" to true)
persistencyStorageProvider.saveCheckpoint(agentCheckpointData, filtersForSave)
println("Checkpoint saved with filters: $filtersForSave")
// Example of calling getLatestCheckpoint with filters
val filtersForLatest = mapOf("agentId" to "12345")
val latestCheckpoint = persistencyStorageProvider.getLatestCheckpoint(filtersForLatest)
println("Latest Checkpoint: $latestCheckpoint")
}
}
Sam
08/14/2025, 10:51 AMPersistencyStorageProvider
and pass this filtering metric into the class constructor so the functions can use it, which isn't the bestSam
08/14/2025, 10:54 AMMap<String, Any>
, it's just Any
or some other generic typeSam
08/14/2025, 11:02 AMPersistencyStorageProvider
can be singletonsVadim Briliantov
08/14/2025, 2:59 PMVadim Briliantov
08/14/2025, 3:02 PMVadim Briliantov
08/14/2025, 3:05 PMVadim Briliantov
08/14/2025, 3:08 PMSam
08/15/2025, 9:07 AMAIAgent
instance shared across all users? If so, that doesn't make sense because each AIAgent
takes in an AIAgentConfig
that provides a prompt, which will be different on a user-by-user basis, so it can't be sharedSam
08/15/2025, 9:11 AMVadim Briliantov
08/15/2025, 10:42 AMSam
08/15/2025, 2:05 PMPersistencyStorageProvider
? For example, to implement my own implementation of this class, I have to override the getCheckpoints()
function, which is used by the Persistency
feature here:
/**
* Retrieves a specific checkpoint by ID for the specified agent.
*
* @param checkpointId The ID of the checkpoint to retrieve
* @return The checkpoint data with the specified ID, or null if not found
*/
public suspend fun getCheckpointById(checkpointId: String): AgentCheckpointData? =
persistencyStorageProvider.getCheckpoints().firstOrNull { it.checkpointId == checkpointId }
That means my implementation of the persistencyStorageProvider.getCheckpoints()
function needs to return a list of checkpoints narrowed to a certain scope, such as a single user. It can't return all of the checkpoints for all users, because then the .firstOrNull
is filtering through checkpoints we already know aren't relevant because they belong to users that aren't the one we care about.
Same thing with the getLatestCheckpoint()
function I will have to override, which is used here in the Persistency
feature:
/**
* Retrieves the latest checkpoint for the specified agent.
*
* @return The latest checkpoint data, or null if no checkpoint exists
*/
public suspend fun getLatestCheckpoint(): AgentCheckpointData? =
persistencyStorageProvider.getLatestCheckpoint()
My implementation of persistencyStorageProvider.getLatestCheckpoint()
NEEDS to be narrowed to the scope of a single user, because otherwise it'll more often than not return a checkpoint that doesn't belong to the user I want at all.
I'm just trying to understand what you would recommend hereSam
08/15/2025, 2:07 PMPersistencyStorageProvider
, in my opinion, should be a singleton, as it's essentially just a repository class. Currently with the way it's designed, it can't be if I need to filter the checkpoints on some metric, as that metric will need to be passed into the constructor of my PersistencyStorageProvider
implementation class, because functions I need to override don't accept any arguments...Sam
08/15/2025, 2:08 PMMark Tkachenko
08/15/2025, 2:21 PMSam
08/15/2025, 2:21 PMSam
08/15/2025, 2:22 PMMark Tkachenko
08/15/2025, 2:22 PMSam
08/15/2025, 2:23 PMSam
08/15/2025, 2:23 PMSam
08/15/2025, 2:23 PMSam
08/15/2025, 2:23 PMSam
08/15/2025, 2:24 PMSam
08/15/2025, 2:25 PMSam
08/15/2025, 2:25 PMSam
08/15/2025, 2:42 PMpackage ai.koog.agents.snapshot.providers
import ai.koog.agents.snapshot.feature.AgentCheckpointData
import com.hazelcast.core.HazelcastInstance
import com.hazelcast.map.IMap
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
class HazelcastPersistencyStorageProvider @Inject constructor(
private val hazelcastInstance: HazelcastInstance
) : PersistencyStorageProvider {
private val mutex = Mutex()
private val userCheckpoints: IMap<String, MutableList<AgentCheckpointData>> =
hazelcastInstance.getMap("userCheckpoints")
// Simulate user context with a thread-local variable
private val currentUser = ThreadLocal<String>()
fun setCurrentUser(userId: String) {
currentUser.set(userId)
}
override suspend fun getCheckpoints(): List<AgentCheckpointData> {
val userId = currentUser.get() ?: throw IllegalStateException("User ID not set")
return mutex.withLock {
userCheckpoints[userId]?.toList() ?: emptyList()
}
}
override suspend fun saveCheckpoint(agentCheckpointData: AgentCheckpointData) {
val userId = currentUser.get() ?: throw IllegalStateException("User ID not set")
mutex.withLock {
val checkpoints = userCheckpoints.computeIfAbsent(userId) { mutableListOf() }
checkpoints.add(agentCheckpointData)
userCheckpoints[userId] = checkpoints
}
}
override suspend fun getLatestCheckpoint(): AgentCheckpointData? {
val userId = currentUser.get() ?: throw IllegalStateException("User ID not set")
return mutex.withLock {
userCheckpoints[userId]?.maxByOrNull { it.createdAt }
}
}
}
Using a class-level field for user context in a concurrent environment can lead to bottlenecks, as each function call must wait for the mutex lock, causing a queue and delaying responses, especially if each call is blocking (need to call DB)Mark Tkachenko
08/15/2025, 2:43 PMhow can my PersistencyStorageProvider use an ID, while maintaining the PersistencyStorageProvider as a singletonWell, it’s not how it’s designed, because on the design stage we decided not to introduce mandatory id for checkpoint/group of checkpoints, leaving it to implementation to decide.
Sam
08/15/2025, 2:45 PMpackage ai.koog.agents.snapshot.providers
import ai.koog.agents.snapshot.feature.AgentCheckpointData
public interface PersistencyStorageProvider {
public suspend fun getCheckpoints(filter: Map<String, Any> = emptyMap()): List<AgentCheckpointData>
public suspend fun saveCheckpoint(agentCheckpointData: AgentCheckpointData)
public suspend fun getLatestCheckpoint(filter: Map<String, Any> = emptyMap()): AgentCheckpointData?
}
Sam
08/15/2025, 2:45 PMSam
08/15/2025, 2:45 PMMark Tkachenko
08/15/2025, 2:46 PMSam
08/15/2025, 2:46 PMSam
08/15/2025, 2:46 PMSam
08/15/2025, 2:49 PMMark Tkachenko
08/15/2025, 2:50 PMSam
08/15/2025, 2:56 PMSam
08/15/2025, 2:56 PMSam
08/15/2025, 3:01 PMMark Tkachenko
08/15/2025, 3:01 PMMark Tkachenko
08/15/2025, 3:01 PMSam
08/15/2025, 3:06 PMpublic interface PersistencyStorageProvider<F> {
public suspend fun getCheckpoints(filter: F): List<AgentCheckpointData>
public suspend fun saveCheckpoint(agentCheckpointData: AgentCheckpointData, filter: F)
public suspend fun getLatestCheckpoint(filter: F): AgentCheckpointData?
}
Then we can have:
/**
* In-memory implementation of [PersistencyStorageProvider].
* This provider stores snapshots in a mutable map.
*/
public class InMemoryPersistencyStorageProvider(private val persistenceId: String) : PersistencyStorageProvider<CheckpointFilter> {
private val mutex = Mutex()
private val snapshotMap = mutableMapOf<String, List<AgentCheckpointData>>()
override suspend fun getCheckpoints(filter: CheckpointFilter): List<AgentCheckpointData> {
....
}
override suspend fun saveCheckpoint(agentCheckpointData: AgentCheckpointData, filter: CheckpointFilter) {
....
}
override suspend fun getLatestCheckpoint(filter: CheckpointFilter): AgentCheckpointData? {
....
}
}
Sam
08/15/2025, 3:06 PMSam
08/15/2025, 3:09 PMSam
08/15/2025, 3:10 PM