Inicio / Artículos

Ingeniería de contexto en la práctica: Un agente de investigación del Claude Agent SDK

Construir un agente de investigación en Python con el Claude Agent SDK utilizando patrones de escritura, selección, compresión e aislamiento, y luego compararlo con una línea de base simple en pruebas medidas.

Editor: REACTAPP.TOP20 min de lectura4633 palabras

Fuentes: Context Engineering in Practice: Building a Production AI Agent with the Claude Agent SDK

Un agente de investigación rara vez se topa con una sola instrucción compleja. Lo que realmente perjudica es la acumulación de solicitudes. Durante una sola investigación, el contexto se llena de instrucciones del usuario, un plan, consultas de búsqueda, páginas descargadas, esquemas de herramientas, metadatos de fuentes, afirmaciones incompletas, informes de asistentes y turnos anteriores. Si se deja todo eso activo, las pruebas válidas terminan compitiendo por la atención del agente con su propio montón de tareas pendientes.

Aquí, el marco de trabajo Write, Select, Compress e Isolate se convierte en una arquitectura funcional en Python a través del Claude Agent SDK. Un único agente mejora paso a paso, con cada decisión planteando la misma pregunta: ¿qué debe ver el modelo en este momento y qué puede quedar fuera de su alcance? El objetivo es un agente de investigación que se pueda ejecutar, no un catálogo de funciones del SDK.

La construcción se lleva a cabo en diez pasos:

  1. Definir qué se está creando.
  2. Establecer una línea de base sencilla.
  3. Escribir: sacar el estado de trabajo fuera de la conversación.
  4. Seleccionar: armar un conjunto de trabajo delimitado.
  5. Comprimir: hacer que las sesiones largas sean recuperables.
  6. Aislar: delegar investigaciones específicas.
  7. Aislar en el entorno las salidas de herramientas complejas.
  8. Transportar el contexto entre sesiones.
  9. Armar el agente diseñado con base en el contexto.
  10. Comparar las dos arquitecturas.

Qué se construye

Ante una pregunta, el agente planifica su investigación, busca fuentes primarias, asigna investigaciones detalladas a asistentes, registra pruebas y finalmente escribe un informe con referencias. La tarea de ejemplo: comparar los principales enfoques de memoria a largo plazo utilizados en agentes de IA en producción y explicar cuál es el costo de cada uno. Requiere artículos académicos, documentación de implementación, pruebas de repositorios y verificaciones de citas, por lo que es lo suficientemente amplia como para ser una verdadera prueba.

El agente principal mantiene el plan y redacta la síntesis final. El estado de trabajo seleccionado se almacena en archivos, los límites de recuperación controlan la cantidad de conocimiento que entra, y los subagentes abordan preguntas menores dentro de sus propios contextos. Nada de esto está integrado en el SDK. Éste proporciona el modelo y el ciclo de herramientas, mientras que la aplicación decide dónde se almacena el estado y cuánto información puede pasar por cada límite.

La primera figura muestra el sistema inicial: un agente principal conectado a herramientas, conocimiento, una pizarra de bocetos y subagentes. El resto de la estructura modifica la forma en que la información se transmite a través de esas conexiones.

Comience creando un entorno e instalando una versión fija del SDK antes de ejecutar cualquier módulo.

# terminal
python3 -m venv .venv
source .venv/bin/activate
python -m pip install claude-agent-sdk==0.2.139
export ANTHROPIC_API_KEY="your-api-key"

El diseño mantiene la configuración de los agentes separada de las operaciones del espacio de trabajo, las herramientas personalizadas, los ganchos y las mediciones. Esa separación es importante porque cada operación en un contexto modificará un límite específico sin necesidad de crear una aplicación completamente nueva.

# code/
research_agent/
  config.py       # naive and engineered ClaudeAgentOptions
  hooks.py        # pre-compaction checkpoint
  metrics.py      # message-stream and context measurements
  runner.py       # repeated runs and comparison
  tools.py        # in-process MCP tools
  workspace.py    # scratchpad and bounded retrieval
knowledge/
  memory_approaches.json
tests/
CLAUDE.md

La ingeniería de contexto comienza con la asignación de responsabilidades. La conversación alberga la decisión actual, mientras que los archivos, las herramientas, los sistemas de recuperación y los subagentes contienen todo aquello que no necesita estar activo en ese momento.

La línea de base ingenua

El programa más sencillo del Agent SDK entrega el bucle de control a query(). Claude recibe la instrucción, decide si se necesita una herramienta, observa el resultado y repite el proceso hasta que obtiene una respuesta o alcanza un límite configurado. Dado que el SDK proporciona este bucle, la aplicación no necesita escribir su propio mecanismo para analizar y enviar solicitudes a herramientas.

# research_agent/minimal.py
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query

QUESTION = "Compare approaches for long-term memory in production AI agents."

