Hacia bases de código autónomas

Nos entusiasma la reacción a nuestra investigación sobre cómo escalar el código autónomo de larga duración.
Este trabajo comenzó como investigación interna para llevar al límite los modelos actuales. Como parte de la investigación, creamos una nueva infraestructura de agentes para orquestar muchos miles de agentes y observar su comportamiento. El mes pasado, nuestro sistema ya era lo bastante estable como para ejecutarse de forma continua durante una semana, realizando la gran mayoría de los commits en nuestro proyecto de investigación (un navegador web). Este navegador no estaba pensado para uso externo y esperábamos que el código tuviera imperfecciones.
Sin embargo, incluso con sus rarezas, el hecho de que miles de agentes pudieran trabajar juntos para producir trabajo que era casi por completo ejecutable sin intervención humana nos pareció un hito digno de compartir. Desde entonces, hemos continuado nuestra investigación y queríamos profundizar más en cómo se construyó esta infraestructura.
También estamos poniendo a disposición de algunos usuarios parte de esta investigación para que puedan probarla.
Antecedentes
Nuestro proyecto de investigación comenzó como un proyecto personal paralelo mío.
Un navegador me parecía un benchmark interesante. Era lo suficientemente complejo como para revelar limitaciones en los modelos de vanguardia, y había muchos subsistemas diferentes que necesitaban funcionar juntos.
Mi plan inicial era admitir el renderizado de páginas web sin soporte de JavaScript. Empecé pidiéndole a Opus 4.5 que escribiera un plan detallado para construir un motor de navegador. Repetidamente lo incitaba a “seguir” para ver hasta dónde llegaría con el plan.
Esto falló rápidamente. El modelo perdió el hilo de lo que estaba haciendo, con frecuencia se detenía para proclamar éxito a pesar de estar lejos de lograrlo, y se quedaba atascado en detalles de implementación complejos. Pero mostraba indicios de conocimiento profundo e inteligencia. Podía escribir buen código en bloques pequeños.
El problema central era que el navegador era una tarea demasiado abrumadora y necesitaba descomponerse en subtareas. Después, hice que el agente planificara un grafo de dependencias del trabajo principal que los agentes pudieran asumir en paralelo. Se lanzaban agentes manualmente para las tareas y se les daba un empujón cuando se detenían. Esto incrementó el throughput, pero los resultados no fueron mucho mejores. Los agentes no podían comunicarse entre sí ni dar feedback sobre el proyecto en su conjunto. El sistema necesitaba ser más dinámico.
Mientras tanto, GPT-5.1 (y luego GPT-5.2) empezó a mostrar mejores resultados en su capacidad de seguir instrucciones con precisión. Esto parecía encajar bien con agentes de larga duración, así que actualizamos nuestro harness para usar modelos de OpenAI basados en estos experimentos.
En este punto, el harness podía construir una versión simple del navegador web sin JavaScript, pero construir un motor de navegador completo con un solo agente sería prohibitivamente lento.
Esto inició nuestra siguiente ronda de investigación. ¿Podíamos gastar 10 veces más en cómputo para obtener 10 veces más throughput significativo?
De un solo agente a multiagente
Empezamos un nuevo repositorio con un entorno sencillo basado en Rust.
En lugar de lidiar con la complejidad de los sistemas distribuidos, ejecutamos el entorno en una sola VM (máquina virtual) Linux grande con muchos recursos. Para controlar el entorno, nos conectábamos por SSH a la VM y usábamos una interfaz de terminal simple.
Invertimos más tiempo al principio en lograr una buena observabilidad del sistema. Registramos todos los mensajes de los agentes, las acciones del sistema y la salida de los comandos, con marcas de tiempo para poder analizar y reproducir sesiones. Esto no solo fue útil para revisarlas manualmente, sino también para canalizarlas de vuelta a Cursor, cribar grandes volúmenes de datos y encontrar patrones rápidamente.
Autocoordinación
Nuestra primera idea de multiagente fue la más simple: hacer que agentes con roles iguales usaran un archivo de estado compartido para ver en qué estaban trabajando los demás, decidir en qué trabajar y actualizar el archivo.


