Skip to main content
Lesson 13 of 25 · Building OpenAI Agents

Context Variables and Agent Memory

Lesson 13 of 25 Updated July 29, 2026 Developer
On this page
  1. Context Variables and Agent Memory
  2. Context Variables: Shared State
  3. Dynamic Instructions
  4. Multi-Turn Conversational Memory
  5. Pattern: Persistent Memory with a Database
  6. Context vs. Instructions: When to Use What
  7. Key Takeaways

Context Variables and Agent Memory

An agent without memory forgets everything between runs: it asks for the customer’s name in every message and starts from scratch each turn. Two complementary mechanisms fix this. Context variables inject shared state into the agent and its tools, while conversational memory maintains the thread of a multi-turn conversation.

Context Variables: Shared State

The RunContextWrapper is a typed object that circulates between the agent, its tools, and its guardrails. Concretely, you define a dataclass describing the session—identity, cart, preferences—and every tool receives it as the first parameter. In the example below, ajouter_au_panier writes to this object and voir_panier reads it back: both tools share the same cart without any data passing through the prompt.

from dataclasses import dataclass, field
from agents import Agent, Runner, RunContextWrapper, function_tool

@dataclass
class SessionUtilisateur:
    user_id: str
    nom: str
    email: str
    panier: list[dict] = field(default_factory=list)
    preferences: dict = field(default_factory=dict)

@function_tool
def ajouter_au_panier(
    ctx: RunContextWrapper[SessionUtilisateur],
    produit: str,
    quantite: int = 1,
) -> str:
    """Adds a product to the user's cart."""
    ctx.context.panier.append({"produit": produit, "quantite": quantite})
    total = len(ctx.context.panier)
    return f"{produit} (x{quantite}) added to cart. Total: {total} item(s)."

@function_tool
def voir_panier(ctx: RunContextWrapper[SessionUtilisateur]) -> str:
    """Displays the cart contents."""
    if not ctx.context.panier:
        return "Your cart is empty."
    lignes = [f"- {item['produit']} x{item['quantite']}" for item in ctx.context.panier]
    return f"Cart for {ctx.context.nom}:\n" + "\n".join(lignes)

agent = Agent(
    name="Shop Agent",
    instructions="You help customers with their shopping.",
    tools=[ajouter_au_panier, voir_panier],
    model="gpt-5.6-terra",
)

session = SessionUtilisateur(
    user_id="usr_789",
    nom="Marie Dupont",
    email="[email protected]",
)

# Context is shared across all tools
result = Runner.run_sync(agent, "Add a laptop and two mice to my cart", context=session)
print(result.final_output)
print(f"Cart after execution: {session.panier}")

The context is mutable: tools can modify it, and changes persist after execution. This is what makes the last line interesting—after run_sync, your application’s session object actually contains the two items, ready to be sent to your order system.

Dynamic Instructions

This state isn’t just for tools: it can also shape the agent’s behavior. Instructions can be a function that receives the context and builds the system prompt on the fly, including the customer’s name and preferred language.

def instructions_dynamiques(ctx: RunContextWrapper[SessionUtilisateur]) -> str:
    prefs = ctx.context.preferences
    langue = prefs.get("langue", "français")
    return f"""You are a sales assistant.
    The customer's name is {ctx.context.nom}.
    Their email is {ctx.context.email}.
    They have {len(ctx.context.panier)} item(s) in their cart.
    Communicate in {langue}."""

agent = Agent(
    name="Personalized Agent",
    instructions=instructions_dynamiques,
    tools=[ajouter_au_panier, voir_panier],
    model="gpt-5.6-terra",
)

Instructions are evaluated on every turn of the agent loop, enabling real-time personalization: if a tool just added an item, the next turn immediately accounts for it in the system prompt.

Multi-Turn Conversational Memory

Context carries data, not the dialogue itself. To maintain a conversation over multiple exchanges, pass the history. The principle comes down to three steps: you add the user’s message, you run the agent on the full list, then you replace your history with the one returned by the result—including messages, tool calls, and responses.

from agents import Agent, Runner

agent = Agent(
    name="Assistant",
    instructions="You are an assistant that remembers the conversation.",
    model="gpt-5.6-terra",
)

historique = []

async def converser(message_utilisateur: str) -> str:
    # Add the user's message to the history
    historique.append({"role": "user", "content": message_utilisateur})

    # Run the agent with the full history
    result = await Runner.run(agent, historique)

    # Save new exchanges for the next turn
    historique.clear()
    historique.extend(result.to_input_list())

    return result.final_output

# Simulate a conversation
async def demo():
    print(await converser("My name is Thomas."))
    print(await converser("What is my first name?"))  # The agent remembers
    print(await converser("Recommend a good restaurant in Paris."))

This memory lives in RAM. It disappears when you redeploy your service, and it grows indefinitely if the conversation lasts: two limits to keep in mind before going to production.

Pattern: Persistent Memory with a Database

For memory that survives restarts, store the history in a database. The approach presented here goes one step further: instead of keeping everything, the agent decides itself what deserves to be retained, via a pair of save and retrieve tools. The customer says “I’m allergic to peanuts,” the agent calls sauvegarder_note, and the information remains available three weeks later.

import json
from agents import Agent, Runner, function_tool

@function_tool
async def sauvegarder_note(ctx: RunContextWrapper, cle: str, valeur: str) -> str:
    """Saves information to remember later."""
    # In production: write to database
    notes = ctx.context.notes
    notes[cle] = valeur
    return f"Note saved: {cle} = {valeur}"

@function_tool
async def recuperer_note(ctx: RunContextWrapper, cle: str) -> str:
    """Retrieves previously saved information."""
    notes = ctx.context.notes
    valeur = notes.get(cle)
    if valeur:
        return f"{cle} = {valeur}"
    return f"No note found for '{cle}'"

@dataclass
class ContexteMemoire:
    user_id: str
    notes: dict = field(default_factory=dict)

agent = Agent(
    name="Assistant with Memory",
    instructions="""You are an assistant with persistent memory.
    Use sauvegarder_note to retain important information
    and recuperer_note to retrieve it.""",
    tools=[sauvegarder_note, recuperer_note],
    model="gpt-5.6-terra",
)

Context vs. Instructions: When to Use What

These four mechanisms partially overlap, and the classic mistake is to route everything through instructions. The table below summarizes the mapping to remember.

NeedSolution
User data (name, role)Context variables
Agent behaviorInstructions (static or dynamic)
Conversation historyresult.to_input_list()
Long-term memoryTools + database
Runtime configurationContext variables

Key Takeaways

  • RunContextWrapper carries typed state accessible by all tools
  • Context is mutable: tools can modify it during execution
  • Dynamic instructions (functions) adapt to context on every turn
  • Conversational history is maintained with result.to_input_list()
  • For persistent memory, combine save tools with a database