Session Persistence and Memory for Long-Running Agent Workflows
Give your agents memory that survives restarts. Learn session backends, context trimming and summarization, and compaction with the OpenAI Agents SDK.
Published on • August 20, 2026
AI Assistant

The first version of an agent is stateless: user asks, agent answers. But the moment your agent handles a real conversation—refunds, onboarding, a multi-step research task—statelessness becomes a bug. The user says “the other one I mentioned,” and the agent has no idea what “the other one” was.
State doesn’t have to be a hard problem. The OpenAI Agents SDK treats memory as a first-class primitive: sessions. A session stores conversation history and automatically prepends it to each turn, then persists everything new after the run completes. You get durable, multi-turn agents without hand-rolling to_input_list() plumbing.
In this tutorial, you will learn how sessions work, which backend to pick, and how to keep long conversations inside the context window with trimming, summarization, and compaction.
Prerequisites
- Python 3.9+ with
pip install openai-agents - An OpenAI API key (or another supported provider)
How Sessions Work
A session is a storage abstraction with a tiny protocol. When you pass one to Runner.run, the SDK handles the rest:
- Before each run, it retrieves the conversation history and prepends it to the input.
- After each run, all new items (user input, assistant responses, tool calls) are stored.
- Each subsequent run with the same session includes the full history.
from agents import Agent, Runner
from agents.memory import SQLiteSession
agent = Agent(
name="Support",
instructions="Answer briefly and keep track of prior context.",
)
session = SQLiteSession("conversation_123")
first = await Runner.run(agent, "What city is the Golden Gate Bridge in?", session=session)
second = await Runner.run(agent, "What state is it in?", session=session)
print(second.final_output) # knows the bridge, answers "California"
No manual history stitching. The session is the memory object.
Choosing a Session Backend
The Session protocol is four async methods: get_items, add_items, pop_item, clear_session. The SDK ships with backends for every scale:
| Session type | Best for |
|---|---|
SQLiteSession | Local development, simple apps (file-backed or in-memory) |
AsyncSQLiteSession | Async SQLite with aiosqlite |
RedisSession | Shared memory across workers/services, low latency |
SQLAlchemySession | Production apps with existing databases |
MongoDBSession | Apps already on MongoDB, multi-process storage |
DaprSession | Cloud-native deployments with Dapr sidecars |
OpenAIConversationsSession | Server-managed storage in OpenAI |
EncryptedSession | Encryption + TTL on top of another backend |
Scale-up is a one-line change:
from agents.memory import RedisSession
session = RedisSession.from_url("conversation_123", url="redis://redis:6379")
Every backend implements the same protocol, so switching requires no other code changes. For production-grade requirements, implement your own backend to add encryption, retention policies, or per-turn metadata.
Four Ways to Carry State Forward
Sessions are one of four continuation strategies. Pick one per conversation:
| Strategy | Where state lives | Best for |
|---|---|---|
result.history | Your application | Small chat loops, maximum control |
session | Your storage + SDK | Persistent chat, resumable runs (best default) |
conversationId | OpenAI Conversations API | Shared server-managed state across workers |
previousResponseId | OpenAI Responses API | Lightest response-to-response continuation |
Mixing local replay with server-managed state can duplicate context unless you’re deliberately reconciling both layers.
Context Engineering: Trimming and Summarization
Sessions store history, but context windows are finite. Two proven techniques keep long conversations healthy:
Trimming — drop older turns, keep the last N:
from agents.memory import SessionSettings
session = SQLiteSession("conversation_123")
await Runner.run(agent, "First message", session=session,
config={"session_settings": SessionSettings(limit=20)})
SessionSettings(limit=N) retrieves only the most recent N items before each run.
Summarization — keep the last N turns verbatim and compress everything older into a synthetic summary. This is the pattern of a support agent handing off a case to the next agent: preserve the critical details, drop the noise.
class SummarizingSession:
def __init__(self, keep_last_n_turns: int = 3, context_limit: int = 3):
self.keep_last_n = keep_last_n_turns
self.context_limit = context_limit
self._records = deque()
self._lock = asyncio.Lock()
self.summarizer = LLMSummarizer(...)
async def get_items(self, limit=None):
async with self._lock:
data = list(self._records)
msgs = [self._sanitize_for_model(rec["msg"]) for rec in data]
return msgs[-limit:] if limit else msgs
async def add_items(self, items):
async with self._lock:
self._records.extend(items)
await self._maybe_summarize()
A well-crafted summarization prompt matters more than the mechanism: it should preserve decisions, IDs, and constraints while keeping the story continuous.
Compaction: Server-Managed Context Shrinking
For automatic context management, wrap any session with OpenAIResponsesCompactionSession. It uses the Responses API responses.compact to replace long stored history with a shorter equivalent list:
from agents.extensions.memory import OpenAIResponsesCompactionSession
session = OpenAIResponsesCompactionSession(
session_id="conversation_123",
underlying_session=SQLiteSession("conversation_123"),
model="gpt-5.4",
should_trigger_compaction=lambda candidate: len(candidate) >= 12,
)
Compaction clears and rewrites history, so the SDK waits for it before the run completes. For low-latency streaming, disable auto-compaction and call session.run_compaction({"force": True}) yourself between turns—at phase boundaries, not after every turn.
Sessions and Human-in-the-Loop
Sessions and approvals compose cleanly. When a run pauses for approval, resume it with the same session (or another instance with the same session ID and backend) so the resumed turn continues the same stored conversation history:
result = await Runner.run(agent, "Refund order 8472", session=session)
if result.interruptions:
decision = await ask_user(result.interruptions[0])
result = await Runner.run(agent, state=result.state, resume=decision, session=session)
Treat approvals as paused runs, not new turns. That keeps turn counts, history, and continuation IDs consistent.
Putting It All Together
For complete, runnable examples of session memory, trimming, summarization, and compaction, see the official cookbooks:
- https://developers.openai.com/cookbook/examples/agents_sdk/session_memory
- https://developers.openai.com/cookbook/examples/agents_sdk/building_reliable_agents_memory_compaction
Conclusion & Next Steps
You now know how to give agents durable memory: pick a session backend, carry state with the right strategy, and keep conversations healthy with trimming, summarization, or compaction.
Next steps:
- Implement a custom session backend for your database with encryption and retention.
- Measure context size over 50 turns with and without summarization.
- Combine compaction with sandbox memory so future runs reuse workflow lessons.
Memory is what turns a stateless demo into a coherent, dependable agent. Sessions make it a solved problem.