async def main() -> None:
    options = ClaudeAgentOptions(
        model="sonnet",
        allowed_tools=["WebSearch", "WebFetch"],
        permission_mode="dontAsk",
        max_turns=20,
    )
    async for message in query(prompt=QUESTION, options=options):
        if isinstance(message, ResultMessage):
            print(message.result or "")

asyncio.run(main())

Ese programa corto oculta algo: query() ejecuta el bucle, pero la aplicación sigue decidiendo qué entra en él. La versión básica ejecutable en research_agent/config.py detalla esas opciones.

# research_agent/config.py
def naive_options(*, run_root, server, model, max_budget_usd):
    tools = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"]
    return ClaudeAgentOptions(
        cwd=run_root,
        model=model,
        tools=tools,
        allowed_tools=[*tools, "mcp__research__*"],
        permission_mode="dontAsk",
        mcp_servers={"research": server},
        strict_mcp_config=True,
        setting_sources=[],
        system_prompt={
            "type": "preset",
            "preset": "claude_code",
            "append": NAIVE_PROMPT,
        },
        env={"ENABLE_TOOL_SEARCH": "false"},
        max_turns=20,
        max_budget_usd=max_budget_usd,
    )

En la configuración básica no hay herramienta de escritura, ni herramienta de agente, ni opción agents, ni gancho, ni archivo de instrucciones del proyecto. El líder debe completar toda la investigación dentro de una sola conversación. Establecer ENABLE_TOOL_SEARCH=false también evita que los esquemas MCP se apliquen de forma diferida. El prompt del sistema completa la política indicando al agente que utilice la herramienta whole-corpus, conserve los hallazgos en la conversación y no escriba notas en el espacio de trabajo ni delegue tareas.

La herramienta whole-corpus está diseñada de forma intencional para ser muy sencilla, ya que el problema radica en lo que devuelve y no en cómo está implementada.

# research_agent/tools.py
@tool(
    "load_knowledge_corpus",
    "Return the entire local memory-research collection. Intended only for the naive baseline.",
    {},
    annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
)
async def load_knowledge(_: dict[str, Any]) -> dict[str, Any]:
    return _text_result(load_corpus(corpus_path))

Cuando Claude lo solicita, el SDK emite el resultado como un UserMessage que contiene un ToolResultBlock. El “usuario” en cuestión es un rol del protocolo y no otro mensaje humano. Ese bloque se agrega a la historia que se pasa a cada paso posterior del modelo, de modo que una búsqueda en la web, tres páginas obtenidas y el corpus completo generan una secuencia repetitiva como esta.

UserMessage(question)
AssistantMessage(ToolUseBlock: load_knowledge_corpus)
UserMessage(ToolResultBlock: entire corpus)
AssistantMessage(ToolUseBlock: WebSearch + WebFetch)
UserMessage(ToolResultBlock: raw search and page content)
AssistantMessage(final report)

Nada en esa secuencia está mal en sí misma. Es ingenua porque la aplicación nunca convierte los resultados brutos en un estado externo organizado ni los elimina de la trayectoria activa.

La segunda figura muestra el contexto ingenuo que recopila instrucciones, definiciones de herramientas, páginas en bruto, hallazgos e historial de conversaciones dentro de una sola ventana. El crecimiento proviene de la política de la aplicación y no de ningún defecto en el bucle del SDK.

La búsqueda de herramientas está activa por defecto en la mayoría de las configuraciones del Agent SDK, con excepciones documentadas. Por lo tanto, un punto de referencia justo exige desactivarla intencionadamente si el experimento pretende cargar por adelantado toda la superficie de herramientas. Como control, el diseño ingenuo es útil porque elimina todas las decisiones de gestión de contexto que los pasos posteriores probarán.

Antes de invertir tiempo y recursos en una comparación, se realizó una prueba preliminar enviando una solicitud limitada a través de la misma configuración. Los resultados obtenidos demostraron que la autenticación, el enrutamiento y la resolución de modelos funcionaban correctamente.

# Captured output: python scripts/smoke_test.py
subtype=success
result=OPENROUTER_OK
models=claude-sonnet-5

Luego se ejecutó tres veces la línea de base completa, y los resultados se presentaron junto a las pruebas modificadas en la comparación final. La primera prueba permite concretar esa acumulación.

# Captured output: python scripts/show_naive_trial.py ../measurements/comparison.json
Naive trial 1
SDK result: success
Assistant steps: 4
Tool calls: 6
  WebFetch: 3
  WebSearch: 1
  mcp__research__load_knowledge_corpus: 1
  mcp__research__search_knowledge: 1
Final active context: 16,478 tokens
Final tool-result payload: 6,851 tokens
Cumulative tree input: 138,182 tokens
Estimated cost: $0.754

