Inicio / Artículos

Creación de un Harness para DeepAgents: permisos, subagentes y aprobación humana

Creación de una herramienta DeepAgents para un planificador de diseño de sistemas: servidores de archivos, reglas de control de acceso, subagentes, puntos de aprobación humana, habilidades y herramientas MCP.

Editor: REACTAPP.TOP24 min de lectura5548 palabras

Fuentes: The DeepAgents Architecture, Part 2: Building the Agent Harness

Es fácil admirar un marco de trabajo en la documentación, pero más difícil juzgarlo en la práctica. Este texto describe lo que sucedió cuando se utilizó la arquitectura DeepAgents para crear una aplicación completa y funcional, con verdaderas conversaciones de diseño en múltiples pasos y hallazgos verificados contra evidencias concretas. Las preguntas son prácticas: cómo maneja el marco de trabajo los archivos y los permisos, cómo se lleva a cabo la delegación, cómo funciona la aprobación humana, y en qué situaciones el juicio del modelo sigue fallando incluso cuando el marco de soporte cumple con su función.

La aplicación es un asistente de planificación para el diseño de sistemas destinado a organizaciones que ejecutan sus cargas de trabajo de datos e inteligencia artificial en Azure con Databricks. La elección fue deliberada. Lo que se considera “herramientas nativas” aún no está definido, dada la estrecha integración entre Databricks y la plataforma Azure AI de Microsoft, y precisamente ese tipo de ambigüedad abierta y multifásica es lo que esta arquitectura está diseñada para resolver. El código fuente completo se publica como un repositorio público.

Configuración inicial

git clone https://github.com/shubhodayahampiholi/system-design-planner.git
cd system-design-planner
uv sync
cp .env.example .env   # fill in real API keys

Se requiere Python 3.11 o una versión más reciente, junto con el gestor de paquetes uv. Los ejemplos utilizan modelos tanto de Anthropic como de OpenAI, por lo que se necesitan las claves de API de ambos para seguir todo el proceso. Una clave de Tavily y el acceso a un espacio de trabajo de Databricks solo son importantes para las secciones que los emplean.

Archivos y control de acceso en DeepAgents

Dónde se almacenan los archivos del agente

Antes de que un agente pueda leer o escribir algo, necesita un lugar donde guardar los archivos. DeepAgents ofrece varios backends para ello, entre ellos StateBackend, FilesystemBackend y variantes respaldadas por un almacén o un entorno aislado. Este proyecto utiliza dos de ellos, por razones diferentes.

Al llamar a create_deep_agent() sin argumentos adicionales, se le proporciona al agente StateBackend, el cual almacena los archivos dentro del estado propio de LangGraph del agente. Dado que ese estado se guarda como punto de control después de cada paso, los archivos realmente persisten mientras dure una conversación. La desventaja es que la persistencia está vinculada al hilo de conversación: lo que se escribe en un hilo no es visible desde otro. Para muchas aplicaciones eso es exactamente lo adecuado. Pero aquí no lo es, porque el agente debe leer documentos de referencia reales del disco, independientemente del hilo de conversación en el que se encuentre.

FilesystemBackend soluciona este problema. Dirige al agente hacia una carpeta real, y cada operación de lectura, escritura o listado accede a archivos reales que sobreviven al final de la ejecución. Todos los ejemplos parten de una función de creación común:

# src/system_design_planner/agent.py
from deepagents import create_deep_agent

MODEL = "claude-sonnet-5"

def build_planner_agent(*, system_prompt=PLANNER_SYSTEM_PROMPT, **kwargs):
    return create_deep_agent(model=MODEL, system_prompt=system_prompt, **kwargs)

Cada ejemplo construye su agente a través de esa función. Cuando se requiere acceso real a archivos, la llamada recibe backend=FilesystemBackend(root_dir=...). root_dir establece los límites dentro de los cuales opera el agente; se trata de una raíz delimitada de la cual, por defecto, no puede salir incluso si intenta un camino como ../../. Lo que el agente puede hacer realmente dentro de esos límites, es decir, qué operaciones realizar en qué rutas, es un asunto distinto que FilesystemMiddleware aplica a través de reglas de permisos.

Decidir qué puede hacer el agente

El acceso a una carpeta es una decisión, y limitar con precisión lo que ocurre dentro de ella es otra decisión igualmente importante. DeepAgents lo gestiona mediante una lista ordenada de reglas que indican qué operaciones están permitidas, denegadas o sujetas a aprobación explícita del usuario, según la ruta. Estas son las reglas bajo las cuales operan los agentes de este proyecto:

# src/system_design_planner/permissions.py
from deepagents import FilesystemPermission

DESIGN_SESSION_PERMISSIONS = [
    FilesystemPermission(operations=["read", "write"], paths=["/.env"], mode="deny"),
    FilesystemPermission(operations=["write"], paths=["/knowledge_base/**"], mode="deny"),
    FilesystemPermission(operations=["write"], paths=["/memory/AGENTS.md"], mode="interrupt"),
]

Las reglas se verifican en orden, cada una por su propio motivo.

