Exploring Anthropic’s Memory Tool

Adding persistent memory to AI agents with the Anthropic Python SDK

Learn how to build an example AI agent with persistent memory in Python using the Claude Developer Platform
Published

November 25, 2025

On September 29, 2025, Anthropic launched the memory tool in beta. This enables Claude-based agents to store and recall information across conversations.

This blog post explores the Claude Developer Platform’s memory tool by implementing a simple agent with memory using the Anthropic Python library. Inspired by Anthropic’s recent pop-up cafe in London, we’re implementing a barista agent that can remember customers and their usual orders across different cafe visits.

Prerequisites

To follow along in this blog post, you will need to install the anthropic Python package (v0.74.1).

%pip install -q -U anthropic

Additionally, you will need an ANTHROPIC_API_KEY, which you can obtain by creating an Anthropic account and navigating to the “API Keys” tab in your dashboard. Once you have your API key, you need to store it in the environment variables.

import os
os.environ['ANTHROPIC_API_KEY'] = "your-anthropic-api-key"

Why do agents need memory?

Without memory, agents aren’t able to remember conversations across different conversations, tasks, or projects. Imagine, frequently visiting the same coffee shop in your neighbourhood over and over again. You’d expect an engaged barista to eventually remember who you are and what your usual order is.

Memory in AI agents allows agents to do exactly that. By storing and recalling information from past conversations, they can learn from past interactions and build knowledge bases over time to improve workflows and user experiences.

Let’s demonstrate this by first implementing a simple agent without memory. (If you’re unfamiliar with implementing an agent without an orchestration framework, you can review my blog post on how to build an AI agent from scratch using Claude.

The code below implements a barista agent as an LLM in a loop that takes the user’s input and responds to it based on the user’s input, the system prompt, and the conversation history (from the current conversation). The user can exit the conversation by typing the /quit command.

from anthropic import Anthropic

SYSTEM_PROMPT = """You're a friendly barista at Anthropic's pop-up cafe.
Respond like an efficient barista during rush hour - friendly but brief.
Always get the name of the customer to write on the cup so that orders don't get mixed up."""

def conversation_without_memory():

    client = Anthropic()
    messages: list[BetaMessageParam] = []

    while True:
        user_input = input("\nYou: ").strip()

        if user_input.lower() == "/quit":
          print("Goodbye!")
          break

        messages.append({"role": "user", "content": user_input})

        response = client.beta.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=2048,
                system = SYSTEM_PROMPT,
                messages=messages,
        )

        for content in response.content:
          print(f"\nClaude: {content.text}", end="", flush=True)

          # Store assistant message
          messages.append({"role": "assistant", "content": content.text})

Let’s start a conversation and order a coffee.

conversation_without_memory()

You: Hi, can I please get a regular flat white with oatmilk please?

Claude: Hey there! Absolutely, one regular oat milk flat white coming right up. 

Can I get a name for the cup?
You: Claudia

Claude: Perfect, Claudia! One regular oat milk flat white for you. That'll be $4.50. 

*starts writing "Claudia" on cup*

Will that be for here or to go?
You: /quit
Goodbye!

Good, the barista agent works.

Now, let’s start a new conversation, visit the coffee shop again and order our usual. Note, that starting a new conversation clears the conversation history from the previous conversation. This is like leaving the coffee shop after receiving your order and coming back on the next day.

conversation_without_memory()

You: Hi, it's me Claudia again. Can I please get my usual?

Claude: Hi there! I'm sorry, but I don't actually have a record of previous orders - each conversation is fresh for me. Could you remind me what your usual is? And just to confirm, that's Claudia for the cup, right?

*grabs cup and marker, ready to write*

What can I get started for you today?
You: /quit
Goodbye!

Unfortunately, the barista agent doesn’t remember us nor our usual order. As you can see, without any memory to remember interactions across conversations, interactions feel impersonal and leaves the user with a bad user experience.

How to use the memory tool with Claude

In contrast to implementations by other providers of the memory layer, Anthropic’s memory tool enables it to store and retrieve memory information through a memory file directory (/memory) that persist between sessions according to the Claude Docs. The agent can create, read, update, and delete files in this memory file directory.

