0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

The desk that remembers with a session

SQLiteSession is a store that keeps a conversation between runs, so the desk's second turn still knows the order from the first.

Last updated: 28 Sep, 2026 · openai-agents 0.22.3

The desk from The desk agent and its tools forgets everything the moment a run ends. A session, from Sessions: memory across runs with SQLiteSession, gives it memory that survives from one run to the next.

The pieces, and where each came from

PieceFrom lesson
The desk with its lookup toolThe desk agent and its tools
SQLiteSession("customer-1")Sessions: memory across runs with SQLiteSession
session= on Runner.run_syncRunner and the RunResult object

The desk and a session

A SQLiteSession is created with an id that names the conversation, one per customer.

python
desk = Agent(name="Shop desk",
             instructions="Help shoppers with their orders.",
             tools=[lookup_order], model=DeskMemoModel())
session = SQLiteSession("customer-1")   # this customer's memory

A stand-in that reads the whole history

The model does not echo the last tool result. It scans every user message for an order id, so a later turn that names no id still knows which order to answer about. A question with no remembered order gets only an offer to help.

python
found = re.search(r"\b[A-Z]\d+\b", all_user_text(input))   # scan the whole history
order_id = found.group(0) if found else None
if order_id and any(w in latest for w in ("ship", "yet", "where")):
    reply = f"Your order {order_id} shipped on 3 March and arrives 7 March."
else:
    reply = "How can I help with your order?"   # no remembered order, no order line

Two turns on one session

Pass the same session to both runs. The second turn reads the first from the store.

python
turn1 = Runner.run_sync(desk, "Where is my order A17?", session=session)
print("Turn 1:", turn1.final_output)
turn2 = Runner.run_sync(desk, "Has it shipped yet?", session=session)
print("Turn 2:", turn2.final_output)   # answered from memory, no new lookup

The desk remembering across two turns

Two runs share one session, so the second turn answers about the order without naming it again. A no-session run and an unrelated question then show the memory is real, not an echo of the last tool result.

Example
from agents import Agent, Runner, function_tool, set_tracing_disabled, SQLiteSession
from agents.models.interface import Model
from agents.items import ModelResponse
from agents.usage import Usage
from openai.types.responses import (
    ResponseOutputMessage, ResponseOutputText, ResponseFunctionToolCall,
)
import re
set_tracing_disabled(True)


def _message(text):
    return ResponseOutputMessage(
        id="msg", role="assistant", type="message", status="completed",
        content=[ResponseOutputText(text=text, type="output_text", annotations=[])],
    )


def _tool_call(name, arguments):
    return ResponseFunctionToolCall(
        id="fc", call_id="call_1", name=name, arguments=arguments, type="function_call",
    )


def all_user_text(input):
    if isinstance(input, str):
        return input
    parts = []
    for it in input:
        d = it if isinstance(it, dict) else it.__dict__
        if d.get("role") == "user":
            c = d.get("content")
            if isinstance(c, str):
                parts.append(c)
            elif isinstance(c, list):
                parts.append(" ".join(
                    (p if isinstance(p, dict) else p.__dict__).get("text", "") for p in c))
    return " ".join(parts)


def latest_user_text(input):
    if isinstance(input, str):
        return input
    for it in reversed(input):
        d = it if isinstance(it, dict) else it.__dict__
        if d.get("role") == "user":
            c = d.get("content")
            if isinstance(c, str):
                return c
            if isinstance(c, list):
                return " ".join(
                    (p if isinstance(p, dict) else p.__dict__).get("text", "") for p in c)
    return ""


def newest_item(input):
    if isinstance(input, str) or not input:
        return {}
    last = input[-1]
    return last if isinstance(last, dict) else last.__dict__


class DeskMemoModel(Model):
    async def get_response(self, system_instructions, input, model_settings, tools,
                           output_schema, handoffs, tracing, **k):
        # A lookup ran this turn: answer from its result.
        if newest_item(input).get("type") == "function_call_output":
            return ModelResponse(output=[_message(newest_item(input).get("output"))],
                                 usage=Usage(), response_id=None)
        latest = latest_user_text(input).lower()
        # Read the order id from the WHOLE history, so a later turn still has it.
        found = re.search(r"\b[A-Z]\d+\b", all_user_text(input))
        order_id = found.group(0) if found else None
        if tools and order_id and "order" in latest:      # a named order: look it up
            return ModelResponse(output=[_tool_call("lookup_order",
                '{"order_id": "%s"}' % order_id)], usage=Usage(), response_id=None)
        if order_id and any(w in latest for w in           # a follow-up about that order
                            ("ship", "arriv", "deliver", "status", "yet", "where")):
            return ModelResponse(output=[_message(
                "Your order %s shipped on 3 March and arrives 7 March." % order_id)],
                usage=Usage(), response_id=None)
        return ModelResponse(output=[_message("How can I help with your order?")],
                             usage=Usage(), response_id=None)

    async def stream_response(self, *a, **k):
        raise NotImplementedError


@function_tool
def lookup_order(order_id: str) -> str:
    "Look up an order by its id."
    return f"Order {order_id}: shipped on 3 March, arriving 7 March."


desk = Agent(name="Shop desk", instructions="Help shoppers with their orders.",
             tools=[lookup_order], model=DeskMemoModel())

# Two turns share one session: turn 2 names no order id and still answers about A17.
session = SQLiteSession("customer-1")
print("Turn 1:", Runner.run_sync(desk, "Where is my order A17?", session=session).final_output)
print("Turn 2:", Runner.run_sync(desk, "Has it shipped yet?", session=session).final_output)

# No session: the same turn 2 has no earlier order in view, so it cannot answer.
print("No-session turn 2:", Runner.run_sync(desk, "Has it shipped yet?").final_output)

# An unrelated follow-up on a remembering session does not print the order line.
other = SQLiteSession("customer-2")
Runner.run_sync(desk, "Where is my order A17?", session=other)
print("Unrelated turn 2:", Runner.run_sync(desk, "Do you sell gift cards?", session=other).final_output)

What the session carried

  • Turn 1 found the id A17 in the message, looked it up with the tool, and answered with the order line.
  • Turn 2 named no id, yet the model read A17 from the replayed first turn and answered about it, with no new lookup.
  • Without a session the same second turn has no earlier order in view, so it can only offer to help.
  • An unrelated question on a remembering session does not print the order line, because the model answers the question that was asked.

A second turn with a session vs without

Second turn setupThe desk's reply
Same SQLiteSession as turn 1Your order A17 shipped on 3 March and arrives 7 March.
No sessionHow can I help with your order?

When the desk needs memory

  • A customer asks a follow-up that only makes sense with the earlier turn in view.
  • One person's chat must not bleed into another's, so each gets their own session id.
Watch out. Memory follows the session id. A new id on the second turn is a new conversation, and the desk forgets what was said a moment ago.
Try it yourself
  • Add a third turn on the same session and confirm the order is still remembered.
  • Give turn 2 a different session id and watch the desk forget.
  • Ask an unrelated question on turn 2 and see the order line does not appear.

Slow is fine. Stopping is the only problem.