Skip to content

Commit 5ccd6b4

Browse files
karpetrosyanstainless-app[bot]
authored andcommitted
feat: add support for filesystem memory tools (#1247)
1 parent 206252f commit 5ccd6b4

File tree

9 files changed

+2628
-628
lines changed

9 files changed

+2628
-628
lines changed

examples/memory/basic.py

Lines changed: 15 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,22 @@
11
import time
2-
import shutil
32
import threading
4-
from typing import List, Optional, cast
5-
from pathlib import Path
6-
from typing_extensions import override
3+
from typing import Optional
74

85
from pydantic import TypeAdapter
96

107
from anthropic import Anthropic
11-
from anthropic.lib.tools import BetaAbstractMemoryTool
8+
from anthropic.tools import BetaLocalFilesystemMemoryTool
129
from anthropic.types.beta import (
1310
BetaMessageParam,
1411
BetaContentBlockParam,
1512
BetaMemoryTool20250818Command,
16-
BetaContextManagementConfigParam,
1713
BetaMemoryTool20250818ViewCommand,
18-
BetaMemoryTool20250818CreateCommand,
19-
BetaMemoryTool20250818DeleteCommand,
20-
BetaMemoryTool20250818InsertCommand,
21-
BetaMemoryTool20250818RenameCommand,
22-
BetaMemoryTool20250818StrReplaceCommand,
2314
)
24-
25-
MEMORY_SYSTEM_PROMPT = """- ***DO NOT just store the conversation history**
26-
- No need to mention your memory tool or what you are writting in it to the user, unless they ask
27-
- Store facts about the user and their preferences
28-
- Before responding, check memory to adjust technical depth and response style appropriately
29-
- Keep memories up-to-date - remove outdated info, add new details as you learn them
30-
- Use an xml format like <xml><name>John Doe</name></user></xml>"""
31-
15+
from anthropic.types.beta.beta_context_management_config_param import BetaContextManagementConfigParam
3216

3317
# Context management automatically clears old tool results to stay within token limits
34-
# Triggers when input exceeds 20k tokens, clears down to 10k tokens
35-
CONTEXT_MANAGEMENT = {
18+
# Triggers when input exceeds 30k tokens, keeps 3 tool uses after clearing
19+
DEFAULT_CONTEXT_MANAGEMENT: BetaContextManagementConfigParam = {
3620
"edits": [
3721
{
3822
"type": "clear_tool_uses_20250919",
@@ -49,145 +33,12 @@
4933
]
5034
}
5135

52-
53-
class LocalFilesystemMemoryTool(BetaAbstractMemoryTool):
54-
"""File-based memory storage implementation for Claude conversations"""
55-
56-
def __init__(self, base_path: str = "./memory"):
57-
super().__init__()
58-
self.base_path = Path(base_path)
59-
self.memory_root = self.base_path / "memories"
60-
self.memory_root.mkdir(parents=True, exist_ok=True)
61-
62-
def _validate_path(self, path: str) -> Path:
63-
"""Validate and resolve memory paths"""
64-
if not path.startswith("/memories"):
65-
raise ValueError(f"Path must start with /memories, got: {path}")
66-
67-
relative_path = path[len("/memories") :].lstrip("/")
68-
full_path = self.memory_root / relative_path if relative_path else self.memory_root
69-
70-
try:
71-
full_path.resolve().relative_to(self.memory_root.resolve())
72-
except ValueError as e:
73-
raise ValueError(f"Path {path} would escape /memories directory") from e
74-
75-
return full_path
76-
77-
@override
78-
def view(self, command: BetaMemoryTool20250818ViewCommand) -> str:
79-
full_path = self._validate_path(command.path)
80-
81-
if full_path.is_dir():
82-
items: List[str] = []
83-
try:
84-
for item in sorted(full_path.iterdir()):
85-
if item.name.startswith("."):
86-
continue
87-
items.append(f"{item.name}/" if item.is_dir() else item.name)
88-
return f"Directory: {command.path}" + "\n".join([f"- {item}" for item in items])
89-
except Exception as e:
90-
raise RuntimeError(f"Cannot read directory {command.path}: {e}") from e
91-
92-
elif full_path.is_file():
93-
try:
94-
content = full_path.read_text(encoding="utf-8")
95-
lines = content.splitlines()
96-
view_range = command.view_range
97-
if view_range:
98-
start_line = max(1, view_range[0]) - 1
99-
end_line = len(lines) if view_range[1] == -1 else view_range[1]
100-
lines = lines[start_line:end_line]
101-
start_num = start_line + 1
102-
else:
103-
start_num = 1
104-
105-
numbered_lines = [f"{i + start_num:4d}: {line}" for i, line in enumerate(lines)]
106-
return "\n".join(numbered_lines)
107-
except Exception as e:
108-
raise RuntimeError(f"Cannot read file {command.path}: {e}") from e
109-
else:
110-
raise RuntimeError(f"Path not found: {command.path}")
111-
112-
@override
113-
def create(self, command: BetaMemoryTool20250818CreateCommand) -> str:
114-
full_path = self._validate_path(command.path)
115-
full_path.parent.mkdir(parents=True, exist_ok=True)
116-
full_path.write_text(command.file_text, encoding="utf-8")
117-
return f"File created successfully at {command.path}"
118-
119-
@override
120-
def str_replace(self, command: BetaMemoryTool20250818StrReplaceCommand) -> str:
121-
full_path = self._validate_path(command.path)
122-
123-
if not full_path.is_file():
124-
raise FileNotFoundError(f"File not found: {command.path}")
125-
126-
content = full_path.read_text(encoding="utf-8")
127-
count = content.count(command.old_str)
128-
if count == 0:
129-
raise ValueError(f"Text not found in {command.path}")
130-
elif count > 1:
131-
raise ValueError(f"Text appears {count} times in {command.path}. Must be unique.")
132-
133-
new_content = content.replace(command.old_str, command.new_str)
134-
full_path.write_text(new_content, encoding="utf-8")
135-
return f"File {command.path} has been edited"
136-
137-
@override
138-
def insert(self, command: BetaMemoryTool20250818InsertCommand) -> str:
139-
full_path = self._validate_path(command.path)
140-
insert_line = command.insert_line
141-
insert_text = command.insert_text
142-
143-
if not full_path.is_file():
144-
raise FileNotFoundError(f"File not found: {command.path}")
145-
146-
lines = full_path.read_text(encoding="utf-8").splitlines()
147-
if insert_line < 0 or insert_line > len(lines):
148-
raise ValueError(f"Invalid insert_line {insert_line}. Must be 0-{len(lines)}")
149-
150-
lines.insert(insert_line, insert_text.rstrip("\n"))
151-
full_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
152-
return f"Text inserted at line {insert_line} in {command.path}"
153-
154-
@override
155-
def delete(self, command: BetaMemoryTool20250818DeleteCommand) -> str:
156-
full_path = self._validate_path(command.path)
157-
158-
if command.path == "/memories":
159-
raise ValueError("Cannot delete the /memories directory itself")
160-
161-
if full_path.is_file():
162-
full_path.unlink()
163-
return f"File deleted: {command.path}"
164-
elif full_path.is_dir():
165-
shutil.rmtree(full_path)
166-
return f"Directory deleted: {command.path}"
167-
else:
168-
raise FileNotFoundError(f"Path not found: {command.path}")
169-
170-
@override
171-
def rename(self, command: BetaMemoryTool20250818RenameCommand) -> str:
172-
old_full_path = self._validate_path(command.old_path)
173-
new_full_path = self._validate_path(command.new_path)
174-
175-
if not old_full_path.exists():
176-
raise FileNotFoundError(f"Source path not found: {command.old_path}")
177-
if new_full_path.exists():
178-
raise ValueError(f"Destination already exists: {command.new_path}")
179-
180-
new_full_path.parent.mkdir(parents=True, exist_ok=True)
181-
old_full_path.rename(new_full_path)
182-
return f"Renamed {command.old_path} to {command.new_path}"
183-
184-
@override
185-
def clear_all_memory(self) -> str:
186-
"""Override the base implementation to provide file system clearing."""
187-
if self.memory_root.exists():
188-
shutil.rmtree(self.memory_root)
189-
self.memory_root.mkdir(parents=True, exist_ok=True)
190-
return "All memory cleared"
36+
DEFAULT_MEMORY_SYSTEM_PROMPT = """- ***DO NOT just store the conversation history**
37+
- No need to mention your memory tool or what you are writing in it to the user, unless they ask
38+
- Store facts about the user and their preferences
39+
- Before responding, check memory to adjust technical depth and response style appropriately
40+
- Keep memories up-to-date - remove outdated info, add new details as you learn them
41+
- Use an xml format like <xml><name>John Doe</name></user></xml>"""
19142

19243

19344
class Spinner:
@@ -218,7 +69,7 @@ def _spin(self):
21869

21970
def conversation_loop():
22071
client = Anthropic()
221-
memory = LocalFilesystemMemoryTool()
72+
memory = BetaLocalFilesystemMemoryTool()
22273

22374
messages: list[BetaMessageParam] = []
22475

@@ -280,7 +131,7 @@ def conversation_loop():
280131
print(f" Cached: {cached_tokens:,} tokens")
281132
print(f" Uncached: {uncached_tokens:,} tokens")
282133

283-
threshold = CONTEXT_MANAGEMENT["edits"][0]["trigger"]["value"] # type: ignore
134+
threshold = DEFAULT_CONTEXT_MANAGEMENT["edits"][0]["trigger"]["value"] # type: ignore
284135
print(f" Context clearing threshold: {threshold:,} tokens")
285136
if input_tokens >= threshold:
286137
print(f" 🧹 Context clearing should trigger soon!")
@@ -325,10 +176,10 @@ def conversation_loop():
325176
betas=["context-management-2025-06-27"],
326177
model="claude-sonnet-4-20250514",
327178
max_tokens=2048,
328-
system=MEMORY_SYSTEM_PROMPT,
179+
system=DEFAULT_MEMORY_SYSTEM_PROMPT,
329180
messages=messages,
330181
tools=[memory],
331-
context_management=cast(BetaContextManagementConfigParam, CONTEXT_MANAGEMENT),
182+
context_management=DEFAULT_CONTEXT_MANAGEMENT,
332183
)
333184
except Exception:
334185
spinner.stop()

0 commit comments

Comments
 (0)