You can enable the memory tool using the Anthropic SDK with just two steps. First, you need to implement the client-side handlers to control where and how the information is stored. Then, you only need to include the beta header context-management-2025-06-27 and the memory tool in your API requests.

Step 1: Implement client-side handlers for memory operations

According to Claude’s developer documentation, the memory tool operates client-side. This means, that the agent makes tool calls to perform memory operations and your application executes them locally. This gives developers the control over where and how the memory is stored (e.g, file-based, database, etc.).

To implement the client-side handlers for the memory operations you can subclass BetaAbstractMemoryTool and implement the handlers for each of the following six memory commands:

  • view: Shows directory contents or file contents
  • create: Create a file
  • str_replace: Replace text in a file
  • insert: Insert text at a specific line in a file
  • delete: Delete a file or directory
  • rename: Rename a file or directory

Note, the following implementation is copied from the example notebook for the memory tool.

import shutil
from typing import List
from pathlib import Path
from typing_extensions import override

from anthropic.lib.tools import BetaAbstractMemoryTool
from anthropic.types.beta import (
    BetaMessageParam,
    BetaContentBlockParam,
    BetaMemoryTool20250818Command,
    BetaContextManagementConfigParam,
    BetaMemoryTool20250818ViewCommand,
    BetaMemoryTool20250818CreateCommand,
    BetaMemoryTool20250818DeleteCommand,
    BetaMemoryTool20250818InsertCommand,
    BetaMemoryTool20250818RenameCommand,
    BetaMemoryTool20250818StrReplaceCommand,
)

