The decision piece argued the case: context editing and the memory tool aren't competitors, they're a division of labor. Context editing evicts the tool output you can afford to lose because it's re-fetchable; the memory tool writes the facts you can't afford to lose to a store outside the window. This is the other half — the request that turns both on, and the four config lines that keep it from quietly wrecking your prompt cache.
The one-line answer: Enable both in oneclient.beta.messages.create(...)call. The memory tool —{"type": "memory_20250818", "name": "memory"}— is now generally available, no beta header. Context editing needs thecontext-management-2025-06-27beta header and acontext_managementblock with aclear_tool_uses_20250919edit. The one setting that separates "works" from "thrashes the cache" isclear_at_least: clearing invalidates the cached prefix, so set a floor (5k–20k tokens) and addexclude_tools: ["memory"]so the durable layer is never what gets cleared.
Step 1 — register the memory tool (and own its storage)#
The memory tool is client-side by design. The API doesn't store anything. It issues file commands — view, create, str_replace, insert, delete, rename — and your code executes them against storage you control. The /memories path in those commands is just a prefix you map onto a real per-user directory, database, or bucket. A later session only continues from the same memory if you register the same tool and serve the same store.
Two non-negotiables:
- You own the path-traversal guard. Validate that every path stays under
/memories, resolve to canonical form, and reject.., encoded traversals, anddelete/renameon the/memoriesroot itself. This is your responsibility, not the API's. - When the memory tool is present, the API auto-prepends a memory protocol telling the model to check its memory directory before it starts and to record progress as it works. You don't write that system prompt — it comes with the tool.
STORE: dict[str, str] = {} # swap for real per-user storage in production
def _validate(path: str) -> None:
if not path.startswith("/memories") or ".." in path:
raise ValueError("path escapes /memories")
def execute_memory(cmd: dict) -> tuple[str, bool]:
"""Return (content, is_error). One arm per command; guard every path."""
c = cmd["command"]
_validate(cmd.get("path") or cmd.get("old_path", "/memories"))
if c == "create":
STORE[cmd["path"]] = cmd["file_text"]
return f"File created successfully at: {cmd['path']}", False
if c == "view":
text = STORE.get(cmd["path"])
if text is None:
return f"The path {cmd['path']} does not exist.", True
body = "\n".join(f"{i+1:6d}\t{l}" for i, l in enumerate(text.split("\n")))
return f"Content of {cmd['path']}:\n{body}", False
if c == "str_replace":
p, old = cmd["path"], cmd["old_str"]
if p not in STORE or old not in STORE[p]:
return f"old_str did not appear verbatim in {p}.", True
STORE[p] = STORE[p].replace(old, cmd.get("new_str", ""), 1)
return "The memory file has been edited.", False
# ... insert / delete / rename follow the same shape
return f"Unknown command {c}", True
The full command semantics (line-numbered view, insert_line offsets, exact return strings) are in the memory-tool docs; each SDK also ships a BetaLocalFilesystemMemoryTool if you'd rather not hand-roll the backend.
Step 2 — turn on context editing in the same request#
Now add the context_management block to the same call — it lives right beside your tools array and needs the beta header flipped on. The whole trick is in four fields, and getting them right is the difference between an agent that quietly manages its own window and one that reprocesses your entire cache every few turns. Read the config below, then the annotation under it explaining what each line actually buys you:
resp = client.beta.messages.create(
model=MODEL, # a current Claude 4+ model id
max_tokens=4096,
messages=messages,
tools=[
{"type": "memory_20250818", "name": "memory"},
{"type": "web_search_20250305", "name": "web_search", "max_uses": 10},
],
betas=["context-management-2025-06-27"], # for context editing
context_management={
"edits": [{
"type": "clear_tool_uses_20250919",
"trigger": {"type": "input_tokens", "value": 100_000},
"keep": {"type": "tool_uses", "value": 3},
"clear_at_least": {"type": "input_tokens", "value": 10_000},
"exclude_tools": ["memory"], # never clear the durable layer
}],
},
)
What each line buys you:
trigger— clearing doesn't run until input crosses this (default 100k tokens). Below it, you keep the full window.keep— the most recent N tool uses are always preserved (default 3), so the model never loses the context it's actively reasoning over.clear_at_least— the field people skip and then wonder why their cache is on fire. Every clearing event invalidates the cached prefix and forces a re-write. Setting a floor means clearing only fires when it can free enough tokens to be worth that re-cache; the next turns then reuse the new prefix. Skip it, and small frequent clears thrash the cache.exclude_tools: ["memory"]— memory results are your persisted knowledge. Excluding them means the clearer only ever evicts the re-fetchable stuff.
When a clear fires, the response echoes it: resp.context_management.applied_edits reports cleared_tool_uses and cleared_input_tokens, so you can log exactly what left the window.
Step 3 — run the loop; let each lever do its job#
The agentic loop is ordinary: append the assistant turn, execute any tool calls, feed results back. The only memory-specific branch is routing memory tool calls into execute_memory; server-side tools like web_search return their own results.
while resp.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": resp.content})
results = []
for block in resp.content:
if block.type == "tool_use" and block.name == "memory":
content, is_error = execute_memory(block.input)
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": content, "is_error": is_error})
if results:
messages.append({"role": "user", "content": results})
resp = client.beta.messages.create(...) # same kwargs as above
The division of labor now runs itself: the auto-injected protocol pushes the model to write specifics to memory as it works, so durable facts land before the window fills; context editing then evicts the bulky, re-fetchable tool output around them. Two more knobs if you need them — pair clear_tool_uses_20250919 with clear_thinking_20251015 to also drop old reasoning (list clear_thinking first in edits), and if you'd rather not hand-write the loop at all, client.beta.messages.tool_runner(...) drives it for you.
Why it's worth the wiring#
Anthropic's own numbers: context editing alone lifts their agentic benchmark ~29%; context editing plus memory, ~39%; a 100-turn web-search run cut token consumption 84% and finished workflows that would otherwise have died from context exhaustion. That gap between 29 and 39 is the entire argument for wiring both — the memory tool is what makes a fact survive the very clearing that keeps the agent alive. Turn one on and you manage the window. Turn both on, exclude memory from the clear, and you keep what matters while you do it.



