Choose persistence based on what you need to recover: a conversation, or unfinished execution.
Three kinds of state
| State | Storage | Recovery |
|---|---|---|
| Ordinary Agent | Process memory | Lives as long as the instance |
| Agent snapshot | Explicit files in a session directory | Restore configuration, messages, usage, and supported extension state |
| AgentRuntime | SQLite, PostgreSQL, or Temporal | Resume persisted execution under replay and approval policies |
Repeated calls to the same Agent keep its conversation. That alone does not make a session durable.
What persists
Three memory layers sit under those states, all optional.
ConversationMemory is created for every agent. It holds the message list. As the context limit approaches, the harness summarizes the oldest turns with the agent's own model and fires HookEvent.CONTEXT_CONDENSED; see Long-run reliability. A snapshot stores it; nothing else does.
agent.memory.get_messages() # current message list
agent.memory.set_messages(saved) # restore
AgentMemory writes titled, tagged notes as markdown files on disk. LongTermMemory stores categorized facts with ids. Both outlive the process on their own. register_memory_tools gives the model the built-in save_memory and recall_memories tools over the stores you pass, and your own tools can share the same stores.
from harnessx import AgentMemory, LongTermMemory, PermissionLevel
from harnessx.builtin import register_memory_tools
notes = AgentMemory(storage_dir=".agent_memory/agent")
facts = LongTermMemory(storage_dir=".agent_memory/long_term")
register_memory_tools(agent.tools, long_term=facts, agent_memory=notes)
@agent.tools.register(permission=PermissionLevel.ALLOW)
async def save_note(title: str, content: str, tags: str = "") -> str:
"""Save a titled note.
Args:
title: Short title for the memory.
content: The information to remember.
tags: Comma-separated tags.
"""
return notes.save(title, content, tags=[t.strip() for t in tags.split(",") if t.strip()])
fact_id = facts.save("User prefers dark mode", category="preference")
facts.update(fact_id, "User prefers light mode")
facts.search(query="dark mode")
Tell the agent in the system prompt which tools exist and when to use them. AgentMemory.search(query=...) returns matching notes. examples/04-context/custom_memory_tools.py combines the built-in tools with two of its own over the same stores.
Save a conversation snapshot
from harnessx import Agent
async def save_and_continue():
async with Agent() as agent:
await agent.run("Remember that this project analyzes invoices.")
session_id = await agent.save_session(storage_dir=".agent_sessions")
async with await Agent.load_session(
session_id, storage_dir=".agent_sessions"
) as restored:
result = await restored.run("What does this project analyze?")
return result
Snapshots contain serializable state, not executable Python handlers or live MCP connections. Rebind custom tools and provide compatible extensions when loading them. Supply the same extension names; explicitly supplied configuration must match the snapshot.
Try it with no API key: uv run python examples/06-durability/session_snapshots.py.
Before summarizing, the harness first clears old tool output, and
agent.memory.prune_tool_results() runs that step on demand. Snapshots also
carry the estimated cost and the model limits learned from provider refusals,
scoped to each provider and model.
When you need crash recovery
Use AgentRuntime when you need to recover an unfinished run or preserve an approval wait across process restarts. This is covered under Advanced topics → Durable agent runs. The PostgreSQL walkthrough provides a runnable crash/restart demonstration.
Saving a conversation snapshot and persisting execution are separate operations. Loading a snapshot does not resume an in-flight tool call.
Ownership
A runtime closes the agent it executes. It closes a backend only if it created that backend. Explicitly close injected backends, providers, and other caller-owned resources. Agent instances support one active run at a time.