Inicio / Artículos

Internos de contexto y memoria de Claude Code, reconstruidos como un agente LangGraph

Cómo Claude Code genera llamadas a API, ejecuta subagentes y comprime sesiones largas, y luego cómo se repiten esas mismas ideas en LangGraph con mecanismos de punto de control y archivos de memoria.

Editor: REACTAPP.TOP11 min de lectura2610 palabras

Fuentes: Scaling Agent Harnesses for Long-Running Sessions

Los detalles del código fuente filtrado de Claude Code v2.1.87 muestran cómo la herramienta maneja el contexto y la memoria durante sesiones de programación prolongadas. Aborda los problemas habituales de los agentes de larga duración: un contexto que supera el límite del modelo, resultados de las herramientas impredecibles, la recuperación de la memoria de sesiones anteriores en el momento adecuado y el mantenimiento de las instrucciones del proyecto intactas tras su resumen. A continuación, se explica el mecanismo y luego se reconstruye una versión simplificada en LangGraph con sus comprobadores de puntos de control integrados.

Cómo gestiona Claude Code el contexto

Una solicitud de API se compone de tres partes. Dos de ellas, el prompt del sistema (identidad más contexto de Git) y el array de herramientas (esquemas JSON para Bash, Leer, Editar, Habilidad, Agente y MCP), permanecen fuera de la lista de mensajes y la compactación nunca los afecta. El resto se incluye en el array de mensajes como entradas de tipo usuario-rol envueltas en etiquetas <system-reminder>: los archivos de instrucciones en la parte superior, la conversación en curso y los resultados de las herramientas en el medio, y las entradas de archivos adjuntos (recuerdos, listas de habilidades y agentes, instrucciones MCP, salida de ganchos) que se añaden después de cada ronda de ejecución de herramientas. Una vez que la ventana se llena, la compactación condensa cada mensaje y regenera los archivos adjuntos esenciales a partir de sus fuentes originales. A continuación vienen el bucle de consultas, los subagentes y la compactación.

El bucle de consultas

Al escribir un mensaje se inicia un bucle en el que cada iteración corresponde a una sola llamada a la API, y este proceso continúa hasta que el modelo responde sin realizar ninguna llamada a herramientas.

Antes de que comience el bucle, una llamada en segundo plano de Sonnet selecciona hasta cinco archivos de memoria relevantes utilizando únicamente el texto de la pregunta del usuario, ya que enviar toda la conversación costaría demasiado. Los resultados no se utilizan de inmediato; se almacenan después de la ejecución de las herramientas, por lo que la primera llamada a la API de cada turno nunca los incluye.

Cada iteración elimina elementos del array de mensajes en memoria antes de enviarlo a la API. Primero reduce la lista a los elementos que se encuentran después del último límite de compactación, sin modificar el archivo JSONL completo en el disco. Luego, escribe cualquier resultado de herramienta de gran tamaño en su propio archivo y sustituye dicho resultado por una vista previa de 2 kb. Si, después de esto, el conteo de tokens sigue superando aproximadamente el 93 % del tamaño de la ventana de contexto, se activa la compactación.

Una respuesta puede ser texto plano, lo que finaliza el turno, o una o más llamadas a herramientas, las cuales se ejecutan y hacen que el bucle comience de nuevo. Las herramientas se dividen en grupos: herramientas generales como Bash, Read, Edit y Grep; la herramienta Skill, que introduce un archivo SKILL.md completo en la conversación; la herramienta Agent, que inicia un subagente aislado que devuelve un único resultado; y ToolSearch, que obtiene un esquema de herramientas diferido para la iteración siguiente. Una vez que finaliza cada ronda de herramientas, se insertan archivos adjuntos de tipo delta para las listas de habilidades, las listas de agentes, las instrucciones MCP y cualquier carga previa de memoria que haya finalizado, y solo se emite el material que ha cambiado desde el paso anterior.

Subagentes

Al utilizar la herramienta “Llamar al agente”, Claude Code crea un contexto aislado dentro del mismo proceso. El subagente comienza con una copia de la conversación del agente principal, recibe sus propias herramientas y prompt del sistema, funciona con el razonamiento desactivado para reducir el costo en tokens de salida, y ejecuta el mismo código de bucle de consultas por su cuenta. Los agentes “Explorar y Planificar” también pierden el contenido de CLAUDE.md y el estado de git de esa copia, lo que ahorra aún más tokens.

Solo el mensaje del resultado final vuelve al agente principal. Las llamadas a herramientas, el razonamiento y cualquier proceso de compactación dentro del subagente se registran en un archivo de transcripción separado, subagents/agent-{id}.jsonl, que el agente principal nunca abre. De esta manera, el costo se mantiene aislado: decenas de llamadas internas a herramientas pueden sumarse para generar un único mensaje en el contexto del agente principal.

