--- title: "DSPy memory: conversation history and recall as a tool" description: "DSPy memory is the caller's job: dspy.History is an input field the application maintains. Add timestamped, source-backed recall to a ReAct agent or a DSPy module." canonical: https://past.dev/integrations/dspy last-updated: 2026-10-09 --- # Add memory to DSPy programs Source: https://past.dev/integrations/dspy DSPy memory is the caller's job. A DSPy program keeps no state between calls, and dspy.History is an input field type whose messages the application appends after each turn. The documentation describes no long-term memory store. Recall from past.dev fits DSPy in two places: as a tool in dspy.ReAct, or as a fixed retrieval step inside a module. Recall returns ranked documents with dates and source excerpts. ## DSPy memory scope The [conversation history tutorial](https://dspy.ai/current/tutorials/conversation_history/) states that DSPy does not persist conversation state between calls. A signature can declare a `dspy.History` input, whose `messages` list holds earlier turns. The application appends each turn and passes the object back. - **History is an input.** The program reads what the caller passes and stores nothing. - **Few-shot examples stay single turns.** DSPy represents each few-shot example as one turn, even when it carries a history. - **No long-term store is documented.** Facts across sessions, users and applications need a service that the program calls. ## The integration: tools for dspy.ReAct A DSPy tool is a plain Python function with type hints and a docstring. `dspy.ReAct` reads the name, the parameters and the docstring to describe it to the model ([ReAct and tools](https://dspy.ai/current/getting-started/react-and-tools/)). ```python import os, requests import dspy BASE = "https://api.past.dev/api/v1" HEADERS = {"Authorization": f"Bearer {os.environ['PAST_API_KEY']}"} def make_past_tools(user_id: str): """Build the memory tools for one signed-in user. The model never sees user_id.""" def remember(text: str, happened_at: str) -> str: """Store timestamped source text, readable by the whole project, authored by the current user.""" return requests.post(f"{BASE}/ingest", headers=HEADERS, json={ "content": text, "timestamp": happened_at, "identity": user_id, }).text def recall(question: str) -> str: """Return ranked documents with dates and source excerpts for the current user.""" return requests.post(f"{BASE}/recall", headers=HEADERS, json={ "query": question, "identity": user_id, }).text return [remember, recall] dspy.configure(lm=lm) # lm: any dspy.LM agent = dspy.ReAct("question -> answer", tools=make_past_tools("demo-user")) # the signed-in user's id result = agent(question="What did we decide about the Q4 budget?") print(result.answer) ``` `make_past_tools` builds the two tools for one request. They close over the signed-in user's id, so no signature field carries the identity and the model never writes it. Build the program for each user's request. > **Identity** > > The application sets the identity. The model never chooses it. Recall returns what the identity in the request may read, so take the identity from the signed-in user in code, and keep it out of every value that the model fills. ## Or call recall as a fixed step When every answer must read memory first, call recall inside a module and pass the documents as context. The model then never skips the read. ```python class AnswerFromMemory(dspy.Module): def __init__(self): super().__init__() self.respond = dspy.ChainOfThought("context, question -> answer") def forward(self, question: str, user_id: str): _, recall = make_past_tools(user_id) context = recall(question) return self.respond(context=context, question=question) ``` The application passes `user_id` to `forward`. The `respond` signature does not carry it, so the model never sees it. Recall returns ranked documents with dates and source excerpts. The application decides whether those documents support an answer, contain a disagreement, or are insufficient; HTTP failures are handled separately. ## Or connect the MCP server DSPy converts MCP tools with `dspy.Tool.from_mcp_tool`, over a client of the MCP Python SDK ([MCP tutorial](https://dspy.ai/current/tutorials/mcp/)). The SDK's `Client` uses Streamable HTTP when it receives a URL ([MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)). The project's end-user MCP server then gives the agent `recall`, `answer` and `who_am_i`, plus `remember` while the project's write toggle is on, with no tool code to write. 1. Turn on the project's end-user MCP server on **Build › MCP server** in the console. 2. Mint an access link there, or with the account server's `create_mcp_access_link` tool. The link has the form `https://api.past.dev/mcp//link/`. It is one URL that is one identity, and the server answers it with no sign-in step. 3. Keep the link in a secret store, such as the `PAST_MCP_URL` environment variable that the code below reads. The secret in its path is the credential. ```python import asyncio, os import dspy from mcp import Client async def main(): async with Client(os.environ["PAST_MCP_URL"]) as client: listed = await client.list_tools() tools = [dspy.Tool.from_mcp_tool(client, t) for t in listed.tools] agent = dspy.ReAct("question -> answer", tools=tools) result = await agent.acall(question="What did we decide about the Q4 budget?") print(result.answer) asyncio.run(main()) ``` Every call through the link runs as its identity, with that identity's audiences. Mint one link per install, so that a revoked link stops one agent only. When the agent serves several people, mint one link per end user, and connect each request with the link of the user who makes it. Never share one link between users. `remember` writes to the identity's own private audience, and only that identity recalls it. Send data that the whole project must recall through the ingest call. The [end-user server documentation](/docs/mcp/serving-your-own-users) describes the tools, the links and revocation. ## When to use which - The turns of the current conversation: a `dspy.History` input that the application maintains. - History that the model consults when it decides to: the recall tool in `dspy.ReAct`, or the MCP server. - History that every answer reads: the fixed recall step in a module. The [quickstart](/docs/memory-api/quickstart) shows how to ingest, wait until ingestion completes, and recall. The [benchmarks](/benchmarks) document how recall is measured. [Context engineering](/context-engineering) covers how to place recalled documents in a prompt. ## Frequently asked questions ### Does DSPy have memory? DSPy keeps no state between calls. A signature can take a dspy.History input that the application maintains, and long-term memory comes from a service that the program calls. ### Should recall be a tool or a fixed step? Use a tool when the model should decide whether history matters. Use a fixed step in a module when every answer must read memory first. ## Related - [Memory for LlamaIndex](https://past.dev/integrations/llamaindex) - [Memory for Haystack](https://past.dev/integrations/haystack) - [Memory for Pydantic AI](https://past.dev/integrations/pydantic-ai) - [Context engineering](https://past.dev/context-engineering) - [Quickstart](https://past.dev/docs/memory-api/quickstart)