|
1 | 1 | import time |
2 | | -import shutil |
3 | 2 | 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 |
7 | 4 |
|
8 | 5 | from pydantic import TypeAdapter |
9 | 6 |
|
10 | 7 | from anthropic import Anthropic |
11 | | -from anthropic.lib.tools import BetaAbstractMemoryTool |
| 8 | +from anthropic.tools import BetaLocalFilesystemMemoryTool |
12 | 9 | from anthropic.types.beta import ( |
13 | 10 | BetaMessageParam, |
14 | 11 | BetaContentBlockParam, |
15 | 12 | BetaMemoryTool20250818Command, |
16 | | - BetaContextManagementConfigParam, |
17 | 13 | BetaMemoryTool20250818ViewCommand, |
18 | | - BetaMemoryTool20250818CreateCommand, |
19 | | - BetaMemoryTool20250818DeleteCommand, |
20 | | - BetaMemoryTool20250818InsertCommand, |
21 | | - BetaMemoryTool20250818RenameCommand, |
22 | | - BetaMemoryTool20250818StrReplaceCommand, |
23 | 14 | ) |
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 |
32 | 16 |
|
33 | 17 | # 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 = { |
36 | 20 | "edits": [ |
37 | 21 | { |
38 | 22 | "type": "clear_tool_uses_20250919", |
|
49 | 33 | ] |
50 | 34 | } |
51 | 35 |
|
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>""" |
191 | 42 |
|
192 | 43 |
|
193 | 44 | class Spinner: |
@@ -218,7 +69,7 @@ def _spin(self): |
218 | 69 |
|
219 | 70 | def conversation_loop(): |
220 | 71 | client = Anthropic() |
221 | | - memory = LocalFilesystemMemoryTool() |
| 72 | + memory = BetaLocalFilesystemMemoryTool() |
222 | 73 |
|
223 | 74 | messages: list[BetaMessageParam] = [] |
224 | 75 |
|
@@ -280,7 +131,7 @@ def conversation_loop(): |
280 | 131 | print(f" Cached: {cached_tokens:,} tokens") |
281 | 132 | print(f" Uncached: {uncached_tokens:,} tokens") |
282 | 133 |
|
283 | | - threshold = CONTEXT_MANAGEMENT["edits"][0]["trigger"]["value"] # type: ignore |
| 134 | + threshold = DEFAULT_CONTEXT_MANAGEMENT["edits"][0]["trigger"]["value"] # type: ignore |
284 | 135 | print(f" Context clearing threshold: {threshold:,} tokens") |
285 | 136 | if input_tokens >= threshold: |
286 | 137 | print(f" 🧹 Context clearing should trigger soon!") |
@@ -325,10 +176,10 @@ def conversation_loop(): |
325 | 176 | betas=["context-management-2025-06-27"], |
326 | 177 | model="claude-sonnet-4-20250514", |
327 | 178 | max_tokens=2048, |
328 | | - system=MEMORY_SYSTEM_PROMPT, |
| 179 | + system=DEFAULT_MEMORY_SYSTEM_PROMPT, |
329 | 180 | messages=messages, |
330 | 181 | tools=[memory], |
331 | | - context_management=cast(BetaContextManagementConfigParam, CONTEXT_MANAGEMENT), |
| 182 | + context_management=DEFAULT_CONTEXT_MANAGEMENT, |
332 | 183 | ) |
333 | 184 | except Exception: |
334 | 185 | spinner.stop() |
|
0 commit comments