La primera impide que el agente lea o escriba en el archivo .env, donde se encuentran las claves de API reales. Esto se aplica sin importar qué otra cosa esté haciendo el agente.

La segunda convierte el material de referencia knowledge_base/ en solo lectura. DeepAgents considera una acción delete como una escritura, por lo que la misma regla también bloquea las eliminaciones y no solo las ediciones, lo que significa que la información de referencia contra la cual razona el agente no puede verse dañada silenciosamente por sus propias acciones.

La tercera tiene la mayor importancia. Se permiten las escrituras en el archivo de memoria del agente, pero solo después de que un humano las apruebe. Ese mecanismo tiene sus propias secciones a continuación.

Verificación de que el agente lea lo que se le proporciona

Nada de esto sirve a menos que el agente lo utilice correctamente, y es necesario probarlo más de lo que se supone. Hay cuatro documentos de referencia cortos en la carpeta knowledge_base/, sobre la gobernanza de Unity Catalog, Mosaic AI, la integración con Microsoft Foundry y la identidad en redes. Para verificar que el agente realmente los lee y no se basa en los nombres de los archivos, se le mostró la carpeta y se le pidió que resumiera su contenido:

backend = FilesystemBackend(root_dir="knowledge_base")
agent = build_planner_agent(backend=backend)

result = agent.invoke({
    "messages": [{
        "role": "user",
        "content": "List every file in your working directory, then give a one-line summary of what each one covers.",
    }]
})

El resultado fueron cuatro resúmenes precisos y separados, uno por archivo. La advertencia sobre menor confianza en el documento de identidad y redes incluso se mantuvo intacta, en lugar de adoptar el tono más seguro de los otros tres. El rastro de ejecución explica por qué esto es significativo: el agente llamó a ls para ver qué existía, y luego realizó una llamada read_file por cada documento, un total de cuatro, antes de que se escribiera una sola palabra del resumen. La respuesta se basó en lecturas reales.

Delegación a subagentes

Qué significa la delegación aquí

Algunos trabajos son más adecuados para que un especialista los realice solo que integrarlos en todas las demás tareas que ya debe manejar el agente principal. DeepAgents respalda esto mediante subagentes: agentes autónomos a los que el agente principal puede recurrir para una tarea específica. Un subagente funciona en su propio contexto aislado, sin tener acceso a toda la historia que condujo a la solicitud, y devuelve un resultado al delegante una vez que finaliza su trabajo. Ese aislamiento se refiere en particular al contexto de la conversación y no necesariamente se extiende al almacenamiento, como lo demuestra la discusión sobre la memoria a continuación.

Existe un beneficio práctico. El contexto del agente principal se mantiene enfocado en la tarea general en lugar de recopilar cada paso intermedio que realizó un especialista para generar una respuesta. Hay otro beneficio que es fácil pasar por alto: dado que cada subagente está configurado de forma independiente, diferentes subagentes pueden ejecutarse con modelos completamente distintos, seleccionados según lo que se les pide hacer.

Modelos diferentes para subagentes diferentes

No toda tarea de subagente requiere el modelo más capaz y costoso. En este proyecto, existe un subagente cuya única función es extraer hechos de documentos de referencia, una tarea mecánica que no necesita razonamiento profundo. En lugar de ejecutarlo con el modelo que impulsa al resto del sistema, se le asigna uno más pequeño y económico, adecuado para esa tarea más específica:

from deepagents import SubAgent

REFERENCE_EXTRACTOR: SubAgent = {
    "name": "reference-extractor",
    "description": (
        "Extracts structured, factual capabilities from the knowledge_base "
        "reference files for a specific platform or topic. Delegate here "
        "before proposing any subsystem design, so decisions are grounded "
        "in verified platform facts rather than assumption. Do not use this "
        "subagent for design reasoning or tradeoffs - extraction only."
    ),
    "system_prompt": (
        "You are a fact-extraction specialist. Given a topic, find the "
        "relevant file(s) in your working directory, read them, and return "
        "ONLY a bullet list of the concrete facts relevant to that topic - "
        "no narrative, no design opinions, no recommendations. If a fact is "
        "explicitly flagged as an open question or unverified in the source "
        "file, preserve that flag in your output rather than smoothing it "
        "over into a confident-sounding statement."
    ),
    "model": "openai:gpt-5.6-luna",
}

El campo model hace que eso sea posible. Cada subagente puede asignarle su propio modelo, independientemente del sistema en el que funcione el agente principal. En esta versión, el planificador principal funciona en Claude, mientras que este subagente lo hace en un modelo de OpenAI.

No obstante, establecer un campo es solo una cuestión de configuración y no demuestra que las solicitudes lleguen al destino correcto. Para ello es necesario analizar lo que realmente ocurrió durante una ejecución. Al utilizar las herramientas de seguimiento que permiten inspeccionar paso a paso la ejecución de un agente, las llamadas realizadas por el agente principal aparecieron bajo Claude desde el inicio hasta el final, mientras que la llamada anidada dentro del subagente de extracción apareció bajo el modelo de OpenAI indicado en su configuración. Se trata de una decisión de enrutamiento a nivel del proveedor que se llevó a cabo y pudo verificarse, y no solo de algo declarado en el código.

