This commit is contained in:
2025-12-06 14:53:20 +01:00
parent 4864acda1d
commit 5c8d39d7df
9 changed files with 819 additions and 3 deletions

2
.gitignore vendored
View File

@@ -38,3 +38,5 @@ build/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
/.idea/ /.idea/
/agent-persistence-test/
/a2a-messages/

31
pom.xml
View File

@@ -16,6 +16,7 @@
<properties> <properties>
<java.version>21</java.version> <java.version>21</java.version>
<kotlin.version>2.2.10</kotlin.version> <kotlin.version>2.2.10</kotlin.version>
<koog.version>0.5.3</koog.version>
</properties> </properties>
<repositories> <repositories>
@@ -50,6 +51,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId> <artifactId>spring-boot-starter</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlin</groupId> <groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId> <artifactId>kotlin-reflect</artifactId>
@@ -63,17 +68,37 @@
<dependency> <dependency>
<groupId>ai.koog</groupId> <groupId>ai.koog</groupId>
<artifactId>koog-agents-jvm</artifactId> <artifactId>koog-agents-jvm</artifactId>
<version>0.5.0</version> <version>${koog.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ai.koog</groupId> <groupId>ai.koog</groupId>
<artifactId>prompt-cache-redis-jvm</artifactId> <artifactId>prompt-cache-redis-jvm</artifactId>
<version>0.5.0</version> <version>${koog.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ai.koog</groupId> <groupId>ai.koog</groupId>
<artifactId>prompt-executor-model-jvm</artifactId> <artifactId>prompt-executor-model-jvm</artifactId>
<version>0.5.0</version> <version>${koog.version}</version>
</dependency>
<dependency>
<groupId>ai.koog</groupId>
<artifactId>agents-features-a2a-client-jvm</artifactId>
<version>${koog.version}</version>
</dependency>
<dependency>
<groupId>ai.koog</groupId>
<artifactId>agents-features-a2a-server-jvm</artifactId>
<version>${koog.version}</version>
</dependency>
<dependency>
<groupId>ai.koog</groupId>
<artifactId>a2a-transport-server-jsonrpc-http-jvm</artifactId>
<version>${koog.version}</version>
</dependency>
<dependency>
<groupId>ai.koog</groupId>
<artifactId>a2a-transport-client-jsonrpc-http-jvm</artifactId>
<version>${koog.version}</version>
</dependency> </dependency>
<!-- Utilities --> <!-- Utilities -->

View File

@@ -0,0 +1,143 @@
package nl.trivion.softwarefabric.agents
import ai.koog.agents.core.agent.AIAgent
import ai.koog.agents.core.agent.config.AIAgentConfig
import ai.koog.agents.core.agent.singleRunStrategy
import ai.koog.agents.core.dsl.builder.strategy
import ai.koog.agents.core.tools.ToolRegistry
import ai.koog.agents.ext.tool.AskUser
import ai.koog.agents.ext.tool.SayToUser
import ai.koog.agents.snapshot.feature.Persistence
import ai.koog.agents.snapshot.providers.file.JVMFilePersistenceStorageProvider
import ai.koog.prompt.dsl.prompt
import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor
import ai.koog.prompt.llm.LLMCapability
import ai.koog.prompt.llm.LLMProvider
import ai.koog.prompt.llm.LLModel
import ai.koog.prompt.llm.OllamaModels
import kotlinx.coroutines.runBlocking
import java.nio.file.Files
import kotlin.io.path.exists
import kotlin.uuid.ExperimentalUuidApi
/**
* This example demonstrates how to use the file-based checkpoint provider with a persistent agent.
*
* The JVMFileAgentCheckpointStorageProvider stores agent checkpoints in a file system,
* allowing the agent's state to persist across multiple runs.
*
* This example shows:
* 1. How to create a file-based checkpoint provider
* 2. How to configure an agent with the file-based checkpoint provider
* 3. How to run an agent that automatically creates checkpoints
* 4. How to restore an agent from a checkpoint
*/
@OptIn(ExperimentalUuidApi::class)
fun main() = runBlocking {
// Create a temporary directory for storing checkpoints
val checkpointDir = Files.createTempDirectory("agent-checkpoints")
println("Checkpoint directory: $checkpointDir")
// Create the file-based checkpoint provider
val provider = JVMFilePersistenceStorageProvider(checkpointDir)
// Create a unique agent ID to identify this agent's checkpoints
val agentId = "persistent-agent-example3"
// Create tool registry with basic tools
val toolRegistry = ToolRegistry {
tool(AskUser)
tool(SayToUser)
}
val customModel: LLModel = LLModel(
provider = LLMProvider.Ollama,
id = "gpt-oss:20b",
capabilities = listOf(
LLMCapability.Temperature
),
contextLength = 2048
)
// Create agent config with a system prompt
val agentConfig = AIAgentConfig(
prompt = prompt("persistent-agent") {
system("You are a helpful assistant that remembers conversations across sessions.")
},
model = customModel,
maxAgentIterations = 20
)
println("Creating and running agent with continuous persistence...")
simpleOllamaAIExecutor().use { executor ->
// Create the agent with the file-based checkpoint provider and continuous persistence
val agent = AIAgent(
promptExecutor = executor,
// strategy = SnapshotStrategy.strategy,
strategy = singleRunStrategy(),
agentConfig = agentConfig,
toolRegistry = toolRegistry,
id = agentId
) {
install(Persistence) {
storage = provider // Use the file-based checkpoint provider
enableAutomaticPersistence = true // Enable automatic checkpoint creation
}
}
// Run the agent with an initial input
val result = agent.run("Hallo ik heet Erik")
println("Agent result: $result")
// Retrieve all checkpoints created during the agent's execution
val checkpoints = provider.getCheckpoints(agentId)
println("\nRetrieved ${checkpoints.size} checkpoints for agent $agentId")
// Print checkpoint details
checkpoints.forEachIndexed { index, checkpoint ->
println("Checkpoint ${index + 1}:")
println(" ID: ${checkpoint.checkpointId}")
println(" Created at: ${checkpoint.createdAt}")
println(" Node ID: ${checkpoint.nodeId}")
println(" Message history size: ${checkpoint.messageHistory.size}")
}
// Verify that the checkpoint files exist in the file system
val checkpointsDir = checkpointDir.resolve("checkpoints").resolve(agentId)
if (checkpointsDir.exists()) {
println("\nCheckpoint files in directory: ${checkpointsDir.toFile().list()?.joinToString()}")
}
println("\nNow creating a new agent instance with the same ID to demonstrate restoration...")
// Create a new agent instance with the same ID
// It will automatically restore from the latest checkpoint
val restoredAgent = AIAgent(
promptExecutor = executor,
// strategy = SnapshotStrategy.strategy,
strategy = singleRunStrategy(),
agentConfig = agentConfig,
toolRegistry = toolRegistry,
id = agentId
) {
install(Persistence) {
storage = provider // Use the file-based checkpoint provider
enableAutomaticPersistence = true // Enable automatic checkpoint creation
}
}
// Run the restored agent with a new input
// The agent will continue the conversation from where it left off
val restoredResult = restoredAgent.run("Hoe heet ik?")
println("Restored agent result: $restoredResult")
// Get the latest checkpoint after the second run
val latestCheckpoint = provider.getLatestCheckpoint(agentId)
println("\nLatest checkpoint after restoration:")
println(" ID: ${latestCheckpoint?.checkpointId}")
println(" Created at: ${latestCheckpoint?.createdAt}")
println(" Node ID: ${latestCheckpoint?.nodeId}")
println(" Message history size: ${latestCheckpoint?.messageHistory?.size}")
}
}

View File

@@ -0,0 +1,188 @@
package nl.trivion.softwarefabric.agents
import ai.koog.agents.core.agent.AIAgent
import ai.koog.agents.core.agent.config.AIAgentConfig
import ai.koog.agents.core.dsl.builder.forwardTo
import ai.koog.agents.core.dsl.builder.strategy
import ai.koog.agents.core.dsl.extension.nodeLLMRequest
import ai.koog.agents.features.eventHandler.feature.EventHandler
import ai.koog.agents.snapshot.feature.Persistence
import ai.koog.agents.snapshot.providers.InMemoryPersistenceStorageProvider
import ai.koog.agents.snapshot.providers.PersistenceStorageProvider
import ai.koog.agents.snapshot.providers.file.JVMFilePersistenceStorageProvider
import ai.koog.prompt.dsl.Prompt
import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor
import ai.koog.prompt.llm.LLMCapability
import ai.koog.prompt.llm.LLMProvider
import ai.koog.prompt.llm.LLModel
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import java.io.File
class AgentTester {
companion object {
private val log = LoggerFactory.getLogger(AgentTester::class.java)
@JvmStatic
fun main(args: Array<String>) {
println("=== Simple Agent Persistence Test ===")
println()
// Parse command line arguments
val issueNumber = args.getOrNull(0)?.toIntOrNull() ?: 1
val message = args.getOrNull(1) ?: "Ik heet Erik."
println("Testing with:")
println("- Issue number: $issueNumber")
println("- Message: $message")
println()
val tester = AgentTester()
runBlocking {
tester.testAgent(issueNumber, message)
}
}
}
private val promptExecutor = simpleOllamaAIExecutor("http://localhost:11434")
private val customModel: LLModel = LLModel(
provider = LLMProvider.Ollama,
id = "gpt-oss:20b",
capabilities = listOf(
LLMCapability.Temperature
),
contextLength = 2048
)
private val systemPrompt = """
You are a helpful assistant. Keep your responses concise and friendly.
""".trimIndent()
private val agentConfig = AIAgentConfig(
prompt = Prompt.build("simple-chat") {
system(systemPrompt)
},
model = customModel,
maxAgentIterations = 5
)
private fun createAgent(agentId: String, provider: PersistenceStorageProvider<*>): AIAgent<String, String> {
return AIAgent(
promptExecutor = promptExecutor,
agentConfig = agentConfig,
id = agentId,
installFeatures = {
install(EventHandler) {
onAgentCompleted { result ->
log.info("✓ Agent completed")
log.info(" Result: ${result.result}")
}
}
install(Persistence) {
storage = provider
enableAutomaticPersistence = true
log.info("✓ Persistence installed with automatic persistence enabled")
}
}
)
}
suspend fun testAgent(issueNumber: Int, message: String) {
val agentId = "developer-agent-issue-$issueNumber"
println("Creating agent with ID: $agentId")
println("-".repeat(50))
try {
val persistenceDir = File("./agent-persistence-test")
if (!persistenceDir.exists()) {
persistenceDir.mkdirs()
log.info("Created persistence directory: ${persistenceDir.absolutePath}")
}
// val provider = JVMFilePersistenceStorageProvider(persistenceDir.toPath())
val provider = InMemoryPersistenceStorageProvider()
log.info("Creating agent with ID: $agentId")
log.info("Persistence directory: ${persistenceDir.absolutePath}")
var agent = createAgent(agentId, provider)
println()
println("Running agent with message: '$message'")
println("-".repeat(50))
var result = agent.run(message)
println()
println("=== RESULT ===")
println(result)
println()
agent = createAgent(agentId, provider)
result = agent.run("Hoe heet ik?")
agent.close()
println()
println("=== RESULT ===")
println(result)
println()
// Wacht even zodat persistence kan schrijven
kotlinx.coroutines.delay(1000)
// Check wat er is opgeslagen
val agentDir = File(persistenceDir, "checkpoints/" + agentId)
println("=== PERSISTENCE CHECK ===")
if (agentDir.exists()) {
println("Snapshot directory exists: ${agentDir.absolutePath}")
agentDir.listFiles()?.forEach {
println(" - ${it.name} (${it.length()} bytes, modified: ${java.util.Date(it.lastModified())})")
}
} else {
println("WARNING: No snapshot directory found!")
}
println()
println("=== TEST COMPLETED ===")
println("To continue this conversation, run:")
println(" ./gradlew run --args=\"$issueNumber 'your next message'\"")
} catch (e: Exception) {
log.error("Error during test: ${e.message}", e)
e.printStackTrace()
}
}
}
/**
* Usage examples:
*
* Run with default values:
* ./gradlew run
*
* Run with specific issue number:
* ./gradlew run --args="5"
*
* Run with issue number and message:
* ./gradlew run --args="5 'Tell me a joke'"
*
* Continue conversation (reuse same issue number):
* ./gradlew run --args="5 'Tell me another one'"
*
* This simple agent has:
* - No tools
* - No custom strategy
* - Only basic chat functionality
* - Persistence enabled
*/

View File

@@ -99,6 +99,14 @@ class DesignerAgent(private val giteaService: GiteaService) {
(nodeSendToolResult forwardTo nodeExecuteTool) (nodeSendToolResult forwardTo nodeExecuteTool)
onToolCall { true } onToolCall { true }
) )
edge(
(nodeSendToolResult forwardTo nodeFinish)
onAssistantMessage {
log.info("content: ${it.content}")
it.content.startsWith("Question is send to")
}
)
} }
val systemPrompt = this::class.java.getResource("/prompts/designer-system-prompt.txt") val systemPrompt = this::class.java.getResource("/prompts/designer-system-prompt.txt")

