# Add Durable Session Storage to the OpenAI Agents SDK

> A working example that persists OpenAI Agents SDK runs with tool calls intact, reloads them after a redeploy, and adds memory and semantic search.

Canonical: https://dialoguedb.com/blog/durable-session-storage-openai-agents-sdk
Published: 2026-08-18
Updated: 2026-08-21

Conversation Persistence & Integrations

# Add Durable Session Storage to the OpenAI Agents SDK

August 18, 2026

![Add Durable Session Storage to the OpenAI Agents SDK](https://dialoguedb.com/images/durable-session-storage-openai-agents-sdk-feature-image.jpg)

[ Back to Blog](https://dialoguedb.com/blog)

Most of what makes an agent feel intelligent is that it seems to be paying attention. It holds the thread of a conversation, remembers what you asked it for, reaches for a tool when it needs one, and builds on what came before instead of starting cold. That sense of continuity is most of the distance between an assistant and a glorified search box, and while you are building it feels almost free. The OpenAI Agents SDK gives you an in-memory session store, [MemorySession](https://openai.github.io/openai-agents-js/guides/sessions/), that keeps the entire conversation in your application’s memory, so every run picks up right where the last one left off.

The catch is hiding in that word. Everything the agent knows about a user lives inside a running process, so it lasts exactly as long as that process does. The SDK is candid about this (its docs describe MemorySession as intended for local development), and the limit is structural: deploy a new version, spread the load across a second instance, or let a serverless function go cold, and that history evaporates along with it. The agent that felt so attentive while you were building it becomes forgetful the moment real traffic arrives, meeting each returning user as though they had never spoken before.

The obvious answer is to write the conversation somewhere permanent, and that is where a lot of first attempts quietly come apart. A run does not hand back a tidy transcript of who said what. It hands back the whole record of how the agent got to its answer: the messages, certainly, but also the tools it called, the results those calls returned, and the points where it passed control to another agent. Flatten all of that into plain text and you have kept the words while throwing away the work, and the SDK will refuse to replay the thinned-out history you try to feed it.

This post follows a small example you can clone and run that keeps that record intact from one end to the other: the history loads before the run, the agent runs exactly as it already does, and only the new items from each turn are saved, tool calls and all. Those runs live in DialogueDB. It’s a conversation database, so the complete structured history of each run stays intact instead of flattened into text. Once that history is durable, three things an in-memory session could never offer come for free: memory that follows a user from one session into the next, search across past conversations by meaning rather than exact wording, and shared context that travels on its own when one agent hands off to another.

## What durable session storage adds to an agent

The integration is a round trip with three legs. Before the run, the stored dialogue is loaded and mapped back into run items. The run itself happens exactly as it does today. After it resolves, the items it added are written back. Every leg is explicit, and nothing about the agent, its tools, or its handoffs changes.

How an agent turn round-trips

Persisting a turn

Agent run run()

result.history

This turn's items message function\_call function\_call\_result message

saveMessages

DialogueDB dialogue history

 redeploy, cold start, or new instance

Seeding the next turn

DialogueDB same dialogue

loadMessages

Validated items ModelItem.parse

...history

Next run seeded with history

MemorySession keeps these items inside the process that created them, so a restart wipes the conversation. Persisted in DialogueDB, the same items seed the next run in whichever process picks it up, tool calls included.

A restart clears whatever MemorySession was holding, but with the items in DialogueDB the next turn rebuilds the same history in whichever process handles it, so the agent continues instead of starting over. And because those runs now live in DialogueDB, three more capabilities come from the same data:

- **[Memory that carries between sessions](https://dialoguedb.com/architecture/conversation-memory):** Keep facts about a user separate from the message list and recall them on the next visit.
- **[Search across past runs by meaning](https://dialoguedb.com/architecture/auto-vectorization):** Find where a topic came up without matching exact words, and without running a vector database.
- **[Per-user isolation](https://dialoguedb.com/architecture/multi-tenant):** Every call carries a namespace, so one user’s agent never reads another user’s history.

## How the integration works

The integration is deliberately explicit: a load before `run()` and a save after, wired around the call rather than tucked inside a drop-in `Session` class. That is the design, and it pays off twice. The wiring is a handful of lines you can read in one sitting, and it leaves you in full control of exactly what gets stored and when.

The load and the save meet at one mapping problem. An `AgentInputItem` is a large discriminated union, and only the three message variants carry a `role`. Tool calls, tool results, and reasoning items have none. Flatten items to `{ role, content: string }` and every tool call is silently destroyed. Worse, the assistant message variant is rejected on reload, because its `content` must be an array and its `status` field is required. The fix is to store each item verbatim as structured content and let the SDK’s own schema validate it on the way back:

```typescript
// persist.ts
import { protocol, type AgentInputItem } from '@openai/agents'

export const toStoredMessages = (items: AgentInputItem[]) =>
  items.map((item) => ({
    role: 'role' in item ? item.role : 'assistant', // storage label only; replay never reads it
    content: item, // stored verbatim, so tool calls survive
  }))

export const fromStoredMessages = (rows: readonly { content: unknown }[]) =>
  rows.map((row) => protocol.ModelItem.parse(row.content))

```

DialogueDB’s message `content` accepts a structured object as readily as a string, which is what makes the verbatim store possible: no stringify, no lossy re-parse, no type assertion on the way out. `protocol.ModelItem.parse` is the SDK’s own Zod schema, so anything that comes back is exactly what `run()` expects.

With the mapper in place, one function runs a fully persisted turn. See how each step maps to the code that runs it:

1 Load the stored run getOrCreateDialogue scopes the dialogue to this user, loadMessages rehydrates every prior item. 2 Recall memory for this user searchMemories matches stored facts against the new input by meaning, in the same namespace. 3 Inject memory into the agent The recalled values go into the agent instructions for this turn. 4 Run on history plus input run() is seeded with the rebuilt items, so the agent picks up exactly where it stopped. 5 Persist what the turn added saveMessages writes only the items result.history gained, tool calls and results included.

src/index.ts

import { run } from "@openai/agents"

import { DialogueDB } from "dialogue-db"

import { buildAgent } from "./agent"

import { fromStoredMessages, toStoredMessages } from "./persist"

const db \= new DialogueDB ({ apiKey: process.env.DIALOGUE\_DB\_API\_KEY! })

const namespace \= "user\_123" // namespace is the user id

export async function turn (id: string, userInput: string) {

const dialogue \= await db.getOrCreateDialogue ({ id, namespace })

await dialogue.loadMessages ({ order: "asc" })

const history \= fromStoredMessages (dialogue.messages)

const { results } \= await db.searchMemories (userInput, { namespace, limit: 3 })

const recalled \= results.map ((r) \=> String (r.item.value))

const agent \= buildAgent (recalled)

const result \= await run (agent, \[

...history,

{ type: "message", role: "user", content: userInput },

\])

await dialogue.saveMessages (

toStoredMessages (result.history.slice (history.length)),

)

return result.finalOutput

}

Hover a step to see the lines it maps to.

Streaming changes nothing structural. A streamed run is `run(agent, input, { stream: true })`. Forward the events to your UI, wait for [stream.completed](https://openai.github.io/openai-agents-js/guides/streaming/), and persist the same slice afterward:

```typescript
const stream = await run(agent, [...history, userMessage], { stream: true })
// forward stream events to your UI as they arrive
await stream.completed
await dialogue.saveMessages(toStoredMessages(stream.history.slice(history.length)))

```

### See the full OpenAI Agents SDK integration

The whole pattern step by step: sessions, memory, semantic search, handoffs, and exactly what DialogueDB sees.

[See the integration](https://dialoguedb.com/integrations/openai-agents-sdk)

## Why a conversation database

The example stores its runs in DialogueDB, a conversation database: one client that persists structured message history and, over that same data, provides memory, semantic search, threading, and per-user isolation, all scoped by a namespace and one API key. The point is not that agent items need a special store. It is that the moment runs are durable, the next three feature requests (remember this user, find that past conversation, keep tenants apart) are already served by the data you just persisted.

Memory in DialogueDB is caller-controlled. No background process reads transcripts and guesses at facts. The application calls `createMemory` with the value worth keeping, and `searchMemories` recalls it by meaning on a later turn, which is exactly what step 2 of the walkthrough above does. [Multi-Agent Memory Architecture](https://dialoguedb.com/blog/multi-agent-memory-architecture) covers how to decide what each agent should be allowed to recall.

Handoffs are where the namespace model earns its place. The Agents SDK moves control between agents cleanly, but the receiving agent still needs the context of turns it was not present for. When every agent in the app loads and saves against the same namespace, a handoff target starts with the full dialogue history and the same memories as the agent that handed off. There is no first-class handoff API in DialogueDB, and none is needed. Shared context falls out of shared storage.

What a handoff inherits

Triage agent takes the first turns

handoff

Billing agent picks up mid-thread

reads + writes

reads + writes

namespace: "user\_123"

Dialogue history

every message and tool call

Memories

facts saved across sessions

Both agents load and save against the same namespace, so the billing agent starts with the full history and the same memories. The handoff carries context because the storage is shared, not because either agent forwarded it.

## Roll-your-own vs DialogueDB for agent sessions

None of this requires DialogueDB. The SDK’s [sessions guide](https://openai.github.io/openai-agents-js/guides/sessions/) points at `OpenAIConversationsSession` for hosted persistence, and plenty of teams write agent items into SQLite or Postgres themselves. The storage is the easy half. The difference is how much sits around the storage before the result matches what the example already does:

| You handle                       | SQLite or roll-your-own     | DialogueDB                   |
| -------------------------------- | --------------------------- | ---------------------------- |
| Item schema and migrations       | Design and maintain         | Provided                     |
| Structured items (tool calls)    | Serialize and rebuild       | Stored as structured content |
| Redeploys and multiple instances | A database server to run    | Managed                      |
| Per-user isolation               | Query code on every read    | namespace on every call      |
| Memory across sessions           | A second store to build     | Same client                  |
| Search past runs by meaning      | Add and sync a vector store | Built in                     |

A SQLite file is a fine local start, and it fails for the same reason MemorySession does: serverless filesystems are ephemeral, and two instances never share a disk. Postgres fixes durability and adds a schema to design, a message format to keep stable, and a vector layer to bolt on once search comes up. DialogueDB is managed, so the storage runs and scales without a server for you to keep healthy, and messages, memory, and search arrive as one integration. For the wider survey of the options, [Where Should Your Agent’s Memory Live?](https://dialoguedb.com/blog/where-should-agent-memory-live) compares storage backends, and [Conversation Persistence in TypeScript AI Agent Frameworks](https://dialoguedb.com/blog/conversation-persistence-typescript-agent-frameworks) traces this same message-shape problem across SDKs.

## Frequently asked questions

Is there a drop-in Session class for DialogueDB?

The integration is explicit wiring rather than a drop-in Session subclass, and that is by design: load messages before the run, run the agent normally, and persist the new items after it resolves. It is a few lines you can read in the example, and it keeps you in control of exactly what is stored and when.

Does it work with streaming runs?

Yes. Start the run with stream: true, forward the events to your UI, and await stream.completed. The history is complete once the run resolves, so the same slice-and-save call persists a streamed turn.

Do tool calls survive persistence?

Yes. Each run item is stored verbatim as structured content rather than flattened to text, and parsed back through the SDK's own item schema on load, so function calls and their results round-trip exactly.

Can I migrate from an existing session store?

Yes. Read your existing conversation table and group rows by session ID, which becomes the dialogue ID. Call saveMessages for each dialogue, and map your user identifier column to the namespace.

Does this work with the Python Agents SDK?

Not today. The dialogue-db client is TypeScript, so this integration targets the OpenAI Agents SDK for JavaScript and TypeScript. There is no official Python SDK yet; from Python you can call the DialogueDB REST API directly.

## Run the example

Clone the example, add your DialogueDB and OpenAI keys, and start it:

```bash
git clone https://github.com/dialoguedb/examples
cd examples/openai-agents-sdk
npm install
cp .env.example .env
npm start

```

The run plays out in three acts: turn one makes two real tool calls, a cold reload prints the `function_call` and `function_call_result` items coming back from DialogueDB intact, and turn two answers from that reloaded history plus a recalled memory. From there, the same `persist.ts` and `turn()` drop into your own app. The [integration page](https://dialoguedb.com/integrations/openai-agents-sdk) walks through the whole pattern, and the [quickstart](https://docs.dialoguedb.com/guide/quickstart) gets you an API key.

### Ready to Build Better Conversations?

Get started with DialogueDB in minutes. Free tier included.

[Get Your API Key](https://app.dialoguedb.com/signup)

### Share

[](https://x.com/intent/tweet?url=https%3A%2F%2Fdialoguedb.com%2Fblog%2Fdurable-session-storage-openai-agents-sdk&text=Add%20Durable%20Session%20Storage%20to%20the%20OpenAI%20Agents%20SDK) [](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fdialoguedb.com%2Fblog%2Fdurable-session-storage-openai-agents-sdk) [](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fdialoguedb.com%2Fblog%2Fdurable-session-storage-openai-agents-sdk)

Conversation Persistence Integrations Agent Memory Semantic Search

### Latest Posts

- [Multi-Tenant Conversation Isolation with Postgres RLS and pgvector](https://dialoguedb.com/blog/postgres-rls-pgvector-multi-tenant-isolation)
- [Add Durable Session Storage to the OpenAI Agents SDK](https://dialoguedb.com/blog/durable-session-storage-openai-agents-sdk)
- [Add Persistent Chat History to the Vercel AI SDK](https://dialoguedb.com/blog/add-persistent-chat-history-vercel-ai-sdk)

[ Previous](https://dialoguedb.com/blog/postgres-rls-pgvector-multi-tenant-isolation) [Next](https://dialoguedb.com/blog/add-persistent-chat-history-vercel-ai-sdk)
