twin memory

OpenAI Agents SDK

One command writes memory into an OpenAI Agents SDK project as code you own - recall before the model, remember after the run, and tools the model calls - with the end user set by your code, never the model.

twinmemory-cli adds memory to an OpenAI Agents SDK project the way shadcn adds a component: it writes one file into your project, and from then on that file is yours to read and edit. It is on PyPI and npm Soon: this page describes version 0.1.0, and until it is published, a function tool over REST or the hosted MCP server does the job.

uvx twinmemory-cli init openai-agents                # a project you have
uvx twinmemory-cli create openai-agents my-agent     # a new one, with memory wired in
uvx twinmemory-cli doctor                            # is it all set up?

openai-agents 0.3 or newer. The file uses the Python SDK.

What init does

Before it writes anything, init openai-agents checks three things, and any of them can stop it with nothing changed:

  • it is in a Python project (or a JavaScript one for npx);
  • the project uses the OpenAI Agents SDK: it is a dependency, or your code imports it. If not, init asks before adding it, and --yes adds it without asking. In Python, a project with an agents package of its own is warned: the SDK is imported as agents too, and the two would clash;
  • the installed SDK is new enough. If not, it says what to upgrade.

Then it:

  • writes memory.py (or memory.ts) beside the file that builds your Agent: Agent(...) in Python, new Agent(...) or Agent.create(...) in TypeScript. With several, it asks which (or --dir); with none, it writes into your project's package. An existing file is replaced only with --force;
  • adds what the file imports, with your project's own tool: uv, poetry, pdm or requirements.txt; npm, pnpm, yarn or bun;
  • sets up your key. Pass --key tm_..., or paste it when asked. Enter skips, and an empty TWIN_MEMORY_API_KEY= waits in .env for later. A key it is given is checked, and a key that only reads is called out, since saving needs one that writes.
FlagWhat it does
--nodes-onlyrecall and remember, no tools
--tools-onlythe tools, no recall or remember
--fastsearches skip the model filter: free and quicker
--key, --base-urlyour key, and memory's address, written into .env
--skip-keydo not ask for a key
--dir, --filewhere the file goes, and what it is called
--forcereplace a file already there
--no-installprint the install command instead of running it
--dry-runmake every check, say what would happen, change nothing
--yesask nothing

--graph and --async are LangGraph's, and refused here: the file is async already.

What it writes

PythonTypeScriptWhat it is
recallrecallthe run's model input filter: searches once a turn, with the user's latest message, and adds what it finds to the instructions
remember(result)remember(result)saves the turn a run answered - the user's message and the final output - under the agent's name
MemoryContext(user_id, session_id)MemoryContext { userId, sessionId? }the run's context: whose memory, and the conversation saves are grouped by
MEMORY_TOOLSMEMORY_TOOLSmemory_search and memory_read, for a model that looks things up itself
MEMORY_WRITE_TOOLSMEMORY_WRITE_TOOLSthe same and memory_add, to let the model save as well

Its settings are at the top of the file: FAST, STRICT, MAX_CHARS and TIMEOUT.

Wire it in

Use recall and remember, the tools, or both.

agent.py
from agents import Agent, RunConfig, Runner

from app.memory import MEMORY_TOOLS, MemoryContext, recall, remember

agent = Agent(name="Assistant", instructions="You are a helpful assistant.", tools=[*MEMORY_TOOLS])

result = await Runner.run(
    agent,
    "Where do I live?",
    context=MemoryContext(user_id="dana", session_id="conv-7"),
    run_config=RunConfig(call_model_input_filter=recall),
)
await remember(result)

Already using a call_model_input_filter? Run both: data.model_data = await recall(data), then yours.

  • Whose memory is the run's context, set by your code: user_id in Python, userId in TypeScript. Any context with that field will do. The SDK never sends the context to the model, and the tools take the user from it, not from their arguments, so a model cannot ask for anybody else's memory. See End users.
  • Recall runs once a turn. The filter runs before every model call, so in a tool loop or after a handoff it reuses what it found at the start of the turn, for that run. Two turns in flight for one user keep their own.
  • Memory goes into the instructions, never into the input, so it is not written into a Session, the Conversations API or the history you keep. It works the same with each, and the agent a run hands off to gets it too.
  • remember(result) saves after the run. For a stream, call it once the stream has finished. A run paused for an approval saves nothing; save the run that resumes it. Saving the same result twice replaces, rather than adds.
  • A long message - a pasted email - is searched by its last 2,000 characters, the most a search takes.
  • Saving needs a key that writes. recall and the read tools work with a read key.