View File

@@ -0,0 +1,53 @@
package nl.trivion.softwarefabric.agents
import ai.koog.agents.core.dsl.builder.AIAgentNodeDelegate
import ai.koog.agents.core.dsl.builder.AIAgentSubgraphBuilderBase
import ai.koog.agents.core.dsl.builder.forwardTo
import ai.koog.agents.core.dsl.builder.strategy
import ai.koog.agents.snapshot.feature.withPersistence
import kotlinx.serialization.json.JsonPrimitive
private fun AIAgentSubgraphBuilderBase<*, *>.simpleNode(
name: String? = null,
output: String,
): AIAgentNodeDelegate<String, String> = node(name) {
return@node it + output
}
private data class TeleportState(var teleported: Boolean = false)
private fun AIAgentSubgraphBuilderBase<*, *>.teleportNode(
name: String? = null,
teleportState: TeleportState = TeleportState()
): AIAgentNodeDelegate<String, String> = node(name) {
if (!teleportState.teleported) {
teleportState.teleported = true
withPersistence {
setExecutionPoint(it, "Node1", listOf(), JsonPrimitive("Teleported!!!"))
return@withPersistence "Teleported"
}
} else {
return@node "$it\nAlready teleported, passing by"
}
}
object SnapshotStrategy {
private val teleportState = TeleportState()
val strategy = strategy("test") {
val node1 by simpleNode(
"Node1",
output = "Node 1 output"
)
val node2 by simpleNode(
"Node2",
output = "Node 2 output"
)
val teleportNode by teleportNode(teleportState = teleportState)
edge(nodeStart forwardTo node1)
edge(node1 forwardTo node2)
edge(node2 forwardTo teleportNode)
edge(teleportNode forwardTo nodeFinish)
}
}