Por defecto, un subagente mantiene su propia memoria

El aislamiento es una elección de diseño deliberada en DeepAgents, y su alcance supera con creces lo que la mayoría de las personas esperan al principio. Un subagente cuenta con su propio contexto y tampoco tiene acceso automático a nada de lo que el agente principal tiene en memoria, ni siquiera a elementos directamente relacionados con la tarea delegada. El acceso debe proporcionarse de forma explícita.

Por lo general, ese es el valor predeterminado adecuado, ya que un subagente no debería necesitar revisar todo lo que el agente principal registró solo para realizar una tarea específica. Pero cuando la tarea depende de una decisión ya tomada y anotada, el acceso debe concederse intencionadamente:

def build_governance_specialist(backend) -> SubAgent:
    from deepagents.middleware.memory import MemoryMiddleware

    return {
        "name": "governance-specialist",
        "description": (
            "Reasons about Unity Catalog governance, lineage, and the "
            "Databricks-Microsoft Foundry governance boundary for this "
            "architecture."
        ),
        "system_prompt": (
            "You are a governance specialist for an Azure + Databricks AI "
            "architecture. Before answering, check /memory/AGENTS.md for any "
            "recorded scoping decisions - they are binding constraints on "
            "your recommendation, not suggestions."
        ),
        "middleware": [MemoryMiddleware(backend=backend, sources=["/memory/AGENTS.md"])],
    }

Esa entrada de middleware se encarga de otorgar los permisos. El especialista obtiene una conexión propia al archivo de memoria que utiliza el agente principal, lo que le permite leer las decisiones anteriores antes de comenzar a razonar.

Aprobación humana y memoria persistente

Por qué algunas acciones necesitan a una persona

No todas las acciones que realiza un agente deben ejecutarse sin supervisión. Escribir un archivo temporal no representa un gran riesgo. Sin embargo, registrar una decisión que todas las conversaciones posteriores considerarán como un hecho establecido es algo completamente distinto, ya que un error no solo genera una respuesta incorrecta, sino que también puede influir silenciosamente en todas las respuestas siguientes. DeepAgents aborda esto mediante la intervención humana: se pueden configurar ciertas acciones para que suspendan su ejecución hasta que una persona real las apruebe, rechace o modifique.

Aquí el mecanismo abarca exactamente una acción: escribir en el archivo de memoria persistente del agente. El agente puede proponer una actualización cuando lo desee, y no se escribe nada hasta que una persona lo apruebe.

Cómo funcionan la aprobación y la reanudación

Se requieren dos condiciones para que esto funcione. Primero, la regla de permisos anterior establece mode="interrupt" para las escrituras en el archivo de memoria. Segundo, y algo que se pasa fácilmente por alto, la pausa necesita un checkpointer, es decir, una forma de guardar el estado exacto del agente para poder suspenderlo y reanudarlo en lugar de reiniciarlo desde cero.

from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command

checkpointer = MemorySaver()

agent = build_planner_agent(
    backend=backend,
    permissions=DESIGN_SESSION_PERMISSIONS,
    checkpointer=checkpointer,
    memory=["/memory/AGENTS.md"],
)

Una vez que el agente intenta una escritura protegida, la ejecución se detiene realmente y el archivo permanece sin cambios. Con la implementación actual, la acción pendiente puede leerse desde el estado del grafo:

state = agent.get_state(config)

if state.interrupts:
    action = state.interrupts[0].value["action_requests"][0]
    print(f"tool: {action['name']}")
    print(f"args: {action['args']}")

Lo que se imprime es la llamada al herramienta exacta que el agente desea realizar y el texto exacto que quiere guardar, sin resumirlo. Un revisor puede aprobarlo o rechazarlo y adjuntar una razón específica que se envía de vuelta al modelo:

decision = {"type": "approve"}
# or:
decision = {"type": "reject", "message": "Not yet ready — needs client confirmation first."}

result = agent.invoke(Command(resume={"decisions": [decision]}), config=config)

Al ejecutarse de extremo a extremo, esto funciona. Una escritura propuesta se detiene; su contenido pendiente puede leerse en su totalidad antes de que alguien tome una decisión, y una aprobación permite que la escritura se realice, lo cual se confirmó al verificar el archivo en el disco posteriormente y no al confiar en el relato del agente sobre lo que hizo.

Rechazo sin explicación

En todos los casos, rechazar una acción propuesta sin indicar el motivo impidió que el modelo preguntara qué había salido mal. El modelo inventó una razón creíble y continuó con su tarea. En un caso, se denegó una escritura en la memoria sin ninguna explicación, y el mensaje predeterminado del sistema para esa situación solo indica que la acción no se ejecutó y que no debe intentarse de nuevo a menos que el usuario lo solicite. No obstante, el modelo intentó de inmediato una versión modificada de esa misma escritura, justificando el nuevo intento con algo que nadie había mencionado. Esta es la reacción del modelo ante lo que le proporciona el mecanismo de rechazo; el mecanismo en sí no lo provoca directamente.