Compactación

Existe una capa microcompacta en el código para reducir los resultados antiguos de las herramientas antes de una compactación completa, pero está desactivada y fuera de alcance aquí. La compactación en sí ofrece dos estrategias, siendo la segunda una solución de respaldo cuando la primera no está disponible. La primera estrategia sustituye inmediatamente por un archivo de memoria de sesión preparado, por lo que la compactación no necesita una llamada a la API; un agente en segundo plano reúne ese archivo paso a paso en turnos anteriores mediante llamadas separadas, y actualmente esta vía está desactivada por filtros de funcionalidad. La segunda estrategia, la que se utiliza actualmente, consiste en una compactación completa del LLM. Un agente bifurcado recibe los mensajes actuales y un prompt estructurado para la compactación, y reduce la conversación a unos cinco mil tokens. Los mensajes más recientes se mantienen tal cual junto a ese resumen para que el modelo siga teniendo su contexto de trabajo inmediato, mientras que todo lo anterior es reemplazado por el resumen.

A continuación, se reconstruyen todos los archivos adjuntos que se encontraban en messages[] y que fueron resumidos. Las listas de agentes, los nombres de herramientas diferidas y las instrucciones MCP se anuncian nuevamente en su totalidad, y se ejecutan de nuevo los ganchos de inicio de sesión. El contenido de las habilidades que se invocaron vuelve a partir de un mapa en memoria con un límite de aproximadamente 25,000 tokens, y los cinco archivos modificados más recientemente se leen de nuevo desde el disco. El prompt del sistema, tools[], Claude.md y el contexto de git nunca estuvieron en messages[], por lo que la compactación no puede afectarlos; sus cachés memorizados se eliminan simplemente para que en la siguiente interacción se muestren valores frescos.

Hay algo que se omite intencionadamente: el catálogo de habilidades no se reconstruye, lo que ahorra aproximadamente 4,000 tokens. El efecto secundario es que, tras la compactación, el modelo no puede encontrar habilidades que aún no haya utilizado, a menos que Claude.md las enumere. La compactación también pierde detalles, ya que el resumen anterior se resume junto con todo lo demás, y tras la tercera pasada los hechos originales han sido comprimidos tres veces. Nada desaparece realmente, porque el JSONL de solo escritura en el disco contiene toda la historia y el marcador de límite de la compactación (parentUuid: null) indica al cargador por dónde comenzar la reanudación.

Una versión simplificada de LangGraph

Se puede crear una versión más pequeña de la misma arquitectura con LangGraph. Ya proporciona herramientas de checkpointing para la persistencia y RemoveMessage para eliminar mensajes. La memoria entre sesiones sigue el mismo enfoque que Claude Code, leyendo archivos en formato markdown del disco en cada turno. La lógica de compactación es la parte que aún debe ser desarrollada.

En el diagrama de mapeo, cada flecha conecta un concepto de Claude Code a la izquierda con su equivalente en LangGraph a la derecha. El borde marcado como “mejora” indica que LangGraph puede almacenar instrucciones fuera de messages[], por lo que la compactación nunca las afecta.

