Claude Code Context und Memory Internals, neu aufgebaut als LangGraph Agent
Wie Claude Code API-Aufrufe erstellt, Subagenten ausführt und lange Sitzungen komprimiert, sowie wie dieselben Konzepte mit Checkpointern und Speicherdateien in LangGraph umgesetzt werden.
Die aus dem geleakten Quellcode von Claude Code v2.1.87 stammenden Details zeigen, wie das Tool Kontext und Speicher über lange Programmier-Sessions verarbeitet. Es bewältigt die üblichen Probleme bei langlaufenden Agenten: einen Kontext, der die Kapazität des Modells übersteigt, Tool-Ausgaben, die niemand vorhersagen kann, das Abrufen von Erinnerungen aus früheren Sessions zum richtigen Zeitpunkt sowie das Beibehalten von Projektanweisungen nach deren Zusammenfassung. Im Folgenden wird dieser Mechanismus aufgeschlüsselt und anschließend eine vereinfachte Version in LangGraph mit dessen integrierten Checkpoint-Funktionen wieder aufgebaut.
Wie Claude Code den Kontext verwalten wird
Eine API-Anfrage setzt sich aus drei Teilen zusammen. Zwei davon, die Systemanweisung (Identität plus Git-Kontext) sowie der Tools-Array (JSON-Schemata für Bash, Read, Edit, Skill, Agent und MCP), bleiben außerhalb der Nachrichtenliste, und die Komprimierung erreicht sie nie. Der Rest wird als Benutzer-Rolle-Einträge in den Messages-Array untergebracht und in <system-reminder>-Tags eingebettet: Anweisungsdateien ganz oben, die laufende Konversation sowie Tool-Ergebnisse in der Mitte, und Anhänge-Einträge (Erinnerungen, Skill- und Agent-Listen, MCP-Anweisungen, Hook-Ausgaben), die nach jeder Tool-Ausführung hinzugefügt werden. Wenn das Fenster voll ist, konsolidiert die Komprimierung jede Nachricht und erzeugt die wichtigen Anhänge erneut aus ihren ursprünglichen Quellen. Anschließend folgen der Abfrageschleifen-Loop, Subagenten und die Komprimierung.
Die Abfrageschleifen-Loop
Das Eingeben einer Nachricht startet einen Schleifenprozess, bei dem jede Iteration eine einzelne API-Aufruf darstellt, und dieser Prozess setzt sich fort, bis das Modell antwortet, ohne weitere Tool-Aufrufe vorzunehmen.
Vor Beginn der Schleife führt ein Hintergrundaufruf von Sonnet bis zu fünf relevante Speicherdateien aus, wobei ausschließlich der Text der Benutzerfrage verwendet wird, da der Versand der gesamten Konversation zu viel Ressourcen verbrauchen würde. Die Ergebnisse werden nicht sofort genutzt; sie werden erst nach Ausführung der Tools bereitgestellt, sodass der erste API-Aufruf einer Runde sie niemals enthält.
Jede Iteration kürzt den in Gedächtnis gespeicherten Nachrichtenarray, bevor er an die API übergeben wird. Zunächst wird die Liste auf die Elemente nach der letzten Komprimierungsgrenze beschnitten, wobei die vollständige JSONL-Datei auf der Festplatte unverändert bleibt. Anschließend werden überdimensionierte Tool-Ergebnisse in eine eigene Datei geschrieben, und eine 2 KB große Vorschau wird direkt eingefügt. Falls die Tokenanzahl danach immer noch etwa 93 Prozent des Kontextfensters ausmacht, wird eine weitere Komprimierung ausgelöst.
Eine Antwort ist entweder reiner Text, der die Runde beendet, oder eine oder mehrere Toolaufrufe, die ausgeführt werden und den Zyklus erneut in Gang setzen. Die Tools lassen sich in Gruppen einteilen: allgemeine Tools wie Bash, Read, Edit und Grep; das Skill-Tool, das einen vollständigen SKILL.md-Dateiinhalt in das Gespräch einfügt; das Agent-Tool, das einen isolierten Subagenten startet, der ein einziges Ergebnis zurückgibt; sowie ToolSearch, das ein verzögertes Tool-Schema für die folgende Iteration abruft. Nach Abschluss jeder Tool-Runde werden Delta-Anhänge für Skill-Listen, Agent-Listen, MCP-Anweisungen sowie alle abgeschlossenen Speichervorläadoweisen eingefügt, und es werden ausschließlich die seit dem vorherigen Durchlauf veränderten Inhalte ausgesendet.
Subagenten
Durch den Aufruf des Agent-Tools lässt Claude Code einen isolierten Kontext im selben Prozess erstellen. Der Subagent beginnt mit einer Kopie des Gesprächs des Elternteils, erhält seine eigenen Tools sowie Systemanweisungen, läuft mit deaktiviertem Denkprozess, um die Kosten für Ausgabe-Token zu senken, und führt den gleichen Abfrageschleifen-Code eigenständig aus. Auch die „Explore“- und „Plan“-Agents verlieren aus dieser Kopie die Dateien CLAUDE.md sowie git status, was weitere Tokens spart.
Nur die endgültige Ergebnisnachricht wird an den Elternteil zurückgesendet. Toolaufrufe, Denkprozesse sowie alle Komprimierungen innerhalb des Subagents werden in eine Sidechain-Transkriptdatei, subagents/agent-{id}.jsonl, geschrieben, die vom Elternteil niemals geöffnet wird. Auf diese Weise bleiben die Kosten isoliert: Dutzende interner Toolaufrufe können zu einer einzigen Nachricht im Kontext des Elternteils zusammengefasst werden.
Komprimierung
In dem Code existiert eine mikrokompakte Schicht, um alte Tool-Ergebnisse vor einer vollständigen Kompression zu verkleinern, doch sie ist hier deaktiviert und außerhalb des Anwendungsbereichs. Die Kompression selbst bietet zwei Strategien, wobei die zweite als Ersatz dient, wenn die erste nicht verfügbar ist. Bei Strategie eins wird sofort eine vorbereitete Sitzungs-Speicherdatei eingesetzt, sodass keine eigene API-Aufruf erforderlich ist; ein Hintergrundprozess erstellt diese Datei schrittweise in früheren Schritten durch separate Aufrufe, wobei dieser Weg derzeit durch Feature-Gates deaktiviert ist. Strategie zwei, die derzeit verwendet wird, ist eine vollständige LLM-Kompression. Ein spezieller Prozess erhält die aktuellen Nachrichten sowie einen strukturierten Kompressionsprompt und reduziert das Gespräch auf etwa fünftausend Token. Die neuesten Nachrichten werden wortwörtlich neben dieser Zusammenfassung beibehalten, damit das Modell weiterhin seinen unmittelbaren Arbeitskontext hat, während alles Ältere durch die Zusammenfassung ersetzt wird.
Danach werden alle Anhänge, die sich ursprünglich in messages[] befanden und zusammengefasst wurden, erneut erstellt. Die Liste der Agenten, die Namen der aufgeschobenen Tools sowie die MCP-Anweisungen werden erneut vollständig angezeigt, und die Hooks zum Starten der Sitzung laufen erneut ab. Der in Erinnerung gespeicherte Inhalt der Skills wird aus einem im Speicher vorhandenen Speicherbereich mit einer Kapazität von etwa 25.000 Tokens zurückgeholt, und die fünf kürzlich bearbeiteten Dateien werden erneut von der Festplatte gelesen. Der Systemprompt, tools[], Claude.md sowie der git-Context befanden sich niemals in messages[], daher kann die Komprimierung sie nicht beeinflussen; ihre im Speicher gespeicherten Caches werden einfach geleert, damit im nächsten Schritt wieder aktuelle Werte verwendet werden.
Eines wird absichtlich weggelassen: Das Fähigkeitskatalog wird nicht neu erstellt, wodurch etwa 4.000 Tokens eingespart werden. Die Nebenwirkung ist, dass das Modell nach der Komprimierung keine Möglichkeit hat, Fähigkeiten zu finden, die es noch nicht verwendet hat, es sei denn, Claude.md listet sie auf. Durch die Komprimierung gehen außerdem Details verloren, da die frühere Zusammenfassung zusammen mit allem anderen erneut zusammengefasst wird, und nach dem dritten Durchlauf wurden die ursprünglichen Fakten bereits dreimal komprimiert. Nichts geht wirklich verloren, denn die nur zum Hinzufügen geeignete JSONL-Datei auf der Festplatte enthält die gesamte Historie, und der Komprimierungs-Markierer (parentUuid: null) zeigt dem Lader an, wo mit der Wiederaufnahme zu beginnen ist.
Eine vereinfachte Version von LangGraph
Mit LangGraph kann eine kleinere Version derselben Architektur erstellt werden. Es bietet bereits Checkpoint-Funktionen für die Persistenz sowie RemoveMessage zur Löschung von Nachrichten. Das Zwischenspeichern im Gedächtnis zwischen Sitzungen folgt demselben Ansatz wie bei Claude Code, wobei bei jeder Aktion Markdown-Dateien auf der Festplatte gelesen werden. Die Komprimierungslogik ist der Teil, der noch implementiert werden muss.
In dem Zuordnungsdiagramm verbindet jede Pfeilspitze ein Konzept aus Claude Code links mit seinem Äquivalent in LangGraph rechts. Die mit „Verbesserungspunkte“ markierte Verbindung weist darauf hin, dass LangGraph Anweisungen außerhalb von messages[] speichern kann, sodass die Komprimierung diese niemals berührt.
Fünf Knoten bilden das Graphen. Die gestrichelten Kanten von call_model sind bedingungsabhängig und leiten Toolaufrufe über tools zurück sowie Textantworten an post_turn. In load_context werden die Speicherdateien (.agent/memory/*.md) von der Festplatte gelesen und anschließend in die SystemMessage in call_model integriert. Dadurch bleiben sie gänzlich außerhalb von messages[], sodass sie unverändert durch die Komprimierung gelangen.
Graphenkonstruktion
Die Knoten werden nacheinander ausgeführt, und nach call_model führt ein bedingter Zweig entweder durch die Tools oder geht zu post_turn. Ein SQLite Checkpointer speichert den gesamten Zustand über Neustarts des Prozesses hinweg.
def build_graph():
builder = StateGraph(AgentState)
builder.add_node("load_context", load_context)
builder.add_node("prune_and_compact", prune_and_compact)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode(TOOLS))
builder.add_node("post_turn", post_turn)
builder.add_edge(START, "load_context")
builder.add_edge("load_context", "prune_and_compact")
builder.add_edge("prune_and_compact", "call_model")
builder.add_conditional_edges(
"call_model", route_after_model,
{"tools": "tools", "post_turn": "post_turn"},
)
builder.add_edge("tools", "call_model")
builder.add_edge("post_turn", END)
checkpointer = SqliteSaver(sqlite3.connect("checkpoints.sqlite"))
return builder.compile(checkpointer=checkpointer)
# Subagent: separate StateGraph with own call_model + tools loop
subagent_builder = StateGraph(SubagentState)
subagent_builder.add_node("call_model", subagent_call_model)
subagent_builder.add_node("tools", ToolNode([bash, read_file, write_file]))
subagent_builder.add_edge(START, "call_model")
subagent_builder.add_conditional_edges("call_model", subagent_route,
{"tools": "tools", "__end__": END})
subagent_builder.add_edge("tools", "call_model")
subagent_graph = subagent_builder.compile()
Der Zusammenhang zwischen den beiden Diagrammen befindet sich innerhalb der run_subagent-Tool:
@tool
def run_subagent(task: str) -> str:
"""Run an isolated subagent for a specific task."""
result = subagent_graph.invoke({"messages": [HumanMessage(content=task)]})
return result["messages"][-1].content # only final result
Sobald das Modell run_subagent aufruft, führt der Hauptknoten tools die Funktion aus, wodurch das Subagenten-Graph aktiviert wird. Dieses führt anschließend seine eigenen Schleifen für call_model und tools getrennt aus. Nur die endgültige Antwort kehrt als Zeichenkette zurück, sodass das Hauptgraph ein gewöhnliches Tool-Ergebnis wahrnimmt. Das Subagenten verfügt über keine Anweisungen, Speicherdateien oder Checkpointer, weshalb sein Kontext einfach ist und jeder Aufruf unabhängig erfolgt. Das Beispiel verwendet einen einzigen Subagententyp. Claude Code wählt zur Laufzeit aus mehreren Typen aus („Explore“ für Suchvorgänge, „Plan“ für Planung, ein allgemeiner Typ mit allen verfügbaren Tools sowie Plugin-Agenten wie vercel:ai-architect), wobei jeder Typ über eigene Tools, Berechtigungen und Systemanweisungen verfügt.
Anweisungen
Es handelt sich dabei um vereinfachte Versionen, die bei der LangGraph-Replikation verwendet werden; die tatsächlichen Anweisungen in Claude Code sind detaillierter.
Claude Code’s eigener kompakter Prompt ist viel umfangreicher und umfasst neun Abschnitte, ein <analysis>-Notizfeld, eine Einleitung mit dem Vermerk „CRITICAL: Respond with TEXT ONLY“ sowie einen Beispiel-Formatblock. Die unten stehende gekürzte Version behält das Wesentliche bei:
Your task is to create a detailed summary of the conversation so far,
paying close attention to the user's explicit requests and your previous actions.
This summary should capture technical details, code patterns, and decisions
that would be essential for continuing the work without losing context.
Before writing the summary, analyze the conversation chronologically in <analysis> tags.
Then provide the summary in <summary> tags with these sections:
1. Primary Request and Intent: The user's explicit requests in detail.
2. Key Technical Concepts: Important technologies, frameworks, and patterns discussed.
3. Files and Code Sections: Specific files examined, modified, or created, with paths
and why each was important.
4. Errors and Fixes: Errors encountered, how they were fixed, and any user corrections
or feedback (pay special attention to "don't do X" or "actually I meant Y").
5. Problem Solving: Problems solved and any ongoing troubleshooting.
6. Pending Tasks: Tasks explicitly requested but not yet completed.
7. Current Work: What was being worked on immediately before this summary,
with specific file names and details from the most recent messages.
In der Code-Implementierung wird zunächst das Gespräch in Teile aufgeteilt, wobei trim_messages den jüngsten Teil identifiziert, der beibehalten werden soll. Nur der ältere Teil wird mit dieser Anweisung an das Modell übermittelt, und dieses Modell sieht alles – Benutzernachrichten, Assistentenantworten sowie Tool-Ergebnisse – und fasst es zusammen. Die vollständige Implementierung finden Sie im Abschnitt zur Komprimierung weiter unten.
Die Anweisung zur Aktualisierung des Sitzungsmerkmals in Claude Code verwendet ein zehnteiliges Template mit Token-Limits pro Abschnitt, der Ersatz durch {{variableName}} sowie strenge Regeln zur Beibehaltung der Abschnittsüberschriften. Eine vereinfachte Version vermittelt die Kernidee:
Based on the conversation above, update the session notes.
Update each section below with specific, actionable details from the conversation.
Write info-dense content: file paths, function names, error messages, exact commands.
Skip a section if there are no new insights to add. Do not add filler.
Keep each section under 500 words. If approaching the limit, cycle out less important
details while preserving the most critical information.
Always update Current State to reflect the most recent work.
Sections:
# Session Title
# Current State
# Task Specification
# Files and Functions
# Errors and Corrections
# Key Results
# Next Steps
Das Erstellungstemplate fügt Abschnitte für Workflow, Codebasis-Dokumentation, Erkenntnisse, Schlüsselergebnisse und Arbeitsprotokoll hinzu, beschränkt jeden Abschnitt auf 2.000 Token und die gesamte Datei auf 12.000 Token und verwendet den Hintergrundagenten, um die Abschnitte mit FileEdit zu bearbeiten, anstatt die Datei neu zu schreiben.
Zustand: Anweisungen außerhalb von Nachrichten
Bei jedem API-Aufruf werden Anweisungen und Erinnerungen von der Festplatte geladen und in eine SystemMessage eingefügt, genau wie prependUserContext() in Claude Code es tut. Sie werden nicht in messages[] gespeichert, weshalb die Komprimierung sie ignoriert. Der Inhalt der aufgerufenen Fähigkeiten sowie die Sitzungsmerkmale befinden sich in Zustandsfeldern, die der Komprimierung standhalten:
class AgentState(MessagesState):
summary: str # compaction summary
instructions: str # CLAUDE.md equivalent (never compacted)
invoked_skills: dict[str, str] # skill name → content (survives compaction)
session_memory: str | None # background summary for instant compaction
# Cross-session memory: .agent/memory/*.md files read from disk every turn.
# Survives compaction (re-read from disk, never in messages[]).
# Agent creates memories via write_file tool. For production at scale,
# use a lightweight LLM call to select relevant files (Claude Code's approach).
Kompaktheit: Alte Inhalte zusammenfassen, aktuelle beibehalten
Das Design folgt dem Vorbild von Claude Code. Aktuelle Nachrichten bleiben unverändert, da das Modell gerade mit ihnen gearbeitet hat, und nur die älteren werden zusammengefasst.
def compact(state: AgentState) -> dict:
messages = state["messages"]
# Keep recent messages (Claude Code uses calculateMessagesToKeepIndex)
kept = trim_messages(
messages, strategy="last",
token_counter=count_tokens_approximately,
max_tokens=COMPACT_THRESHOLD // 5,
start_on="human", end_on=("human", "tool"),
)
kept_ids = {id(m) for m in kept}
# Summarise only the OLDER messages
to_summarise = [m for m in messages if id(m) not in kept_ids]
if not to_summarise:
return {}
response = model.invoke(
to_summarise + [HumanMessage(content=COMPACT_PROMPT)]
)
# Delete old, keep recent, inject summary + re-inject skills
delete = [RemoveMessage(id=m.id) for m in to_summarise]
summary = HumanMessage(content=f"[Summary]\n{response.content}")
skill_msgs = [
HumanMessage(content=f"[Skill: {n}]\n{c[:5000]}")
for n, c in state.get("invoked_skills", {}).items()
]
return {"messages": [*delete, summary, *skill_msgs], "summary": response.content}
Das Ergebnis ist zunächst die Zusammenfassung, danach die gespeicherten neueren Nachrichten sowie die erneut eingefügten Fähigkeiten – es handelt sich dabei nicht um eine einzige Zusammenfassung, die alles ersetzt. Durch Aufruf von RemoveMessage entsteht ein neuer Checkpoint ohne die alten Nachrichten, während frühere Checkpoints zur Wiederherstellung in der Datenbank verbleiben, genauso wie JSONL in Claude Code die vorherigen Einträge bewahrt.
Gedächtnis: dateibasiert, wird bei jeder Runde gelesen
Das Beispiel zeigt, wie Claude Code dies tatsächlich umsetzt. Die Gedächtnisdateien befinden sich auf der Festplatte unter .agent/memory/*.md; call_model liest sie bei jeder Runde und sie gelangen niemals in messages[]. Da sie von der Festplatte neu gelesen und nicht aus dem Gespräch neu erstellt werden, kann die Komprimierung sie nicht verlieren.
# In call_model — read memory files from disk every turn
MEMORY_DIR = Path(".agent/memory")
if MEMORY_DIR.exists():
for mem_file in MEMORY_DIR.glob("*.md"):
content = mem_file.read_text()[:4096]
system_parts.append(f"Memory ({mem_file.stem}):\n{content}")
# Agent creates memories by writing files:
# write_file(".agent/memory/user_prefs.md", "User prefers concise responses.")
Claude Code verwendet pro Runde einen separaten Sonnet-Aufruf, um die relevanten Speicherdateien auszuwählen – bis zu 5 aus möglicherweise vielen: Ein schnelles Modell erhält die Frage sowie eine Liste mit Dateinamen und Beschreibungen und gibt die besten Ergebnisse zurück. Das Beispiel lädt alle Dateien, was einfacher ist, aber nicht skalierbar ist. Produktivumgebungen mit vielen Speichern sollten dem ursprünglichen Ansatz folgen: Ein leichtgewichtiges LLM-Aufruf mit der Frage und dem Speicherindex entscheidet, welche Dateien geladen werden sollen.
Fähigkeiten: Ein Tool mit schrittweiser Offenlegung
@tool
def load_skill(skill_name: str) -> str:
"""Load a specialised skill prompt by name."""
return SKILL_REGISTRY.get(skill_name, f"Unknown skill: {skill_name}")
Listen Sie die kritischen Fähigkeiten in den Anleitungsdokumenten auf, damit sie bei der Komprimierung erhalten bleiben. Das Fähigkeitskatalog ist eine Bequemlichkeit und keine Voraussetzung.
Was wird nach einer Sitzung gespeichert?
Nehmen Sie eine Sitzung an, in der der Benutzer mehrere Fragen stellt, eine Komprimierung auslöst und anschließend wieder zurückkehrt. Die gespeicherten Daten des Agents sehen so aus:
.agent-data/
checkpoints.sqlite ~5 MB (all state snapshots, grows with session)
tool-results/
bash-84ffc64a.txt 15 KB (large bash output persisted with preview)
.agent/
memory/
project_goal.md 206 B (cross-session, survives compaction)
user_prefs.md 125 B (read from disk every turn)
Für dieselbe Arbeit speichert Claude Code mehr, weil es ein ausschließlich zum Hinzufügen verwendbares JSONL-Transkript nutzt, das niemals gelöscht wird, und separate Dateien für jeden Subagenten sowie eine kompakte Sicherungskopie bereithält:
~/.claude/projects/{cwd}/
{sessionId}.jsonl 22 MB (full transcript, never deleted)
{sessionId}/
tool-results/ 12 files
subagents/ 96 files (subagent transcripts + compact backups)
session-memory/ (disabled)
history.jsonl 13k lines (prompt recall, UI only)
memory/ 4 files (auto-memory)
LangGraphs Checkpointer speichert Zustandsaufnahmen anstelle eines Ereignislogs, wodurch der Speicherbedarf geringer ist. Frühere Checkpoints bleiben erhalten und können bei der Fehlersuche oder Wiederherstellung über graph.get_state_history(config) eingesehen werden.
Was die Replik abdeckt
Die LangGraph-Replik kümmert sich um den Kern der Aufgabe: Das Laden von Anweisungen vom Festplattenspeicher, das Komprimieren älterer Nachrichten, während neuere erhalten bleiben, das Speichern großer Tool-Ergebnisse in Dateien sowie das Wiederlesen des Zwischenspeichers zwischen den Sitzungen. Das Produktionsystem fügt eine durch LLMs gesteuerte Speicherauswahl über einen schnellen Sonnet-Aufruf hinzu, Delta-Anhänge für Fähigkeiten und Agenten, isolierte Subagenten, Hooks aus Plugins sowie die Verwaltung des Prompt-Caches. Mit dieser Struktur bietet es eine nützliche Referenz für alle, die ihren eigenen Agenten entwickeln.