Pydantic AI message history
Pydantic AI serializes a run’s message history to JSON and leaves storage to
you. PerSQL makes that one table: store the serialized history per thread, load
it back with ModelMessagesTypeAdapter, and pass it to the next run.
pip install pydantic-ai persqlimport osfrom pydantic_ai import Agentfrom pydantic_ai.messages import ModelMessagesTypeAdapterfrom persql import PerSQL
client = PerSQL(token=os.environ["PERSQL_TOKEN"])db = client.database("acme/agent-state")
db.query( """CREATE TABLE IF NOT EXISTS chat_history ( thread_id TEXT PRIMARY KEY, messages TEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)""")
agent = Agent("openai:gpt-5", instructions="Be concise.")
def load_history(thread_id: str): rows = db.query( "SELECT messages FROM chat_history WHERE thread_id = ?", [thread_id] )["data"] if not rows: return None return ModelMessagesTypeAdapter.validate_json(rows[0]["messages"])
def save_history(thread_id: str, result) -> None: db.query( """INSERT INTO chat_history (thread_id, messages) VALUES (?, ?) ON CONFLICT(thread_id) DO UPDATE SET messages = excluded.messages, updated_at = CURRENT_TIMESTAMP""", [thread_id, result.all_messages_json().decode()], )
result = agent.run_sync("hi", message_history=load_history("user-42"))save_history("user-42", result)The next run_sync with the same thread_id continues the conversation —
history survives restarts and lives in a database you can inspect with SQL.
Tool calls and SQL belong next to the history
Section titled “Tool calls and SQL belong next to the history”The same database that holds the history can be the agent’s workspace: hand the agent typed query tools over its own tables, or lease it a branch with a TTL for ephemeral runs. For durable graph state in LangGraph instead, use the LangGraph checkpointer.