Cinco nodos componen el grafo. Los bordes punteados que salen de call_model son condicionales: envían las llamadas a herramientas a través de tools y las respuestas de texto a post_turn. En load_context, los archivos de memoria (.agent/memory/*.md) se leen del disco y luego se integran en el SystemMessage de call_model. Esto evita que aparezcan en messages[] por completo, de modo que permanecen intactos tras la compactación.

Construcción del grafo

Los nodos se ejecutan en secuencia, y después de call_model un ramal condicional ya sea recorre las herramientas o pasa a post_turn. Un checkpointer de SQLite mantiene todo el estado durante los reinicios del proceso.

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

El enlace entre los dos gráficos se encuentra dentro de la herramienta 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

Una vez que el modelo llama a run_subagent, el nodo principal tools ejecuta la función, lo que a su vez invoca el grafo del subagente, el cual ejecuta su propio bucle call_model y tools de forma independiente. Solo la respuesta final vuelve como cadena de texto, por lo que el grafo principal considera que se trata de un resultado normal de una herramienta. El subagente no cuenta con instrucciones, archivos de memoria ni checkpointer, por lo que su contexto es sencillo y cada llamada funciona de forma autónoma. La muestra incluye un único tipo de subagente. Claude Code elige entre varios en tiempo de ejecución (Explore para búsquedas, Plan para planificación, un tipo de uso general con todas las herramientas, y agentes de complemento como vercel:ai-architect), cada uno con sus propias herramientas, permisos y prompt del sistema.

Prompts

Estas son versiones simplificadas utilizadas en la replicación de LangGraph; los prompt reales de Claude Code son más detallados.

El prompt compacto de Claude Code es mucho más extenso, con nueve secciones, una zona de trabajo <analysis>, un preámbulo que indica “CRITICAL: Respond with TEXT ONLY” y un bloque de formato de ejemplo. La versión simplificada que se muestra a continuación mantiene lo esencial:

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.

En el código, primero se divide la conversación, siendo trim_messages el encargado de identificar la parte más reciente que se mantendrá. Solo la parte más antigua se envía al modelo con esta instrucción, y ese modelo recibe todo el contenido (mensajes del usuario, respuestas del asistente, resultados de las herramientas) y lo condensa. La implementación completa se encuentra en la sección de compactación más abajo.

La instrucción de actualización de la memoria de sesión en Claude Code utiliza un template de diez secciones con límites de tokens por sección, sustituciones de {{variableName}} y reglas estrictas para preservar los encabezados de las secciones. Una versión simplificada resume la idea principal:

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

La plantilla de producción agrega secciones para Flujo de trabajo, Documentación del código, Aprendizajes, Resultados clave y Diario de trabajo, limita cada sección a 2,000 tokens y todo el archivo a 12,000, y cuenta con un agente de fondo que edita las secciones mediante FileEdit en lugar de reescribir el archivo.

Estado: instrucciones fuera de los mensajes

En cada llamada a la API, las instrucciones y los recuerdos se cargan del disco y se colocan en un SystemMessage, tal como lo hace prependUserContext() en Claude Code. No se conservan en messages[], por lo que la compactación los ignora. El contenido de las habilidades invocadas y la memoria de la sesión permanecen en campos de estado que sobreviven a la compactación:

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

Compresión: resumir lo antiguo, conservar lo reciente

El diseño sigue el modelo de Claude Code. Los mensajes recientes permanecen intactos, ya que el modelo acababa de trabajar con ellos, y solo se resumen los más antiguos.

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}

El resultado es primero el resumen, luego los mensajes recientes conservados, seguidos de las habilidades reinyectadas, y no un único resumen que reemplace todo. Llamar a RemoveMessage genera un punto de control nuevo sin los mensajes antiguos, mientras que los puntos de control anteriores permanecen en la base de datos para su recuperación, al igual que JSONL de Claude Code conserva las entradas anteriores a la compactación.

Memoria: basada en archivos, se lee en cada turno

El ejemplo reproduce exactamente cómo lo hace Claude Code. Los archivos de memoria se encuentran en el disco bajo .agent/memory/*.md; call_model los lee en cada turno y nunca entran en messages[]. Dado que se vuelven a leer del disco y no se reconstruyen a partir de la conversación, la compactación no puede perderlos.

# 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 dedica una llamada separada de Sonnet por turno para seleccionar los archivos de memoria relevantes, hasta 5 de entre muchos posibles: un modelo rápido recibe la pregunta junto con un listado de nombres y descripciones de archivos y devuelve las mejores coincidencias. El ejemplo carga todos los archivos, lo cual es más sencillo pero no es escalable. Las configuraciones en producción con muchas memorias deben seguir el enfoque original: una llamada a un LLM ligero con la pregunta y el índice de memoria decide qué cargar.

Habilidades: una herramienta con revelación progresiva

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

Enumere las habilidades críticas en los archivos de instrucciones para que sobrevivan a la compactación. El catálogo de habilidades es una conveniencia y no un requisito.

Qué se almacena después de una sesión

Considere una sesión en la que el usuario hace varias preguntas, activa la compactación y luego vuelve más tarde. Los datos almacenados del agente se verán así:

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

Para el mismo trabajo, Claude Code almacena más datos, ya que utiliza un archivo de transcripción JSONL de solo escritura que nunca se elimina y mantiene archivos separados para cada subagente, además de una copia de seguridad compacta:

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

El checkpointer de LangGraph mantiene instantáneas del estado en lugar de un registro de eventos, por lo que su huella es más pequeña. Los puntos de control anteriores permanecen y pueden ser examinados mediante graph.get_state_history(config) durante la depuración o la recuperación.

Qué cubre la réplica

La réplica de LangGraph se encarga de la parte fundamental del trabajo: cargar instrucciones desde el disco, comprimir mensajes antiguos mientras se conservan los recientes, guardar los resultados de las herramientas en archivos y volver a leer la memoria entre sesiones en cada turno. El sistema de producción añade una selección de memoria controlada por modelos LLM a través de una llamada rápida a Sonnet, adjuntos delta que incluyen habilidades y agentes, subagentes aislados, conectores de plugins y gestión de la caché de prompts. Con este diseño, sirve como referencia útil para quienes crean sus propios agentes.

Leer más