Skip to content

Commit dcca913

Browse files
feat: Extract shared database drivers for session, multimodal, and response stores (#357)
* feat: Extract shared database drivers for session, multimodal, and response stores * feat: Refactor shared database drivers and retry logic - Parameterize retry helper by exception types, allowing scoped retries for drivers. - Migrate CosmosDB and Firestore drivers to shared `core/util/drivers/`. - Consolidate `RedisLikeDriver` retry and health-check behavior across sessions and response stores. - Standardize method names in CosmosDB to match DynamoDB drivers. - Update consumer stores to explicit driver parameters and adjust related tests. * feat: Refactor driver state management and connection handling for Redis and DynamoDB * feat: Migrate thread stores to shared database drivers and consolidate connection handling * fix: Correct line numbers in spec.md for thread store connection logic * fix: Update `spec.md` to reflect package rename from `drivers` to `driver` across shared database driver sections * feat: Add shared database connection drivers for Cosmos DB, DynamoDB, Firestore, Redis, and Valkey * fix: Handle `None` type for `last_err` in retry logic and enforce minimum retry count * feat: Introduce BaseDriver class for shared database connection logic and refactor drivers to inherit from it
1 parent 0c1c492 commit dcca913

38 files changed

Lines changed: 2956 additions & 1526 deletions

.agents/skills/ak-dev-architecture/SKILL.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ Provides image and file attachment support via a pluggable storage and PreHook a
149149
| Backend | Class | Module | Key traits |
150150
|---------|-------|--------|------------|
151151
| In-memory | `InMemoryAttachmentStore` | `storage/in_memory.py` | `ClassVar` dict, ephemeral, zero setup |
152-
| Redis | `RedisAttachmentStore` | `storage/redis.py` | Persistent, TTL, connection pooling |
152+
| Redis | `RedisAttachmentStore` | `storage/redis.py` | Persistent, TTL, shared `RedisDriver` (lazy connect, retry, ping/reconnect) |
153153
| DynamoDB | `DynamoDBAttachmentStore` | `storage/dynamodb.py` | Serverless/AWS, TTL via `expiry_time` |
154154
| Session cache | `SessionNonVolatileCacheAttachmentStore` | `storage/session_cache.py` | Legacy, stores in `nv_cache` (not recommended) |
155155

@@ -239,6 +239,26 @@ Pluggable storage backends agents can read from and write to as tools:
239239
- **`KnowledgeBuilder`** (`knowledgebuilder.py`): Wraps one or more `KnowledgeBase` instances and `build()`s plain-function tools (`get_schemas`, `read_kb`, `write_kb`, `get_all_kb_descriptions`) for binding via a framework's `ToolBuilder`
240240
- **Backends**: `ChromaManager` (vector, `chroma.py`), `Neo4jManager` (graph, `neo4j.py`), `StarburstManager` (read-only SQL via Trino, `starburst.py`) — each behind an optional dependency extra (`chromadb`, `neo4j`, `trino`)
241241

242+
## Shared Database Drivers (`ak-py/src/agentkernel/core/util/driver/`)
243+
244+
The Session, Multimodal attachment, Response Store, and Thread backends share one set of
245+
connection drivers: `RedisDriver`, `ValkeyDriver` (both subclassing `_RedisLikeDriver`),
246+
`DynamoDBDriver`, `CosmosDBDriver`, and `FirestoreDriver`. Three rules govern the package:
247+
248+
1. **Drivers never read `AKConfig`** — all connection parameters are explicit constructor
249+
arguments; config reading and validation stay in the stores and factories
250+
2. **Drivers own the connection lifecycle** (lazy connect, 3-retry/2s back-off, Redis/Valkey
251+
ping health-check with reconnect, `socket_connect_timeout=5`, TTL plumbing) plus a generic
252+
command surface; key schemas, serialization, and data layouts stay in the store classes
253+
3. **Drivers expose their native handle** (`client` / `table` / `table_client` / `collection`)
254+
for consumers whose data operations exceed the generic surface (e.g. the DynamoDB/Cosmos/
255+
Firestore thread stores)
256+
257+
`driver/__init__.py` has no eager imports — `redis`, `valkey`, `azure`, and `gcp` extras stay
258+
optional; consumers import the concrete module (`from agentkernel.core.util.driver.redis import
259+
RedisDriver`). Connect/reconnect is serialized by a per-instance `threading.Lock`, so drivers
260+
are safe to share across threads (e.g. response stores under `ECSOutputConsumer`).
261+
242262
## Directory Structure
243263

244264
```
@@ -256,6 +276,7 @@ ak-py/src/agentkernel/
256276
│ ├── chat_service.py # ChatService, RequestBuilder, AgentHandler, ResponseBuilder
257277
│ ├── logger.py # Logging setup
258278
│ ├── util/ # Shared utilities
279+
│ │ └── driver/ # Shared DB connection drivers (Redis, Valkey, DynamoDB, Cosmos DB, Firestore)
259280
│ └── session/ # Session store implementations
260281
│ ├── base.py # SessionStore, SessionCache
261282
│ ├── serde.py # Session (de)serialization helpers

.agents/skills/ak-dev-new-multimodal-storage/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ This guide walks through adding a new attachment storage backend to Agent Kernel
2121
| Backend | Config value | Features | Extras |
2222
|---|---|---|---|
2323
| In-memory | `in_memory` | Ephemeral `ClassVar` dict, zero setup, single-process only | None |
24-
| Redis | `redis` | Persistent, TTL, connection pooling, distributed | `agentkernel[multimodal,redis]` |
24+
| Redis | `redis` | Persistent, TTL, shared `RedisDriver` (lazy connect, retry, ping/reconnect), distributed | `agentkernel[multimodal,redis]` |
2525
| DynamoDB | `dynamodb` | Serverless/AWS, TTL via `expiry_time`, fully managed | `agentkernel[multimodal,aws]` |
2626
| Session cache | `session_cache` | Legacy — stores in session `nv_cache` (causes bloat, not recommended) | None |
2727

@@ -141,7 +141,7 @@ class <Backend>AttachmentStore(AttachmentStore):
141141
- **Max attachments pruning**: When `save()` is called and the session exceeds `max_attachments`, delete the oldest entry. Track order via timestamps or a per-session index
142142
- **Index consistency on delete**: When `delete()` is called, remove the attachment ID from the per-session index in addition to deleting the data entry. Skipping this step causes the index count to drift — the backend will think more attachments exist than actually do, breaking `max_attachments` enforcement on subsequent saves
143143
- **TTL**: If the backend supports time-based expiry (like Redis TTL or DynamoDB `expiry_time`), use it for automatic cleanup. Read the TTL value from the backend-specific config
144-
- **Connection management**: Use lazy initialization and connection pooling where possible. The store may be instantiated per-request (inside `AttachmentStorageManager.__init__`)
144+
- **Connection management**: Reuse the shared connection drivers in `agentkernel/core/util/driver/` (`RedisDriver`, `DynamoDBDriver`, etc.) — they provide lazy connect, 3-retry back-off, and (for Redis-like backends) ping/reconnect. The store may be instantiated per-request (inside `AttachmentStorageManager.__init__`), so keep the driver construction cheap (no eager connect)
145145
- **Serialization**: Attachment dicts must be JSON-serializable. The `data` field contains base64-encoded binary, so all values are strings, floats, or ints
146146

147147
### 3. Add Backend-Specific Configuration

.agents/skills/ak-dev-testing-conventions/SKILL.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ Tests live in `ak-py/tests/` and follow the naming convention `test_<module>.py`
4141
| `test_session.py` | Session state, caches, context vars |
4242
| `test_session_cache.py` | LRU SessionCache |
4343
| `test_sessions_in_memory.py` | InMemorySessionStore |
44+
| `test_sessions_redis.py` | RedisSessionStore missing-config error, shared RedisDriver retry exhaustion |
45+
| `test_sessions_valkey.py` | ValkeySessionStore round trips (fake client), shared ValkeyDriver retry exhaustion |
46+
| `test_sessions_dynamodb.py` | DynamoDBSessionStore Binary wrap/unwrap, missing-item skip (mocked driver) |
47+
| `test_shared_drivers.py` | Shared DB drivers (`core/util/driver/`): retry scope, ping/reconnect, command surface, DynamoDB item-dict semantics |
48+
| `test_multimodal_redis_store.py` | RedisAttachmentStore index TTL refresh, JSON round trip, pruning (mocked driver) |
4449
| `test_config.py` | AKConfig loading, env vars |
4550
| `test_test_config.py` | AKTestConfig (Test framework config) loading, defaults |
4651
| `test_tool.py` | ToolContext, cache |
@@ -64,7 +69,7 @@ Tests live in `ak-py/tests/` and follow the naming convention `test_<module>.py`
6469
| `test_lambda_router.py` | Lambda routing |
6570
| `test_sqs_handler.py` | AWS SQSHandler config, client, message sending |
6671
| `test_serverless_request_handle.py` | BaseRequest/BaseRunRequest parsing from serverless payloads |
67-
| `test_firestore_database_id.py` | FirestoreDriver named `database_id` configuration |
72+
| `test_firestore_database_id.py` | Shared `FirestoreDriver` (`core/util/driver/firestore.py`, explicit constructor params) named `database_id` configuration |
6873
| `test_ak_logger.py` | AKLogger level resolution, configuration |
6974
| `test_error_util.py` | `user_facing_error_message` error mapping |
7075
| `test_thread_runner.py` | ThreadRunner task validation, failure/shutdown semantics |

ak-py/src/agentkernel/core/builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def build() -> SessionStore:
127127
or InMemorySessionStore (for all other types).
128128
129129
:raises: Any exceptions raised by SessionStoreBuilder.Types.from_str(), AKConfig.get(),
130-
RedisDriver(), RedisSessionStore(), or InMemorySessionStore() initialization.
130+
RedisSessionStore(), or InMemorySessionStore() initialization.
131131
"""
132132
session_store_type: SessionStoreBuilder.Types = SessionStoreBuilder.Types.from_str(AKConfig.get().session.type)
133133
Builder._log.info(f"Building {session_store_type} session store")

ak-py/src/agentkernel/core/config.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,12 @@ class _GmailConfig(BaseModel):
175175
label_filter: str = Field(default="INBOX", description="Gmail label to monitor (e.g., INBOX, UNREAD)")
176176

177177

178-
class _MultimodalStorageRedisConfig(BaseModel):
179-
url: str = Field(default="redis://localhost:6379", description="Redis connection URL")
178+
class _MultimodalStorageRedisConfig(_RedisConfig):
180179
ttl: int = Field(default=604800, description="Attachment TTL in seconds")
181180
prefix: str = Field(default="ak:attachments:", description="Key prefix for attachment keys")
182181

183182

184-
class _MultimodalStorageDynamoDBConfig(BaseModel):
183+
class _MultimodalStorageDynamoDBConfig(_DynamoDBConfig):
185184
table_name: str = Field(default="ak-attachments", description="DynamoDB table name for attachment storage")
186185
ttl: int = Field(default=604800, description="Attachment TTL in seconds (0 disables)")
187186

@@ -212,27 +211,24 @@ class _MultimodalConfig(BaseModel):
212211
dynamodb: Optional[_MultimodalStorageDynamoDBConfig] = None
213212

214213

215-
class _ThreadRedisConfig(BaseModel):
216-
url: str = Field(default="redis://localhost:6379", description="Redis connection URL. Use rediss:// for SSL")
214+
class _ThreadRedisConfig(_RedisConfig):
217215
ttl: int = Field(default=2592000, description="Thread TTL in seconds (0 disables)")
218216
prefix: str = Field(default="ak:thread:", description="Key prefix for Redis thread storage")
219217

220218

221-
class _ThreadDynamoDBConfig(BaseModel):
219+
class _ThreadDynamoDBConfig(_DynamoDBConfig):
222220
table_name: str = Field(
223221
default="ak-agent-threads",
224222
description="DynamoDB table name for thread storage. Table should have a partition key named 'session_id' (S) and a sort key named 'sk' (S)",
225223
)
226224
ttl: int = Field(default=0, description="DynamoDB item TTL in seconds (0 disables)")
227225

228226

229-
class _ThreadFirestoreConfig(BaseModel):
227+
class _ThreadFirestoreConfig(_FirestoreConfig):
230228
collection_name: str = Field(
231229
default="ak-agent-threads",
232230
description="Firestore collection name for thread storage. Each document ID is a session_id.",
233231
)
234-
project_id: Optional[str] = Field(default=None, description="GCP project ID. If null, inferred from Application Default Credentials.")
235-
database_id: Optional[str] = Field(default=None, description="Firestore database ID. If null, defaults to '(default)' database.")
236232
ttl: int = Field(default=0, description="Thread TTL in seconds (0 disables)")
237233

238234

0 commit comments

Comments
 (0)