Skip to content

Agents SDK session

The OpenAI Agents SDK keeps conversation history in a session. openai-agents-persql (Python) and @persql/openai-agents (TypeScript) store those sessions in a PerSQL database, so agent threads survive restarts and live in an isolated SQLite database you can query like any other.

pip install openai-agents-persql
import os
from agents import Agent, Runner
from persql import PerSQL
from openai_agents_persql import PerSQLSession
client = PerSQL(token=os.environ["PERSQL_TOKEN"])
session = PerSQLSession("user-42", client.database("acme/agent-state"))
agent = Agent(name="assistant", instructions="Be helpful.")
await Runner.run(agent, "hi", session=session)
await Runner.run(agent, "remember me?", session=session) # history persists

Async apps pass an async database handle instead — session calls are awaited natively rather than dispatched to a thread:

from persql import AsyncPerSQL
async with AsyncPerSQL(token=os.environ["PERSQL_TOKEN"]) as client:
session = PerSQLSession("user-42", client.database("acme/agent-state"))
await Runner.run(agent, "hi", session=session)

The session creates two tables on first use (agent_sessions, agent_messages) — the same layout as the SDK’s built-in SQLiteSession, so get_items, pop_item, and clear_session behave identically.

@persql/openai-agents is the same session for the JS Agents SDK:

npm install @persql/openai-agents @persql/sdk
import { Agent, run } from "@openai/agents";
import { PerSQL } from "@persql/sdk";
import { PerSQLSession } from "@persql/openai-agents";
const client = new PerSQL({ token: process.env.PERSQL_TOKEN });
const session = new PerSQLSession("user-42", client.database("acme/agent-state"));
const agent = new Agent({ name: "assistant", instructions: "Be helpful." });
await run(agent, "hi", { session });
await run(agent, "remember me?", { session }); // history persists

Both packages use the same table layout, but the two SDKs name tool-call fields differently (callId vs call_id) — keep a given session’s history to one SDK.

A session keys history by session_id inside one database. PerSQL’s shape lets you go further: give each agent — or each run — its own database, so state, blast radius, and spend are isolated per agent rather than commingled.

info = client.databases.create(f"agent-{run_id}", ttl_days=1)
session = PerSQLSession(run_id, client.database(f"acme/{info['slug']}"))

For ephemeral runs, lease a branch with a TTL and a scoped token instead — the branch (and its sessions) clean themselves up.

Conversation items are rows. Operational questions that are awkward against an opaque store are one query here — via the console, MCP, or any SDK:

SELECT session_id, COUNT(*) AS items, MAX(created_at) AS last_active
FROM agent_messages GROUP BY session_id ORDER BY 2 DESC LIMIT 20;

Both SDKs’ local modes run the same session against in-process SQLite:

session = PerSQLSession("test", PerSQL(local=":memory:").database("test/db"))
const session = new PerSQLSession("test", new PerSQL({ local: ":memory:" }).database("test/db"));

The Runner reads history once and appends new items once per turn, so a remote session adds two HTTP round-trips per turn — the same trade-off as the SDK’s SQLAlchemy session against a remote database. Items are metered like any other usage: a typical turn reads the history and writes a few rows. See pricing for rates.

Pair the session with structured agent memory — the same database that holds the conversation can hold facts and episodes the agent recalls with SQL.