AI
OpenAI Agents SDK Sessions: Memory SQLite, Redis dan Postgres
Oktober 202612 menit baca

Session adalah objek yang menyimpan riwayat percakapan untuk runner Agents SDK. Sebelum setiap run, runner membaca item tersimpan dan menaruhnya di depan input, lalu setelah run menambahkan setiap pesan, tool call dan tool output yang baru. Class apa pun yang punya session_id serta method async get_items, add_items, pop_item dan clear_session bisa berfungsi sebagai session.
Gunakan backend yang bisa dijangkau semua worker: RedisSession, SQLAlchemySession di Postgres atau MySQL, MongoDBSession, DaprSession atau OpenAIConversationsSession yang berjalan di server. SQLiteSession dan AsyncSQLiteSession menyimpan riwayat di file lokal, sehingga setiap container atau host punya salinannya sendiri. Pilih backend bersama yang sudah Anda operasikan dan backup.
Tidak. Agents SDK tidak mengizinkan session dalam satu run bersama opsi yang dikelola server, yaitu conversation_id, previous_response_id atau auto_previous_response_id. Session adalah memory yang dikelola client, dan mencampur dua lapisan itu bisa menggandakan konteks. Pilih satu strategi persistence per percakapan.
Objek conversation beserta item-nya tidak terkena TTL 30 hari yang berlaku untuk objek Response tersimpan, jadi tetap ada sampai Anda menghapusnya. Response yang berdiri sendiri disimpan 30 hari secara default kecuali Anda mengatur store menjadi false. Jika kebijakan Anda mewajibkan log chat dihapus setelah periode tertentu, penghapusan itu harus Anda jalankan sendiri terhadap OpenAI API.
Bungkus session apa pun dengan EncryptedSession, yang diinstal lewat extra encrypt dari openai-agents. Wrapper ini menurunkan Fernet key per session dari master key Anda dengan HKDF, memakai session id sebagai salt, dan bisa mengabaikan item yang lebih tua dari ttl opsional. Simpan master key di secret manager, karena menggantinya tanpa enkripsi ulang membuat riwayat lama tidak bisa dibaca.

