Restore SQLite history#

Reopen a real temporary database, restore history, paginate messages, update the title, and delete the session.

Run it#

From the repository root:

uv run python docs/_examples/sessions.py

The output includes:

Raw roles: system, user, assistant, system, user, 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.

What to take into your application#

Keep one stable session_id for a conversation and open a persistence extension over the same database when the process restarts. The example closes the first connection before creating a second client, so the restored history comes from SQLite, not an in-memory cache.

async def main() -> None:
    # No demo touches the default database under the user's home directory.
    with TemporaryDirectory(prefix="zett-docs-") as directory:
        path = Path(directory) / "sessions.sqlite"
        session_id = "project-notes"
        config = AgentRunConfig(session_id=session_id)
        first_storage = SQLiteSessionExtension(path)
        try:
            first = await create_agent(TurnModel(), config=config, extensions=[first_storage])
            print((await first.run("Remember Python", metadata={"origin": "tutorial"})).content)
        finally:
            await first_storage.close()

        restored_storage = SQLiteSessionExtension(path)
        try:
            restored = await create_agent(TurnModel(), config=config, extensions=[restored_storage])
            print((await restored.run("Continue")).content)
            rows = await restored_storage.list_raw_messages(session_id)
            assert [row.message.role.value for row in rows] == [
                "system",
                "user",
                "assistant",
                "system",
                "user",
                "assistant",
            ]
            assert rows[0].metadata == {"origin": "tutorial"}
            assert len(await restored_storage.list_raw_messages(session_id, limit=2, offset=2)) == 2
            view = await restored_storage.storage.load(session_id)
            assert view.snapshot is None and len(view.raw_tail) == 6
            await restored_storage.update_session(session_id, title="Project notes")
            renamed = await restored_storage.get_session(session_id)
            assert renamed is not None and renamed.title == "Project notes"
            assert await restored_storage.delete_session(session_id)
            assert await restored_storage.list_raw_messages(session_id) == []
            print("Raw roles: system, user, assistant, system, user, assistant")
            print("Session restored, renamed, and deleted in a temporary database")
        finally:
            await restored_storage.close()

The example also paginates messages, renames the conversation, and deletes it. It only deletes a session in a temporary database. In your application, session deletion removes history; put it behind an explicit user action.

Complete source#

Download sessions.py.

 1"""Restore a conversation through a new SQLite connection and a new Agent."""
 2
 3import asyncio
 4from collections.abc import AsyncIterator
 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.sqlite import SQLiteSessionExtension
11from zett_agent.messages import (
12    AssistantMessage,
13    UserMessage,
14)
15from zett_agent.model import (
16    ModelEvent,
17    ModelRequest,
18    ModelResponse,
19    RetryOptions,
20)
21
22
23class TurnModel:
24    retry = RetryOptions(max_retries=0)
25
26    async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
27        turn = sum(isinstance(message, UserMessage) for message in request.messages)
28        yield ModelEvent.completed(ModelResponse(AssistantMessage(content=f"Stored turn {turn}")))
29
30
31async def main() -> None:
32    # No demo touches the default database under the user's home directory.
33    with TemporaryDirectory(prefix="zett-docs-") as directory:
34        path = Path(directory) / "sessions.sqlite"
35        session_id = "project-notes"
36        config = AgentRunConfig(session_id=session_id)
37        first_storage = SQLiteSessionExtension(path)
38        try:
39            first = await create_agent(TurnModel(), config=config, extensions=[first_storage])
40            print((await first.run("Remember Python", metadata={"origin": "tutorial"})).content)
41        finally:
42            await first_storage.close()
43
44        restored_storage = SQLiteSessionExtension(path)
45        try:
46            restored = await create_agent(TurnModel(), config=config, extensions=[restored_storage])
47            print((await restored.run("Continue")).content)
48            rows = await restored_storage.list_raw_messages(session_id)
49            assert [row.message.role.value for row in rows] == [
50                "system",
51                "user",
52                "assistant",
53                "system",
54                "user",
55                "assistant",
56            ]
57            assert rows[0].metadata == {"origin": "tutorial"}
58            assert len(await restored_storage.list_raw_messages(session_id, limit=2, offset=2)) == 2
59            view = await restored_storage.storage.load(session_id)
60            assert view.snapshot is None and len(view.raw_tail) == 6
61            await restored_storage.update_session(session_id, title="Project notes")
62            renamed = await restored_storage.get_session(session_id)
63            assert renamed is not None and renamed.title == "Project notes"
64            assert await restored_storage.delete_session(session_id)
65            assert await restored_storage.list_raw_messages(session_id) == []
66            print("Raw roles: system, user, assistant, system, user, assistant")
67            print("Session restored, renamed, and deleted in a temporary database")
68        finally:
69            await restored_storage.close()
70
71
72if __name__ == "__main__":
73    asyncio.run(main())

Next: Keep and restore conversations for storage choices and resource ownership, or Compact without losing history for long conversations.