Accueil / Articles

Claude Code : Internes de contexte et de mémoire, reconstruits en tant qu’agent LangGraph

Comment Claude Code construit les appels API, exécute des sous-agents et compresse les sessions longues, puis comment les mêmes idées sont reconstituées dans LangGraph à l’aide de vérificateurs d’état et de fichiers de mémoire.

Éditeur : REACTAPP.TOP11 min de lecture2610 mots

Sources: Scaling Agent Harnesses for Long-Running Sessions

Les détails provenant du code source fuité de Claude Code v2.1.87 montrent comment l’outil gère le contexte et la mémoire au cours de longues sessions de codage. Il fait face aux problèmes habituels liés aux agents à long terme : un contexte qui dépasse la capacité du modèle, des sorties d’outil impossibles à prédire, le rappel de la mémoire des sessions précédentes au bon moment, ainsi que le maintien intact des instructions de projet après synthèse. Ce qui suit décrit ce mécanisme avant de reconstruire une version simplifiée à l’aide de LangGraph et de ses outils de sauvegarde intégrés.

Comment Claude Code gère le contexte

Une requête API est composée de trois parties. Deux d’entre elles, l’indication système (identité ainsi que le contexte Git) et le tableau des outils (schémas JSON pour Bash, Read, Edit, Skill, Agent et MCP), restent en dehors du tableau des messages et ne sont jamais concernées par la compression. Le reste est intégré au tableau des messages sous forme d’entrées utilisateur-rôle encadrées par les balises <system-reminder> : les fichiers d’instructions en haut, la conversation en cours ainsi que les résultats des outils au milieu, et les entrées d’annexes (mémoires, listes de compétences et d’agents, instructions MCP, sorties des hooks) ajoutées après chaque exécution d’outil. Lorsque la fenêtre est remplie, la compression condense chaque message et régénère les annexes essentielles à partir de leurs sources originales. Viennent ensuite la boucle de requête, les sous-agents et la compression.

La boucle de requête

La saisie d’un message déclenche une boucle dans laquelle chaque itération correspond à une seule appel API, et cela se poursuit jusqu’à ce que le modèle réponde sans effectuer d’appels d’outils.

Avant le début de la boucle, une requête Sonnet en arrière-plan sélectionne jusqu’à cinq fichiers de mémoire pertinents en se basant uniquement sur le texte de la question de l’utilisateur, car l’envoi de toute la conversation coûterait trop cher. Les résultats ne sont pas utilisés immédiatement ; ils sont ajoutés après l’exécution des outils, de sorte que le premier appel API d’une manche ne les inclut jamais.

Chaque itération élimine les éléments de l’array des messages en mémoire avant de l’envoyer à l’API. Elle réduit d’abord la liste aux éléments situés après la dernière frontière de compactage, sans modifier le fichier JSONL complet sur le disque. Ensuite, elle enregistre tout résultat d’outil trop volumineux dans son propre fichier et remplace celui-ci par une prévisualisation de 2 Ko intégrée. Si le nombre de tokens reste supérieur à environ 93 % de la fenêtre de contexte, une opération de compactage est alors déclenchée.

Une réponse peut être soit du texte brut, ce qui met fin à ce tour, soit une ou plusieurs appels d’outils, ce qui exécute les opérations et fait reprendre la boucle. Les outils se divisent en groupes : des outils généraux tels que Bash, Read, Edit et Grep ; l’outil Skill, qui insère un fichier SKILL.md complet dans la conversation ; l’outil Agent, qui lance un sous-agent isolé qui renvoie un seul résultat ; et ToolSearch, qui récupère un schéma d’outil différé pour l’itération suivante. Une fois chaque tour d’outils terminé, des pièces jointes delta sont ajoutées pour les listes de compétences, les listes d’agents, les instructions MCP ainsi que tout préchargement de mémoire achevé, et seuls les éléments qui ont changé depuis le passage précédent sont émis.

Sous-agents

L’appel à l’outil Agent fait en sorte que Claude Code crée un contexte isolé au sein du même processus. Le sous-agent démarre avec une copie de la conversation du parent, reçoit ses propres outils et son prompt système, fonctionne avec la réflexion désactivée pour réduire les coûts en tokens de sortie, et exécute lui-même exactement le même code en boucle de requête. Les agents Explore et Plan perdent également CLAUDE.md et git status de cette copie, ce qui permet d’économiser davantage de tokens.

Seul le message de résultat final est renvoyé au parent. Les appels aux outils, le raisonnement ainsi que toute compression au sein du sous-agent sont enregistrés dans un transcript de sidechain, subagents/agent-{id}.jsonl, que le parent n’ouvre jamais. Les coûts restent ainsi isolés : des dizaines d’appels internes aux outils peuvent se traduire par un seul message dans le contexte du parent.

Compression