When memory is down

The file fails open: with STRICT = False, the default, memory being unreachable, slow or failing costs a turn its memory, not the turn. recall adds nothing, remember skips the save, the tools tell the model memory is unavailable, and each is logged. When a tool fails because of what the model asked - an argument the service rejects - the model is told why, and can try again. A wrong key still raises - that is a mistake to fix. In Python, a wrong key in a tool reaches you as the SDK's UserError, with memory's error as its cause. Set STRICT = True to raise on everything.

create

create openai-agents <name> makes a new project with memory already wired in: an agent, a terminal chat, and the memory file. It takes init's presets and key flags, and --model, the model by the SDK's name (MODEL in .env; empty, the SDK's default). Fill in OPENAI_API_KEY in .env too.

uvx twinmemory-cli create openai-agents my-agent --key tm_...
cd my-agent
uv run python -m my_agent.chat --user dana    # talk to it

doctor

doctor checks the memory file, the dependencies and their installed versions, the key (with one free request), and that .env is not committed. Each failure says how to fix it.

Through the hosted MCP server

MCPServerStreamableHttp connects an agent to a remote MCP server and sends the headers you give it. Point it at the hosted server with a read key and the end user's id. The agent then sees every memory tool, such as memory_search, memory_grep and memory_read. Nothing recalls before the model, and one connection serves one end user.

agent_mcp.py
import asyncio
import os

from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp


async def main() -> None:
    async with MCPServerStreamableHttp(
        name="twin memory",
        params={
            "url": "https://api.twinmemory.dev/v1/mcp",
            "headers": {
                "Authorization": f"Bearer {os.environ['TWIN_READ_KEY']}",
                "X-Twin-Account": "user_42",
            },
            "timeout": 30,
        },
        cache_tools_list=True,
    ) as memory:
        agent = Agent(
            name="Assistant",
            instructions="Use the memory tools before you answer a question about the user's past.",
            mcp_servers=[memory],
        )
        result = await Runner.run(agent, "Where do I live now?")
        print(result.final_output)


asyncio.run(main())

With a read and write key instead, the server also lists memory_add, and the agent can write to the user's memory. Give it that key only if you mean it to.

Until it is published

A function tool that calls search over REST gives the model the user's memory, and your backend adds the turn after the run. The tool is made per end user, so the user id comes from your code and never from the model.

agent.py
import asyncio
import os

import requests
from agents import Agent, Runner, function_tool

TWIN = "https://api.twinmemory.dev/v1/memory"


def twin(key: str, user_id: str, route: str, body: dict) -> dict | None:
    r = requests.post(
        f"{TWIN}/{route}",
        headers={"Authorization": f"Bearer {key}", "X-Twin-Account": user_id},
        json=body,
        timeout=60,
    )
    if r.status_code == 404:  # no memory for this end user yet
        return None
    r.raise_for_status()
    return r.json()


def memory_tool(user_id: str):
    @function_tool
    def search_memory(question: str) -> str:
        """Search what this user has said before. Returns dated facts and passages, each with its address.

        Args:
            question: The question to look up, in plain words.
        """
        brief = twin(os.environ["TWIN_READ_KEY"], user_id, "search", {"query": question[-2000:]}) or {}
        lines = [*brief.get("known", []), *(f"- {f['text']} [{f['source']}]" for f in brief.get("facts", []))]
        lines += [f"[{h['path']}] {h['text']}" for h in brief.get("hits", [])]
        return "\n".join(lines) or "Memory holds nothing about this."

    return search_memory


async def reply(user_id: str, conversation: str, turn: int, message: str) -> str:
    agent = Agent(
        name="Assistant",
        instructions="Search memory before you answer a question about the user's past. Cite addresses.",
        tools=[memory_tool(user_id)],
    )
    result = await Runner.run(agent, message)
    twin(os.environ["TWIN_MEMORY_API_KEY"], user_id, "add", {"items": [
        {"id": f"{conversation}-{turn:04d}", "session": conversation, "role": "user", "text": message},
        {"id": f"{conversation}-{turn + 1:04d}", "session": conversation, "role": "assistant",
         "author": "Assistant", "text": result.final_output},
    ]})
    return result.final_output


print(asyncio.run(reply("user_42", "conv-7", 12, "Where do I live now?")))

The SDK reads the function's type hints and docstring to build the tool's schema. The search uses a read key; the add uses the read and write key, outside the tool.

Checked against openai.github.io/openai-agents-python, openai.github.io/openai-agents-js, openai-agents 0.24.0 and @openai/agents 0.21.0 on 2026-10-23.

On this page