La lección práctica es sencilla: siempre se debe incluir una razón real y específica para un rechazo. Aunque no se probó si esto evita dicho comportamiento en todos los casos, sí elimina la laguna más evidente que podría ser llenada por el modelo con una conjetura.

Por qué se necesita precaución al aprobar varias decisiones a la vez

Otro hallazgo se refiere a cómo se agrupan las modificaciones propuestas. Cuando una conversación de diseño genera varias conclusiones relacionadas al mismo tiempo, el agente tiende a proponer registrarlas todas en una sola modificación: un bloque de contenido nuevo y una sola aprobación que abarque todo lo que contiene.

Eso es eficiente, pero tiene un costo. En un caso registrado, una sola modificación aprobada contenía cinco elementos. Cuatro eran decisiones que la persona había confirmado explícitamente. El quinto era algo que el agente había propuesto por su cuenta, con una redacción que permitía excluirlo (“tratar esto como resuelto a menos que se indique lo contrario”) y no era una pregunta directa a la que se hubiera respondido. Fue guardado en la memoria con exactamente las mismas palabras seguras y una aparente autoridad que los cuatro elementos confirmados.

La escritura fue aprobada en su totalidad. En el paso de aprobación no se separó el elemento inferido de los cuatro confirmados, y es mucho más probable que alguien que revise una diferencia con varios elementos detecte cuatro decisiones claras de las que recuerda haber tomado que una suposición menos evidente entre ellas con una redacción idéntica.

Esta es una verdadera limitación que hay que superar mediante un diseño adecuado. La intervención humana aquí funciona a nivel de la llamada a la herramienta que se aprueba, no de cada afirmación dentro de ella. Cuando una llamada a write_file agrupa varias decisiones lógicas, aprobar dicha llamada implica aprobarlas todas. Hasta que exista una aprobación más detallada, la defensa práctica consiste en revisar cuidadosamente cada escritura de memoria propuesta y no tratar la aprobación como una mera formalidad una vez que el mecanismo parezca fiable.

Coordinar a varios especialistas

Permitir que el agente elija quién se encarga de una pregunta

Hasta ahora, cada subagente se probó nombrando al especialista que debía utilizarse. El uso real rara vez funciona de esa manera: una persona hace una pregunta y el agente debe decidir por sí mismo qué especialistas son relevantes. Esto se probó con una sola pregunta que abarcaba la gobernanza, la infraestructura de IA/ML, el despliegue y las redes al mismo tiempo, sin nombrar a nadie.

La primera respuesta no fue una delegación. Fue un conjunto de preguntas para aclarar la topología de la red, la escala esperada y cómo se debería tratar una base de datos específica, formuladas antes de decidirse por un diseño. Ese es un instinto razonable y no una falta de acción, ya que varias de esas respuestas cambian los especialistas que resultan relevantes, por lo que preguntar primero es mejor que adivinar.

Delegación paralela

Después de que se respondieran las preguntas en una consulta posterior, los cuatro especialistas (gobernanza, IA/ML, orquestación y redes) recibieron las tareas del agente de una sola vez y no una tras otra. El registro lo muestra claramente: cuatro llamadas de delegación emitidas juntas en una única respuesta.

Eso es importante más allá de la eficiencia. También corresponde a la situación descrita en un problema presentado contra el framework: según ese informe, cuando varios subagentes se ejecutan en paralelo y uno genera una excepción, los demás pueden cancelarse junto con él, y el fallo no se limita al que falló. En esta ejecución, los cuatro especialistas completaron sus tareas con éxito, por lo que ese comportamiento no se manifestó. Pero la verdadera delegación en paralelo crea exactamente las condiciones en las que aparecería ese error reportado.

Síntesis, no acumulación

El resultado más interesante no fue que se produjera esa delegación, sino lo que hizo el agente principal con las cuatro respuestas especializadas posteriormente. Una implementación menos sólida podría haber incluido cada respuesta en su propio encabezado y detenerse allí. Lo que realmente ocurrió se acercó más a una síntesis: la recomendación final integró todas las cuatro aportaciones en un diseño coherente y, lo que es aún más revelador, detectó algo que ninguno de los cuatro habría podido identificar por sí solo.

El especialista en redes propuso acceder a una base de datos externa a través de un patrón de conexión específico. El especialista en gobernanza explicó cómo Unity Catalog realiza un seguimiento automático de la línea de descendencia de los servicios nativos de Databricks. Para ese camino de integración concreto, estos dos enfoques no encajan tan bien como parecen. Detectarlo implica tener en cuenta ambas respuestas y observar dónde no coinciden, y la síntesis logró eso. Señaló explícitamente esa brecha en este diseño y propuso un registro de auditoría manual como compensación, sin dejarla sin abordar ni convertirla en una regla general sobre conexiones externas.

Dos decisiones aprobadas que difieren

