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
| Piece | From lesson |
|---|---|
| The desk with its lookup tool | The desk agent and its tools |
| SQLiteSession("customer-1") | Sessions: memory across runs with SQLiteSession |
| session= on Runner.run_sync | Runner and the RunResult object |
The desk and a session
A SQLiteSession is created with an id that names the conversation, one per customer.
desk = Agent(name="Shop desk",
instructions="Help shoppers with their orders.",
tools=[lookup_order], model=DeskMemoModel())
session = SQLiteSession("customer-1") # this customer's memoryA 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.
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 lineTwo turns on one session
Pass the same session to both runs. The second turn reads the first from the store.
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 lookupThe 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.
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 setup | The desk's reply |
|---|---|
| Same SQLiteSession as turn 1 | Your order A17 shipped on 3 March and arrives 7 March. |
| No session | How 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.
Related
- Previous: Guarding the desk's input and output
- Next: A full run, and when it refuses
- Reference: Sessions
- 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.