class LocalFilesystemMemoryTool(BetaAbstractMemoryTool):
    """File-based memory storage implementation for Claude conversations"""

    def __init__(self, base_path: str = "./memory"):
        super().__init__()
        self.base_path = Path(base_path)
        self.memory_root = self.base_path / "memories"
        self.memory_root.mkdir(parents=True, exist_ok=True)

    def _validate_path(self, path: str) -> Path:
        """Validate and resolve memory paths"""
        if not path.startswith("/memories"):
            raise ValueError(f"Path must start with /memories, got: {path}")

        relative_path = path[len("/memories") :].lstrip("/")
        full_path = self.memory_root / relative_path if relative_path else self.memory_root

        try:
            full_path.resolve().relative_to(self.memory_root.resolve())
        except ValueError as e:
            raise ValueError(f"Path {path} would escape /memories directory") from e

        return full_path

    @override
    def view(self, command: BetaMemoryTool20250818ViewCommand) -> str:
        full_path = self._validate_path(command.path)

        if full_path.is_dir():
            items: List[str] = []
            try:
                for item in sorted(full_path.iterdir()):
                    if item.name.startswith("."):
                        continue
                    items.append(f"{item.name}/" if item.is_dir() else item.name)
                return f"Directory: {command.path}" + "\n".join([f"- {item}" for item in items])
            except Exception as e:
                raise RuntimeError(f"Cannot read directory {command.path}: {e}") from e

        elif full_path.is_file():
            try:
                content = full_path.read_text(encoding="utf-8")
                lines = content.splitlines()
                view_range = command.view_range
                if view_range:
                    start_line = max(1, view_range[0]) - 1
                    end_line = len(lines) if view_range[1] == -1 else view_range[1]
                    lines = lines[start_line:end_line]
                    start_num = start_line + 1
                else:
                    start_num = 1

                numbered_lines = [f"{i + start_num:4d}: {line}" for i, line in enumerate(lines)]
                return "\n".join(numbered_lines)
            except Exception as e:
                raise RuntimeError(f"Cannot read file {command.path}: {e}") from e
        else:
            raise RuntimeError(f"Path not found: {command.path}")

    @override
    def create(self, command: BetaMemoryTool20250818CreateCommand) -> str:
        full_path = self._validate_path(command.path)
        full_path.parent.mkdir(parents=True, exist_ok=True)
        full_path.write_text(command.file_text, encoding="utf-8")
        return f"File created successfully at {command.path}"

    @override
    def str_replace(self, command: BetaMemoryTool20250818StrReplaceCommand) -> str:
        full_path = self._validate_path(command.path)

        if not full_path.is_file():
            raise FileNotFoundError(f"File not found: {command.path}")

        content = full_path.read_text(encoding="utf-8")
        count = content.count(command.old_str)
        if count == 0:
            raise ValueError(f"Text not found in {command.path}")
        elif count > 1:
            raise ValueError(f"Text appears {count} times in {command.path}. Must be unique.")

        new_content = content.replace(command.old_str, command.new_str)
        full_path.write_text(new_content, encoding="utf-8")
        return f"File {command.path} has been edited"

    @override
    def insert(self, command: BetaMemoryTool20250818InsertCommand) -> str:
        full_path = self._validate_path(command.path)
        insert_line = command.insert_line
        insert_text = command.insert_text

        if not full_path.is_file():
            raise FileNotFoundError(f"File not found: {command.path}")

        lines = full_path.read_text(encoding="utf-8").splitlines()
        if insert_line < 0 or insert_line > len(lines):
            raise ValueError(f"Invalid insert_line {insert_line}. Must be 0-{len(lines)}")

        lines.insert(insert_line, insert_text.rstrip("\n"))
        full_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
        return f"Text inserted at line {insert_line} in {command.path}"

    @override
    def delete(self, command: BetaMemoryTool20250818DeleteCommand) -> str:
        full_path = self._validate_path(command.path)

        if command.path == "/memories":
            raise ValueError("Cannot delete the /memories directory itself")

        if full_path.is_file():
            full_path.unlink()
            return f"File deleted: {command.path}"
        elif full_path.is_dir():
            shutil.rmtree(full_path)
            return f"Directory deleted: {command.path}"
        else:
            raise FileNotFoundError(f"Path not found: {command.path}")

    @override
    def rename(self, command: BetaMemoryTool20250818RenameCommand) -> str:
        old_full_path = self._validate_path(command.old_path)
        new_full_path = self._validate_path(command.new_path)

        if not old_full_path.exists():
            raise FileNotFoundError(f"Source path not found: {command.old_path}")
        if new_full_path.exists():
            raise ValueError(f"Destination already exists: {command.new_path}")

        new_full_path.parent.mkdir(parents=True, exist_ok=True)
        old_full_path.rename(new_full_path)
        return f"Renamed {command.old_path} to {command.new_path}"

    @override
    def clear_all_memory(self) -> str:
        """Override the base implementation to provide file system clearing."""
        if self.memory_root.exists():
            shutil.rmtree(self.memory_root)
        self.memory_root.mkdir(parents=True, exist_ok=True)
        return "All memory cleared"

Step 2: Adjust API requests for memory tool

Next, we need to adjust the API request.

  1. Add the beta header context-management-2025-06-27 in the API request
  2. Add a system prompt for memory handling (MEMORY_SYSTEM_PROMPT)
  3. Add the memory tool to the API request
  4. Replace the create method with the tool_runner, which is an out-of-the-box solution for executing tools instead of manually handling tool calls, tool results, and conversation management (in beta) since we’re now using the memory tool
MEMORY_SYSTEM_PROMPT = """- ***DO NOT just store the conversation history**
        - ...
        - Use a simple list format."""

memory = LocalFilesystemMemoryTool()

runner = client.beta.messages.tool_runner(
        betas=["context-management-2025-06-27"],
        model=...,
        max_tokens=...,
        system=SYSTEM_PROMPT + MEMORY_SYSTEM_PROMPT,
        messages=...,
        tools=[memory],
)

When we put everything together in the agent’s conversation loop, the code looks like follows:

MEMORY_SYSTEM_PROMPT = """- ***DO NOT just store the conversation history**
        - No need to mention your memory tool or what you are writting in it to the user, unless they ask
        - Store facts about the customer, and their order. Do not store information about the order process or status.
        - Before responding, check memory to adjust technical depth and response style appropriately
        - Keep memories up-to-date - remove outdated info, add new details as you learn them
        - Use a simple list format."""