View File

@@ -0,0 +1,59 @@
package nl.trivion.softwarefabric.client
import ai.koog.a2a.client.A2AClient
import ai.koog.a2a.client.UrlAgentCardResolver
import ai.koog.a2a.consts.A2AConsts
import ai.koog.a2a.model.Message
import ai.koog.a2a.model.MessageSendConfiguration
import ai.koog.a2a.model.MessageSendParams
import ai.koog.a2a.model.Role
import ai.koog.a2a.model.TextPart
import ai.koog.a2a.transport.Request
import ai.koog.a2a.transport.client.jsonrpc.http.HttpJSONRPCClientTransport
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@OptIn(ExperimentalUuidApi::class)
suspend fun main() {
val agentUrl = "http://localhost:8080/a2a"
val cardResolver = UrlAgentCardResolver(
baseUrl = agentUrl,
path = A2AConsts.AGENT_CARD_WELL_KNOWN_PATH,
)
val transport = HttpJSONRPCClientTransport(
url = agentUrl,
)
val a2aClient = A2AClient(
transport = transport,
agentCardResolver = cardResolver
)
// Initialize client and fetch the card
println("connect")
a2aClient.connect()
println("connected")
val message = Message(
role = Role.User,
parts = listOf(
TextPart("Hoe heet ik?")
),
contextId = "blog-project-456",
messageId = Uuid.random().toString()
)
val request = Request(
data = MessageSendParams(
message = message,
configuration = MessageSendConfiguration(
blocking = false, // Get first response
historyLength = 5 // Include context
)
)
)
val response = a2aClient.sendMessage(request)
println(response.data)
}

