Por qué desmonté mi plataforma de agentes

Siete semanas de autooperación de una plataforma de agentes, unos 340 $ en costos de modelos y un incidente que la telemetría solo explicó a posteriori: por qué el alcance autónomo adicional no justificó la autooperación y qué funciona desde entonces.

El 27 de agosto de 2026 mi plataforma de agentes funcionó como nunca: tres issues en unos 87 minutos. Implementación, reviews por judges de LLM, correcciones y merge, sin mi intervención. Después empezó el cuarto issue. Duró más de lo esperado, OpenRouter respondió con HTTP 429, y me encontré ante una pregunta que la plataforma no podía responder: seguir esperando, cambiar el routing, abortar la ejecución?

Trabajo con programación asistida por IA de forma privada desde hace unos dos años. En la plataforma de agentes trabajé siete semanas activas. La factura de OpenRouter ascendió a unos 340 $. Mi propio tiempo y la infraestructura no estaban incluidos.

La factura no terminó en tokens y alquiler del VPS. A ello se sumó el software que se quedó parado mientras tanto.

Siete semanas bastaron para construir mucha automatización, pero no para construir una plataforma cuya autooperación valiera la pena para mi propósito.

El momento en que la telemetría ayudó: demasiado tarde

Había restringido el routing a los tres proveedores con la latencia medida más baja. Eso había acelerado las ejecuciones anteriores. Por el routing restringido llegaron repetidamente respuestas 429 (ajustes de routing, HTTP 429 como rate limit). Con la restricción también había reducido las alternativas de escape.

Las respuestas 429 eran visibles. Lo que significaban para el worker activo apenas pude evaluarlo.

La plataforma tenía varias rutas de telemetría: un state.json en los límites de los nodos, logs de Docker en marcha, la trayectoria ReAct de un worker como archivo JSONL, pero solo tras su finalización. Spans de OpenTelemetry, pero solo al final de la ejecución. Métricas, pero solo tras un ciclo exitoso. ADR-0016 nombra la limitación explícitamente como «Deferred Visibility»: el export reconstruido no ofrecía una vista en vivo durante la ejecución. La ruta de métricas separada (ADR-0029) no cerró esta brecha.

La condición oculta de esta observabilidad era una finalización suficientemente ordenada. Precisamente en el caso de fallo no se podía confiar en ella. Para entender las transiciones de estado y los puntos de guardado, hice que un LLM analizara el código. El análisis fue bueno. Solo llegó demasiado tarde para la pregunta operativa. Una observabilidad cuyo funcionamiento primero debo reconstruir mediante un análisis de código es apenas observabilidad para la operación.

Y no faltaba solo la visibilidad. Faltaba la conexión entre las señales existentes y una acción:

Señal → evento reconocible → gravedad → notificación → reacción

Ninguna notificación reportaba un worker activo durante un tiempo inusualmente largo. Ninguna regla distinguía una breve serie de 429 de un problema persistente del proveedor. Incluso el fin de toda la ejecución por el límite de tiempo no disparó ninguna alarma. Los costos de los reviews completados eran rastreables. La facturación de judges y el reporting de KPI por review están documentados (ADR-0058). El progreso del worker activo no.

Solo más tarde quedó completamente claro qué había terminado la ejecución. Mientras tanto, había retirado la restricción de proveedores y dejado el routing a OpenRouter. Entonces actuó el presupuesto máximo de tiempo del nodo Execute. Antes lo había aumentado de 30 a 45 minutos. Tampoco bastó. Antes de que el issue estuviera terminado, el límite de tiempo se había superado y el nodo Execute se reinició. Si el worker avanzaba o se había enredado en un bucle no era posible verlo de forma transparente.

Lo que además tuvo que soportar la plataforma

La brecha de observabilidad no era una excepción. Era una línea en una lista más larga. Las soluciones concretas dependen del framework. LangChain, LangGraph, código propio. Las preguntas de operación no desaparecen por eso. Para muchas ya había construido respuestas. En el cuarto issue se mostró cuáles faltaban.

