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.