View File

@@ -0,0 +1,166 @@
package nl.trivion.softwarefabric.controllers
import ai.koog.a2a.consts.A2AConsts
import ai.koog.a2a.model.*
import ai.koog.a2a.server.A2AServer
import ai.koog.a2a.server.agent.AgentExecutor
import ai.koog.a2a.server.session.RequestContext
import ai.koog.a2a.server.session.SessionEventProcessor
import ai.koog.a2a.transport.server.jsonrpc.http.HttpJSONRPCServerTransport
import ai.koog.agents.a2a.core.A2AMessage
import ai.koog.agents.a2a.core.toA2AMessage
import ai.koog.agents.a2a.core.toKoogMessage
import ai.koog.agents.a2a.server.feature.A2AAgentServer
import ai.koog.agents.a2a.server.feature.nodeA2ARespondMessage
import ai.koog.agents.core.agent.AIAgent
import ai.koog.agents.core.agent.config.AIAgentConfig
import ai.koog.agents.core.dsl.builder.strategy
import ai.koog.agents.features.eventHandler.feature.handleEvents
import ai.koog.prompt.dsl.Prompt
import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor
import ai.koog.prompt.llm.LLMCapability
import ai.koog.prompt.llm.LLMProvider
import ai.koog.prompt.llm.LLModel
import io.ktor.server.cio.CIO
import nl.trivion.softwarefabric.storage.FileMessageStorage
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
class A2AController2 {
@OptIn(ExperimentalUuidApi::class)
fun blogpostWritingStrategy() = strategy<MessageSendParams, A2AMessage>("blogpost-writer-strategy") {
val blogpostRequest by node<MessageSendParams, A2AMessage> { input ->
val userMessage = input.message.toKoogMessage().content
val contextId = input.message.contextId
val taskId = input.message.taskId
llm.writeSession {
appendPrompt { user(userMessage) }
val llmResponse = requestLLM()
A2AMessage(
messageId = Uuid.random().toString(),
role = Role.Agent,
parts = listOf(TextPart(llmResponse.content)),
contextId = contextId,
taskId = taskId
)
}
}
val sendMessage by nodeA2ARespondMessage()
nodeStart then blogpostRequest then sendMessage then nodeFinish
}
private val customModel: LLModel = LLModel(
provider = LLMProvider.Ollama,
id = "gpt-oss:20b",
capabilities = listOf(LLMCapability.Temperature),
contextLength = 2048
)
private val systemPrompt = """
You are a helpful assistant. Keep your responses concise and friendly.
""".trimIndent()
private val promptExecutor = simpleOllamaAIExecutor("http://localhost:11434")
suspend fun createBlogpostWritingAgent(
requestContext: RequestContext<MessageSendParams>,
eventProcessor: SessionEventProcessor
): AIAgent<MessageSendParams, A2AMessage> {
requestContext.messageStorage.save(requestContext.params.message)
// De A2AServer heeft al het inkomende bericht opgeslagen via de messageStorageFactory
// Haal de volledige geschiedenis op (inclusief het huidige bericht)
val messageHistory = requestContext.messageStorage.getAll().map { it.toKoogMessage() }
val agentConfig = AIAgentConfig(
prompt = Prompt.build("simple-chat") {
system(systemPrompt)
messages(messageHistory)
},
model = customModel,
maxAgentIterations = 5
)
return AIAgent(
promptExecutor = promptExecutor,
strategy = blogpostWritingStrategy(),
agentConfig = agentConfig
) {
install(A2AAgentServer) {
this.context = requestContext
this.eventProcessor = eventProcessor
}
handleEvents {
onAgentCompleted { ctx ->
val resultMessage = ctx.result as A2AMessage
// Sla het antwoord op via de messageStorage van de context
requestContext.messageStorage.save(resultMessage)
}
}
}
}
inner class BlogpostAgentExecutor : AgentExecutor {
override suspend fun execute(
context: RequestContext<MessageSendParams>,
eventProcessor: SessionEventProcessor
) {
createBlogpostWritingAgent(context, eventProcessor)
.run(context.params)
}
}
val agentCard = AgentCard(
name = "Blog Writer",
description = "AI agent that creates high-quality blog posts and articles",
url = "https://api.blog-writer.com/a2a/v1",
version = "1.0.0",
capabilities = AgentCapabilities(streaming = true),
defaultInputModes = listOf("text/plain"),
defaultOutputModes = listOf("text/markdown"),
skills = listOf(
AgentSkill(
id = "write-post",
name = "Blog Post Writing",
description = "Generate engaging blog posts on any topic",
tags = listOf("writing", "content", "blog"),
examples = listOf("Write a post about AI trends")
)
)
)
suspend fun start() {
// Maak een file-based message storage aan
val fileMessageStorage = FileMessageStorage(
storageDir = "./a2a-messages"
)
val a2aServer = A2AServer(
agentExecutor = BlogpostAgentExecutor(),
agentCard = agentCard,
messageStorage = fileMessageStorage
)
val transport = HttpJSONRPCServerTransport(
requestHandler = a2aServer
)
transport.start(
engineFactory = CIO,
port = 8080,
path = "/a2a",
wait = true,
agentCard = agentCard,
agentCardPath = A2AConsts.AGENT_CARD_WELL_KNOWN_PATH
)
}
}
suspend fun main() {
val controller = A2AController2()
controller.start()
}

