n4nAI

How Claude's context editing tools trim agent conversations

Hands-on tutorial: build a Claude tool-calling loop that trims chat history so context editing agent conversations stays within token limits.

n4n Team3 min read687 words

Audio narration

Coming soon — every post will get a voice note here.

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
  • anthropic SDK >= 0.39.0 (pip install anthropic)
  • An ANTHROPIC_API_KEY with access to a current Claude model (we use claude-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.

Tagsclaudecontext-editingconversation-historycontext-window

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All context window & token management for agents posts →