Daríamos la menor cantidad posible de instrucciones sobre qué hacer y, en su lugar, dejaríamos que los agentes averiguaran cómo coordinarse por sí mismos. Esto falló rápidamente.
El archivo de coordinación rápidamente creó más problemas. Los agentes mantenían bloqueos durante demasiado tiempo, olvidaban liberarlos, intentaban bloquear o desbloquear cuando no estaba permitido y, en general, no entendían la importancia de mantener un bloqueo sobre el archivo de coordinación. El bloqueo es fácil de hacer mal y difícil de hacer bien, y más indicaciones no ayudaron.
El bloqueo también causó demasiada contención. 20 agentes se ralentizaban hasta el rendimiento de 1 a 3, con la mayor parte del tiempo dedicado a esperar a que se liberaran bloqueos. Probamos dar a los agentes una herramienta para esperar explícitamente al trabajo de otro agente, pero casi no la usaron. También probamos un enfoque de control de concurrencia optimista sin bloqueos, que redujo la sobrecarga pero no eliminó la confusión.
La falta de estructura entre los agentes significaba que ningún agente asumía tareas grandes y complejas. Evitaban la contención y el conflicto, optando por cambios más pequeños y seguros en lugar de asumir la responsabilidad del proyecto en su conjunto.
Añadiendo estructura y roles
A continuación, separamos los roles para dar a los agentes control y responsabilidad:


Un planificador primero definiría con precisión el enfoque y los entregables para avanzar en el cumplimiento de las instrucciones del usuario. Esto se entregaría a un ejecutor, que se convertía en el único agente principal responsable de asegurar que el plan se cumpliera por completo. El ejecutor podía generar tareas para trabajadores, lo que proporcionaba escalado lineal y mayor rendimiento.
Para mantener el impulso y la responsabilidad, un juez independiente entraba en acción después de que el ejecutor terminara, para determinar si había completado la tarea y si debía ejecutarse otra iteración. Esto resolvía muchos problemas de coordinación. Tener un único rol dedicado a asumir y supervisar la ejecución permitía a los trabajadores concentrarse exclusivamente en su tarea mientras el sistema en su conjunto seguía cumpliendo.
Observando y optimizando incrementalmente
Llegar a este diseño requirió una observación detallada del sistema.
Si había un problema importante, tendía a ocurrir repetidamente y en muchos agentes y llamadas a herramientas. Por ejemplo, notamos que había demasiada contención porque muchos agentes estaban ejecutando git restore al mismo tiempo. Usamos Cursor para analizar logs y compararlos con nuestros prompts para entender por qué el comportamiento no coincidía con las expectativas.
En última instancia, descubrimos que este sistema estaba limitado por el worker más lento. Era demasiado rígido.
Hacer toda la planificación por adelantado también dificultaba que el sistema se reajustara dinámicamente a medida que se descubrían nuevos problemas. Algunos agentes terminaban yendo en direcciones contraproducentes, incapaces de autocorregirse hasta la siguiente iteración del ciclo.
Ejecutor continuo
La siguiente versión eliminó el planificador independiente.
El ejecutor ahora también podía planificar cómo alcanzar el objetivo, además de generar tareas. Como era el único agente, no necesitaba escribir un plan en ninguna parte, ceñirse a un plan estático e inmutable ni esperar rígidamente a todos los workers.
Garantizar la frescura
Para asegurarnos de que los agentes en todos los roles no se desviaran con el tiempo, introdujimos mecanismos para mantenerlos actualizados:
- Un
scratchpad.mddebería reescribirse con frecuencia en lugar de simplemente añadir contenido. - Los agentes individuales deberían resumir automáticamente cuando alcanzaran los límites de contexto.
- Agregamos autorreflexión y recordatorios de alineación a los prompts del sistema.
- Se animó a los agentes a cambiar de enfoque y cuestionar sus propias suposiciones en cualquier momento.
El sistema ahora era muy dinámico y flexible: podía explorar el código de forma proactiva, reconsiderar decisiones, gestionar workers, intercalar tareas y reflejar continuamente la información más reciente. Descubrimos que los agentes eran razonablemente buenos siguiendo instrucciones hasta completarlas, por lo que se eliminó el juez para mantener el sistema simple.