Ringkasan Utama
Session di OpenAI Agents SDK menyimpan riwayat percakapan agar runner bisa menambahkannya di depan setiap turn. SQLiteSession cocok untuk satu proses; RedisSession dan SQLAlchemySession di Postgres cocok untuk banyak worker; OpenAIConversationsSession menyimpan riwayat di OpenAI tanpa batas 30 hari. EncryptedSession dan OpenAIResponsesCompactionSession membungkus backend apa pun untuk enkripsi dan compaction.
Laporan bug untuk masalah session hampir tidak pernah menyebut kata session. Isinya: asisten lupa purchase order yang disebut user dua pesan sebelumnya, dan itu pun hanya kadang-kadang. Agent-nya berjalan sempurna di laptop dengan SQLiteSession dan file lokal, lalu dipasang di belakang load balancer dengan tiga replica, dan setiap replica menyimpan conversations.db miliknya sendiri. User yang request keduanya mendarat di container lain sedang berbicara dengan agent yang sama sekali tidak mengingatnya.
Panduan ini membahas cara memilih backend memory untuk session OpenAI Agents SDK di deployment sungguhan, bukan di notebook. Isinya: apa yang sebenarnya dilakukan protokol Session, tabel perbandingan semua backend bawaan dan extension, konfigurasi production untuk RedisSession dan SQLAlchemySession di Postgres, aturan retensi di balik OpenAIConversationsSession yang berjalan di server, serta wrapper compaction dan enkripsi yang bisa dipasang di atas semuanya. Setiap nama class, argumen dan nilai default diambil dari dokumentasi Python SDK, versi 0.22.x saat tulisan ini dibuat.
Session adalah kontrak kecil, bukan framework. Objek apa pun yang punya session_id dan empat method async, yaitu get_items, add_items, pop_item dan clear_session, sudah dihitung sebagai session. Saat Anda memberikannya ke Runner.run, runner membaca item tersimpan sebelum pemanggilan dan menaruhnya di depan input, lalu setelah pemanggilan menambahkan setiap item baru yang dihasilkan run: pesan user, pesan assistant, tool call dan tool output. Anda tidak perlu lagi mengelola result.to_input_list() secara manual, dan beberapa agent bisa berbagi satu session, itulah yang membuat handoff terasa menyambung.
from agents import Agent, Runner, SQLiteSession
agent = Agent(name="ERP helpdesk", instructions="Answer questions about purchase orders.")
# Wrong for anything long-lived: no db_path means an in-memory database,
# so every conversation disappears when the process restarts.
session = SQLiteSession("user_123")
# Right for a single-process tool, a CLI or a notebook: a file on disk.
session = SQLiteSession("user_123", "conversations.db")
await Runner.run(agent, "What is the status of PO-2026-0412?", session=session)
# Before this second call the runner reads the history and prepends it;
# after it, every new item (messages, tool calls, tool outputs) is appended.
await Runner.run(agent, "And who approved it?", session=session)
# Wrong: a session plus server-managed continuation in the same run.
# The SDK does not allow it; pick one persistence strategy per conversation.
await Runner.run(agent, "...", session=session, previous_response_id="resp_...")Ada dua aturan yang lahir dari desain ini. Pertama, SQLiteSession default tanpa path file adalah database in-memory, jadi hilang begitu proses berhenti; itu fixture untuk test, bukan storage. Kedua, session adalah memory yang dikelola client, dan SDK tidak mengizinkan Anda menggabungkannya dalam satu run dengan opsi yang dikelola server, yaitu conversation_id, previous_response_id atau auto_previous_response_id. Dokumentasinya terus terang soal alasannya: mencampur riwayat yang dikelola client dengan state yang dikelola OpenAI bisa menggandakan konteks. Tentukan sekali di mana sebuah percakapan disimpan, per percakapan, dan sisa tulisan ini membahas cara membuat keputusan itu dengan benar.
SDK menyertakan tiga session di package intinya dan daftar yang lebih panjang di agents.extensions.memory. Perbedaannya bukan pada API, karena semuanya memenuhi empat method yang sama, melainkan pada tempat data disimpan dan karena itu apa yang terjadi saat Anda menjalankan lebih dari satu worker.
| Class session | Instalasi | Lokasi riwayat | Aman untuk banyak worker | Kedaluwarsa dan fitur tambahan |
|---|---|---|---|---|
| SQLiteSession | Package inti | In-memory, atau file SQLite lokal jika Anda mengisi db_path | Tidak. Setiap container atau host punya file sendiri | Tidak ada. Mempercayai file sepenuhnya dan tidak mendeteksi perubahan dari luar |
| AsyncSQLiteSession | aiosqlite | File SQLite lokal, diakses secara async | Tidak, dengan alasan yang sama | Tidak ada. Menghindari event loop yang terblokir di aplikasi async |
| RedisSession | openai-agents[redis] | Redis list berisi item ditambah hash metadata, di bawah key_prefix | Ya. Semua worker terhubung ke server yang sama | ttl opsional dalam detik. Ketahanan data bergantung pada setting persistence Redis Anda |
| SQLAlchemySession | openai-agents[sqlalchemy] | Tabel agent_sessions dan agent_messages di Postgres, MySQL atau SQLite | Ya, di database jaringan. Penulisan berjalan dalam transaction | Tidak ada kedaluwarsa bawaan. Retensi, backup dan migration tanggung jawab Anda |
| MongoDBSession | openai-agents[mongodb] | Collection agent_sessions dan agent_messages, satu dokumen batch per pemanggilan add_items | Ya | Urutan berdasarkan field seq yang terus naik. ping() memeriksa koneksi |
| DaprSession | openai-agents[dapr] | State store Dapr mana pun yang Anda konfigurasi | Ya | ttl opsional dan opsi strong consistency |
| OpenAIConversationsSession | Package inti | OpenAI Conversations API, di server OpenAI | Ya. Worker hanya butuh conversation id | Tidak terkena TTL response 30 hari. Tidak bisa dibungkus compaction session |
| AdvancedSQLiteSession | agents.extensions.memory | File SQLite lokal berisi turn, branch dan data usage | Tidak | Branching dari turn mana pun dan token usage per turn |
Baca kolom keempat lebih dulu. Jika jawabannya tidak, backend itu cocok untuk CLI, satu worker yang berjalan lama atau test suite, dan salah untuk apa pun yang di-scale secara horizontal. Di antara yang menjawab ya, pilihannya kebanyakan soal apa yang sudah Anda operasikan. Tim yang punya database Postgres dan tool migration paling diuntungkan oleh SQLAlchemySession; tim yang sudah menjalankan Redis untuk cache atau queue mendapat latency terendah dari RedisSession; tim yang tidak mau mengoperasikan apa pun memilih OpenAIConversationsSession dan menerima bahwa riwayat disimpan di OpenAI.
SQLiteSession bukan mainan, ia storage untuk satu proses, dan agent satu proses itu banyak. Backend ini tepat dalam tiga situasi:
Di luar kasus itu, kegagalannya sama seperti di pembuka: riwayatnya ada, hanya saja di disk lain. Sticky session di load balancer menyembunyikannya sampai deploy atau crash memindahkan user ke container baru. Perhatikan juga batas kepercayaan yang didokumentasikan SDK: SQLiteSession berasumsi aplikasi mempercayai file dan storage-nya, serta tidak mengautentikasi baris atau mendeteksi edit, penghapusan, perubahan urutan maupun replay. Apa pun yang bisa menulis ke file itu bisa mengubah apa yang diyakini agent pernah dikatakan. Jika Anda memakai SQLite di aplikasi async, gunakan AsyncSQLiteSession agar pembacaan riwayat tidak memblokir event loop.
RedisSession menyimpan setiap percakapan sebagai Redis list berisi item yang sudah diserialisasi, dengan hash di sampingnya untuk session_id, created_at dan updated_at. Key diberi namespace lewat key_prefix, default-nya agents:session, dan ttl opsional dalam detik membuat Redis menghapus percakapan yang ditinggalkan tanpa cleanup job. Keputusan pentingnya adalah siapa pemilik client. Di aplikasi web, buat satu async client per worker saat startup lalu inject, jangan memanggil from_url di setiap request.
# pip install "openai-agents[redis]"
from contextlib import asynccontextmanager
import redis.asyncio as redis
from fastapi import FastAPI, Request
from agents import Agent, Runner
from agents.extensions.memory import RedisSession
@asynccontextmanager
async def lifespan(app: FastAPI):
# One client and connection pool per worker, owned by the app, not the session.
app.state.redis = redis.from_url("redis://redis:6379/0")
yield
await app.state.redis.aclose()
app = FastAPI(lifespan=lifespan)
agent = Agent(name="ERP helpdesk")
@app.post("/chat/{conversation_id}")
async def chat(conversation_id: str, body: dict, request: Request):
user = request.state.user # set by your auth middleware
# Wrong: RedisSession(conversation_id, ...) lets any caller who guesses
# an id read someone else's history. Scope the key to the identity.
session = RedisSession(
f"{user.tenant}:{user.id}:{conversation_id}",
redis_client=app.state.redis, # injected, so close() is a no-op
key_prefix="erp:helpdesk", # default is "agents:session"
ttl=60 * 60 * 24 * 7, # time-to-live, in seconds
)
result = await Runner.run(agent, body["message"], session=session)
return {"reply": result.final_output}Session id adalah keputusan berikutnya, dan ini soal keamanan. Conversation id yang datang lewat URL adalah input user. Jika langsung dijadikan session id, siapa pun yang bisa menebak atau me-replay sebuah id dapat membaca riwayat user lain, termasuk setiap tool output yang diambil agent atas nama mereka, yang di konteks ERP berarti nilai invoice dan nama supplier. Susun id dari tenant dan user yang sudah terautentikasi ditambah percakapannya, seperti contoh di atas. Sebelum mengandalkan ttl sebagai sliding expiry untuk chat yang menganggur, periksa di Redis Anda sendiri apakah TTL key diperbarui setiap kali ada penulisan, karena dokumentasinya hanya menyebut ttl sebagai time-to-live dalam detik.
RedisSession.from_url membuat dan memiliki Redis client-nya sendiri. Setelah close(), session itu berakhir dan setiap operasi berikutnya melempar RuntimeError. Session yang dibuat dengan redis_client=... berperilaku sebaliknya: close() tidak melakukan apa-apa dan pemanggil tetap memiliki client. Mencampur keduanya dalam satu codebase adalah cara shutdown hook akhirnya merusak request yang masih berjalan.
SQLAlchemySession adalah backend yang akan saya pilih untuk back office ERP, karena percakapan tersimpan di dekat data yang dibicarakan, di bawah backup, kontrol akses dan kebijakan retensi yang sama. Backend ini bekerja dengan database apa pun yang didukung SQLAlchemy lewat driver async; extra sqlalchemy sudah menyertakan asyncpg untuk URL yang diawali postgresql+asyncpg, MySQL butuh aiomysql, dan SQLite butuh aiosqlite.
# pip install "openai-agents[sqlalchemy]" (asyncpg comes with the extra)
from sqlalchemy.ext.asyncio import create_async_engine
from agents.extensions.memory import SQLAlchemySession
# One engine per worker process. Building it per request opens a new pool
# per request, and Postgres runs out of connections long before you run out of users.
engine = create_async_engine(
"postgresql+asyncpg://agents:***@db:5432/erp",
pool_size=10,
pool_pre_ping=True,
)
def session_for(tenant: str, user_id: str, conversation_id: str) -> SQLAlchemySession:
return SQLAlchemySession(
f"{tenant}:{user_id}:{conversation_id}",
engine=engine,
create_tables=False, # the constructor default: tables come from migrations
sessions_table="agent_sessions",
messages_table="agent_messages",
ensure_ascii=False, # keep "Faktur sudah disetujui" readable in the JSON column
)
# Note: SQLAlchemySession.from_url(...) builds its own engine. Fine for a script,
# wasteful in a web app. On shutdown: await engine.dispose()Tiga argumen di blok itu lebih penting daripada kelihatannya. create_tables default-nya False di constructor, dan itu disengaja, karena di production tabel agent_sessions dan agent_messages seharusnya dibuat oleh migration Anda, bukan oleh pod mana pun yang start duluan; contoh quick-start di dokumentasi memakai True demi kemudahan. ensure_ascii default-nya True untuk mempertahankan format storage lama, yang menyimpan teks bahasa Indonesia atau teks non-ASCII apa pun sebagai escape sequence, jadi set ke False jika ada yang akan meng-query JSON-nya. Dan gunakan ulang satu engine: from_url membuat engine baru setiap kali dipanggil, tidak masalah di script tetapi menjadi kebocoran koneksi di request handler. Di jalur penulisan, add_items memasukkan batch di dalam transaction yang mengunci session, dan pop_item menghapus item terbaru dengan DELETE ... RETURNING jika database mendukungnya, sehingga dua penulisan bersamaan ke satu percakapan tidak saling menyela di tengah batch.
Untuk sistem multi-tenant, custom session bisa menerima argumen keyword-only wrapper di keempat method dan menerima RunContextWrapper milik run. Dengan begitu satu class session bisa mengarahkan setiap tenant ke schema atau database-nya sendiri, atau menolak akses, memakai objek context yang sama dengan yang sudah dibaca tool Anda.
OpenAIConversationsSession menyimpan riwayat di OpenAI Conversations API, bukan di infrastruktur Anda. Buat tanpa argumen untuk membuat percakapan baru, atau isi conversation_id untuk melanjutkan percakapan lama; setiap worker cukup memegang id itu. Conversations menyimpan pesan, tool call, tool output dan item lain sebagai objek yang tahan lama.
Retensi adalah pembeda utamanya dari semua backend lain di tabel, dan mudah terbalik memahaminya. Objek response disimpan 30 hari secara default, dan Anda bisa mematikannya dengan store bernilai false. Objek conversation beserta item-nya tidak terkena TTL 30 hari, dan setiap response yang terhubung ke sebuah conversation menyimpan item-nya tanpa TTL 30 hari. Jadi opsi di server justru bukan yang berumur pendek; riwayat disimpan sampai Anda menghapusnya. Jika kebijakan perlindungan data Anda mewajibkan log chat dihapus setelah 90 hari, penghapusan itu kini menjadi job yang Anda jalankan terhadap OpenAI API, bukan cron terhadap database Anda.
Biaya adalah pertimbangan berikutnya. Dengan state yang dikelola server Anda hanya mengirim turn baru, tetapi dokumentasinya mencatat bahwa bahkan dengan previous_response_id, semua input token sebelumnya di rantai itu tetap ditagih sebagai input token. Storage di server menghemat bandwidth dan kode, bukan token. Yang mengendalikan tagihan token adalah seberapa banyak riwayat yang sampai ke model, dan itu tugas wrapper serta limit di dua bagian berikutnya.
Dua wrapper menerima session apa pun sebagai underlying_session dan dirinya sendiri juga session, sehingga bisa disusun bertingkat. OpenAIResponsesCompactionSession memanggil endpoint compaction di Responses API untuk mengganti riwayat panjang dengan context window yang sudah dipadatkan, otomatis setelah sebuah turn berdasarkan should_trigger_compaction, atau sesuai permintaan lewat run_compaction. EncryptedSession mengenkripsi setiap item sebelum sampai ke storage di bawahnya, dengan ttl opsional.
# pip install "openai-agents[encrypt,sqlalchemy]"
import os
from agents import Runner
from agents.extensions.memory import EncryptedSession, SQLAlchemySession
from agents.memory import OpenAIResponsesCompactionSession
base = SQLAlchemySession(session_id, engine=engine)
encrypted = EncryptedSession(
session_id=session_id,
underlying_session=base,
# Master key. HKDF derives a separate Fernet key per session, salted
# with the session id, so one leaked row decrypts nothing else.
encryption_key=os.environ["AGENT_SESSION_KEY"],
ttl=60 * 60 * 24 * 30, # items older than 30 days are silently ignored on read
)
session = OpenAIResponsesCompactionSession(
session_id=session_id,
underlying_session=encrypted,
# Auto-compaction waits before the run completes and can stall a stream.
# Disable it on the hot path and compact from a background job instead.
should_trigger_compaction=lambda _: False,
)
result = await Runner.run(agent, message, session=session)
# Later, from an idle hook or a worker, through the same wrapper:
await session.run_compaction({"force": True})
# Wrong: base.add_items(...) while compaction runs. Direct writes to the
# underlying session bypass the wrapper's serialisation and recovery.Detail yang menentukan apakah ini berjalan di production semuanya ada di dokumentasi dan mudah terlewat. Auto-compaction bisa memblokir streaming, karena berjalan sebelum run selesai; di endpoint chat, matikan saja dan lakukan compaction saat idle atau setiap N turn. compaction_mode default-nya auto, yang beralih ke membangun ulang request dari item session saat agent berjalan dengan store bernilai false, dan mode input menjadikan isi session sebagai sumber kebenaran. Wrapper ini menserialisasi add_items, pop_item dan clear_session terhadap compaction yang sedang berjalan, dan melewati compaction yang sudah basi jika run lain mengubah riwayat lebih dulu, tetapi hanya untuk penulisan yang lewat wrapper. Jangan bungkus OpenAIConversationsSession dengannya, karena keduanya mengelola riwayat dengan cara berbeda. Untuk enkripsi, EncryptedSession menurunkan Fernet key 32 byte per session dengan HKDF, memakai master key Anda dan session id sebagai salt, dan item yang lebih tua dari ttl diabaikan diam-diam saat dibaca, bukan melempar error.
Instal wrapper dengan extra encrypt, misalnya pip install openai-agents[encrypt,sqlalchemy] untuk session Postgres yang terenkripsi. Simpan master key di secret manager, bukan di repo: mengganti key tanpa mengenkripsi ulang membuat semua percakapan lama tidak bisa dibaca.
Menyimpan semuanya dan mengirim semuanya adalah dua keputusan terpisah. SessionSettings dengan limit membatasi berapa banyak item terbaru yang dikembalikan session, dan RunConfig menerima session_input_callback yang menerima riwayat yang diambil serta input baru, lalu mengembalikan persis apa yang dikirim ke model. AdvancedSQLiteSession menambahkan hal yang tidak dimiliki backend lain: daftar turn, pencarian konten, branching dari turn lama dan token usage per turn.
from agents import RunConfig, Runner, SessionSettings
from agents.extensions.memory import AdvancedSQLiteSession, SQLAlchemySession
# Cap what is read back per run, not what is stored.
session = SQLAlchemySession(sid, engine=engine, session_settings=SessionSettings(limit=40))
def keep_recent_history(history, new_input):
# history = what the session returned; new_input = this turn.
# The return value is exactly what the model sees.
return history[-10:] + new_input
result = await Runner.run(
agent, message, session=session,
run_config=RunConfig(session_input_callback=keep_recent_history),
)
# Branching and per-turn token accounting: AdvancedSQLiteSession only.
review = AdvancedSQLiteSession(
session_id="po-review-77", db_path="review.db", create_tables=True
)
result = await Runner.run(agent, "Draft the approval note for PO-77", session=review)
await review.store_run_usage(result)
turns = await review.get_conversation_turns()
await review.create_branch_from_turn(2) # try another answer from turn 2
result = await Runner.run(agent, "Make it stricter on budget", session=review)
await review.switch_to_branch("main")
print(await review.get_session_usage())Sebisa mungkin pangkas di batas turn, bukan berdasarkan jumlah item mentah. Satu turn user bisa menghasilkan pesan, beberapa tool call, output-nya dan reasoning item, dan SDK mendokumentasikan error di run multi-turn ketika sebuah reasoning item terpisah dari item pasangannya. Uji setiap callback terhadap percakapan yang berisi tool call sebelum dirilis. Branching lebih sempit dari kedengarannya: ini fitur AdvancedSQLiteSession, jadi cocoknya untuk tool review dan harness evaluasi, di mana Anda ingin mengulang turn kedua dengan instruksi berbeda lalu membandingkan biaya token dengan get_session_usage, bukan untuk backend chat yang di-scale horizontal.
Aturan yang saya pegang: pilih session berdasarkan bentuk deployment dulu, fitur belakangan. Satu proses berarti SQLiteSession; banyak worker berarti RedisSession atau SQLAlchemySession di database yang sudah Anda backup; tanpa infrastruktur berarti OpenAIConversationsSession dengan retensi yang Anda hapus sendiri. Setelah itu susun session id dari user yang terautentikasi, tambahkan EncryptedSession saat riwayat berisi data bisnis, dan jalankan compaction di luar jalur utama request.