Los tres últimos valores miden cosas diferentes. La cifra de 16,478 corresponde al contexto activo final, y 6,851 de esos tokens provienen de los resultados de las herramientas. La cantidad de 138,182 representa la entrada acumulada en las cuatro etapas del asistente, incluyendo tanto la creación como la lectura de caché; por lo tanto, no significa que el agente haya mantenido una ventana de 138,182 tokens en un momento dado. Esta distinción es importante cuando se incluyen subagentes en la comparación.

Un agente ingenuo solo constituye un control válido si se desactivan explícitamente comodidades como la búsqueda de herramientas. De lo contrario, los valores predeterminados del SDK pueden hacer que el punto de referencia sea más inteligente de lo previsto.

Escribir: archivos de borrador

El primer cambio añade una pizarra de bocetos respaldada por el sistema de archivos. plan.md contiene la pregunta, las dimensiones de la investigación, lo que ya se ha hecho y qué queda por hacer. research_notes.md reúne afirmaciones, pruebas y contradicciones; sources.json alberga los metadatos de las citaciones, y open_questions.md muestra los asuntos pendientes. El material de fuente voluminoso se guarda en una carpeta de artefactos.

# research_agent/workspace.py
def initialize_workspace(root: Path, question: str) -> Path:
    workspace = root / "workspace"
    workspace.mkdir(parents=True, exist_ok=True)
    (workspace / "artifacts").mkdir(exist_ok=True)
    (workspace / "checkpoints").mkdir(exist_ok=True)

    for name, template in WORKSPACE_FILES.items():
        path = workspace / name
        if not path.exists():
            path.write_text(template.format(question=question), encoding="utf-8")
    return workspace

Dado que estos archivos son duraderos, el agente puede actualizarlos después de cada paso de investigación y leer solo aquel que necesita a continuación. Una interrupción o un evento de compactación mantiene intacto el plan y las pruebas elaborados, permitiendo que una persona lea directamente su estado sin tener que reconstruirlo a partir de un transcripción.

La tercera figura proviene de un script sin conexión que inicializa el espacio de trabajo e imprime los archivos exactos que utiliza el agente como área de trabajo temporal.

Una sesión cumple otra función. Los documentos de la sesión la describen como el almacenamiento para los prompts, las llamadas a herramientas, los resultados de estas y las respuestas necesarias para continuar una conversación. Por su parte, el área de trabajo temporal alberga el estado de investigación seleccionado por la aplicación. Reanudar una sesión restablece la trayectoria seguida, mientras que leer plan.md devuelve una visión del trabajo mantenida intencionadamente.

Esa distinción evita dos errores comunes. Si la transcripción es la única base de datos, la recuperación depende de un historial cada vez más extenso. Si el boceto se convierte en un desecho de transcripciones, el mismo problema de contexto se reproduce en el disco. Por lo tanto, cada archivo tiene un esquema limitado y alberga conclusiones, pruebas y trabajos pendientes, no conversaciones copiadas. El historial de sesiones registra cómo llegó el agente hasta allí, y el boceto mantiene el estado cuidadosamente seleccionado necesario para decidir qué sucederá a continuación.

Selección: un conjunto de trabajo limitado

El estado externo puede superar la capacidad que debería abarcar una sola llamada al modelo. La selección soluciona esto cargando los datos según la fase: la planificación lee plan.md, la síntesis lee research_notes.md, y la generación de citas lee sources.json. Las herramientas y el conocimiento siguen el mismo principio, ya que incluso sus metadatos pueden ocupar una parte importante del prompt.

En cuanto a las herramientas, la configuración diseñada permite realizar búsquedas entre ellas. La documentación de Anthropic indica que un catálogo puede contener hasta 10,000 herramientas, y que por búsqueda se devuelven hasta cinco resultados relevantes, a menos que se modifique esa configuración. Abarca tanto herramientas MCP como herramientas MCP personalizadas con SDK; por lo tanto, un catálogo grande puede permanecer fuera del contexto hasta que Claude requiera un esquema específico. Según la documentación, cuando hay aproximadamente diez herramientas o menos, cargar todo de antemano suele ser más rápido, lo que hace que la carga diferida sea una solución orientada a la escalabilidad y no un beneficio garantizado.

En cuanto al conocimiento, existe una herramienta personalizada search_knowledge basada en SQLite FTS5. Este componente recibe una consulta y un valor limitado de top_k, y devuelve los fragmentos citados en lugar de toda la colección. Las herramientas personalizadas del SDK funcionan a través de un servidor MCP dentro del mismo proceso, por lo que el ejemplo permanece autónomo aunque atraviesa los mismos límites que un servicio externo.