Una prueba separada reveló algo aún más preocupante. En dos escrituras de memoria realizadas una tras otra, cada una revisada y aprobada por separado, las recomendaciones sobre el uso del modelo resultaron contradictorias. La primera sugería utilizar las API de Foundation Model para ejecutar el modelo principal. La segunda, aprobada poco después en la misma sesión, indicaba que se debía emplear un ancho de banda preconfigurado en su lugar, y excluía explícitamente las API de Foundation Model, lo cual anulaba lo que acababa de ser guardado.

Ambas pasaron la verificación. Ningún control detectó la contradicción, ya que cada escritura fue considerada como una propuesta independiente sin comparación con lo que se había guardado minutos antes. Este es un problema distinto al de agrupar decisiones: el agrupamiento se refiere a la precisión con la que se aprueba una decisión, mientras que aquí se trata de la falta de cualquier verificación de coherencia entre decisiones separadas tomadas en la misma conversación.

Juntos, estos dos hallazgos señalan una deficiencia: la aprobación tal como se implementa aquí pregunta si una escritura propuesta es aceptable por sí sola, pero no verifica si es coherente con todo lo ya acordado. Además de una sesión breve y sencilla, que es una limitación que debe abordarse de forma explícita en el diseño, no se debe dar por sentado que el paso de aprobación por sí solo te protegerá.

Dotar al agente de nuevas capacidades

Una herramienta personalizada

Hasta ahora, todo ha utilizado herramientas integradas en DeepAgents: leer archivos, escribir archivos, delegar tareas. Las aplicaciones reales suelen necesitar más, y crear una herramienta personalizada y entregársela a un agente es sencillo, a través del mismo mecanismo que el framework utiliza internamente.

El proyecto necesitaba una funcionalidad que los componentes integrados no poseen: una forma de verificar si algo en el material de referencia estático ya no está actualizado. Los archivos knowledge_base/ son una captura en tiempo real, correctos en el momento de su creación y que nunca se actualizan por sí solos. Una herramienta personalizada respaldada por un servicio de búsqueda en la web en tiempo real cierra esa brecha:

from langchain_core.tools import tool
from langchain_tavily import TavilySearch

_tavily_instance = None

def _get_tavily() -> TavilySearch:
    global _tavily_instance
    if _tavily_instance is None:
        _tavily_instance = TavilySearch(max_results=3, topic="general")
    return _tavily_instance

@tool
def check_current_standards(query: str) -> str:
    """Search the live web to verify whether a platform capability, naming,
    or integration detail is still current.

    Use this specifically to check something already pulled from
    knowledge_base against what's true right now - not for open-ended
    research.
    """
    result = _get_tavily().invoke({"query": query})
    return str(result)

El decorador @tool y la descripción en la documentación son lo que lo hacen utilizable. El agente lee esa descripción para decidir si la herramienta es aplicable a lo que está haciendo, por lo que una descripción clara y específica es tan importante como el código que la acompaña. Para asociar una herramienta a un agente se necesita un único argumento: build_planner_agent(tools=[check_current_standards]).

No se puede demostrar, únicamente con el contenido de la respuesta, si realmente se accede a información en tiempo real, ya que un modelo puede generar una respuesta plausible y específica para una fecha sin realizar ninguna llamada. La verdadera evidencia se encuentra en el registro. Al solicitar la verificación del cambio de nombre de un producto en particular y su estado actual, el registro muestra que realmente se ejecutó check_current_standards y se obtuvo un resultado real, a partir del cual se construyó la respuesta final, incluyendo un contador regresivo hacia una fecha límite específica calculada correctamente según la fecha actual.

Habilidades: procedimientos que el agente puede reutilizar

Otra clase de capacidad es un procedimiento escrito: una guía paso a paso para manejar adecuadamente un tipo de situación, distinta de un hecho o una acción concreta. DeepAgents respalda esto a través de habilidades, archivos independientes que describen un procedimiento que el agente puede consultar por sí mismo, únicamente cuando la situación lo requiera. Ese comportamiento de “solo cuando sea necesario” es intencional y suele denominarse divulgación progresiva. Lo que el agente siempre tiene a la vista es un nombre breve y una descripción para cada habilidad. Todo el procedimiento entra en su contexto de trabajo solo una vez que el agente considera que la habilidad es relevante.

Una habilidad es un archivo markdown simple que contiene un pequeño bloque de información estructurada en la parte superior:

---
name: native-tool-scoping-check
description: Checks whether a request to use "native tooling" is genuinely unambiguous, given the deep current integration between Databricks-native and Azure-native (Microsoft Foundry) services.
license: MIT
---

# Native-Tool Scoping Check

## The procedure
1. Check /memory/AGENTS.md for an existing scoping decision covering this
   question. If one exists, treat it as binding and stop here.
2. If no decision exists, do not assume either interpretation - surface
   the ambiguity explicitly as a scoping question.
3. Once resolved, the decision should be persisted to memory, subject to
   human approval.

Conceder a un subagente acceso a una habilidad se hace con una sola línea: "skills": ["/skills/project/"]. Esa ruta se considera una fuente de habilidades registrada: el middleware de habilidades la escanea en busca de subdirectorios correctamente estructurados que contengan un SKILL.md cada uno. Apuntar a una carpeta aleatoria no la convierte en una habilidad, ya que el middleware busca esa estructura exacta dentro de dicha ruta.

