deepagents en un cluster propio: el SDK es libre, el servidor no, y lo que hay que construir en medio
Índice
Apertura de la vertical de plataforma agéntica. Los artículos anteriores trataron las piezas por separado: el gateway de inferencia, su segunda puerta, qué cuesta una flota de agentes y el aislamiento. Este trata lo que va encima. Verificado contra
deepagents0.7.14 y el repositorio de LangGraph, commits del 14 de septiembre de 2026.
TL;DR
deepagents no es un runtime. Es una factoría que ensambla middleware y delega en la factoría de agentes de LangChain. Devuelve un grafo compilado de LangGraph y nada más. La documentación del propio repositorio lo dice sin adornos: no introduce un runtime nuevo. Eso es una buena noticia para quien despliega, porque corre donde corra Python.
La herramienta de planificación ya no existe. Desapareció en la 0.7.0 como cambio incompatible, junto con el canal de estado que la sostenía. Casi todo lo escrito sobre este armazón sigue describiéndola como una de sus tres patas. Hoy hay que pedirla explícitamente, y hay que pedirla también en cada subagente.
Con la configuración por defecto, los ficheros del agente viven dentro del checkpoint. El backend de estado guarda el contenido completo, los binarios en base64, en el mismo sitio donde se persiste la conversación. Cambiar eso es una línea y es la primera decisión de arquitectura del despliegue.
La base de datos crece por paso del bucle, no por turno. Cuatro tablas, una fila por superstep, una por canal y versión, otra por escritura de tarea. Y de las cuatro operaciones de limpieza que la clase base declara, en la implementación de Postgres solo hay una: borrar un hilo entero. Las otras tres lanzan excepción de no implementado.
Podar a mano puede vaciar el historial en silencio. El propio código avisa de que un borrado parcial rompe la cadena de deltas y deja los canales reconstruyéndose vacíos sin lanzar ningún error. La única poda segura es por hilo completo.
El aislamiento entre inquilinos es el identificador de hilo, y no hay ninguna comprobación de propietario. Quien pueda enviar un identificador de hilo lee ese hilo. El backend de ficheros en disco fija su raíz al construir el agente, así que no separa a nadie. Y el ejemplo de la documentación para calcular el espacio de nombres por usuario no funciona fuera de la plataforma gestionada.
Hay una trampa de configuración que deja un modelo de ventana corta sin compactar. Si el modelo no expone su ventana, el umbral de compactación cae a 170.000 tokens. Servido en un vLLM con ventana de 32.768, el agente revienta por longitud de contexto mucho antes de compactar. El fallo es silencioso y se arregla pasando el perfil a mano.
La observabilidad sin plataforma existe, y pasa por un paquete que no es evidente. El núcleo de LangChain no emite OpenTelemetry: cero referencias. El exportador vive dentro del SDK de LangSmith, y hay un modo que emite solo por OTLP sin hablar con el servicio. Es el camino a un Langfuse propio, y conviene saber por dónde pasa.
Y aquí se acaba lo libre: el servidor. El SDK, el motor del grafo y los checkpointers son MIT. El servidor que los publica por HTTP tiene licencia Elastic 2.0, pide una clave de licencia comercial para producción, añade Redis al stack y reporta metadatos a un endpoint externo salvo acuerdo de aislamiento. La herramienta de desarrollo es explícitamente de desarrollo.
Lo que queda es una lista de cosas que hay que escribir. Autorización por hilo, retención, cola de ejecuciones, cancelación, drenado limpio y migraciones. Ninguna es difícil. Todas son trabajo, y ninguna aparece en el ejemplo de veinte líneas de la portada.
Estás aquí: el armazón llega después del modelo
La secuencia se repite en todos los proyectos. Primero se monta la inferencia, que es lo visible. Luego el gateway, porque hacen falta claves y presupuestos. Luego la observabilidad, porque nadie sabe qué está pasando. Y cuando todo eso funciona, alguien pregunta por qué el asistente no puede hacer tareas de varios pasos, y entra en escena la palabra agente.
En ese punto la tentación es escribir el bucle a mano, y durante dos semanas parece buena idea. El bucle es fácil. Lo que no es fácil es todo lo que se descubre después: qué pasa cuando el contexto se llena, dónde se guardan los ficheros intermedios, cómo se reanuda una tarea que se quedó a medias, cómo se pide confirmación a un humano sin perder el estado.
Un armazón de agentes es exactamente eso: la respuesta empaquetada a esas preguntas. Y elegirlo parece una decisión de librería, del tamaño de elegir un cliente HTTP. No lo es. Decide dónde vive el estado de tus usuarios, qué base de datos crece, qué se puede aislar y qué no, y en el caso que nos ocupa, si hay una licencia comercial esperando al final del camino.
Este artículo mira deepagents con esos ojos. No es una guía de uso, y no evalúo si sus agentes son buenos. Miro qué obliga a construir.
La analogía: el andamio y la grúa
Una obra necesita andamio y necesita grúa, y no son lo mismo aunque las dos sean estructuras metálicas que rodean al edificio.
El andamio lo monta uno con piezas compradas. Se adapta a la fachada, se puede ampliar, y el día que la obra termina se desmonta y se guarda. Nadie cobra por usarlo una vez comprado.
La grúa es otra cosa. Se alquila, viene con operador, tiene número de serie y hay que avisar a alguien cuando se monta. Sin grúa la obra avanza, más despacio y con más gente, pero avanza.
Aquí el andamio es el SDK y el motor del grafo, y es de quien lo compra. La grúa es el servidor que expone el agente por HTTP, y esa viene con contrato. El artículo trata de dónde está exactamente la línea entre las dos, porque en el folleto no se ve, y de cuánta obra se puede hacer sin grúa.
Qué es deepagents de verdad
Lo primero que hay que quitarse de la cabeza es que sea un sistema. Es una función.
La factoría principal ensambla una lista de middleware y llama a la factoría de agentes de LangChain, que es quien construye el grafo. Lo que devuelve es un grafo compilado, con una configuración añadida encima que sube el límite de recursión a un número muy alto y añade una etiqueta de integración en los metadatos. El documento de arquitectura del propio repositorio lo declara: no se introduce un runtime nuevo.
Compilado con las opciones por defecto, el grafo tiene los nodos que cabría esperar de un bucle de razonamiento y acción: inicio, un nodo previo que repara llamadas a herramienta incompletas, el nodo del modelo, el nodo de herramientas y el final. Las aristas condicionales van del modelo a herramientas, al propio modelo o al final. No hay más. Todo lo que parece un sistema es middleware que añade nodos solo cuando declara sus propios enganches.
Las herramientas que aparecen de serie son las de sistema de ficheros, más la de delegación en subagentes: listar, leer, escribir, editar, borrar, buscar por patrón, buscar por contenido, ejecutar, y la de tarea.
La consecuencia buena de esto es la portabilidad. Si algo es un grafo de LangGraph y nada más, corre en un contenedor de Python cualquiera, con el checkpointer que uno elija, contra el modelo que uno elija. No hay servicio escondido detrás.
Lo que ya no está
Merece un apartado porque contradice a casi todo lo publicado. La herramienta de planificación, la que escribía una lista de tareas y la mantenía en un canal del estado, se eliminó en la 0.7.0 como cambio incompatible declarado en el registro de cambios. No está la herramienta, no está el canal, y no está el fragmento de prompt que la acompañaba.
Sobrevive como middleware de LangChain, y hay que pedirla a mano. Con un detalle que muerde: hay que pedirla también en cada subagente, porque cada subagente monta su propia pila. Lo único que queda de la etapa anterior son restos arqueológicos, como la clave de estado que sigue en la lista de claves excluidas al pasar contexto a un subagente.
El plan, hoy, se apoya en ficheros. Que es coherente con el resto del diseño, y es otra razón para mirar con atención dónde viven esos ficheros.
Dónde vive el estado
Esta es la primera decisión de arquitectura y la que más caro sale equivocar.
El estado del agente tiene dos canales grandes. Los mensajes, con un reductor propio que deduplica por identificador y trata los borrados como lápidas. Y los ficheros, con su propio reductor donde un valor nulo significa borrado. Los dos usan un tipo de canal que escribe deltas y guarda una instantánea completa cada cincuenta actualizaciones, y el comentario del código explica el motivo: pasar el crecimiento del checkpoint de cuadrático a lineal respecto al número de mensajes.
Ese tipo de canal está marcado como beta en su propio código, con el aviso de que la representación en disco puede cambiar. Conviene saberlo antes de apoyar una plataforma encima.
Los backends
Hay una interfaz común y varias implementaciones. Las que importan para decidir:
| Backend | Dónde viven los ficheros | Persistencia |
|---|---|---|
| Estado | Dentro del estado del grafo, o sea del checkpoint | Por hilo, en la base de datos del checkpointer |
| Disco | En un directorio real anclado a una raíz | Donde monte el volumen |
| Almacén | En el almacén de LangGraph, con espacio de nombres | Cruza hilos, con tiempo de vida |
| Compuesto | Enrutado por prefijo de ruta entre los anteriores | Mixta |
El backend por defecto es el de estado. Es decir: por defecto, todo lo que el agente escriba acaba dentro del checkpoint, y los binarios van codificados en base64 dentro del mismo JSON. Para una demostración es cómodo, porque no hay que montar nada. Para una plataforma con usuarios reales es una decisión de dimensionado tomada por omisión.
Hay un detalle que redondea el asunto. El middleware de ficheros desaloja automáticamente los resultados de herramienta grandes a un fichero cuando superan un umbral de tokens, para que no ocupen la ventana de contexto. Es un buen mecanismo. Pero con el backend de estado, desalojar a fichero significa mover el contenido de un sitio del checkpoint a otro sitio del mismo checkpoint. Sale de la ventana del modelo y no sale de la base de datos.
Lo que crece en la base de datos
El checkpointer de Postgres crea cuatro tablas: una de migraciones, una de checkpoints, una de blobs por canal y versión, y una de escrituras por tarea. Las claves primarias empiezan todas por el identificador de hilo, y hay un índice por tabla sobre esa columna.
Lo que hay que interiorizar es el ritmo. Se escribe una fila de checkpoint por superstep del motor, no por turno de conversación. Un turno con cuatro llamadas a herramienta son varios supersteps. Nada de eso se recicla solo.
Y aquí llega la parte incómoda. La clase base del checkpointer declara cuatro operaciones de mantenimiento: borrar un hilo, borrar por ejecución, copiar un hilo y podar con estrategia. En la implementación de Postgres solo está implementada la primera. Las otras tres heredan la excepción de no implementado de la clase base. Borrar un hilo son tres sentencias de borrado por identificador, y ya está.
O sea: la retención te la montas tú. Y con un cuidado que el propio código documenta y que conviene citar entero, porque es el tipo de aviso que se lee después del incidente. Una poda ingenua que elimine checkpoints intermedios y sus escrituras puede cortar la cadena de deltas; el checkpoint superviviente rara vez es un punto de instantánea, así que sus canales se reconstruirían vacíos sin que se lance ningún error.
Traducido a operación: el barrido periódico borra hilos enteros y solo hilos enteros. Nunca filas sueltas. Y como la tabla de checkpoints no tiene columna de fecha, para saber qué hilo es viejo hay que llevar la cuenta por fuera o apoyarse en que el identificador de checkpoint es un UUID ordenable por tiempo.
Existe una variante del checkpointer que guarda solo el último estado y no retiene historia, que acota el crecimiento de raíz. Con una reserva importante que hay que verificar antes de usarla: no está claro que sea compatible con los canales de deltas que este armazón usa por defecto en mensajes y ficheros. Combinar las dos cosas sin comprobarlo es pedir un problema silencioso.
El almacén, que sí tiene limpieza
Curiosamente, la pieza hermana está mejor resuelta. El almacén tiene tiempo de vida configurable, con refresco en lectura, omisión de expirados y un intervalo de barrido, y el barrido es un borrado por fecha de expiración.
Dos avisos. El barredor es un hilo dentro del proceso, así que con varias réplicas hay varios barredores compitiendo por el mismo borrado; es idempotente, pero es contención gratuita y se evita apagándolo y poniendo el borrado en una tarea programada del cluster. Y la búsqueda semántica del almacén exige la extensión de vectores en la base de datos, lo que condiciona la imagen del Postgres gestionado, que normalmente no la trae.
El aislamiento que no existe
Si la plataforma va a tener más de un inquilino, este es el apartado que decide la topología del despliegue.
Lo único que separa un hilo de otro es el identificador de hilo. Es la primera columna de la clave primaria en las tres tablas. No hay ninguna comprobación de propietario en ninguna parte del checkpointer. Quien consiga enviar un identificador de hilo en la configuración de la llamada, lee ese hilo. La autorización es responsabilidad de la capa que uno escriba por encima, y como esa capa también hay que escribirla, conviene anotarlo ya.
Por backend, la situación es distinta:
- El de estado hereda el aislamiento del hilo, que es suficiente si la capa HTTP valida que el usuario es dueño del hilo.
- El de almacén calcula el espacio de nombres por llamada, mediante una función que recibe el contexto de ejecución. Es la opción correcta para multiinquilino.
- El de disco fija su raíz en el constructor, cuando se construye el agente. No hay ningún camino en el código que la recalcule por petición, por hilo o por inquilino. Compartir un despliegue entre inquilinos con este backend significa que todos ven el mismo directorio.
Sobre ese último punto conviene no confundirse con la protección que sí existe. El modo virtual, que ahora viene activado por defecto, bloquea las rutas que se salen de la raíz. Eso protege del recorrido de directorios. No protege del vecino. El propio documento de amenazas del proyecto lo admite: ese modo existe para dar soporte al enrutado por prefijo del backend compuesto, no como frontera de seguridad.
Hay además una trampa documental que vale la pena señalar porque cuesta una tarde. El ejemplo que la documentación propone para calcular el espacio de nombres por usuario lee la identidad de un campo de información de servidor del contexto de ejecución. Ese campo está anotado en el código de LangGraph como metadatos inyectados por el servidor gestionado, y nulo cuando se ejecuta LangGraph de código abierto sin despliegues gestionados. En un cluster propio con una capa HTTP propia, ese ejemplo falla. Lo que hay que usar es el contexto que uno mismo rellena, con el identificador que venga validado del proveedor de identidad.
Y una frontera más que cruza sin avisar: los middleware de memoria y de habilidades interpolan el contenido de ficheros en el prompt de sistema tal cual, sin sanear. Si dos inquilinos comparten un directorio que sirve de origen para eso, el primero escribe instrucciones que ejecuta el agente del segundo. Es inyección de prompt por almacenamiento compartido, y se evita con la misma medida que todo lo anterior: no compartir el backend de disco entre inquilinos.
La conclusión de topología es corta. Multiinquilino de verdad con ficheros significa un despliegue por inquilino, con su espacio de nombres, su volumen y su cuota, o bien el backend de almacén con espacio de nombres calculado por petición. No hay una tercera vía.
El modelo, y la trampa que hay que conocer
El modelo por defecto es un proveedor comercial concreto, y está marcado como obsoleto: pasar el modelo vacío avisa y dejará de funcionar. Se puede inyectar cualquier modelo de chat de LangChain, que es lo que hará quien sirva sus propios pesos.
Con dos detalles de integración que no son evidentes.
El primero. Para apuntar a un endpoint compatible con OpenAI, que es como se ve un vLLM detrás de un gateway, hay que pasar la instancia ya construida, no la cadena con prefijo de proveedor. Con la cadena se aplica un perfil de proveedor que fuerza el uso de la API de respuestas, que un vLLM normalmente no implementa. Pasando el objeto construido no se aplica ningún perfil y el agente queda limpio.
El segundo es la trampa, y en mi opinión es el fallo de configuración más probable de todo el despliegue.
El middleware de compactación calcula sus umbrales a partir del perfil del modelo. Si el modelo declara su ventana de entrada, compacta al llegar al 85 % de la ventana y conserva el 10 %. Si no la declara, cae a un valor fijo: 170.000 tokens.
La cadena que lleva a no declararla es la habitual en un despliegue propio. El cliente de OpenAI resuelve el perfil buscando el nombre del modelo en una tabla estática de modelos del proveedor. Un modelo servido como qwen3-30b-a3b no está en esa tabla, así que el perfil queda vacío, y el vacío se convierte en nulo. El método que lo asigna traga cualquier excepción, así que no hay aviso.
Resultado: un vLLM con ventana de 32.768 tokens no compactará hasta los 170.000. El servidor devolverá error de longitud de contexto mucho antes, y lo hará de forma intermitente, solo cuando las conversaciones se alarguen. Hay una red de seguridad que captura el error y recorta, pero eso es recuperación reactiva después de una llamada fallida, no planificación.
El arreglo es una línea, porque el perfil es un campo público y solo se rellena solo cuando está vacío: se pasa al construir el modelo, con un valor por debajo de la ventana real para dejar sitio a la generación. Y merece un assert en el arranque, porque es el tipo de cosa que nadie mira hasta que falla.
Un apunte de dimensionado relacionado. Con los umbrales por defecto y una ventana de 32.768, la compactación dispararía a 27.852 tokens, mientras que el desalojo de resultados de herramienta grandes está fijado en 20.000. Es decir, un solo resultado de herramienta puede ocupar casi todo el presupuesto antes de que nada lo desaloje. Con ventanas de 128.000 los defaults están proporcionados; por debajo de 64.000 hay que bajar esos dos umbrales a mano.
Las herramientas, y el MCP que no está donde parece
Este armazón tiene sus herramientas de sistema de ficheros y su herramienta de delegación, y las demás se pasan como una lista normal de herramientas de LangChain.
El dato que importa a quien tenga un gateway MCP montado: el SDK no integra MCP. No hay ninguna referencia al protocolo en el paquete, ni dependencia de adaptadores. La integración vive en la herramienta de línea de comandos que acompaña al proyecto, que sí depende de los adaptadores de MCP para LangChain, resuelve configuraciones al estilo de los clientes de escritorio, y convierte cada herramienta remota en una herramienta de LangChain que luego pasa por la lista normal.
O sea que el camino existe y está probado, pero hay que recorrerlo uno: cargar las herramientas del gateway con los adaptadores y pasarlas en la lista. Lo cual, dicho sea de paso, encaja bien con lo que ya sabemos del catálogo: el filtrado por credencial que hace el gateway se aplica en el listado, así que cada agente recibe el subconjunto que le toca sin que el armazón tenga que saber nada.
Los subagentes
Se declaran de tres formas y se invocan con una herramienta de tarea que recibe una descripción y un tipo. Lo que hay que saber para diseñar con ellos:
El subagente recibe el estado del padre excepto los mensajes y unas cuantas claves privadas, y arranca con un único mensaje humano que es la descripción de la tarea. Los ficheros sí se heredan, y comparte el mismo objeto de backend, así que padre e hijo trabajan sobre el mismo sistema de ficheros lógico. El resultado vuelve como un mensaje de herramienta con la respuesta estructurada o el último texto del hijo.
La profundidad es efectivamente de un nivel. Verificado compilando el grafo: la pila que se monta para un subagente declarativo no incluye el middleware de subagentes, así que el hijo no tiene la herramienta de tarea y no puede delegar a su vez.
Y no hay límite de concurrencia propio. El paralelismo es el de las llamadas paralelas a herramienta del motor, y el prompt de la herramienta de tarea invita explícitamente a lanzarlas en paralelo. Quien tenga cuota de GPU por inquilino querrá poner el freno en su sitio, porque aquí no está.
La ejecución de código
El protocolo de sandbox está bien definido y las implementaciones empaquetadas son casi todas de servicios de pago: cuatro proveedores comerciales, más un intérprete de JavaScript embebido y un sandbox del propio proveedor de la plataforma.
Queda un backend de shell local que conviene mirar de cerca antes de considerarlo. Ejecuta el comando con la shell del sistema, y su propio docstring enumera lo que no hace: nada de aislamiento, nada de separación de procesos, nada de límites de recursos, y lista los entornos de producción y multiinquilino como casos de uso inapropiados. Tiene además una opción de heredar el entorno del proceso que, activada, entrega al modelo todas las variables, incluidas las credenciales del gateway y la cadena de conexión de la base de datos. Viene desactivada, y es un pie de bala fácil de pegarse.
La salida razonable en un cluster propio es implementar el protocolo uno mismo, y es más pequeño de lo que parece. La clase base de sandbox exige cuatro cosas: un identificador, ejecutar, subir ficheros y bajar ficheros. Todo lo demás, listar, leer, escribir, editar, borrar y buscar, lo construye la clase base encima de ejecutar. Mirando una de las implementaciones comerciales, son del orden de ciento cincuenta líneas.
Un requisito de la imagen que condiciona el Dockerfile y que conviene saber antes: los ayudantes de la clase base inyectan pequeños scripts de Python codificados para resolver búsquedas y comprobaciones, así que la imagen del sandbox necesita Python y una shell POSIX o la mitad de las llamadas fallan.
Sobre cómo implementarlo, la forma que encaja con un Kubernetes propio es un contenedor efímero por sesión con tiempo de vida acotado, límites de recursos, sin token de cuenta de servicio montado y con una política de red que solo permita salir hacia el gateway de inferencia. Ejecutar se resuelve contra la API de ejecución del cluster, y subir y bajar ficheros por el mismo canal. La alternativa con menos código es una clase de runtime aislada para el pod del agente, un pod por inquilino, y el backend de shell dentro; se pierde el multiinquilino en un solo pod y no hay que escribir nada.
La observabilidad, y el paquete por el que pasa
Aquí hay un dato que sorprende y que conviene tener claro antes de diseñar el pipeline de trazas.
El núcleo de LangChain no emite OpenTelemetry. Cero referencias en todo el paquete. Su trazador nativo habla con el servicio de observabilidad del fabricante y punto.
El exportador OTLP existe, pero vive dentro del SDK de ese servicio. Y ahí está la buena noticia: ese SDK tiene un modo de trazado que emite solo por OTLP, sin hablar con el servicio, leyendo el destino y las cabeceras de las variables de entorno estándar de OpenTelemetry. El transporte es HTTP con protobuf, que es justo lo que acepta un Langfuse autoalojado en su ruta de ingesta.
Es decir: para llevar las trazas del agente a la observabilidad propia hay que instalar el paquete del fabricante y pedirle que no hable con el fabricante. Funciona, es soportado y está documentado, pero es una dependencia que hay que declarar en el análisis de la cadena de suministro, no un detalle de configuración. Y si faltan los paquetes de OpenTelemetry, emite un aviso y deja de trazar en silencio, así que el arranque debería comprobarlo.
Existe la alternativa del manejador de retrollamadas que publica el propio Langfuse, con menos piezas. La diferencia práctica es la correlación: por la vía OTLP los spans del agente comparten identificador de traza con los del gateway y los del motor de inferencia, y se ve una petición entera de punta a punta. Por la vía de retrollamadas, no. Para quien ya tenga un colector desplegado, la vía OTLP es la que paga.
El servidor: aquí se acaba lo libre
Todo lo anterior son decisiones de arquitectura. Esto es una decisión de licencia, y es la que da título al artículo.
El SDK del armazón es MIT. El motor del grafo es MIT. Los checkpointers de memoria, SQLite y Postgres son MIT. Todo eso corre en cualquier sitio, sin clave, sin registro y sin llamar a nadie.
El servidor que expone un grafo por HTTP es otra cosa. El paquete que lo implementa tiene licencia Elastic 2.0, cuyas cláusulas relevantes prohíben ofrecer el software a terceros como servicio gestionado y prohíben modificar o eludir la funcionalidad de clave de licencia. El propio comando de la herramienta de línea imprime las condiciones: para desarrollo local pide una clave de API del servicio, y para uso en producción pide una clave de licencia en una variable de entorno.
Hay además reporte de uso. El código del servidor contiene un endpoint de baliza al que envía metadatos, con una cabecera que lleva la clave de licencia. La documentación de autoalojamiento lo confirma desde el otro lado, al listar entre los requisitos la salida de red hacia ese dominio para verificación de licencia y reporte de uso si no se ejecuta en modo aislado. El modo aislado es una variante contractual, no una casilla que uno marca.
Y arrastra infraestructura: además de Postgres, pide Redis.
La herramienta de desarrollo, que es la que sale en todos los tutoriales, se describe a sí misma como modo de desarrollo con recarga en caliente y servidor en memoria. No es un servidor de producción y no pretende serlo.
Nada de esto es un reproche. Es un modelo de negocio legítimo y bastante habitual: el núcleo permisivo y el plano de control comercial. Lo que no es legítimo es enterarse tarde. Quien esté montando una plataforma con requisito de soberanía, sin dependencias de servicios externos y con el argumento de que todo el stack es abierto, tiene que saber que la pieza que convierte el grafo en un servicio no cumple ese requisito.
Lo que hay que construir
La salida es escribir la capa HTTP uno mismo, y la buena noticia es que es aburrida. Un servicio que compile el grafo una vez con el checkpointer apuntando a la base de datos, y exponga un endpoint que valide el testigo del proveedor de identidad, derive el identificador de hilo y el contexto de inquilino, e invoque el grafo. Streaming por eventos si hace falta.
Lo que hay que escribir además, y que el servidor comercial daba hecho:
- Autorización por hilo. El motor no la tiene. Es la más importante y la más fácil de olvidar porque nada falla si no está.
- Retención y borrado. Tarea programada que borre hilos enteros, nunca filas sueltas, por las razones del apartado del estado.
- Migraciones. La preparación del esquema hay que llamarla explícitamente y necesita permisos de definición de datos, así que va en un trabajo aparte con un rol distinto del de ejecución. Y como crea índices de forma concurrente, no puede pasar por un agrupador de conexiones en modo transacción; conviene apuntar contra el servicio de escritura directo.
- Sondas de salud y drenado. El motor tiene un mecanismo de drenado cooperativo, pero conectarlo a la señal de terminación del contenedor es trabajo propio.
- Cola de ejecuciones y cancelación, si hacen falta ejecuciones largas en segundo plano. Aquí es donde más se nota la ausencia, y la respuesta razonable es una cola sobre la misma base de datos antes que otro sistema.
- Durabilidad decidida a conciencia. Hay tres modos: persistir antes del paso siguiente, persistir en paralelo, o persistir solo al salir. El último no vale en Kubernetes: si el pod muere, se pierde el turno entero.
Sobre este último punto conviene ser explícito, porque afecta al diseño de las herramientas. El motor guarda las escrituras de cada tarea en cuanto termina, y al reanudar reaplica las que ya estaban y no las repite. Una herramienta que terminó no se vuelve a ejecutar. Una herramienta que estaba en vuelo cuando el pod murió sí, porque su escritura nunca llegó. La semántica es de al menos una vez, así que toda herramienta con efecto externo tiene que ser idempotente o llevar clave de idempotencia. No hay exactamente una vez.
Y lo mismo aplica a las interrupciones para confirmación humana: al reanudar, el nodo se re-ejecuta entero, así que cualquier efecto lateral que estuviera antes de la interrupción dentro del mismo nodo se repite. La regla es que la interrupción vaya primero.
El encaje con el registro de actividad
Dos cosas de este montaje tocan el cumplimiento de lleno, y las dos salen de apartados anteriores.
La primera es que el checkpoint es, de facto, un repositorio de datos personales. Contiene la conversación entera y, con el backend por defecto, los ficheros que el agente haya escrito. Si la plataforma atiende a personas identificables, esa base de datos necesita cifrado, retención propia y control de acceso separado, exactamente igual que la tabla de gasto del gateway. La diferencia es que aquí no hay ningún interruptor de redacción que valga: el estado es el estado.
La segunda es la trazabilidad. Un agente que delega en subagentes y ejecuta herramientas produce una cadena de acciones que hay que poder reconstruir, y el único sitio donde esa cadena queda completa es la traza. Que la traza salga por OTLP hacia la observabilidad propia deja de ser una preferencia técnica y pasa a ser el mecanismo de registro. Conviene diseñarlo así desde el principio, y no añadirlo después.
Como siempre en esta serie, los códigos concretos del anexo hay que contrastarlos contra el texto vigente antes de llevarlos a un documento de cumplimiento; el planteamiento está desarrollado en el artículo de controles técnicos.
Checklist
- Decidir el backend de ficheros antes de la primera demostración, porque el de por defecto mete el contenido en la base de datos.
- Pasar el perfil del modelo a mano con la ventana real, y comprobarlo en el arranque.
- Bajar los umbrales de desalojo si la ventana del modelo está por debajo de 64.000 tokens.
- Construir el modelo como instancia, nunca como cadena con prefijo de proveedor, si detrás hay un endpoint compatible con OpenAI.
- Escribir la autorización por hilo antes de abrir el servicio a más de un usuario.
- Un despliegue por inquilino si se usa el backend de disco; espacio de nombres calculado por petición si se usa el de almacén.
- Calcular ese espacio de nombres desde el contexto propio, no desde el campo de información de servidor, que es nulo fuera de la plataforma gestionada.
- Tarea programada de retención que borre hilos enteros y solo hilos enteros.
- Migraciones del esquema en un trabajo aparte, con rol propio y contra el servicio de escritura directo.
- Durabilidad síncrona, y herramientas con efectos externos idempotentes.
- Trazas por OTLP con el modo que no habla con el servicio del fabricante, y comprobación en el arranque de que los paquetes están.
- Poner límite de concurrencia a los subagentes si hay cuota de GPU de por medio.
- Si hace falta ejecución de código, implementar el protocolo de sandbox contra un contenedor efímero; el backend de shell local está desaconsejado por su propia documentación.
- Anotar en el análisis de la cadena de suministro las dependencias duras de proveedores comerciales que el paquete arrastra aunque no se usen.
Trampas y cosas que no son lo que parecen
- La herramienta de planificación se eliminó en la 0.7.0. Hay que añadirla a mano, y también en cada subagente.
- El backend por defecto guarda los ficheros dentro del checkpoint, binarios incluidos en base64.
- Desalojar un resultado grande a fichero no lo saca de la base de datos con ese backend, solo de la ventana de contexto.
- Se escribe un checkpoint por superstep, no por turno.
- De las cuatro operaciones de limpieza declaradas, en Postgres solo existe borrar un hilo.
- Una poda parcial puede vaciar el historial sin lanzar ningún error.
- El canal de deltas está en beta y su representación en disco puede cambiar.
- No hay comprobación de propietario en el checkpointer. El identificador de hilo no es una credencial.
- La raíz del backend de disco se fija al construir el agente y no separa inquilinos.
- El ejemplo de la documentación para el espacio de nombres por usuario no funciona fuera de la plataforma gestionada.
- El middleware de ficheros y el de subagentes no se pueden quitar.
- Si el modelo no declara su ventana, la compactación no dispara hasta los 170.000 tokens, en silencio.
- Pasar el modelo como cadena con prefijo de proveedor fuerza una API que un vLLM no implementa.
- El SDK no integra MCP; la integración vive en la herramienta de línea de comandos.
- Los subagentes no delegan: la profundidad es de un nivel.
- El núcleo de LangChain no emite OpenTelemetry; el exportador vive en el SDK del servicio del fabricante.
- El servidor HTTP tiene licencia Elastic 2.0, pide clave de licencia para producción y reporta uso salvo acuerdo de aislamiento.
- La herramienta de desarrollo es de desarrollo, con servidor en memoria.
- La reanudación es de al menos una vez para herramientas en vuelo.
Cierre
La pregunta con la que empezó este artículo era si deepagents encaja en una plataforma propia. La respuesta es que sí, y que encaja mejor de lo que esperaba, precisamente porque es menos de lo que aparenta. Una pila de middleware sobre un grafo es fácil de alojar, fácil de entender y fácil de sustituir. Lo que no encaja es la pieza de al lado.
Esa asimetría es el patrón que llevo viendo todo el año en el ecosistema, y merece nombrarlo. El núcleo se publica con licencia permisiva porque el núcleo es donde se compite por adopción. El plano de control se publica con licencia restrictiva porque el plano de control es donde se cobra. Pasó con los gateways, pasa con la observabilidad, y pasa aquí. Quien monte plataforma soberana no puede evaluar un proyecto entero por la licencia de su repositorio principal: hay que mirar paquete a paquete, y mirar cuál de ellos es el que uno va a necesitar el día que aquello tenga usuarios.
La parte buena es que la factura, en este caso, se paga en trabajo y no en dinero. Lo que hay que construir para prescindir del servidor comercial es un servicio HTTP sin misterio, una tarea de retención, un trabajo de migración y una capa de autorización. Nada de eso es investigación. Todo eso es una semana de un ingeniero que sepa lo que hace, y a cambio queda un sistema que se despliega igual en un instituto que en un centro de datos clasificado, sin salida a internet y sin nadie contando ejecuciones al otro lado.
Lo que sí recomiendo es hacer esa lista antes de la primera demostración y no después. Porque el ejemplo de la portada funciona en veinte líneas, y las veinte líneas tienen el backend por defecto, el modelo sin perfil y ninguna autorización.
Ver también
- Elegir gateway de inferencia — la pieza de debajo, y el mismo patrón de licencia mirado en otro sitio.
- El gateway MCP de LiteLLM — de dónde salen las herramientas que este armazón consume.
- Prioridad de herramientas MCP — cómo se decide qué subconjunto del catálogo ve cada agente.
- Dimensionar para agentes — la carga real que genera un bucle de herramientas sobre el gateway y el motor.
- El contratista con la llave maestra — el aislamiento de red que este artículo da por necesario.
- El segundo vector de coste de los agentes — ejecución durable y qué cuesta un bucle que falla a medias.
- Controles técnicos para ENS, 42001 y el AI Act — el marco donde encajan el registro y la retención.
- LiteLLM y Langfuse: el par operativo — el destino de las trazas OTLP de las que habla el artículo.
Fuentes
langchain-ai/deepagents, código y documento de arquitectura: https://github.com/langchain-ai/deepagents.langchain-ai/deepagents, registro de cambios de la rama del SDK (eliminación de la herramienta de planificación en la 0.7.0): https://github.com/langchain-ai/deepagents/blob/main/libs/deepagents/CHANGELOG.md.langchain-ai/langgraph, motor, canales y checkpointers: https://github.com/langchain-ai/langgraph.langgraph-checkpoint-postgres, esquema de tablas y operaciones de mantenimiento: https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres.langgraph-checkpoint, clase base del checkpointer y aviso sobre poda con canales de deltas: https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint.- LangChain, despliegue de servidor autónomo (requisitos de licencia, Postgres, Redis y salida de red): https://docs.langchain.com/langsmith/deploy-standalone-server.
- LangChain, trazado con OpenTelemetry: https://docs.langchain.com/langsmith/trace-with-opentelemetry.
- Elastic License 2.0: https://www.elastic.co/licensing/elastic-license.
- Langfuse, integración nativa OpenTelemetry (ruta de ingesta y autenticación): https://langfuse.com/integrations/native/opentelemetry.
- Langfuse, integración con LangChain por retrollamadas: https://langfuse.com/integrations/frameworks/langchain.
langchain-ai/helm, chart del servidor: https://github.com/langchain-ai/helm.- Boletín Oficial del Estado, Real Decreto 311/2022, Esquema Nacional de Seguridad: https://www.boe.es/buscar/act.php?id=BOE-A-2022-7191.