def conversation_with_memory():

    client = Anthropic()
    memory = LocalFilesystemMemoryTool()
    messages: list[BetaMessageParam] = []

    while True:
        user_input = input("\nYou: ").strip()

        if user_input.lower() == "/quit":
          print("Goodbye!")
          break

        messages.append({"role": "user", "content": user_input})

        # Use tool_runner with memory tool
        runner = client.beta.messages.tool_runner(
                betas=["context-management-2025-06-27"], # The memory tool is currently in beta. To enable it, use the beta header context-management-2025-06-27 in your API requests.
                model="claude-sonnet-4-20250514",
                max_tokens=2048,
                system=SYSTEM_PROMPT + MEMORY_SYSTEM_PROMPT,
                messages=messages,
                tools=[memory],
        )

        # Process all messages from the runner
        for message in runner:
          # Process content blocks
          assistant_content: list[BetaContentBlockParam] = []

          for content in message.content:
              if content.type == "text":
                  print(f"\nClaude: {content.text}", end="", flush=True)
                  assistant_content.append({"type": "text", "text": content.text})

              elif content.type == "tool_use" and content.name == "memory":
                  print(f"\n[Memory tool {content.name} called with {content.input}]")
                  assistant_content.append({"type": "tool_use", "id": content.id, "name": content.name, "input": content.input,})

          # Store assistant message
          if assistant_content:
              messages.append({"role": "assistant", "content": assistant_content})

          # Generate tool response automatically
          tool_response = runner.generate_tool_call_response()
          if tool_response and tool_response["content"]:
              # Add tool results to messages
              messages.append({"role": "user", "content": tool_response["content"]})

              for result in tool_response["content"]:
                  if isinstance(result, dict) and result.get("type") == "tool_result":
                      print(f"[Tool result processed: {result.get("content")}]")

Example demo of agent with memory

Let’s see how the barista agent’s behavior changes with access to the memory tool by repeating our conversation from earlier.

conversation_with_memory()

You: Hi, can I please get a regular flat white with oatmilk please? 

[Memory tool memory called with {'command': 'view', 'path': '/memories'}]
[Tool result processed: Directory: /memories]

[Memory tool memory called with {'command': 'create', 'path': '/memories/customers.txt', 'file_text': 'CUSTOMERS AND ORDERS\n\nNew customer:\n- Order: Regular flat white with oat milk\n- Name: (waiting for name)\n'}]
[Tool result processed: File created successfully at /memories/customers.txt]

Claude: Absolutely! One regular flat white with oat milk coming up. Can I get a name for the order?
You: Claudia

[Memory tool memory called with {'command': 'str_replace', 'path': '/memories/customers.txt', 'old_str': 'New customer:\n- Order: Regular flat white with oat milk\n- Name: (waiting for name)', 'new_str': 'Claudia:\n- Order: Regular flat white with oat milk'}]
[Tool result processed: File /memories/customers.txt has been edited]

Claude: Perfect, Claudia! One regular flat white with oat milk. That'll be $4.50. I'll have that ready for you in just a few minutes - keep an ear out for "Claudia!"
You: /quit
Goodbye!

Storing information in memory

You can see that the conversation is similar to the conversation with the barista agent without memory, but this time, the agent stores information based on the interaction.

  1. User request

    We initiate the conversation with the same input:

    "Hi, can I please get a regular flat white with oatmilk please?"
  2. Agent checks memory directory

    Before responding to the user, the agent first checks the memory directory.

    {
      "type": "tool_use",
      "id": "...",
      "name": "memory",
      "input": {
        "command": "view",
        "path": "/memories"
       }
    }
  3. The application returns the tool call results

    The tool call returns the directory contents of \memory.

    {
      "type": "tool_result",
      "tool_use_id": "...",
      "content": "Directory: /memories"
    }
  4. Agent creates a new memory file

    The agent now creates a memory file and stores the user’s order. (Note that the file name and contents can differ between API calls due to the non-deterministic nature of LLMs.)

    {
      "type": "tool_use",
      "id": "...",
      "name": "memory",
      "input": {
        "command": "create", 
        "path": "/memories/customers.txt", 
        "file_text": "CUSTOMERS AND ORDERS\n\nNew customer:\n- Order: Regular flat white with oat milk\n- Name: (waiting for name)\n"
      }
    }
  5. Agent responds

    Finally, the agent responds.

    "Absolutely! One regular flat white with oat milk coming up. Can I get a name for the order?"

