Hacia bases de código autónomas

Nos entusiasma la reacción a nuestra investigación sobre el escalado de la programación autónoma de larga duración.
Este trabajo comenzó como una investigación interna para llevar al límite los modelos actuales. Como parte de esa investigación, creamos una nueva infraestructura de agentes para orquestar 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 y realizar 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 peculiaridades, el hecho de que miles de agentes pudieran trabajar juntos para producir resultados casi totalmente ejecutables sin intervención humana nos pareció un hito que valía la pena compartir. Desde entonces, hemos seguido avanzando en nuestra investigación y queríamos profundizar más en cómo se creó esta infraestructura.
También estamos poniendo parte de esta investigación a disposición para que algunos usuarios la prueben.
Antecedentes
Nuestro proyecto de investigación comenzó como un proyecto personal paralelo.
Un navegador parecía una referencia interesante. Era lo bastante complejo como para revelar las limitaciones de los modelos de vanguardia, y había muchos subsistemas distintos que debían funcionar juntos.
Mi plan inicial era admitir el renderizado de páginas web sin compatibilidad con JavaScript. Empecé pidiéndole a Opus 4.5 que escribiera un plan detallado para crear un motor de navegador. Una y otra vez, le decía "sigue" para ver hasta dónde podía llegar con el plan.
Esto fracasó rápidamente. El modelo perdía el hilo de lo que estaba haciendo, a menudo se detenía para proclamar éxito pese a estar muy lejos de lograrlo, y se atascaba en detalles complejos de implementación. Pero mostraba señales de conocimiento profundo e inteligencia. Podía escribir buen código en partes pequeñas.
El problema principal era que el navegador era una tarea demasiado abrumadora y había que dividirla en subtareas. Después, hice que el agente planificara un grafo de dependencias del trabajo principal que los agentes pudieran abordar en paralelo. Los agentes se lanzaban manualmente para las tareas y se los empujaba cuando se detenían. Esto aumentó el rendimiento, pero los resultados no fueron mucho mejores. Los agentes no podían comunicarse entre sí ni aportar retroalimentación sobre el proyecto en su conjunto. El sistema tenía que ser más dinámico.
Mientras tanto, GPT-5.1 (y más tarde GPT-5.2) empezó a mostrar mejores resultados por su capacidad para 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 basándonos en estos experimentos.
En este punto, el harness podía crear una versión simple del navegador web sin JavaScript, pero crear un motor de navegador completo con un solo agente sería prohibitivamente lento.
Esto dio comienzo a nuestra siguiente ronda de investigación. ¿Podríamos gastar 10 veces más en cómputo para obtener un rendimiento significativo 10 veces mayor?
De un solo agente a multiagente
Iniciamos un nuevo repositorio con un harness simple basado en Rust.
En lugar de lidiar con la complejidad de los sistemas distribuidos, ejecutamos el harness en una sola VM (máquina virtual) Linux grande con muchos recursos. Para controlar el harness, nos conectábamos por SSH a la VM y usábamos una interfaz de terminal sencilla.
Desde el principio, dedicamos más tiempo a contar con una observabilidad adecuada del sistema. Registramos todos los mensajes de los agentes, las acciones del sistema y los resultados de los comandos, con marcas de tiempo para poder analizar y reproducir sesiones. Esto no solo nos resultó útil para revisarlo manualmente, sino también para volver a pasar esos datos a Cursor, examinar grandes volúmenes de información y encontrar patrones rápidamente.
Autocoordinación
Nuestra primera idea multiagente era la más sencilla: hacer que agentes con roles equivalentes usaran un archivo de estado compartido para ver en qué estaban trabajando los demás, decidir en qué trabajar y actualizar el archivo.


