When a Claude-powered agent runs for dozens of turns, the raw message list grows until it blows the context window. Context editing agent conversations is the practice of letting the model itself propose cuts or summaries of earlier turns, keeping only what matters for the task. This tutorial builds a minimal but production-shaped loop where Claude calls a custom edit_context tool to trim its own history.
Prerequisites
- Python 3.11 or newer
anthropicSDK >= 0.39.0 (pip install anthropic)- An
ANTHROPIC_API_KEYwith access to a current Claude model (we useclaude-3-5-sonnet-20241022) - Comfort with synchronous Python and the Messages API
No external vector store or framework. We stay close to the metal so you can port the pattern to your own stack.
Why unbounded history fails
Every turn appends user and assistant messages. Tool results add more. A 20-turn debugging session can easily hit 30k tokens of redundant stack traces. Claude does not auto-forget; the API sends the full messages array each call.
You can manually summarize, but a hardcoded summarizer loses nuance. Instead, we give Claude a tool and let it decide what to drop. That is context editing agent conversations done by the model that knows the task state.
Define the edit tool
We expose one tool with two operations: delete a range of messages, or summarize a range into a single compressed user message. Indexes refer to the messages list positions.
{
"name": "edit_context",
"description": "Trim or compress the conversation history to save tokens. Operations apply in order.",
"input_schema": {
"type": "object",
"properties": {
"operations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"op": { "type": "string", "enum": ["delete", "summarize"] },
"start": { "type": "integer", "description": "Inclusive index" },
"end": { "type": "integer", "description": "Exclusive index" }
},
"required": ["op", "start", "end"]
}
}
},
"required": ["operations"]
}
}
Agent loop skeleton
We keep a messages list, call Claude, and if it returns tool_use for edit_context, we mutate the list before the next call.
import os
from anthropic import Anthropic
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
MODEL = "claude-3-5-sonnet-20241022"
TOOL = {
"name": "edit_context",
"description": "Trim or compress the conversation history to save tokens. Operations apply in order.",
"input_schema": {
"type": "object",
"properties": {
"operations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"op": {"type": "string", "enum": ["delete", "summarize"]},
"start": {"type": "integer"},
"end": {"type": "integer"}
},
"required": ["op", "start", "end"]
}
}
},
"required": ["operations"]
}
}
messages = [
{"role": "user", "content": "You are a coding agent. Use edit_context to manage history."}
]
def run_loop(user_input: str, max_turns: int = 10):
messages.append({"role": "user", "content": user_input})
for _ in range(max_turns):
resp = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=[TOOL],
messages=messages,
)
if resp.stop_reason == "tool_use":
tool_ids = []
for block in resp.content:
if block.type == "tool_use" and block.name == "edit_context":
apply_edits(messages, block.input["operations"])
tool_ids.append(block.id)
for tid in tool_ids:
messages.append({
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": tid, "content": "applied"}]
})
continue
messages.append({"role": "assistant", "content": resp.content})
print("Claude:", resp.content[0].text)
break
Applying edits safely
We must apply operations on the original indexes carefully, because deleting shifts indexes. Simplest: collect ranges to remove or replace, then rebuild the list. Never delete index 0 if it holds system instructions.
def apply_edits(messages, operations):
keep = [True] * len(messages)
summaries = {}
for op in operations:
start, end = op["start"], op["end"]
if start <= 0:
start = 1 # protect system/user seed at index 0
if op["op"] == "delete":
for i in range(start, min(end, len(messages))):
keep[i] = False
elif op["op"] == "summarize":
summaries[(start, min(end, len(messages)))] = True
for i in range(start, min(end, len(messages))):
keep[i] = False
new_msgs = []
i = 0
while i < len(messages):
if not keep[i]:
handled = False
for (s, e) in summaries:
if s == i:
new_msgs.append({
"role": "user",
"content": f"[summary of turns {s}-{e}: {str(messages[s]['content'])[:50]}...]"
})
i = e
handled = True
break
if not handled:
i += 1
else:
new_msgs.append(messages[i])
i += 1
messages.clear()
messages.extend(new_msgs)
This is minimal; in production you would call Claude again with a summarize prompt rather than stubbing.
Checkpoint: observe the trim
Seed a conversation with filler turns, then ask Claude to trim.
for i in range(5):
messages.append({"role": "user", "content": f"Unimportant log line {i}"})
run_loop("Now trim the first 5 log lines using edit_context")
Expected console output before the edit shows Claude acknowledging the tool:
Claude: I'll remove those log lines by calling edit_context.
After the loop, len(messages) drops from ~7 to 3. The tool result acknowledges, and the next call sees a shorter list. That is context editing agent conversations working: the model identified low-value turns and removed them.
Summarization instead of deletion
Deletion loses information. For agent tasks, summarizing a debug loop into one note is better. Extend the summarize branch to call a fast model:
def summarize_range(msgs):
text = "\n".join(str(m["content"]) for m in msgs)
r = client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=256,
messages=[{"role": "user", "content": f"Summarize for future context:\n{text}"}]
)
return r.content[0].text
Swap the stub in apply_edits with summarize_range(messages[s:e]). Now the history keeps semantic content at a fraction of the tokens.
Estimating token pressure
Before each call, compute approximate tokens. Anthropic doesn’t ship a local tokenizer, but you can use a rough heuristic: 1 token ≈ 4 characters.
def approx_tokens(msgs):
return sum(len(str(m["content"])) // 4 for m in msgs)
if approx_tokens(messages) > 12000:
run_loop("History is large; use edit_context to reduce it.")
This proactive nudge keeps the context editing agent conversations loop self-aware and prevents mid-task context overflow.
Pair with prompt caching
Anthropic supports cache_control on system or message blocks. When you summarize, mark the summary block with cache_control: {"type": "ephemeral"} so repeated calls after edits don’t re-price the static prefix.
messages[0] = {
"role": "system",
"content": [
{"type": "text", "text": "You are a coding agent.", "cache_control": {"type": "ephemeral"}}
]
}
This complements context editing agent conversations: edits change the volatile tail, caching stabilizes the head.
Routing through a gateway
If you front Claude with an OpenRouter-class gateway such as n4n.ai, the same tool schemas pass through unchanged because it honors client routing directives and forwards provider cache-control hints. You get automatic fallback to another provider if Anthropic is degraded, without rewriting the edit logic.
Production caveats
- Validate indexes server-side. A buggy tool call can truncate instructions. Guard
start >= 1. - Cap operations per turn. Letting Claude edit every turn risks thrash. Trigger edits only when
approx_tokens(messages) > 0.8 * MAX_WINDOW. - Log the diff. Store pre-edit and post-edit message counts for debuggability.
- Use a cheaper model for summarization to control cost.
Context editing agent conversations is not a one-shot fix; it’s a control loop. The code above is the spine. Drop it into your agent runner, add real token counting via the Anthropic beta count_tokens endpoint, and you have a self-trimming session that survives long tasks.
Final check
Run the full script with a real key. You should see the agent call edit_context, the list shrink, and subsequent replies reference the condensed history. That’s the whole mechanism: give the model a scalpel, constrain the grip, and let it cut.