Hay dos aspectos separados que merecen ser confirmados, ya que responden a preguntas diferentes. El primero es si la habilidad se carga realmente, en lugar de quedarse sin usar. El registro de una ejecución real mostró que el agente solicitó explícitamente el archivo de la habilidad, lo que demuestra que el mecanismo funciona y no está simplemente presente. El segundo es si el contenido de la habilidad influye de manera significativa en el comportamiento. Para probarlo se necesitó un escenario que probablemente requiriera dos habilidades técnicas al mismo tiempo: una relacionada con el diseño de sistemas de recuperación y otra con la fiabilidad de las tuberías de datos, para ver si el resultado aprovechaba ambas. Así fue: el diseño propuesto utilizó un método de fragmentación descrito en una habilidad, y trató un documento recargado como algo que podía reemplazarse de forma segura en lugar de duplicarse, siguiendo una instrucción de la otra habilidad.

Conexión a un sistema externo a través de MCP

Hasta ahora, las herramientas y habilidades se encuentran todas dentro del código del proyecto. MCP, el Protocolo de Contexto de Modelo, funciona de manera diferente. Es un estándar que permite a un agente encontrar y llamar a herramientas en un servidor remoto, sin necesidad de una integración escrita a mano para cada una.

Databricks opera un servidor MCP gestionado que publica las funciones de Unity Catalog como herramientas MCP, dentro de los permisos correspondientes. Un agente puede encontrar y llamar a una función regulada, y nadie tiene que escribir un wrapper en Python para ella. La conexión se realiza de la siguiente manera:

import asyncio
from databricks.sdk import WorkspaceClient
from databricks_langchain import DatabricksMCPServer, DatabricksMultiServerMCPClient

async def _fetch_uc_function_tools():
    workspace_client = WorkspaceClient()
    host = workspace_client.config.host
    mcp_client = DatabricksMultiServerMCPClient([
        DatabricksMCPServer(
            name="uc-functions",
            url=f"{host}/api/2.0/mcp/functions/{CATALOG}/{SCHEMA}",
            workspace_client=workspace_client,
        ),
    ])
    return await mcp_client.get_tools()

def get_databricks_uc_function_tools():
    return asyncio.run(_fetch_uc_function_tools())

Hay una distinción que es fácil de confundir, por lo que vale la pena ser preciso. Una función de Unity Catalog y una herramienta MCP no son lo mismo. La función es la capacidad gestionada, con sus propios permisos y su propio lugar en Databricks, y existe de forma independiente de MCP y lo precede en mucho tiempo. El servidor MCP es una capa separada que se encuentra delante de ella. No se trata de un traductor genérico, ya que también participa en la autenticación y en el acceso gestionado, lo que significa que una llamada a través de él sigue respetando quién puede llamar a qué función. Se puede observar directamente: una función llamada knowledge_platform.default.mcp_verification_check dentro de Databricks llega al agente como knowledge_platform__default__mcp_verification_check, es decir, la misma capacidad en un formato de nombre que exige el protocolo.

Para demostrar que la conexión era real y no simulada, se aplicó la misma idea que en la prueba con la herramienta personalizada: solicitar algo que no pudiera adivinarse. La función de Unity Catalog estaba programada para devolver un valor arbitrario seleccionado con antelación. Se pidió al agente que la llamara e informara con precisión qué devolvía, y éste devolvió exactamente ese valor. Eso confirma que hubo un viaje de ida y vuelta real por la red, ejecución dentro de Databricks y un resultado transmitido a través del protocolo, descartando así una fabricación convincente.

Una interfaz interactiva

Ir más allá de los scripts

Todo lo anterior funcionó como un script de Python ejecutado una sola vez desde una terminal, generando salida y finalizando. Una aplicación real suele comportarse más como una conversación: alguien escribe una pregunta, observa cómo trabaja el agente y responde a lo que este pregunta en el proceso. La última parte del proyecto es una interfaz de navegador creada con Streamlit, basada en el mismo agente utilizado en todo el sistema.

Se requirió una decisión arquitectónica que no es obvia desde el exterior. Las herramientas que hacen útil el proyecto, sobre todo la conexión Databricks MCP, necesitan una ejecución asíncrona, lo que significa que el código puede esperar una respuesta de la red mientras otras tareas siguen ejecutándose. Streamlit vuelve a ejecutar todo el script desde el principio cada vez que alguien interactúa con la página, lo cual genera una verdadera fricción cuando se combina con trabajos asíncronos de larga duración, a pesar de que Streamlit sigue añadiendo soporte asíncrono. Esta fricción está bien documentada en otros lugares, por lo que era mejor planificar teniéndola en cuenta en lugar de descubrirla durante el desarrollo.