Queríamos ser lo menos prescriptivos posible sobre qué hacer y, en cambio, dejar que los agentes resolvieran cómo autocoordinarse. Esto fracasó rápidamente.
El archivo de coordinación pronto generó más problemas. Los agentes mantenían bloqueos durante demasiado tiempo, olvidaban liberarlos, intentaban bloquear o desbloquear cuando no correspondía y, en general, no entendían la importancia de mantener un bloqueo sobre el archivo de coordinación. Es fácil equivocarse con los bloqueos y acertar solo de forma muy limitada, y añadir más instrucciones al prompt no ayudó.
Los bloqueos también causaban demasiada contención. 20 agentes terminaban con el rendimiento de 1-3, y la mayor parte del tiempo se iba en esperar bloqueos. Intentamos darles a los agentes una herramienta para esperar explícitamente el trabajo de otro agente, pero rara vez la usaban. 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 agentes hacía que ningún agente asumiera tareas grandes y complejas. Evitaban la contención y el conflicto, y optaban por cambios más pequeños y seguros en lugar de asumir la responsabilidad del proyecto en su conjunto.
Añadir estructura y roles
A continuación, separamos los roles para dar a los agentes propiedad y responsabilidad:


Un planificador primero definía el enfoque exacto y los entregables para avanzar según las instrucciones del usuario. Esto se pasaba a un ejecutor, que se convertía en el único agente principal responsable de garantizar que el plan se cumpliera por completo. El ejecutor podía generar tareas para workers, lo que permitía un escalado lineal y mayor rendimiento.
Para mantener el avance y la responsabilidad, un juez independiente intervenía después de que el ejecutor terminara para determinar si había completado el trabajo y si debía ejecutarse otra iteración. Esto resolvió muchas incidencias de coordinación. Tener un único rol dedicado a asumir y supervisar la ejecución permitió que los workers se centraran específicamente en su tarea, mientras el sistema en su conjunto seguía cumpliendo.
Observación y mejora incremental
Llegar a este diseño requirió una observación minuciosa del sistema.
Si había un problema importante, tendía a repetirse y a aparecer en muchos agentes y llamadas a herramienta. Por ejemplo, notamos que había demasiada contención porque muchos agentes estaban ejecutando git restore a la vez. Usamos Cursor para analizar logs y compararlos con nuestros prompts para entender por qué el comportamiento no coincidía con lo esperado.
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 nuevas incidencias. Algunos agentes acababan avanzando en direcciones contraproducentes, sin poder autocorregirse hasta la siguiente iteración del bucle.
Ejecutor continuo
La siguiente versión eliminó el planificador independiente.
Ahora, el ejecutor también podía planificar cómo lograr el objetivo, además de generar tareas. Como era el único agente, no necesitaba escribir un plan en ningún sitio, ceñirse a un único plan estático e inmutable ni esperar rígidamente a todos los workers.
Garantizar la actualización
Para garantizar que los agentes de todos los roles no se desviaran con el paso del tiempo, introdujimos mecanismos de actualización:
- El archivo
scratchpad.mddebía reescribirse con frecuencia, en lugar de seguir agregándole contenido. - Los agentes individuales debían resumir automáticamente al alcanzar 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 rumbo y cuestionar supuestos 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 bastante buenos para seguir instrucciones hasta completarlas, así que se eliminó el juez para mantener el sistema simple.


Comportamientos patológicos
A pesar de estas mejoras, el ejecutor continuo empezó a mostrar comportamientos patológicos. Se quedaba inactivo aleatoriamente, dejaba de ejecutar agentes, hacía el trabajo por sí mismo, se negaba a planificar y generar más que unas pocas tareas muy acotadas, no fusionaba correctamente los cambios de los workers y afirmaba haber terminado antes de tiempo.
Descubrimos que se le estaban asignando demasiados roles y objetivos al mismo tiempo, entre ellos: planificar, explorar, investigar, generar tareas, supervisar a los workers, revisar código, realizar ediciones, fusionar resultados y determinar si el bucle estaba hecho. Visto en retrospectiva, es lógico que estuviera desbordado.
El diseño final del sistema
El diseño final incorpora todo lo que hemos aprendido:
- Un planificador raíz abarca todo el alcance de las instrucciones del usuario. Es responsable de entender el estado actual y de asignar tareas específicas y bien enfocadas que hagan avanzar hacia el objetivo. No programa por sí mismo. No sabe si sus tareas están siendo recogidas ni por quién.
- Cuando un planificador considera que su alcance puede subdividirse, genera subplanificadores que asumen por completo la parte delegada y acotada, con plena responsabilidad de forma similar, pero solo sobre esa parte. Esto es recursivo.
- Los workers recogen tareas y son los únicos responsables de llevarlas hasta completarlas. No conocen el sistema en su conjunto. No se comunican con ningún otro planificador ni worker. Trabajan sobre su propia copia del repo y, cuando terminan, redactan una única entrega que el sistema envía al planificador que solicitó la tarea.
Curiosamente, esto sí refleja cómo operan hoy algunos equipos de software.