# research_agent/tools.py
@tool(
    "search_knowledge",
    "Search the local memory-research collection and return only the most relevant cited passages.",
    {
        "type": "object",
        "properties": {
            "query": {"type": "string", "minLength": 1},
            "top_k": {"type": "integer", "minimum": 1, "maximum": 10},
        },
        "required": ["query", "top_k"],
        "additionalProperties": False,
    },
    annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
)
async def search_knowledge(args: dict[str, Any]) -> dict[str, Any]:
    try:
        return _text_result(
            search_corpus(corpus_path, args["query"], args["top_k"])
        )
    except (KeyError, TypeError, ValueError, sqlite3.Error) as error:
        return _error_result(error)

La solicitud indica top_k=5, mientras que el esquema permite valores desde uno hasta diez. De esta manera se separan la política y el límite estricto: se puede ajustar la calidad de la recuperación posteriormente sin modificar el contrato de la herramienta, y una solicitud errónea que solicite miles de registros fallará antes de poder saturar el contexto.

La cuarta figura muestra las lecturas de archivos, la búsqueda en la herramienta y la recuperación de conocimientos como tres formas de seleccionar diferentes tipos de contexto, todas concluyendo con el mismo resultado: un conjunto de trabajo limitado para el paso actual.

Una prueba sin conexión ejecuta el código de búsqueda real y verifica que cinco sea un límite máximo, que cada elemento devuelto mantenga su URL de origen y que se rechacen los valores inválidos de top_k. No dice nada sobre la calidad de la recuperación semántica porque el conjunto de pruebas es muy pequeño. En producción, esa evaluación corresponde al sistema de recuperación, respaldado por consultas analizadas y métricas de recuerdo, en lugar de basarse en cómo se lee el texto final. Al eliminar datos de la conversación se libera espacio, pero la selección determina si el agente aún puede encontrar los cinco elementos adecuados cuando estos se vuelven relevantes.

Compresión: sesiones largas recuperables

La selección determina qué información se incluye, pero una sesión prolongada hace que se acumulen intercambios antiguos. A medida que se acerca el límite de contexto, el Agent SDK comprime la historia por sí mismo: los mensajes más antiguos se convierten en un resumen, mientras que las respuestas recientes y el estado importante se conservan. Aparece en la secuencia un SystemMessage con el subtipo compact_boundary, lo que permite al código de medición contar cada vez que ocurre.

Un resumen genérico podría omitir precisamente los datos en los que se basa un trabajo de investigación. Por eso, el proyecto incluye reglas específicas para la conservación de datos en CLAUDE.md: se deben preservar la pregunta original, los hallazgos verificados, las preguntas pendientes, las contradicciones, las URLs de las fuentes, las decisiones importantes y el plan actual. Este archivo determina qué elementos debe incluir el resumen, de modo que todo el historial no tenga que transportarse en cada llamada.

Antes de cualquier compactación, ya sea automática o manual, un gancho PreCompact guarda un punto de control. Cuando está disponible la transcripción del SDK, esta se copia, y los metadatos registran el desencadenante, la sesión, el directorio de trabajo y de dónde proviene la transcripción.

# research_agent/hooks.py

def build_precompact_hook(workspace: Path):
    async def archive_before_compaction(
        input_data: dict[str, Any],
        tool_use_id: str | None,
        context: Any,
    ) -> dict[str, Any]:
        del tool_use_id, context

        checkpoint_dir = workspace / "checkpoints"
        checkpoint_dir.mkdir(parents=True, exist_ok=True)
        timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
        session_id = input_data["session_id"]
        trigger = input_data["trigger"]
        stem = f"{timestamp}-{session_id}-{trigger}"
        transcript = Path(input_data["transcript_path"])

        metadata = {
            "session_id": session_id,
            "trigger": trigger,
            "created_at": datetime.now(timezone.utc).isoformat(),
            "source_transcript": str(transcript),
            "custom_instructions": input_data.get("custom_instructions"),
            "archived": transcript.is_file(),
        }
        if transcript.is_file():
            shutil.copy2(transcript, checkpoint_dir / f"{stem}.jsonl")
        (checkpoint_dir / f"{stem}.json").write_text(
            json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
        )
        return {}

    return archive_before_compaction

La quinta figura muestra cómo la compactación reduce las conversaciones acumuladas a un resumen que respeta las reglas de preservación, mientras que el punto de control y la pizarra de bocetos permanecen como registros independientes en el entorno.

Las pizarras de bocetos y la compactación satisfacen necesidades diferentes. La pizarra de bocetos es un estado en el que el agente escribe intencionadamente a lo largo del proceso, organizado para su lectura posterior. La compactación consiste en una reducción con pérdida de la conversación acumulada, que ocurre porque el historial activo se ha vuelto demasiado grande. Con ambos elementos en su lugar, una sesión compactada puede recurrir a sources.json o research_notes.md si el resumen ha perdido algo.

