Skip to main content

Sessions

An AIAgent keeps its live conversation history across sequential calls. To persist that history across agent instances or processes, attach a SessionDB explicitly.

In-memory lifecycle​

from run_agent import AIAgent


async def in_memory_conversation():
async with AIAgent(...) as agent:
await agent.chat("My project uses Python 3.12.")
return await agent.chat("Which Python version did I mention?")

One instance represents one ordered conversation. Concurrent turns on that instance are serialized; separate instances can run concurrently.

Enable durable storage​

from hermes_state import SessionDB
from run_agent import AIAgent

async def stored_conversation():
db = SessionDB("./state.db")
async with AIAgent(
...,
session_db=db,
session_id="project-review",
) as agent:
return await agent.run_conversation("Review this project.")

SessionDB uses aiosqlite. Its constructor is state-only; the connection and schema initialize on the first awaited operation. Passing it explicitly starts durable transcript persistence at the turn prologue and selects the database path. A recall tool may otherwise open the default $HERMES_HOME/state.db lazily. An injected store is borrowed by the agent; the host that created it closes it once during worker shutdown. Only a lazily created default store is owned by the agent. SessionDB.close() is idempotent.

Use PostgreSQL for a service​

SQLite remains the default and is a good fit for a single-process application. For a service whose workers share a durable store, install the optional PostgreSQL backend and inject it through the same existing session_db= argument:

uv sync --extra postgres
from hermes_state_postgres import SessionDB
from run_agent import AIAgent


async def postgres_conversation():
db = SessionDB(
"postgresql+psycopg://user:password@db.example:5432/hermes",
)
try:
async with AIAgent(
...,
session_db=db,
session_id="project-review",
) as agent:
return await agent.run_conversation("Review this project.")
finally:
await db.close()

The PostgreSQL SessionDB keeps the SQLite method names and awaited calling style; only the import and explicit DSN change. A store reads the active profile's database.postgres pool and driver settings once when its first database operation initializes the engine. Create a new store after changing those settings. See SessionDB storage and PostgreSQL settings for the supported options. Writable initialization performs only a known, versioned PostgreSQL migration under a transaction advisory lock. It never guesses the source of a partial schema or silently adds arbitrary missing columns. Read-only initialization requires the current logical and PostgreSQL physical schema and never migrates an existing database.

For production, drain writer workers and run one preflight initialization against the direct PostgreSQL endpoint before starting serving workers. The preflight may create normal indexes and therefore blocks writes while it runs; it is intentionally atomic and retryable rather than a zero-downtime online migration. Configure a backup/PITR point and lock_timeout/statement_timeout before the preflight. Do not run old and new writer versions through the migration at the same time.

For a read-only endpoint, pass read_only=True:

readonly_db = SessionDB(read_replica_url, read_only=True)

This blocks SessionDB writes and enables PostgreSQL transaction read-only mode; it does not choose a replica automatically. In a multi-worker service, create and close one store per worker lifespan and share it with that worker's agents. Plan for a possible connection count of workers * (pool_size + max_overflow). Other retained stores, such as memory plugin databases, remain separate from the core PostgreSQL SessionDB.

Resume in a new agent​

Passing only the old session_id does not automatically load its messages. Resolve the current compression descendant, load model history, and provide it to run_conversation():

from hermes_state import SessionDB
from run_agent import AIAgent

async def resume_conversation():
db = SessionDB("./state.db")
tip = await db.resolve_resume_session_id("project-review")
model_history, display_history = await db.get_resume_conversations(tip)

async with AIAgent(
...,
session_db=db,
session_id=tip,
) as agent:
result = await agent.run_conversation(
"Continue from the saved review.",
conversation_history=model_history,
)
return result, display_history

model_history is the alternation-repaired history fed to the model. display_history includes the full ancestor-to-tip lineage for applications that render a transcript.

Search and compression​

The store retains message order, structured tool calls, reasoning, display metadata, and compression lineage. Its FTS-backed search powers the optional session_search toolset:

async def search_prior_sessions():
async with AIAgent(
...,
session_db=SessionDB("./state.db"),
enabled_toolsets=["session_search"],
) as agent:
return await agent.chat("Find the earlier deployment discussion.")

Context compression can end one database session and continue in a linked child session. Always call resolve_resume_session_id() before a cross-process resume so post-compression messages are not missed.

Cancellation of an active conversation attempts a crash-safe partial persist before propagating CancelledError. This protects recoverability but does not replace application-level backups or SQLite filesystem durability planning.