flowchart TB
    subgraph plan["Planificación, parte de la plataforma"]
        human["Autor: entrevistas y sesiones de preguntas con el planner"]
        planner["agentic-planner-core: crea varias docenas de issues"]
        human <--> planner
    end
    subgraph start["Inicio, manual por ejecución"]
        lauf["El autor inicia una ejecución con varios issues"]
    end
    subgraph own["Autooperación en VPS propio, autónomo hasta el merge"]
        orch["agentic-developer-core: orquestador, reanudación, retries, presupuesto de tiempo"]
        worker["Worker en el mismo VPS"]
        ext["Servicios externos: OpenRouter con restricción de proveedores y GitHub"]
        gates["Verificación: controles de calidad deterministas y cuatro judges de LLM"]
        merge["Merge por la plataforma"]
        orch <--> worker
        orch <--> ext
        worker -->|"Pull Request"| gates
        gates -->|"hallazgos, bucles de corrección en marcha"| worker
        gates -->|"verde"| merge
    end
    planner --> lauf
    lauf --> orch

Reanudación. Tras un reinicio, la plataforma retomaba en el último nodo guardado en lugar de empezar el issue desde el principio. Eso era útil y, lamentablemente, más grueso de lo que yo había asumido al principio. El plan interno de un worker no consistía en pasos de ejecución guardados individualmente. Si planificaba diez pasos y era interrumpido en el cuarto, el nodo Execute volvía a empezar. El directorio de trabajo todavía contenía los cambios de los primeros pasos, y el modelo podía examinarlos y construir sobre ellos. Si el nuevo intento conectaba de forma sensata dependía de cómo el modelo interpretara los cambios existentes. La plataforma reanudó el estado del repositorio, no el hilo de pensamiento del agente.

Manejo de errores. Retries por capas con fallback de modelo, detección de bucles (3 tool calls idénticos: redirigir, 5: abortar) y un límite de recursión como salvaguarda ofrecían controles sensatos. Sin embargo, resolvían los problemas equivocados: un worker puede bloquearse mucho tiempo sin repetir llamadas, y errores acumulados del proveedor no activan necesariamente el límite de recursión.

Seguridad y aislamiento. Allowlist de entorno en lugar de denylist, validación SSRF para fetches de URL, supervisión de procesos y una sandbox de ejecución propia en un VPS separado. Un servidor que yo tenía que operar, parchear y pagar.

Verificación. Controles de calidad deterministas, cuatro judges de LLM sobre el diff del PR, un feedback loop hasta el merge verde. Los detalles están con el resto de la lista en el apéndice.

La lista tiene tres puntos sin número de ADR. Existen solo como planes:

Cada una de estas soluciones habría justificado la siguiente función sensata de plataforma. Las tres quedaron planificadas y nunca se construyeron.

Menos plataforma, no una plataforma más barata

Los tres puntos juntos habrían significado trabajo de plataforma permanente: definir eventos, calibrar umbrales, operar notificaciones, probar abortos, asegurar reanudaciones. Mientras tanto esperaba el software que en realidad quería construir con la plataforma.

El incidente me mostró lo que costaría la siguiente ampliación: más trabajo de plataforma. Ya no el uso más sensato de mi tiempo.

La decisión que tomé después la describo con la mayor precisión así: no reconstruí la misma plataforma más barata. Antes: implementación, reviews, correcciones y merge sin mi intervención. Hoy: iniciar una sesión, poner la etiqueta agent-ready y hacer el merge yo mismo. Reduje el alcance autónomo y asumí de nuevo la decisión de merge. La diferencia no está en la línea de costos, sino en el tipo de responsabilidad.

Lo que quedó tampoco es un estado sin operación. Un CLI harness también orquesta. GitHub Actions y el proveedor de modelos siguen asumiendo operación. Ejecuto solo un issue por sesión y reviso tras unos 60 minutos si está terminado o necesita dirección. En unos dos de diez issues tengo que abortar la sesión y la reinicio desde el último paso de trabajo. No perfecto, pero el esfuerzo se mantiene pequeño. La comparación honesta no es «operación contra ninguna operación», sino «menos autooperación contra más autooperación».

Lo que funciona hoy

flowchart TB
    subgraph plan["Planificación, una vez por eje temático"]
        human["Autor: entrevistas y sesiones de preguntas con el planner"]
        planner["agentic-planner-core: crea varias docenas de issues"]
        human <--> planner
    end
    subgraph pick["Selección, manual por issue"]
        release["El autor pone la etiqueta agent-ready"]
    end
    subgraph run["Ejecución y verificación, autónomo hasta el pull request verde"]
        dcode["dcode CLI harness en un VPS desechable, una sesión por issue, capa de políticas"]
        ext["Servicios externos: OpenRouter con GLM 5.3 Flash, Tavily y GitHub"]
        gates["quality-gates-toolkit: controles de calidad deterministas y judges de LLM"]
        merge["Autor: merge"]
        dcode <--> ext
        dcode -->|"Pull Request"| gates
        gates -->|"hallazgos, en promedio dos bucles de corrección"| dcode
        gates -->|"verde"| merge
    end
    planner --> release
    release --> dcode