Una prueba sin conexión proporcionó al mecanismo un transcripción temporal y confirmó tres cosas: apareció el punto de control, se copió la transcripción y los metadatos del disparador sobrevivieron. En las seis pruebas de comparación en tiempo real, nunca apareció compact_boundary, ya que el contexto más extenso observado fue de 34,309 tokens frente a un rango reportado de 200,000 tokens. Por lo tanto, el experimento confirma que el mecanismo funciona, pero no indica nada sobre la calidad de la compactación en tiempo real. El resumen debe incluir las decisiones y el trabajo pendiente, mientras que la zona de trabajo temporal debe contener las pruebas en un formato legible.

Aislar: subagentes especializados

Las líneas de investigación independientes son más fáciles de gestionar cuando no comparten una misma trayectoria en constante expansión. Después de que el agente principal redacte el plan, tres subagentes designados se encargan del análisis documental, de las pruebas almacenadas y de la verificación de referencias. Cada definición limita la tarea, las herramientas y la forma en que se transfiere el trabajo.

# research_agent/config.py
"paper-researcher": AgentDefinition(
    description="Analyzes primary research papers for memory mechanisms and trade-offs.",
    prompt=(
        "Investigate only the assigned paper question. Use primary sources. "
        "Return at most five findings, each with a URL and an explicit limitation."
    ),
    tools=["WebSearch", "WebFetch", "mcp__research__search_knowledge"],
    model=model,
),

En el código, los subagentes se declaran con AgentDefinition. Según la documentación de Anthropic sobre subagentes, cada uno inicia una conversación nueva con su propio contexto, mantiene allí sus llamadas a herramientas intermedias y sus resultados, y solo envía un mensaje final al agente principal. Por lo tanto, la transferencia representa un verdadero límite de contexto, y no solo una convención dentro de la ventana existente del líder.

La sexta figura muestra a cada especialista recibiendo una tarea específica y una ventana nueva. El líder ve la transferencia resumida, pero no las búsquedas, las páginas recuperadas ni los pasos de extracción que la generaron.

El aislamiento tiene un costo. Tres subagentes pueden redescubrir las mismas fuentes, consumir más tokens y llegar a terminologías contradictorias. Para evitarlo, las instrucciones exigen URLs principales, limitaciones especificadas y no más de cinco hallazgos. El verificador de hechos solo ve las afirmaciones controvertidas y no todo el resumen, lo que mantiene su labor auditable y reduce el riesgo de que reescriba en silencio la síntesis.

Un responsable debe poder evaluar lo que se recibe. Cinco líneas sin identificadores de fuente son compactas pero inútiles, y una transcripción completa del subagente elimina los límites. El contrato aquí incluye hallazgos, citas, restricciones de versión y limitaciones, los campos que el responsable necesita al comparar las afirmaciones entre sí. Ocultar la conversación del subagente por sí solo no es suficiente, ya que el aislamiento solo funciona si la respuesta contiene evidencias y elementos de incertidumbre.

Aislar las salidas de herramientas complejas

Ciertos payloads deberían cruzar los límites de la herramienta únicamente como referencias. materialize_source escribe un documento seleccionado en workspace/artifacts/ y devuelve su ruta, tamaño en bytes y una breve vista previa. Más tarde, Claude puede abrir el rango relevante mediante herramientas de archivos, de modo que todo el documento nunca queda contenido en un solo resultado de la herramienta.

# research_agent/workspace.py (full function; use the Gist when publishing)
def materialize_source(corpus_path: Path, workspace: Path, source_id: str) -> dict:
    document = next(
        (item for item in load_corpus(corpus_path) if item["source_id"] == source_id),
        None,
    )
    if document is None:
        raise ValueError(f"unknown source_id: {source_id}")

    path = safe_artifact_path(workspace, f"{source_id}.txt")
    content = (
        f"Title: {document['title']}\n"
        f"URL: {document['url']}\n"
        f"Published: {document['published']}\n\n"
        f"{document['text']}\n"
    )
    path.write_text(content, encoding="utf-8")
    return {
        "path": str(path),
        "characters": len(content),
        "preview": content[:240],
    }

El SDK cuenta con una medida de seguridad similar para los resultados de MCP. La documentación de MCP de Anthropic marca como problema las salidas superiores a 10,000 tokens y establece 25,000 tokens como límite por defecto; el texto de gran tamaño puede escribirse en disco y reemplazarse en el contexto por una referencia a archivo. Ese límite por defecto sirve como medida de respaldo. Al devolver rutas y metadatos, se proporciona intencionadamente a la aplicación un marco estable mucho antes de alcanzar el límite de emergencia.

La séptima figura proviene de una prueba de fallo intencionada que intenta salir del directorio de artefactos mediante ../outside.txt. El espacio de trabajo rechaza esa ruta antes de que se escriba cualquier cosa.