Los subplanificadores aumentan el rendimiento al distribuir rápidamente trabajo entre workers, al tiempo que garantizan que todo el sistema siga estando completamente bajo la responsabilidad de un agente. Esto también ayudó con proyectos y tareas grandes, en los que, de otro modo, un solo planificador se vería sobrepasado y desarrollaría visión de túnel.
La entrega contiene no solo lo que se hizo, sino también notas importantes, inquietudes, desviaciones, hallazgos, reflexiones y comentarios. El planificador lo recibe como un mensaje de seguimiento. Esto mantiene al sistema en movimiento continuo: incluso si un planificador está "hecho", sigue recibiendo actualizaciones, incorpora la última versión del repo y puede seguir planificando y tomando decisiones posteriores.
Todos los agentes cuentan con este mecanismo, lo que permite que el sistema siga siendo increíblemente dinámico y converja por sí solo, propagando información hacia arriba en la cadena hasta llegar a responsables con visiones cada vez más globales, sin la sobrecarga de una sincronización global ni interferencias entre componentes.
Eliminar el integrador
Originalmente agregamos un integrador para tener un control de calidad central con visibilidad global y para evitar la contención causada por demasiados workers intentando hacer push, rebase, resolver conflictos y fusionar simultáneamente.
Rápidamente se convirtió en un cuello de botella evidente. Había cientos de workers y un único punto de control (es decir, «burocracia») por el que debía pasar todo el trabajo. Probamos cambios en los prompts, pero al final decidimos que era innecesario y que podía eliminarse para simplificar el sistema.
Rendimiento y concesiones
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 puesto en marcha, no requirió ninguna intervención por nuestra parte.
Hubo concesiones deliberadas para lograr este rendimiento.
Corrección de los commits
Cuando exigíamos una corrección del 100 % antes de cada commit, eso provocaba una gran serialización y reducía mucho el rendimiento real. Incluso un solo error pequeño, como un cambio en la API o una errata, hacía que todo el sistema se paralizara. Los workers se salían de su ámbito y empezaban a solucionar cosas irrelevantes. Muchos agentes se amontonaban y se estorbaban entre sí al intentar solucionar la misma incidencia.
Este comportamiento no era útil ni necesario. Permitir cierto margen significa que los agentes pueden confiar en que otros agentes solucionarán pronto otras incidencias, lo cual es cierto, ya que el sistema tiene una propiedad efectiva y capacidad de delegación sobre toda la base de código. Los errores aparecen y luego se solucionan rápidamente. La tasa de errores se mantiene baja y constante; quizá rara vez llegue a estar completamente limpia, pero sigue siendo estable y gestionable, sin dispararse ni degradarse.
Esto puede indicar que el sistema eficiente ideal acepta cierta tasa de error, pero necesita una rama final "verde" en la que un agente tome instantáneas con regularidad y haga una pasada rápida de ajustes antes del lanzamiento.
Sobrecarga de sincronización
A veces varios agentes modifican el mismo archivo o refactorizan el mismo código. En lugar de intentar eliminar por completo este tipo de situaciones o sobrediseñar una solución, aceptamos ciertos momentos de turbulencia y dejamos que el sistema converja de forma natural y se estabilice en poco tiempo.
Esto consume algunos tokens adicionales y genera contención local, pero mantiene el sistema más simple en general: es más fácil alinear los modelos sin abrumarlos, más fácil de gestionar y observar, con menos fricción y una mejor productividad global. También evita enfoques excesivamente complejos.
Aprendizajes sobre infraestructura
Cada ejecución multiagente corría en su propia máquina grande con abundantes recursos del sistema, para evitar una complejidad prematura en torno a los sistemas distribuidos. Esto funcionó bien, ya que la mayoría de las ejecuciones alcanzaban picos de varios cientos de agentes, lo que por lo general saturaba estas máquinas sin sobrecargarlas. Esta arquitectura facilitaba observar las métricas del sistema, así como compartir y copiar el estado cuando fuera necesario.
Después de limitar el uso de RAM de los agentes, el disco pasó a ser el cuello de botella. Especialmente en un proyecto monolítico, cientos de agentes compilando de forma simultánea daban lugar a muchas lecturas y escrituras de artefactos de compilación de varios GB/s. Esto tuvo un impacto significativo en el rendimiento general del arnés, y dejó una lección interesante: la estructura del proyecto, las decisiones arquitectónicas y la experiencia del desarrollador pueden afectar el rendimiento de tokens y commits, simplemente porque trabajar con la base de código (p. ej., la compilación) pasa a ocupar la mayor parte del tiempo, en lugar de dedicarse idealmente al razonamiento y la programación.
También había limitaciones e ineficiencias en el entorno de desarrollo general: cosas que tienen sentido o no son significativas en un workspace de un solo usuario pueden volverse evidentes cuando cientos de agentes hacen lo mismo en una sola máquina. Una forma trivial de resolverlo es proporcionar a cada agente su propia máquina. Pero también hay oportunidades claras e interesantes para lograr grandes mejoras 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 sencillo de control de concurrencia. ¿Podría la incorporación de mecanismos consolidados de sistemas concurrentes, como las bases de datos, hacer que esto funcionara igual de bien en sistemas multiagente? Todos los agentes tienen su propia copia del repo, pero la mayoría de los archivos y artefactos son idénticos; ¿podrían funcionalidades sencillas de copy-on-write y deduplicación, presentes en sistemas de almacenamiento de producción más sofisticados, aportar ventajas similares a un sistema típicamente "de un solo usuario" sin necesidad de crear una infraestructura aparte?
Especificar la intención para los agentes
Las instrucciones dadas a este sistema multiagente fueron muy importantes.
Al principio, no las convertimos en nuestro objetivo principal, sino que nos centramos en una infraestructura estable y eficaz. Pero la importancia de las instrucciones se hizo evidente rápidamente. En esencia, 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. Eso 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, avanzar por esos caminos y no cambiarlos ni anularlos, incluso si son malos.
Queríamos tener éxito en nuestros proyectos de investigación, así que fuimos ajustando nuestras instrucciones iniciales a medida que evolucionaban el proyecto y la infraestructura. Estábamos aprendiendo a crear un navegador a la vez que aprendíamos a operar este nuevo sistema multiagente, y podíamos ver cómo las especificaciones deficientes o poco detalladas se reflejaban en la calidad de los resultados, algo que no se debía a la infraestructura en sí. La infraestructura 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 corregir errores. Instrucciones como "implementación de especificaciones" eran lo bastante vagas como para que los agentes profundizaran en funcionalidades poco comunes y rara vez usadas, en lugar de priorizar con criterio.
- Dábamos por sentado que había expectativas de rendimiento dentro de límites razonables para el usuario. Pero hicieron falta instrucciones explícitas y límites de tiempo forzados para obligar a los agentes a equilibrar el rendimiento con otros objetivos.
- En partes complejas del sistema, los agentes pueden escribir código con fugas de memoria o que cause interbloqueos. Los humanos lo detectarían, pero no siempre era evidente para los agentes. Hicieron falta herramientas explícitas de gestión de recursos basadas en procesos para permitir que el sistema se recuperara de forma controlada y fuera más defensivo.
Nuestra primera versión del navegador simple sin JavaScript convergió en una arquitectura que no era apta para evolucionar hasta convertirse en un navegador completo. Esto fue un fallo de la especificación inicial.
Del mismo modo, aunque se indicó a los agentes que el proyecto era un navegador desde cero, aun así añadieron algunas dependencias que podrían haber implementado ellos mismos, o usado como andamiaje temporal mientras avanzaba la implementación adecuada. Esto fue una omisión en las instrucciones. En una ejecución posterior, se estableció explícitamente la filosofía respecto de las dependencias y qué bibliotecas no debían usarse, lo que corrigió este problema.
Esa ejecución posterior también llevó a cabo una reestructuración importante en muchos crates autocontenidos, alejándose de un monolito. El repo estaba en un estado muy deteriorado, pero el sistema multiagente convergió hacia código funcional en unos pocos días. Esto mostró que el sistema tiene una gran capacidad para trabajar de forma colaborativa e inteligente, incluso en estados completamente rotos, en lugar de degradarse aún más o quedarse atascado. Esa ejecución también pasó mucho menos tiempo esperando compilaciones y funcionó con un rendimiento varias veces superior al anterior.
La arquitectura y las instrucciones importan. Los agentes tienen una enorme capacidad de ingeniería, pero seguirán las instrucciones hasta el final, sean buenas o malas. Encontrar el equilibrio entre métricas demasiado estrechas y libertad sin estructura fue complicado, al igual que distinguir entre lo que era obvio y lo que necesitaba mencionarse explícitamente.
Todo esto indica la importancia de extraer, especificar y entender la intención, algo que cobra aún más relevancia a esta escala. La controlabilidad 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 para cosas que el modelo ya sabe hacer, sino solo para las que no sabe (p. ej., la colaboración multiagente) o las que son específicas del dominio en cuestión (p. ej., cómo ejecutar pruebas o tu pipeline de despliegue). Trata al modelo como a una persona brillante recién incorporada, que sabe de ingeniería pero no conoce tu codebase ni tus procesos específicos.
Las restricciones son más eficaces que las instrucciones. "No TODOs, no partial implementations" funciona mejor que "recuerda terminar las implementaciones". Por lo general, los modelos hacen lo correcto por defecto. Las restricciones definen sus límites.
Evita la mentalidad de lista de verificación en tareas de más alto nivel o más complejas. Proporciona instrucciones detalladas sobre tu intención, pero recuerda que dar tareas específicas tiende a hacer que el modelo se centre en cumplirlas en lugar de abarcar el alcance más amplio. Además, restas prioridad implícitamente a lo que no aparece en la lista. Por lo general, es mejor dejar que el modelo use su criterio y autonomía.
También nos resultó útil proporcionar números y rangos concretos al hablar de la magnitud del alcance. Instrucciones como "generate many tasks" tienden a producir pocas tareas: un valor por defecto conservador, ir a lo seguro, aunque técnicamente siga las instrucciones. "Generate 20-100 tasks" transmite que la intención es abarcar más, que debería ser ambicioso, y observamos un comportamiento general muy distinto.
Aprendizajes sobre el diseño del sistema
Establecimos algunos principios a partir de nuestra investigación:
- El sistema debe ser antifrágil. A medida que ampliamos la cantidad de agentes que se ejecutan simultáneamente, también aumenta la probabilidad de fallos. Nuestro sistema debe poder resistir que agentes individuales fallen, permitiendo que otros se recuperen o prueben enfoques alternativos.
- Priorizar lo empírico sobre las suposiciones. Queríamos usar datos y observación para hacer ajustes, en lugar de partir de suposiciones sobre cómo debería funcionar basadas en organizaciones humanas o en diseños de sistemas existentes.
- Diseñar explícitamente para el rendimiento. Esto implicó aceptar compensaciones en otros aspectos de la programación, como una tasa de errores baja pero estable que requiere una pasada final de conciliación, en lugar de aspirar a un código que funcione perfectamente el 100 % del tiempo, lo 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 actual del sistema ha estado funcionando con una sobrecarga mínima y proporciona un escalado lineal del rendimiento de tokens de forma útil. No han sido necesarias más iteraciones importantes en el harness.
Conclusión
Si bien la sensibilidad, el criterio y la dirección provinieron de personas, la IA fue un importante factor multiplicador para iterar y explorar rápidamente esta investigación.
Esto guarda cierta semejanza con el círculo "virtuoso" de la IA, en el que la IA se usa para desarrollar IA y, a medida que mejoran los modelos, los agentes y los sistemas que los sustentan, el proceso se retroalimenta y se acelera cada vez más. Damos forma a las herramientas que nos dan forma.
En esta investigación hay un parecido poético con la forma en que operan hoy algunos equipos de desarrollo de software. Estos modelos no se entrenaron 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, a fin de cuentas.
Seguiremos investigando agentes de muy larga duración, y nuestros hallazgos orientarán el futuro de nuestro producto.