Editing existing information in memory

You can also see in the above conversation that, after the user tells the barista agent their name, another memory tool call with the command str_replace is invoked to edit the customer profile with their name.

Recalling from memory

Now, let’s start a new conversation. This clears the conversation history like in our example above, but this time, important information from the earlier conversation is stored in the memory directory.

Let’s start the second conversation like in our example earlier again:

conversation_with_memory()

You: Hi, it's me Claudia again. Can I please get my usual?

[Memory tool memory called with {'command': 'view', 'path': '/memories'}]
[Tool result processed: Directory: /memories- customers.txt]

[Memory tool memory called with {'command': 'view', 'path': '/memories/customers.txt'}]
[Tool result processed:    1: CUSTOMERS AND ORDERS
   2: 
   3: Claudia:
   4: - Order: Regular flat white with oat milk]

Claude: Hey Claudia! Welcome back. One regular flat white with oat milk coming right up! 

*starts steaming oat milk and pulls espresso shots*

Should be ready in just a couple minutes. How's your day going?
You: /quit
Goodbye!

As you can see, this time the barista agent remembers the customer and is able to recall their usual order from the information stored in the memory directory.

Recalling from conversation history (not memory)

But does the barista agent now always check their memory? No. Only when it needs to. In the example below, you can see that if asked about something that was discussed earlier in the conversation ("Did I say almond milk or cashew milk?") instead of calling the memory tool, the agent uses the information in their context window to answer the user input.

conversation_with_memory()

You: Hi, it's me Claudia again. Can I please get my usual?

[Memory tool memory called with {'command': 'view', 'path': '/memories'}]
[Tool result processed: Directory: /memories- customers.txt]

[Memory tool memory called with {'command': 'view', 'path': '/memories/customers.txt'}]
[Tool result processed:    1: CUSTOMERS AND ORDERS
   2: 
   3: Claudia:
   4: - Order: Regular flat white with oat milk]

Claude: Hey Claudia! Welcome back! Absolutely - one regular flat white with oat milk coming right up. I'll get that started for you now!

*starts steaming oat milk and pulling espresso shot*

That'll be ready in just a moment!
You: Actually, I would like to try almond milk today.

[Memory tool memory called with {'command': 'str_replace', 'path': '/memories/customers.txt', 'old_str': 'Claudia:\n- Order: Regular flat white with oat milk', 'new_str': "Claudia:\n- Usual order: Regular flat white with oat milk\n- Today's order: Regular flat white with almond milk"}]
[Tool result processed: File /memories/customers.txt has been edited]

Claude: Perfect! Switching that to almond milk for you. One regular flat white with almond milk instead - got it!

*adjusts the milk steamer to almond milk*

Great choice! The almond milk has a nice nutty flavor that pairs really well with our espresso. This'll be ready in just a sec!
You: Oh, wait. Did I say almond milk or cashew milk? I want to make sure I ordered almond milk

Claude: You said almond milk! I've got you down for a regular flat white with almond milk. That's what I'm making for you right now.

*continues steaming the almond milk*

So we're all good - one flat white with almond milk for Claudia!
You: /quit
Goodbye!

Summary

This blog post explored how agents can use the memory tool (in beta) with the Claude Developer Platform. We’ve implemented two agents using Anthropic’s Python SDK: one without memory and one with memory. The agents were instructed to act as baristas. When asked to get the user their usual order, only the agent with access to the memory tool was able to fulfill this request.

This exploratory tutorial showed that Anthropic’s approach to memory is different from other providers of developer tools for the memory layer in AI agents: Claude stores memory information as files in a memory directory. I recommend this blog post by Shlok Khemani on “Anthropic’s Opinionated Memory Bet” for a detailed explanation of what this means.

You can also combine the memory tool with context editing to clear old tool results to keep the context concise and allow long-running agentic workflows.

References

Back to top