Comportamientos patológicos
A pesar de estas mejoras, el ejecutor continuo empezó a exhibir comportamientos patológicos. Se quedaba dormido aleatoriamente, dejaba de ejecutar agentes, se ponía a hacer el trabajo él mismo, se negaba a planificar y a generar más que unas pocas tareas muy específicas, no fusionaba correctamente los cambios de los workers y afirmaba haber terminado demasiado pronto.
Descubrimos que se le estaban asignando demasiados roles y objetivos simultáneamente, entre ellos: planificar, explorar, investigar, generar tareas, supervisar a los workers, revisar código, realizar ediciones, fusionar resultados y determinar si el bucle había terminado. En retrospectiva, tiene sentido que estuviera abrumado.
El diseño final del sistema
El diseño final incorpora todo lo que hemos aprendido:
- Un planificador raíz tiene a su cargo todo el alcance de las instrucciones del usuario. Es responsable de entender el estado actual y de generar tareas específicas y dirigidas que hagan avanzar hacia el objetivo. No escribe código por sí mismo. No sabe si sus tareas están siendo tomadas ni por quién.
- Cuando un planificador considera que su alcance puede subdividirse, genera subplanificadores que asumen por completo la fracción específica delegada, tomando plena responsabilidad de forma similar, pero solo sobre esa parte. Esto es recursivo.
- Los workers asumen las tareas y son los únicos responsables de llevarlas hasta su finalización. No tienen visibilidad del sistema más amplio. No se comunican con otros planificadores ni con otros workers. Trabajan en su propia copia del repositorio y, cuando terminan, redactan una única entrega que el sistema envía al planificador que solicitó la tarea.
Curiosamente, esto sí representa cómo operan algunos equipos de software hoy en día.


