Connect a storage adapter#
Wrap an existing storage backend to record persistence operations, while letting the persistence extension manage conversation history.
Run it#
From the repository root:
uv run python docs/_examples/storage_adapter.py
The output includes:
load, append:system, append:user, append:assistant
This program uses deterministic offline models and temporary storage where needed. It requires no API key and is executed by the documentation test suite.
Adapt the store, not the agent loop#
AuditedStorage delegates reads and writes to SQLite while recording their
order. The extension only needs to construct that adapter:
class AuditedPersistence(BaseSessionPersistenceExtension[AuditedStorage]):
"""Only construct the adapter; the base owns all lifecycle integration."""
def __init__(self, database: SQLiteSessionStorage) -> None:
super().__init__(AuditedStorage(database))
The complete source implements the storage methods for loading history, appending messages, and saving a compaction checkpoint. If you replace SQLite with another database, preserve sequence ordering, transactions, and version checks; those guarantees matter when restoring a conversation safely.
Complete source#
1"""Adapt a storage implementation without reimplementing persistence hooks."""
2
3import asyncio
4from collections.abc import AsyncIterator, Mapping
5from pathlib import Path
6from tempfile import TemporaryDirectory
7
8from zett_agent.agent import AgentRunConfig
9from zett_agent.client import create_agent
10from zett_agent.extensions.compaction import CompactedMessage
11from zett_agent.extensions.events import MessageTiming
12from zett_agent.extensions.persistence import (
13 BaseSessionPersistenceExtension,
14 ContextSnapshot,
15 SessionView,
16)
17from zett_agent.json_types import JsonValue
18from zett_agent.messages import (
19 AnyMessage,
20 AssistantMessage,
21)
22from zett_agent.model import (
23 ModelEvent,
24 ModelRequest,
25 ModelResponse,
26 ModelUsage,
27 RetryOptions,
28)
29from zett_agent.storage import SQLiteSessionStorage
30
31
32class AuditedStorage:
33 """A transparent adapter with an audit trail around an existing SQLite store."""
34
35 def __init__(self, database: SQLiteSessionStorage) -> None:
36 self.database = database
37 self.operations: list[str] = []
38
39 async def load(self, session_id: str) -> SessionView:
40 self.operations.append("load")
41 return await self.database.load(session_id)
42
43 async def append(
44 self,
45 session_id: str,
46 request_id: str,
47 message: AnyMessage,
48 timing: MessageTiming | None = None,
49 parent_session_id: str | None = None,
50 title: str | None = None,
51 agent_name: str | None = None,
52 metadata: Mapping[str, JsonValue] | None = None,
53 tags: Mapping[str, JsonValue] | None = None,
54 usage: ModelUsage | None = None,
55 ) -> int:
56 self.operations.append(f"append:{message.role.value}")
57 return await self.database.append(
58 session_id, request_id, message, timing, parent_session_id, title, agent_name, metadata, tags, usage
59 )
60
61 async def snapshot(
62 self,
63 session_id: str,
64 compacted_message: CompactedMessage,
65 compacted_through_sequence: int,
66 expected_version: int,
67 ) -> ContextSnapshot:
68 self.operations.append("snapshot")
69 return await self.database.snapshot(session_id, compacted_message, compacted_through_sequence, expected_version)
70
71
72class AuditedPersistence(BaseSessionPersistenceExtension[AuditedStorage]):
73 """Only construct the adapter; the base owns all lifecycle integration."""
74
75 def __init__(self, database: SQLiteSessionStorage) -> None:
76 super().__init__(AuditedStorage(database))
77
78
79class FixedModel:
80 retry = RetryOptions(max_retries=0)
81
82 async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
83 yield ModelEvent.completed(ModelResponse(AssistantMessage(content="Saved.")))
84
85
86async def main() -> None:
87 with TemporaryDirectory(prefix="zett-storage-adapter-") as directory:
88 database = SQLiteSessionStorage(Path(directory) / "sessions.sqlite")
89 try:
90 extension = AuditedPersistence(database)
91 client = await create_agent(FixedModel(), config=AgentRunConfig(session_id="audit"), extensions=[extension])
92 await client.run("Remember this")
93 assert extension.storage.operations == ["load", "append:system", "append:user", "append:assistant"]
94 print(", ".join(extension.storage.operations))
95 assert len(await database.list_raw_messages("audit")) == 3
96 finally:
97 await database.close()
98
99
100if __name__ == "__main__":
101 asyncio.run(main())
Next: Implement a storage adapter for the contract, or Keep and restore conversations if SQLite already meets your needs.