Un mécanisme de compression microcompacte existe dans le code afin de réduire la taille des résultats des anciennes requêtes avant une compression complète, mais il est désactivé et hors champ ici. La compression elle-même propose deux stratégies, la seconde servant de solution de repli lorsque la première n’est pas disponible. La première stratégie remplace immédiatement le fichier de mémoire de session préparé, de sorte que la compression n’a pas besoin d’appel API propre ; un agent en arrière-plan assemble ce fichier étape par étape lors de tours précédents à l’aide d’appels distincts, et ce chemin est actuellement désactivé par des mécanismes de contrôle de fonctionnalités. La deuxième stratégie, celle qui est utilisée, consiste en une compression complète du LLM. Un agent dérivé reçoit les messages actuels ainsi qu’un prompt structuré pour la compression et résume la conversation à environ cinq mille tokens. Les messages les plus récents sont conservés tels quels à côté de ce résumé afin que le modèle dispose toujours de son contexte de travail immédiat, tandis que tout ce qui est plus ancien est remplacé par le résumé.

Ensuite, tous les fichiers joints qui se trouvaient dans messages[] et qui ont été regroupés sont reconstruits. Les listes d’agents, les noms d’outils différés ainsi que les instructions MCP sont annoncés intégralement une nouvelle fois, et les fonctions de démarrage de session s’exécutent à nouveau. Le contenu des compétences qui ont été invoquées est récupéré à partir d’un tableau en mémoire limité à environ 25 000 tokens, et les cinq fichiers les plus récemment consultés sont lu à nouveau depuis le disque. Le prompt du système, tools[], Claude.md et le contexte git n’ont jamais fait partie de messages[], donc la compression ne peut pas les affecter ; leurs caches mémorisés sont simplement effacés afin que la prochaine étape reçoive des valeurs fraîches.

Une chose a été intentionnellement omise : le catalogue des compétences n’est pas reconstruit, ce qui permet d’économiser environ 4 000 tokens. L’effet secondaire est que, après la compression, le modèle ne peut pas trouver les compétences qu’il n’a pas encore utilisées, à moins que Claude.md ne les liste. La compression entraîne également la perte de détails, car le résumé initial est regroupé avec tout le reste, et après le troisième passage, les faits originaux ont été compressés trois fois. Rien n’a vraiment disparu, car le JSONL en écriture seule sur le disque conserve toute l’histoire, et le marqueur de limite de compression (parentUuid: null) indique au chargeur par où commencer la reprise.

Une version simplifiée de LangGraph

Une version plus petite de la même architecture peut être construite avec LangGraph. Il dispose déjà de mécanismes de sauvegarde pour la persistance ainsi que de la fonction RemoveMessage pour supprimer les messages. La mémoire entre sessions suit la même approche que Claude Code, avec des fichiers Markdown lus sur le disque à chaque tour. C’est la logique de compactage qui doit être développée.

Dans le diagramme de correspondance, chaque flèche relie un concept de Claude Code à gauche à son équivalent LangGraph à droite. Le bord marqué « amélioration » indique que LangGraph peut conserver les instructions en dehors de messages[], de sorte que la compactation ne les affecte jamais.