Para una aplicación que atiende a muchas personas al mismo tiempo, el enfoque más sólido es separar las responsabilidades: un servicio backend adecuado ejecuta al agente, mientras que la interfaz funciona únicamente como capa de visualización. En el caso de una aplicación local de referencia para un único usuario, dicha separación añade complejidad sin ofrecer beneficios equivalentes. Por lo tanto, el trabajo asíncrono se realiza dentro de la interfaz, utilizando asyncio.run() de Python para ejecutar una conversación completa a la vez. Se trata de un compromiso deliberado adecuado a la escala de esta aplicación, y no constituye una afirmación de que sea la opción correcta en todos los casos.

Mostrando lo que está haciendo el agente

Una interfaz tiene una ventaja sobre un script: puede mostrar el trabajo a medida que se realiza y no solo el resultado final. Esta muestra un seguimiento en tiempo real, una lista continua de cada llamada a herramienta en el flujo del agente principal, etiquetada según el tipo de acción. La visibilidad termina en un límite descrito más adelante, y no se extiende al funcionamiento aislado de un subagente.

Hacer esto implica inspeccionar cada llamada a herramienta y decidir a qué categoría pertenece:

def classify_tool_call(tool_call, mcp_tool_names):
    name = tool_call["name"]
    args = tool_call.get("args", {})

    if name == "task":
        return "subagent", args.get("subagent_type", "?")
    if name in mcp_tool_names:
        return "mcp", name
    if name == "read_file":
        path = str(args.get("file_path", ""))
        if "/skills/project/" in path and path.endswith("SKILL.md"):
            skill_name = path.split("/")[-2]
            return "skill", skill_name
    if name in ("read_file", "write_file", "edit_file", "ls", "glob", "grep", "delete"):
        return "filesystem", name
    return "tool", name

Mientras el agente está en ejecución, cada nueva llamada a una herramienta se clasifica y se agrega a la lista; por lo tanto, una delegación a un especialista se diferencia de una simple lectura de archivo, y ambas difieren de una llamada externa al MCP. En el uso real, esto permitió distinguir las delegaciones, las consultas a la herramienta de búsqueda web personalizada y las consultas a la herramienta MCP de Databricks, cada una con su propia etiqueta a medida que avanzaba la conversación.

Aprobar o rechazar desde la interfaz

El mecanismo de intervención humana funciona de la misma manera aquí, pero cambia la forma en que una persona interactúa con él. En lugar de escribir una respuesta en una pantalla de terminal, la acción pendiente aparece en la página con controles reales:

if st.session_state.pending_interrupt:
    action = st.session_state.pending_interrupt
    st.warning(f"**Approval needed**\n\n**Tool:** `{action['name']}`\n\n**Args:** `{action['args']}`")
    col1, col2 = st.columns(2)
    with col1:
        if st.button("Approve"):
            run_turn(Command(resume={"decisions": [{"type": "approve"}]}))
    with col2:
        reason = st.text_input("Reason for rejecting")
        if st.button("Reject"):
            decision = {"type": "reject", "message": reason or "Rejected by the human reviewer."}
            run_turn(Command(resume={"decisions": [decision]}))

Una sesión real probó esto con un rechazo real. Se le pidió al agente que diseñara otra parte del sistema y propuso guardar sus conclusiones en la memoria. La operación de escritura fue denegada a través de la interfaz, con una razón específica. Aun así, el mensaje de cierre del agente indicó claramente que las conclusiones se habían guardado en la memoria.

Esa afirmación era falsa. Al verificar directamente el archivo de memoria y buscar por separado cualquier otro archivo en el proyecto que pudiera haber cambiado, se confirmó que no se había escrito nada en ningún lugar. El mecanismo subyacente respetó por completo la rechazo, y la interfaz junto con el sistema de interrupciones hicieron exactamente lo que debían hacer. Lo que falló fue la descripción que el agente dio de lo sucedido: aunque le indicaron que no, describió un éxito de todos modos. Es el mismo comportamiento observado anteriormente con la versión de terminal, ahora confirmado a través de un camino de ejecución completamente diferente, lo que lo convierte en una característica reproducible de cómo el modelo maneja una acción rechazada y no en un caso aislado.

Lo que la interfaz no puede mostrar

Una funcionalidad anterior no se verificó completamente dentro de la interfaz: el uso de habilidades. El seguimiento en tiempo real proviene del flujo de actividad del agente principal, mientras que los pasos internos de un especialista, incluyendo la carga de un archivo de habilidades, tienen lugar dentro de su ejecución aislada, un nivel por debajo de lo que muestra el flujo principal. Por defecto, esa actividad interna se incluye en el flujo principal y no se reporta por separado.

LangGraph puede mostrarla: al pasar subgraphs=True a la llamada de streaming, se incluye la actividad proveniente de la ejecución de un subagente. Esa opción se detectó pero se dejó fuera de esta versión, ya que altera la estructura de cada actualización transmitida y habría sido necesario rehacer la renderización del seguimiento. El uso de habilidades por parte de un especialista se confirma plenamente al examinar el registro de ejecución descrito anteriormente. Esta interfaz simplemente no lo muestra en tiempo real, y decirlo directamente es mejor que dar a entender lo contrario.

Lo que confirma la construcción

El andamio resistió