Un CLI harness (dcode) usa GLM 5.3 Flash vía OpenRouter, iniciado por sesión con --yolo, una confirmación única, después el agente ejecuta acciones aprobadas sin más preguntas. La capa de políticas restringe las herramientas previstas de red y sistema de archivos: investigación vía Tavily, obtención de datos vía fetches de solo lectura. El alcance del sistema de archivos de una ejecución es el checkout del proyecto. No ofrece aislamiento duro de comandos de shell y subprocesos. Con la operación en un VPS dedicado como infraestructura desechable no solo he reducido la autooperación, sino que también prescindo deliberadamente de una sandbox de ejecución separada. Los issues los prepara agentic-planner-core. La verificación la asume mi quality-gates-toolkit, como punto de verificación desplegable por separado, sin la autooperación de los otros componentes.

flowchart LR
    pr["Pull Request"] --> gates["Controles de calidad deterministas: Ruff, mypy, pytest, Coverage, Semgrep, pip-audit"]
    gates -->|"todo verde"| judges["Cuatro judges de LLM sobre el diff del PR"]
    judges -->|"PASS"| merge["Merge aprobado"]
    judges -->|"FAIL"| fix["Corrección y nuevo push"]
    fix --> pr

El prompt para una ejecución completa es una frase: «Trabaja el siguiente issue agent-ready». He publicado la definición completa del skill, SKILL.md, ambas referencias de fase y el script de selección como Gist.

Una ejecución completa de esta cadena está documentada públicamente: en agentic-planner-core, el issue #81 pasó por el PR #82 hasta el merge. Cuatro reviews de judges, todas PASS, cifras de tokens y costos por judge visibles en el review.

Lo que cuesta hoy la operación es un orden de magnitud, no una prueba de ahorro: en las tres semanas hasta el 13 de septiembre la factura de OpenRouter ascendió a unos 30 $ (exclusivamente por el uso del nuevo setup). Una ejecución del issue al PR listo para merge cuesta típicamente entre 0,25 y 0,75 $. La magnitud de referencia es el PR listo para merge: todo lo que un issue consume en el camino hasta allí, por ejemplo judges, en promedio dos bucles de corrección y ejecuciones fallidas, lo atribuyo al PR.

La prueba

Antes de la siguiente ejecución más larga sin supervisión, probaría dos casos de error: un worker atascado y una ejecución abortada. La prueba debe responder tres preguntas sin análisis de código retrospectivo:

Mientras falten estas respuestas, no aumento el tiempo de ejecución autónomo. La siguiente prueba sensata no es un cuarto issue exitoso, sino una interrupción que detecte a tiempo y pueda corregir de forma controlada.

Apéndice: La lista completa de operación

Los problemas de operación descritos en el texto principal, mis decisiones al respecto y las fuentes públicas de un vistazo:

Problema de operación Decisión (ADR) Fuente
Reanudación tras interrupción Reanudación en el último nodo guardado; el modelo examina los cambios existentes 0013
Clases de error Retries por capas con fallback de modelo; detección de bucles (tres llamadas idénticas: redirigir, cinco: terminar); presupuesto de recursión separado 0021, 0046, 0045
Aislamiento de subprocesos y red Allowlist de entorno, validación SSRF, supervisión de procesos, sandbox en un VPS separado 0043, 0028, 0015, 0056
Verificación de PR Controles de calidad deterministas, cuatro judges de LLM, bloque de veredicto oculto, feedback loop hasta el merge verde 0020, 0014, 0019, 0036
Estado del workspace al abortar Snapshot + presupuesto de recursión 0053
Asegurar las rutas de escritura Read-before-edit, rango de líneas, validación de rutas 0006, 0033, 0012, 0035
Respuestas vacías/truncadas Rigor de max-tokens, logging de finish reason, caps de presupuesto 0040, 0049, 0051
Timeouts robustos Aplicación dura contra retries del SDK planner 0021
Rollback duro en el último intento Híbrido de retry con rollback 0034
Búsqueda web como ruta de herramientas Controles de calidad, framework-first 0026, 0041, 0042
Defensa contra prompt injection Validación zero-trust (vía de planificación) planner 0020
Cobertura de diff Control determinista propio 0052
Hacer visibles los costos de los judges Usage accounting, reporting de KPI 0058

Repositorios referenciados