El aislamiento del entorno también crea una barrera de seguridad. El nombre de un artefacto controlado por el modelo nunca debe convertirse en una ruta arbitraria del sistema de archivos. safe_artifact_path acepta un nombre de archivo conservador y verifica que la ubicación final resultante permanezca dentro de workspace/artifacts/. El fallo observado se presentó de esta manera.

# Captured output: python scripts/demonstrate_failure.py
Blocked artifact path: artifact name must use only letters, numbers, dots, underscores, or hyphens

La verificación no hace mención alguna al aislamiento en entorno sandbox del sistema operativo ni a la separación entre usuarios. Confirma una regla específica: un argumento de herramienta no puede utilizar técnicas de travesía de rutas para escribir fuera del directorio del artefacto. Las implementaciones reales siguen necesitando permisos a nivel de proceso y espacios de trabajo separados para cada usuario o ejecución. Además, reemplazar una carga útil grande por una ruta solo ahorra contexto cuando dicha ruta está restringida y el agente puede obtener posteriormente una porción limitada de datos.

Transportar contexto entre sesiones

Los trabajos largos superan en duración tanto a los procesos como a las ventanas de contexto. El SDK ofrece tres opciones: continuar la última conversación, reanudar una sesión específica por su ID o crear una bifurcación de la sesión. “Continuar” se utiliza para un trabajo en curso, “reanudar” implica una recuperación explícita, y “crear bifurcación” es adecuado para tareas posteriores que deben heredar el contexto anterior sin alterar el futuro de la sesión original.

# examples/session_modes.py
def session_options(session_id: str) -> dict[str, ClaudeAgentOptions]:
    return {
        "continue": ClaudeAgentOptions(continue_conversation=True),
        "resume": ClaudeAgentOptions(resume=session_id),
        "fork": ClaudeAgentOptions(resume=session_id, fork_session=True),
    }

Cada opción mantiene el estado de la conversación: prompts, llamadas a herramientas, resultados y respuestas. A un fork se le asigna un ID de sesión nuevo mientras que el original permanece intacto. Esto es adecuado para solicitudes como “Ahora compare estos hallazgos específicamente para agentes de codificación”, ya que la nueva rama puede basarse en la trayectoria de investigación existente.

La octava figura contrasta los modos: continuar extiende la trayectoria actual, reanudar restaura una ya seleccionada, y fork inicia una nueva rama a partir del mismo punto de control.

Los archivos en el disco requieren un manejo específico. Las ramas creadas mediante un fork comparten un directorio de trabajo a menos que la aplicación les asigne otro, por lo que ambas podrían sobrescribir plan.md o final_report.md. El ejecutor evita colisiones al crear una raíz de ejecución única a partir de una marca de tiempo UTC y un sufijo aleatorio; además, un fork de producción debería copiar o versionar el área temporal en una nueva raíz de ejecución antes de que cualquiera de las ramas escriba en ella. Tenga en cuenta que un ID de sesión designa el historial de la conversación. No se trata de un ID de transacción para los sistemas externos con los que interactuó el agente, por lo que las bases de datos, almacenes de objetos, índices de búsqueda y espacios de trabajo necesitan cada uno su propia política de línea de descendencia y concurrencia. La estructura de los archivos mantiene esto visible y no lo oculta tras el comportamiento de reanudación. Un fork solo separa la conversación, mientras que el estado externo permanece compartido hasta que el espacio de trabajo también se versione o se divida.

Montaje del agente diseñado con contexto

La configuración final combina las cuatro operaciones sin añadir ningún marco de orquestación adicional. “Write” es el espacio de trabajo estructurado. “Select” consiste en la lectura de archivos por fases, esquemas de herramientas diferidos y recuperación de conocimiento limitada. “Compress” combina la compactación mediante SDK con instrucciones de retención y un punto de control tomado justo antes. “Isolate” significa subagentes con alcance definido y resultados de herramientas almacenados en archivos.

# research_agent/config.py
tools = ["Read", "Write", "Edit", "Glob", "Grep", "WebSearch", "WebFetch", "Agent"]
return ClaudeAgentOptions(
    cwd=run_root,
    model=model,
    tools=tools,
    allowed_tools=[*tools, "mcp__research__*"],
    permission_mode="dontAsk",
    mcp_servers={"research": server},
    strict_mcp_config=True,
    setting_sources=["project"],
    system_prompt={"type": "preset", "preset": "claude_code", "append": ENGINEERED_PROMPT},
    env={"ENABLE_TOOL_SEARCH": "true"},
    hooks={"PreCompact": [HookMatcher(hooks=[build_precompact_hook(workspace)])]},
    agents=research_subagents(model),
    max_turns=30,
    max_budget_usd=max_budget_usd,
)