View File

@@ -0,0 +1,172 @@
package nl.trivion.softwarefabric.storage
import ai.koog.a2a.model.Message
import ai.koog.a2a.server.messages.MessageStorage
import ai.koog.a2a.server.exceptions.MessageOperationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.SerializationException
import java.io.File
import java.nio.file.Paths
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* File-based implementation of MessageStorage for A2A server.
* Stores messages per contextId in separate JSON files.
* Thread-safe implementation using Mutex.
*
* Usage:
* ```
* val messageStorage = FileMessageStorage(storageDir = "./a2a-messages")
* val a2aServer = A2AServer(
* agentExecutor = myExecutor,
* agentCard = myCard,
* messageStorage = messageStorage
* )
* ```
*/
class FileMessageStorage(
private val storageDir: String = "./a2a-messages"
) : MessageStorage {
private val json = Json {
prettyPrint = true
ignoreUnknownKeys = true
}
private val mutex = Mutex()
init {
// Zorg ervoor dat de storage directory bestaat
File(storageDir).mkdirs()
}
private fun getContextFile(contextId: String): File {
// Sanitize contextId voor bestandsnaam (verwijder ongeldige karakters)
val safeContextId = contextId.replace(Regex("[^a-zA-Z0-9._-]"), "_")
return Paths.get(storageDir, "$safeContextId.json").toFile()
}
override suspend fun save(message: Message) {
val contextId = message.contextId ?: throw MessageOperationException(
"Cannot save message without contextId"
)
mutex.withLock {
try {
val file = getContextFile(contextId)
val messages = if (file.exists()) {
try {
val content = file.readText()
json.decodeFromString<MutableList<Message>>(content)
} catch (e: SerializationException) {
throw MessageOperationException(
"Failed to parse existing messages for context $contextId",
e
)
}
} else {
mutableListOf()
}
messages.add(message)
val jsonContent = json.encodeToString(messages)
file.writeText(jsonContent)
} catch (e: Exception) {
if (e is MessageOperationException) throw e
throw MessageOperationException(
"Failed to save message for context $contextId",
e
)
}
}
}
override suspend fun getByContext(contextId: String): List<Message> {
return mutex.withLock {
try {
val file = getContextFile(contextId)
if (file.exists()) {
val content = file.readText()
json.decodeFromString<List<Message>>(content)
} else {
emptyList()
}
} catch (e: SerializationException) {
throw MessageOperationException(
"Failed to read messages for context $contextId",
e
)
} catch (e: Exception) {
throw MessageOperationException(
"Failed to retrieve messages for context $contextId",
e
)
}
}
}
override suspend fun deleteByContext(contextId: String) {
mutex.withLock {
try {
val file = getContextFile(contextId)
if (file.exists() && !file.delete()) {
throw MessageOperationException(
"Failed to delete messages for context $contextId"
)
}
} catch (e: Exception) {
if (e is MessageOperationException) throw e
throw MessageOperationException(
"Failed to delete messages for context $contextId",
e
)
}
}
}
override suspend fun replaceByContext(contextId: String, messages: List<Message>) {
mutex.withLock {
try {
// Valideer dat alle messages de juiste contextId hebben
if (messages.any { it.contextId != contextId }) {
throw MessageOperationException(
"All messages must have contextId '$contextId'"
)
}
val file = getContextFile(contextId)
val jsonContent = json.encodeToString(messages)
file.writeText(jsonContent)
} catch (e: Exception) {
if (e is MessageOperationException) throw e
throw MessageOperationException(
"Failed to replace messages for context $contextId",
e
)
}
}
}
/**
* Utility: Clear all stored messages across all contexts
*/
suspend fun clearAll() {
mutex.withLock {
File(storageDir).listFiles()?.forEach { it.delete() }
}
}
/**
* Utility: Get list of all context IDs that have stored messages
*/
fun getAllContextIds(): List<String> {
return File(storageDir).listFiles()
?.filter { it.extension == "json" }
?.map { it.nameWithoutExtension }
?: emptyList()
}
}