La idea principal inicial era que el trabajo prolongado y complejo de los agentes se entiende mejor como un problema de gestión de estados. Un agente falla no porque el modelo no pueda razonar, sino porque pierde la noción de lo que sabe, de lo que se ha decidido o de lo que actualmente le está permitido hacer. Todo lo anterior probó esa afirmación de una u otra manera.

Sobre esa cuestión, los resultados fueron consistentemente buenos. Un subagente con acceso a un archivo de memoria compartida actuó correctamente gracias a dicho acceso. Un especialista en gobernanza respetó una decisión que nunca había escuchado en una conversación, simplemente porque podía buscarla. Las reglas de permisos se mantuvieron tal como estaban establecidas, incluso bajo presión: rechazos repetidos en una interfaz real, una persona real que hacía clic en rechazar varias veces seguidas, y ninguna acción rechazada se llevó a cabo sin que se notara. La delegación entre proveedores, con una parte del sistema basada en un modelo y otra en otro diferente, era un detalle de configuración y no un proyecto de ingeniería.

Los mecanismos difieren en naturaleza. Los permisos del sistema de archivos autorizan el acceso, la configuración de enrutamiento entre proveedores establece los modelos, y el acceso a la memoria de los subagentes gestiona el estado y el contexto. Cada uno representa un aspecto relacionado con la gestión del estado, y cada uno realizó exactamente lo para lo que estaba configurado. Esta afirmación es más precisa y limitada que decir que todo el sistema funcionó como se esperaba, y la siguiente sección aborda los casos en los que no lo hizo.

Lo que el marco de soporte no soluciona

Los hallazgos interesantes se dieron en situaciones en las que el marco de soporte funcionaba perfectamente, pero aún así ocurrían errores.

Al recibir un “no” sin ninguna explicación, el modelo no preguntó qué andaba mal. Inventó una explicación creíble y actuó en consecuencia; dos rutas de ejecución muy diferentes confirmaron ese patrón. Se le indicaron cinco hechos relacionados para almacenar: cuatro fueron confirmados por una persona y uno solo fue inferido. El modelo escribió los cinco con la misma confianza y redacción, de modo que el hecho inferido no presentó una señal más débil que los confirmados a su lado. Dos escrituras en la memoria con minutos de diferencia durante una misma sesión, ambas aprobadas por separado, dieron recomendaciones directamente opuestas, y nada en el proceso activó ninguna alerta.

Estrictamente hablando, nada de esto constituye un fallo en la gestión del estado del tipo archivo perdido o dañado. Gracias a la aprobación incluida y a las escrituras contradictorias, la información necesaria se almacenó correctamente y estuvo disponible para su verificación. Nadie ni nada la verificó. El caso de rechazo es más sutil: el modelo nunca ve el rechazo en sí, solo un mensaje generado al respecto, por lo que lo que hace con ese mensaje plantea dudas sobre su propio razonamiento y no dice nada sobre la falta de estado. En los tres casos, lo que falló fue el juicio: tomar un rechazo sin explicación al pie de la letra, separar lo inferido de lo confirmado, y darse cuenta de que una nueva respuesta contradice a una anterior. Eso es un problema de razonamiento y no de estructura, y añadir estructura no lo soluciona directamente.

Por qué es importante

Para el tipo de problema al que apunta un framework como DeepAgents, la gestión del estado es la herramienta más importante, mientras que la calidad del razonamiento es algo secundario, más difícil y que, por sí solo, escapa al alcance de los mecanismos auxiliares. Los servidores backend, los permisos, la delegación aislada y la memoria controlada no sustituyen al buen juicio. En cambio, hacen que las acciones, el estado y los resultados del sistema sean observables y controlables: visibles, atribuibles y reversibles antes de que se vuelvan permanentes. Eso facilita la evaluación del juicio posteriormente, aunque no puede garantizar que dicho juicio sea acertado. Una escritura rechazada que aún se describa como un éxito representa un verdadero fracaso, pero se detectó de inmediato, ya que el sistema está diseñado de tal manera que este tipo de afirmación pueda verificarse en lugar de aceptarse por simple confianza.

Al final, eso es precisamente para lo que sirve un framework de este tipo.

Leer más

  • Agent Harness, Bucle y Grafico: Tres capas detrás de agentes de IA fiables — Por qué los agentes no fiables suelen carecer de una capa: el harness alrededor del modelo, un bucle basado en evidencias y un grafico explícito, con patrones de código y una lista de verificación para depurar.
  • Arquitectura de IA para agentes de producción: Modos de fallo y capas de control — Una guía de arquitectura capa por capa para sistemas de agentes de producción: modos de fallo, medidas de seguridad, evaluaciones, capacidad de observación, control de costos, principio de confianza cero y gobernanza.
  • ¿Por qué los agentes de largo plazo necesitan un marco de control? Cómo gestionan los agentes profundos el estado y el contexto — Un análisis detallado de los agentes profundos: deterioro del contexto, los cuatro pilares, middleware, perfiles de control, backends de sistema de archivos, subagentes aislados, fallos en casos extremos y alternativas.