Al establecer setting_sources=["project"], se permite que el CLAUDE.md específico de la ejecución se integre en la configuración, y strict_mcp_config=True restringe el SDK al servidor proporcionado por esta aplicación. El permission_mode se establece en dontAsk únicamente porque el ejemplo está orientado a ejecuciones aisladas sin supervisión, y no constituye una recomendación general de permisos. Los flujos avanzados de aprobación y las políticas de despliegue se excluyen intencionadamente del alcance.

En la novena figura, la arquitectura completada regula cuatro aspectos: escrituras duraderas, lecturas selectivas, compactación de la conversación y ejecución aislada. El modelo principal recibe únicamente un contexto activo específico.

El ejecutor final depende de ClaudeSDKClient y omite la herramienta más ligera query(). Comparten el mismo bucle de agente, pero el cliente ofrece un límite de respuesta explícito y una llamada final a get_context_usage(). La instrumentación motivó esa elección, y la arquitectura se mantuvo igual.

El primer intento de implementación, basado únicamente en indicaciones, reveló otro límite en la producción. Las tres ejecuciones del SDK finalizaron, pero solo una generó final_report.md; los dos agentes principales regresaron mientras el trabajo delegado quedó efectivamente inconcluso. Por lo tanto, una salida limpia del SDK no implica que la tarea de la aplicación esté completada. La solución fue hacer que las llamadas a los subagentes fueran explícitamente síncronas y proporcionar al controlador su propio contrato de finalización: este busca un informe detallado que contenga al menos dos URLs y permite una única consulta de reparación enfocada dentro de la misma sesión.

# research_agent/runner.py
while True:
    async for message in client.receive_response():
        metrics.observe(message)

    report_path = workspace / "final_report.md"
    if mode == "naive" or _report_meets_contract(report_path):
        break
    if metrics.completion_retries >= MAX_COMPLETION_RETRIES:
        break

    metrics.completion_retries += 1
    await client.query(COMPLETION_REPAIR_PROMPT)

El resultado fallido basado únicamente en la solicitud permanece en measurements/initial-prompt-only-comparison.json, ya que un controlador debe verificar los resultados duraderos y no inferirlos a partir del estado final del modelo.

El conjunto de pruebas sin conexión maneja todo lo que no requiere modelo: ambos conjuntos de opciones, la creación del espacio de trabajo, la recuperación limitada, el rechazo de rutas inválidas, los puntos de control de compactación y la contabilidad de mensajes. Esto es lo que informó en el entorno indicado.

# Captured output: python -m unittest discover -s tests -q
----------------------------------------------------------------------
Ran 12 tests in 0.048s

OK

En ese punto, el framework está operativo: cada verbo está vinculado a un límite que tiene su propio código, un responsable de su estado y una invariante que puede ser verificada mediante pruebas.

Comparación de las dos arquitecturas

Las afirmaciones sobre la arquitectura deben convertirse en números. RunMetrics observa el flujo de mensajes del SDK, elimina los pasos duplicados del asistente en el nivel superior utilizando IDs de mensaje, cuenta cada llamada a una herramienta y cada límite de compactación, calcula el número de caracteres en los resultados de las herramientas y solicita una captura final del contexto. El uso acumulado del modelo en todo el árbol de agentes, incluidos los subagentes, proviene de ResultMessage.model_usage, mientras que total_cost_usd es una estimación local y no representa un estado de facturación.

# research_agent/metrics.py
if isinstance(message, ResultMessage):
    self.query_results += 1
    self.session_id = message.session_id
    self.result_subtype = message.subtype
    result = message.result or ""
    self.sdk_success = (
        message.subtype == "success"
        and bool(result.strip())
        and "not logged in" not in result.lower()
    )
    self.estimated_cost_usd = (
        (self.estimated_cost_usd or 0.0) + (message.total_cost_usd or 0.0)
    )
    self.result_usage = message.usage
    self.model_usage = self._merge_model_usage(self.model_usage, message.model_usage)
    self.total_tree_input_tokens = self._tree_input_tokens(self.model_usage)

if isinstance(message, SystemMessage) and message.subtype == "compact_boundary":
    self.compactions += 1

Cada arquitectura se ejecutó tres veces, de forma independiente, con una misma pregunta compartida, utilizando el mismo SDK, alias de modelo, proveedor y configuración de conocimiento local idénticos. Los registros de cada ejecución y los valores promedio resultantes se encuentran en measurements/comparison.json, y un pequeño script de visualización regenera la tabla compacta a partir de ese archivo.

# terminal
python scripts/show_comparison.py ../measurements/comparison.json

La décima figura muestra las medias de tres carreras y revela un verdadero compromiso. El aislamiento redujo la carga final de resultados que mostró el líder, pero el árbol de subagentes consumió muchos más tokens y un costo estimado mucho mayor. A continuación se presenta la salida capturada del script de visualización.

