Skip to content

Commit 7189d06

Browse files
Rizzenariawispclaude
authored
Ariawisp/postgres persistence provider (#705)
rebased and fixed version of #481 by @ariawisp --------- Co-authored-by: Aria Wisp <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 3c87f2a commit 7189d06

File tree

20 files changed

+1734
-2
lines changed

20 files changed

+1734
-2
lines changed

agents/agents-features/agents-features-snapshot/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ kotlin {
2929

3030
jvmMain {
3131
dependencies {
32-
api(libs.ktor.client.cio)
32+
// SQL dependencies moved to agents-features-sql module
3333
}
3434
}
3535

@@ -38,6 +38,8 @@ kotlin {
3838
implementation(kotlin("test-junit5"))
3939
implementation(project(":agents:agents-test"))
4040
implementation(libs.mockk)
41+
implementation(libs.testcontainers)
42+
implementation(libs.testcontainers.postgresql)
4143
}
4244
}
4345
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# SQL Features Module
2+
3+
Provides SQL-based persistence providers for agent checkpoints using JetBrains Exposed ORM with support for multiple database engines.
4+
5+
## Features
6+
7+
- **Multi-Database Support**: PostgreSQL, MySQL, H2, SQLite
8+
- **Exposed ORM Integration**: Type-safe SQL operations with Kotlin DSL
9+
- **Connection Pooling**: HikariCP integration for production environments
10+
- **TTL Support**: Automatic cleanup of expired checkpoints with configurable intervals
11+
- **Database-Specific Optimizations**: Vendor-specific performance tuning
12+
13+
## Dependencies
14+
15+
- `org.jetbrains.exposed:*` - JetBrains Exposed ORM framework
16+
- `com.zaxxer:HikariCP` - Connection pooling
17+
- Database drivers: PostgreSQL, MySQL, H2, SQLite
18+
- `org.testcontainers:postgresql` - Testing infrastructure
19+
20+
## Providers
21+
22+
### ExposedPersistencyStorageProvider
23+
Base provider using Exposed ORM with configurable cleanup behavior.
24+
25+
### PostgresPersistencyStorageProvider
26+
Production-ready provider with JSONB support and HikariCP pooling.
27+
28+
### MySQLPersistencyStorageProvider
29+
Enterprise provider with JSON column support (MySQL 5.7+).
30+
31+
### H2PersistencyStorageProvider
32+
Perfect for testing and embedded applications.
33+
34+
### SQLitePersistencyStorageProvider
35+
Zero-configuration provider for desktop and mobile applications.
36+
37+
All providers implement `AutoCloseable` for proper resource management and support configurable TTL cleanup.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import ai.koog.gradle.publish.maven.Publishing.publishToMaven
2+
3+
group = rootProject.group
4+
version = rootProject.version
5+
6+
plugins {
7+
id("ai.kotlin.multiplatform")
8+
alias(libs.plugins.kotlin.serialization)
9+
}
10+
11+
kotlin {
12+
sourceSets {
13+
commonMain {
14+
dependencies {
15+
api(project(":agents:agents-core"))
16+
api(project(":agents:agents-features:agents-features-snapshot"))
17+
api(project(":rag:rag-base"))
18+
19+
api(libs.kotlinx.serialization.json)
20+
api(libs.ktor.serialization.kotlinx.json)
21+
}
22+
}
23+
24+
commonTest {
25+
dependencies {
26+
implementation(kotlin("test"))
27+
implementation(libs.kotlinx.coroutines.test)
28+
}
29+
}
30+
31+
jvmMain {
32+
dependencies {
33+
api(libs.exposed.core)
34+
api(libs.exposed.dao)
35+
api(libs.exposed.jdbc)
36+
api(libs.exposed.json)
37+
api(libs.exposed.kotlin.datetime)
38+
api(libs.postgresql)
39+
api(libs.mysql)
40+
api(libs.h2)
41+
api(libs.sqlite)
42+
implementation(libs.hikaricp)
43+
}
44+
}
45+
46+
jvmTest {
47+
dependencies {
48+
implementation(kotlin("test-junit5"))
49+
implementation(project(":agents:agents-test"))
50+
implementation(libs.mockk)
51+
implementation(libs.testcontainers)
52+
implementation(libs.testcontainers.postgresql)
53+
implementation(libs.testcontainers.mysql)
54+
}
55+
}
56+
}
57+
58+
explicitApi()
59+
}
60+
61+
publishToMaven()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package ai.koog.agents.features.sql.providers
2+
3+
/**
4+
* Represents an interface to handle database schema migrations using Exposed SQL library.
5+
*
6+
* This interface is designed to be implemented by classes that need to handle schema migrations
7+
* for databases, ensuring compatibility and flexibility in schema evolution.
8+
*
9+
* ExposedSQLMigrator provides an abstraction for executing migrations asynchronously, allowing
10+
* for better control and management of database schema changes as part of the application's lifecycle.
11+
*
12+
* The primary function to be implemented is [migrate], which encapsulates the details of performing
13+
* the required schema update or adjustments based on the application's requirements.
14+
*/
15+
public interface SQLPersistenceSchemaMigrator {
16+
/**
17+
* Performs a database schema migration asynchronously.
18+
*/
19+
public suspend fun migrate()
20+
}
21+
22+
/**
23+
* A no-operation implementation of the [SQLPersistenceSchemaMigrator] interface.
24+
*
25+
* This class is designed to be used in scenarios where schema migrations are not required
26+
* or are managed externally. It provides an empty implementation of the [migrate] method,
27+
* effectively acting as a placeholder.
28+
*
29+
* Use this class if you need to satisfy the dependency on [SQLPersistenceSchemaMigrator] but do not
30+
* want to perform any migration actions.
31+
*/
32+
public object NoOpSQLPersistenceSchemaMigrator : SQLPersistenceSchemaMigrator {
33+
override suspend fun migrate() { }
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package ai.koog.agents.features.sql.providers
2+
3+
import ai.koog.agents.snapshot.providers.PersistencyStorageProvider
4+
import kotlinx.datetime.Instant
5+
import kotlinx.serialization.json.Json
6+
7+
/**
8+
* Abstract base class for SQL-based implementations of [PersistencyStorageProvider].
9+
*
10+
* This provider offers a generic SQL abstraction for persisting agent checkpoints
11+
* to relational databases. Concrete implementations should handle specific SQL
12+
* dialects and connection management.
13+
*
14+
* ## Storage Schema:
15+
* Implementations should create a table with the following structure:
16+
* - persistence_id: String (part of primary key)
17+
* - checkpoint_id: String (part of primary key)
18+
* - created_at: Long (epoch milliseconds)
19+
* - checkpoint_json: String (JSON-serialized checkpoint data)
20+
* - ttl_timestamp: Long? (optional expiration timestamp)
21+
*
22+
* ## Design Decisions:
23+
* - Uses JSON serialization for checkpoint storage (leveraging database JSON support where available)
24+
* - Composite key on (persistence_id, checkpoint_id) ensures uniqueness
25+
* - Timestamp stored as epoch milliseconds for cross-database compatibility
26+
* - TTL is implemented via a nullable ttl_timestamp column for query-based cleanup
27+
*
28+
* ## Thread Safety:
29+
* Implementations must ensure thread-safe database access, typically through connection pooling.
30+
*
31+
* @constructor Initializes the SQL persistence provider.
32+
* @param persistenceId Unique identifier for this agent's persistence data
33+
* @param tableName Name of the table to store checkpoints (default: "agent_checkpoints")
34+
* @param ttlSeconds Optional TTL for checkpoint entries in seconds (null = no expiration)
35+
*/
36+
public abstract class SQLPersistencyStorageProvider(
37+
protected val persistenceId: String,
38+
protected val tableName: String = "agent_checkpoints",
39+
protected val ttlSeconds: Long? = null,
40+
protected val migrator: SQLPersistenceSchemaMigrator
41+
) : PersistencyStorageProvider {
42+
43+
protected val json: Json = Json {
44+
prettyPrint = true
45+
ignoreUnknownKeys = true
46+
}
47+
48+
/**
49+
* Initializes the database schema if it doesn't exist.
50+
* This should be called once during provider initialization.
51+
*/
52+
public open suspend fun migrate() {
53+
migrator.migrate()
54+
}
55+
56+
/**
57+
* Executes a database transaction with the given operations.
58+
* Implementations should ensure proper transaction isolation and rollback on failure.
59+
*/
60+
protected abstract suspend fun <T> transaction(block: suspend () -> T): T
61+
62+
/**
63+
* Cleans up expired checkpoints based on TTL.
64+
* This should be called periodically or before operations to maintain database hygiene.
65+
*/
66+
public abstract suspend fun cleanupExpired()
67+
68+
/**
69+
* Calculates the TTL timestamp for a checkpoint if TTL is configured.
70+
*/
71+
public fun calculateTtlTimestamp(timestamp: Instant): Long? {
72+
return ttlSeconds?.let {
73+
timestamp.toEpochMilliseconds() + (it * 1000)
74+
}
75+
}
76+
77+
/**
78+
* Deletes a specific checkpoint by ID
79+
*/
80+
public abstract suspend fun deleteCheckpoint(checkpointId: String)
81+
82+
/**
83+
* Deletes all checkpoints for this persistence ID
84+
*/
85+
public abstract suspend fun deleteAllCheckpoints()
86+
87+
/**
88+
* Gets the total number of checkpoints stored
89+
*/
90+
public abstract suspend fun getCheckpointCount(): Long
91+
}

0 commit comments

Comments
 (0)