Los subplanificadores aumentan el rendimiento al desplegar rápidamente más workers, a la vez que garantizan que todo el sistema siga teniendo un único agente como responsable último. Esto también ayudó con proyectos y tareas grandes donde, de otro modo, un único planificador se vería abrumado y desarrollaría una visión de túnel.
La entrega contiene no solo lo que se hizo, sino notas importantes, preocupaciones, desviaciones, hallazgos, ideas y comentarios. El planificador recibe esto como un mensaje de seguimiento. Esto mantiene el sistema en movimiento continuo: incluso si un planificador ha “terminado”, sigue recibiendo actualizaciones, descarga el estado más reciente del repositorio y puede continuar planificando y tomando decisiones posteriores.
Todos los agentes tienen este mecanismo, lo que permite que el sistema se mantenga increíblemente dinámico y autoconvergente, propagando la información hacia arriba en la cadena hasta los responsables con visiones cada vez más globales, sin la sobrecarga de sincronización global ni de comunicación cruzada.
Eliminando el integrador
Originalmente añadimos un integrador para tener un control de calidad centralizado y con visión global, y para eliminar la competencia generada por demasiados workers intentando hacer push, rebase, resolver conflictos y hacer merge simultáneamente.
Rápidamente se convirtió en un cuello de botella evidente. Había cientos de workers y una sola puerta (es decir, una especie de "burocracia") por la que todo el trabajo debía pasar. Probamos cambios en el prompt, pero finalmente decidimos que era innecesario y que podía eliminarse para simplificar el sistema.
Rendimiento y compromisos
El sistema alcanzó un máximo de ~1,000 commits por hora a lo largo de 10M llamadas a herramientas durante una semana. Una vez que el sistema estuvo en funcionamiento, no requirió ninguna intervención por nuestra parte.
Se asumieron compromisos deliberados para lograr este nivel de rendimiento.
Corrección de commits
Cuando exigíamos un 100% de corrección antes de cada commit, eso provocaba una fuerte serialización y reducía drásticamente el rendimiento efectivo. Incluso un solo error pequeño, como un cambio en una API o un error tipográfico, hacía que todo el sistema se detuviera. Los workers se salían de su ámbito y empezaban a corregir cosas irrelevantes. Muchos agentes se amontonaban y se estorbaban entre sí intentando arreglar el mismo problema.
Este comportamiento no era útil ni necesario. Permitir cierto margen hace que los agentes puedan confiar en que otros problemas serán corregidos pronto por otros agentes, lo cual es cierto dado que el sistema tiene propiedad y delegación efectivas sobre toda la base de código. Surgen errores y luego se corrigen rápidamente. La tasa de errores se mantiene pequeña y constante; quizá rara vez esté completamente limpia, pero se mantiene estable y manejable, sin explotar ni deteriorarse.
Esto puede indicar que el sistema eficiente ideal acepta cierta tasa de error, pero se necesita una rama final "verde" donde un agente tome instantáneas con regularidad y haga una pasada rápida de correcciones antes del lanzamiento.
Sobrecarga de sincronización
A veces varios agentes tocan el mismo archivo o refactorizan el mismo código. En lugar de intentar eliminar por completo estos casos o sobrediseñar una solución, aceptamos algunos momentos de turbulencia y dejamos que el sistema converja y se estabilice de forma natural en un corto período de tiempo.
Esto consume algunos tokens adicionales y genera conflictos locales, pero mantiene el sistema en general más simple: es más fácil alinear los modelos sin abrumarlos, más fácil de gestionar y observar, con menos fricción y mejor productividad global. También evita enfoques excesivamente complejos.
Aprendizajes de infraestructura
Cada ejecución multiagente se realizaba en su propia máquina grande con abundantes recursos de sistema, para evitar una complejidad prematura en torno a sistemas distribuidos. Esto resultó adecuado, ya que la mayoría de las ejecuciones alcanzaban picos de varios cientos de agentes, lo que normalmente saturaba, pero no sobreaprovisionaba, estas máquinas. Esta arquitectura facilitaba la observación de métricas del sistema y el compartir y copiar estado cuando fuera necesario.
Después de limitar el uso de RAM de los agentes, el disco se convirtió en el cuello de botella. Especialmente con un proyecto monolítico, cientos de agentes compilando simultáneamente daban lugar a lecturas y escrituras de artefactos de compilación de varios GB/s. Esto tuvo un impacto significativo en la capacidad de procesamiento total del sistema, lo cual fue una lección interesante: la estructura del proyecto, las decisiones de arquitectura y la experiencia del desarrollador pueden afectar el throughput de tokens y commits, simplemente porque trabajar con la base de código (por ejemplo, la compilación) pasa a dominar el tiempo, en lugar de, idealmente, pensar y programar.
También había limitaciones e ineficiencias en el entorno general de desarrollo: cosas que tienen sentido o no son significativas para el espacio de trabajo de un solo usuario pueden volverse muy visibles cuando cientos de agentes hacen lo mismo en una sola máquina. Una forma trivial de resolver esto es dar a cada agente su propia máquina. Pero hay oportunidades interesantes y sencillas de grandes ganancias de eficiencia simplemente replanteando y rediseñando algunas de estas primitivas y herramientas.
Por ejemplo, muchas herramientas como Git y Cargo usan bloqueos compartidos, en gran medida como un mecanismo simple de control de concurrencia. ¿Podría la adopción de mecanismos bien establecidos de sistemas concurrentes como las bases de datos lograr que funcionen igual de bien en sistemas multiagente? Todos los agentes tienen su propia copia del repositorio, pero la mayoría de los archivos y artefactos son idénticos; ¿podría la incorporación de funciones sencillas de copy-on-write y deduplicación, presentes en sistemas de almacenamiento en producción más sofisticados, aportar beneficios similares y fáciles a un sistema típicamente de «un solo usuario» sin tener que construir infraestructura separada?
Especificar la intención a los agentes
Las instrucciones dadas a este sistema multiagente fueron muy importantes.
Al principio, no las convertimos en nuestro objetivo principal, sino que apuntamos a un harness estable y eficaz. Pero la importancia de las instrucciones se hizo evidente rápidamente. Básicamente estábamos interactuando con un agente de programación típico, solo que con órdenes de magnitud más tiempo y capacidad de cómputo. Esto amplifica todo, incluidas las instrucciones subóptimas y poco claras.
Tiene sentido dedicar más tiempo a las instrucciones iniciales. En última instancia, los agentes siguen siendo agentes: están entrenados para seguir tus instrucciones estrictamente, recorrer esos caminos, no cambiarlos ni ignorarlos, incluso si son malas.
Queríamos ver resultados exitosos en nuestros proyectos de investigación, así que fuimos modificando nuestras instrucciones iniciales a medida que el proyecto y el harness evolucionaban. Estábamos aprendiendo a construir un navegador al mismo tiempo que aprendíamos a operar este nuevo sistema multiagente, y podíamos ver especificaciones deficientes o insuficientemente definidas reflejadas en la calidad de los resultados, lo cual no se debía al propio harness. El harness simplemente seguía nuestras instrucciones al pie de la letra.
Algunos ejemplos del proyecto del navegador:
- Al principio, las instrucciones se centraban en implementar especificaciones y eliminar bugs. Instrucciones como "spec implementation" eran lo suficientemente vagas como para que los agentes profundizaran en funcionalidades oscuras y poco usadas en lugar de priorizar de forma inteligente.
- Asumimos implícitamente que había expectativas de rendimiento dentro de márgenes aceptables para el usuario. Pero hicieron falta instrucciones explícitas y timeouts estrictos para obligar a los agentes a equilibrar el rendimiento junto con otros objetivos.
- En partes complejas del sistema, los agentes pueden escribir código que tenga fugas de memoria o provoque interbloqueos. Los humanos se darían cuenta de esto, pero no siempre era evidente para los agentes. Fue necesario disponer de herramientas explícitas de gestión de recursos basadas en procesos para permitir que el sistema se recuperara de forma elegante y fuera más defensivo.
Nuestra primera versión del navegador simple sin JavaScript convergió en una arquitectura inadecuada para evolucionar hacia un navegador completo. Esto fue un fallo de la especificación inicial.
De forma similar, aunque se les dijo a los agentes que el proyecto era un navegador desde cero, igualmente incorporaron algunas dependencias que podrían haber implementado ellos mismos o haber usado como andamiaje temporal mientras se realizaba la implementación adecuada. Esto fue una omisión en las instrucciones. Una ejecución posterior expuso explícitamente la filosofía de dependencias y qué bibliotecas no debían usarse, lo que corrigió esto.
Esa ejecución posterior también hizo una reestructuración importante en muchos crates autocontenidos, alejándose de un monolito. El repositorio estaba en un estado muy roto, pero el sistema multiagente convergió hacia código funcional en unos pocos días. Esto mostró que el sistema tiene una fuerte capacidad para trabajar de forma colaborativa e inteligente, manteniéndose operativo incluso a través de estados totalmente rotos en lugar de degradarse más o quedarse atascado. Esa ejecución también pasó mucho menos tiempo esperando la compilación, funcionando con un rendimiento varias veces superior al anterior.
La arquitectura y las instrucciones importan. Los agentes tienen una enorme habilidad de ingeniería, pero seguirán las instrucciones hasta el final, sean buenas o malas. Encontrar el equilibrio entre métricas demasiado restrictivas y libertad no estructurada fue complicado, al igual que saber qué era obvio y qué requería mención explícita.
Todo esto indica la importancia de extraer, especificar y comprender la intención, que se vuelve aún más significativa a esta escala. La capacidad de dirigir el sistema y la observabilidad serán áreas de investigación interesantes para seguir explorando.
Optimización de prompts
El prompting fue una parte importante del proceso de evolución.
Descubrimos que era mejor no dar instrucciones sobre cosas que el modelo ya sabe hacer, solo sobre aquello que no sabe (p. ej., colaboración entre múltiples agentes) o que es específico del dominio relevante (p. ej., cómo ejecutar pruebas, tu pipeline de despliegue). Trata al modelo como a una nueva incorporación brillante que sabe de ingeniería, pero no de tu base de código ni de tus procesos específicos.
Las restricciones son más efectivas que las instrucciones. «Sin TODOs, sin implementaciones parciales» funciona mejor que «recuerda terminar las implementaciones». Los modelos generalmente hacen cosas buenas por defecto. Las restricciones definen sus límites.
Evita la mentalidad de checklist para tareas de nivel más alto o más profundas. Da instrucciones detalladas sobre tu intención, pero recuerda que dar cosas específicas que hacer tiende a hacer que el modelo se centre en lograrlas, en lugar de abarcar el alcance más amplio. Además, implícitamente dejas en segundo plano lo que no está en la lista. Normalmente, es mejor dejar que el modelo use su criterio y autonomía.
Comprobamos que era útil dar números y rangos concretos al hablar de cantidad o alcance. Instrucciones como «genera muchas tareas» tienden a producir una cantidad pequeña: un valor por defecto conservador, jugando a lo seguro, pero técnicamente siguiendo las instrucciones. «Genera entre 20 y 100 tareas» transmite que la intención es un alcance mayor, que debe ser ambicioso, y observamos un comportamiento general muy distinto y más amplio.
Aprendizajes de diseño de sistemas
Establecimos algunos principios a partir de nuestra investigación:
- El sistema debe ser antifrágil. A medida que escalamos el número de agentes ejecutándose simultáneamente, también aumenta la probabilidad de fallos. Nuestro sistema debe tolerar fallos de agentes individuales, permitiendo que otros se recuperen o prueben enfoques alternativos.
- Empírico por encima de basado en suposiciones. Queríamos usar datos y observación para hacer ajustes, en lugar de llegar con suposiciones sobre cómo debería funcionar basadas en organizaciones humanas o diseños de sistemas existentes.
- Diseñar explícitamente para el rendimiento (throughput). Esto significó sacrificar otros aspectos de la programación, como aceptar una tasa pequeña pero estable de errores que requiere una pasada final de reconciliación, en lugar de código que funcione perfectamente el 100% del tiempo y que ralentizaría drásticamente el sistema.
Estos sistemas tienden a ser elegantemente simples cuando se hacen bien, pero no estaba claro qué enfoque simple funcionaría hasta que exploramos muchos enfoques diferentes. El diseño del sistema actual ha estado funcionando con una sobrecarga mínima y ofrece un escalado lineal del rendimiento de tokens de una manera útil. No han sido necesarias iteraciones importantes adicionales sobre el arnés.
Conclusión
Aunque el gusto, el criterio y la dirección provinieron de humanos, la IA fue un potente multiplicador de fuerza para iterar y explorar rápidamente esta investigación.
Esto se parece en cierta medida al “círculo virtuoso” de la IA, donde se utiliza IA para desarrollar IA y, a medida que los modelos, los agentes y las herramientas de soporte mejoran, el sistema se retroalimenta y se acelera cada vez más. Damos forma a las herramientas que nos dan forma.
Hay una semejanza poética entre esta investigación y la forma en que algunos equipos de desarrollo de software trabajan hoy en día. Estos modelos no fueron entrenados explícitamente de esta manera, lo que sugiere que se trata de un comportamiento emergente y posiblemente de la forma correcta de estructurar proyectos de software, después de todo.
Seguiremos investigando agentes diseñados para ejecutarse durante períodos extremadamente largos, y nuestros hallazgos guiarán el futuro de nuestro producto.