# Captured output: python scripts/show_comparison.py ../measurements/comparison.json
Measured comparison - 3 runs per architecture
Metric                               Naive      Engineered
Artifact success                       3/3             3/3
SDK success                            3/3             1/3
Mean tree input                    102,210         737,036
Mean peak context                   17,184          32,668
Final tool-result tokens             7,351           6,202
Mean subagents                           0               3
Mean estimated cost                 $0.663          $3.270
Compactions                              0               0

El éxito tiene dos formas. El éxito por artefacto significa que se cumplió el contrato de salida de la arquitectura: para el agente simple, un informe devuelto que mencione al menos dos URLs distintas; para el agente diseñado, un archivo final_report.md duradero de 500 caracteres o más que contenga dos URLs. Las seis ejecuciones lograron esto.

Para que el SDK tenga éxito, el subtipo final de ResultMessage debe ser success. Las ejecuciones simples lo lograron en tres de tres ocasiones, mientras que las ejecuciones optimizadas lo lograron en una de tres. Dos ejecuciones optimizadas generaron informes detallados pero finalizaron con el código error_max_budget_usd; una de ellas utilizó la única consulta de reparación del controlador. El ejemplo de presupuesto de Anthropic explica que el gasto se verifica después de una llamada a la API, por lo que una ejecución puede completar su resultado y aún superar la estimación configurada antes de que llegue el resultado final. Considere este límite como una medida de protección, y no como una señal de que la aplicación ha finalizado.

Los números:

  • La pista recibió menos información. Los caracteres visibles relacionados con la herramienta en el resultado promediaron 28,572 en la fase inicial y 23,428 una vez aislados, lo que representa una disminución del 18.0%; además, la proporción de resultados de la herramienta en el snapshot final pasó de 7,351 a 6,202 tokens (un 15.6% menos). En cada prueba diseñada participaron exactamente tres subagentes, y sus búsquedas intermedias nunca influyeron en la trayectoria de la pista.
  • El costo total aumentó drásticamente. La cantidad total de entrada en todo el árbol promedió 102,210 tokens al principio y 737,036 posteriormente, lo que supone un incremento del 621.1%. La cantidad máxima de entrada relacionada con la pista y el contexto promedió 17,184 y luego 32,668, es decir, un 90.1% más; además, la estimación de costo del SDK pasó de $0.663 a $3.270, un aumento del 393.4%
  • También aumentó la actividad de las herramientas: unas 30.3 llamadas en total, frente a seis en la línea de base. En una tarea con límites definidos como esta, tres especialistas, sus esquemas y las transferencias de trabajo entre ellos costaron más que el contexto que se ahorró al líder del equipo.
  • Nunca se aplicó la compactación, ya que cada ejecución reportó cero límites. Eso no demuestra que la compactación consciente de la preservación sea ineficaz; muestra simplemente que esta tarea nunca llegó siquiera cerca del umbral de 200,000 tokens. El experimento tampoco permite aislar el valor de la búsqueda de herramientas, ya que el catálogo MCP era pequeño y el descubrimiento diferido tuvo poco margen para compensar su sobrecarga de selección.

    Qué indican y qué no indican los resultados

    Nada aquí constituye una crítica generalizada contra la ingeniería de contexto. Los datos muestran por qué las cuatro operaciones deben utilizarse de forma selectiva. La escritura de archivos y la recuperación limitada generan un estado duradero que puede ser inspeccionado. El aislamiento resulta útil cuando la trayectoria de un hijo, de otro modo, contaminaría un contexto principal que ya es realmente largo, y nunca es algo gratuito. Con este tamaño de tarea, un único contexto principal con una zona de trabajo temporal podría ser el diseño más adecuado para la producción, a menos que las mejoras en la calidad especializada justifiquen el costo adicional.

    Lo que queda es un artefacto funcional con una arquitectura de contexto, un fallo detectado localmente y unos límites de evaluación claros. Una pregunta lógica que surge es sobre la calidad: ¿es realmente el material que se está escribiendo, seleccionando, comprimiendo e isolando el adecuado? Para responder a esto se necesitan evaluaciones a nivel de tarea en cuanto a la cobertura de evidencias, el manejo de contradicciones, la validez de las citaciones y la recuperación tras la compresión. En este caso, el aislamiento de tareas redujo la carga de resultados de las herramientas del líder del proyecto, pero multiplicó los tokens y el costo para todo el árbol estructural; por lo tanto, la ingeniería de contexto debe medirse como un sistema completo y no asumirse automáticamente que constituye una optimización.

    Lecturas relacionadas

  • Construyendo un compilador de contexto seguro para agentes de IA en .NET 10 — Compila el contexto del modelo en .NET 10 con etiquetas de confianza, verificaciones de actualidad, censura, cuarentena contra inyecciones, presupuestos deterministas de tokens y un manifiesto SHA-256 que puedes probar.