Cinq nœuds composent le graphe. Les arêtes pointillées provenant de call_model sont conditionnelles : elles envoient les appels aux outils via tools et les réponses textuelles vers post_turn. Dans load_context, les fichiers de mémoire (.agent/memory/*.md) sont lus depuis le disque, puis intégrés dans le SystemMessage de call_model. Cela les maintient complètement en dehors de messages[], de sorte qu’ils restent inchangés après compression.

Construction du graphe

Les nœuds s’exécutent séquentiellement, et après call_model, un branchement conditionnel fait soit défiler les outils, soit passe à post_turn. Un checkpointer SQLite conserve tout l’état entre les redémarrages du processus.

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()

Le lien entre les deux graphiques se trouve à l’intérieur de l’outil run_subagent :

@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

Lorsque le modèle appelle run_subagent, le nœud principal tools exécute la fonction, ce qui déclenche l’exécution du graphe du sous-agent, lequel met en œuvre ses propres boucles call_model et tools de manière indépendante. Seule la réponse finale revient, sous forme de chaîne de caractères, de sorte que le graphe principal perçoit un résultat ordinaire d’outil. Le sous-agent ne dispose d’aucune instruction, de fichiers de mémoire ni de point de contrôle, ce qui rend son contexte léger et chaque appel autonome. L’échantillon présente un seul type de sous-agent. Claude Code choisit parmi plusieurs types en temps de exécution (Explore pour la recherche, Plan pour la planification, un type polyvalent disposant de tous les outils, ainsi que des agents de type plugin comme vercel:ai-architect), chacun ayant ses propres outils, permissions et prompt système.

Prompt

Il s’agit de versions simplifiées utilisées dans la réplication de LangGraph ; les prompts réels de Claude Code sont plus détaillés.

Le prompt compact de Claude Code est bien plus volumineux, avec neuf sections, un espace de travail <analysis>, une introduction indiquant « CRITICAL : Répondez UNIQUEMENT en texte » et un bloc de formatage d’exemple. La version simplifiée ci-dessous conserve les éléments essentiels :

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.

Dans le code, la conversation est d’abord divisée, trim_messages identifiant la partie la plus récente à conserver. Seule la partie plus ancienne est envoyée au modèle avec cet indice, et ce modèle en prend connaissance dans son intégralité (messages de l’utilisateur, réponses de l’assistant, résultats des outils) avant de la condenser. La mise en œuvre complète se trouve dans la section dédiée à la compression plus bas.

L’indice d’actualisation de la mémoire de session dans Claude Code utilise un modèle à dix sections, avec des limites de tokens par section, des substitutions {{variableName}} et des règles strictes concernant la préservation des en-têtes de section. Une version simplifiée résume l’idée principale :

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

Le modèle de production ajoute des sections pour le Flux de travail, la Documentation du code, les Leçons apprises, les Résultats clés et le Journal de travail, limite chaque section à 2 000 tokens et le fichier entier à 12 000 tokens, et fait en sorte que l’agent en arrière-plan modifie les sections à l’aide de FileEdit plutôt que de réécrire le fichier.

État : instructions en dehors des messages

À chaque appel API, les instructions et les mémoires sont chargés depuis le disque et placés dans un SystemMessage, tout comme le fait prependUserContext() dans Claude Code. Ils ne sont pas conservés dans messages[], c’est pourquoi la compression les ignore. Le contenu des compétences invoquées et la mémoire de session résident dans des champs d’état qui survivent à la compression :

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).

Compaction : résumer les anciens, conserver les récents

La conception suit les principes de Claude Code. Les messages récents restent intacts, car le modèle vient juste de les traiter, et seuls les plus anciens sont résumés.

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}

Le résultat est d’abord un résumé, suivi des messages récents conservés, puis des compétences réinjectées, et non un simple résumé qui remplace tout. Appeler RemoveMessage crée un nouveau point de contrôle sans les anciens messages, tandis que les points de contrôle antérieurs restent dans la base de données pour être récupérés, tout comme JSONL de Claude Code conserve les entrées avant compression.

Mémoire : basée sur des fichiers, lue à chaque tour

L’exemple reproduit exactement la manière dont Claude Code fonctionne réellement. Les fichiers de mémoire se trouvent sur le disque sous .agent/memory/*.md ; call_model les lit à chaque tour, et ils ne font jamais partie de messages[]. Comme ils sont relus depuis le disque et non reconstruits à partir de la conversation, la compression ne peut pas les perdre.

# 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 consacre un appel Sonnet distinct par tour pour sélectionner les fichiers de mémoire pertinents, jusqu’à 5 parmi de nombreux fichiers possibles : un modèle rapide reçoit la question ainsi qu’une liste des noms et des descriptions des fichiers, puis retourne les meilleures correspondances. L’exemple charge tous les fichiers, ce qui est plus simple mais ne permet pas une expansion efficace. Les configurations de production disposant de nombreuses mémoires doivent suivre la méthode originale : un appel à un LLM léger avec la question et l’index de la mémoire détermine quels fichiers charger.

Compétences : un outil avec une divulgation progressive

@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}")

Indiquez les compétences essentielles dans les fichiers d’instructions afin qu’elles survivent à la compression. Le catalogue des compétences est une facilité et non une exigence.

Que ce qui est stocké après une session

Imaginons une session où l’utilisateur pose plusieurs questions, déclenche une compression, puis revient plus tard. Les données stockées par l’agent ressembleront à ceci :

.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)

Pour le même travail, Claude Code stocke davantage de données, car il utilise un fichier de transcription JSONL écrit uniquement en ajout qui n’est jamais supprimé et conserve des fichiers séparés pour chaque sous-agent ainsi qu’une sauvegarde compactée :

~/.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)

Le checkpointer de LangGraph conserve des captures d’état à la place d’un journal d’événements, ce qui réduit l’empreinte mémoire. Les checkpoints antérieurs restent en place et peuvent être consultés via graph.get_state_history(config) lors du débogage ou de la récupération.

Ce que couvre la réplique

La réplique de LangGraph gère l’essentiel du travail : le chargement des instructions depuis le disque, la compression des messages anciens tout en conservant les plus récents, l’enregistrement des résultats des outils volumineux dans des fichiers, ainsi que la rélecture de la mémoire inter-sessions à chaque tour. Le système de production ajoute une sélection de mémoire pilotée par un LLM grâce à une appel rapide à Sonnet, des fichiers d’annexes détaillant les compétences et les agents, des sous-agents isolés, des connecteurs provenant de plugins, ainsi que la gestion du cache des prompts. Avec une telle architecture, c’est une référence utile pour quiconque souhaite créer son propre agent.