Rumo a bases de código autônomas

Estamos entusiasmados com a repercussão da nossa pesquisa sobre o escalonamento da programação autônoma de longa duração.
Esse trabalho começou como uma pesquisa interna para expandir os limites dos modelos atuais. Como parte da pesquisa, criamos uma nova infraestrutura de agentes para orquestrar muitos milhares de agentes e observar seu comportamento. Até o mês passado, nosso sistema já era estável o suficiente para operar continuamente por uma semana, fazendo a grande maioria dos commits no nosso projeto de pesquisa (um navegador web). Esse navegador não foi desenvolvido para uso externo, e esperávamos que o código tivesse imperfeições.
No entanto, mesmo com essas peculiaridades, o fato de milhares de agentes conseguirem trabalhar juntos para entregar algo quase totalmente executável sem intervenção humana pareceu um marco que valia a pena compartilhar. Desde então, seguimos avançando nessa pesquisa e quisemos explicar em mais detalhes como essa infraestrutura foi desenvolvida.
Também estamos disponibilizando parte desta pesquisa para alguns usuários experimentarem.
Contexto
Nosso projeto de pesquisa começou como um projeto paralelo pessoal.
Um navegador parecia um benchmark interessante. Era complexo o suficiente para revelar limitações dos modelos de ponta, e havia muitos subsistemas diferentes que precisavam funcionar em conjunto.
Meu plano inicial era oferecer suporte à renderização de páginas Web sem suporte a JavaScript. Comecei pedindo ao Opus 4.5 que escrevesse um plano detalhado para desenvolver um motor de navegador. Eu o incentivava repetidamente a "continuar" para ver até onde ele iria com o plano.
Isso fracassou rapidamente. O modelo perdia o fio do que estava fazendo, frequentemente parava para declarar sucesso apesar de ainda estar longe disso e ficava preso em detalhes complexos de implementação. Mas mostrou sinais de conhecimento profundo e inteligência. Ele conseguia escrever bons trechos de código.
O problema central era que o navegador era uma tarefa complexa demais e precisava ser dividido em subtarefas. Em seguida, pedi ao agente que montasse um grafo de dependências das principais frentes de trabalho que os agentes pudessem executar em paralelo. Os agentes eram iniciados manualmente para as tarefas e recebiam novos estímulos quando paravam. Isso aumentou a produtividade, mas os resultados não foram muito melhores. Os agentes não conseguiam se comunicar entre si nem fornecer feedback sobre o projeto como um todo. O sistema precisava ser mais dinâmico.
Enquanto isso, o GPT-5.1 (e depois o GPT-5.2) começou a mostrar resultados melhores na capacidade de seguir instruções com precisão. Isso pareceu ser uma boa opção para agentes de longa duração, então atualizamos nosso harness para usar modelos da OpenAI com base nesses experimentos.
Nesse ponto, o harness conseguia construir uma versão simples do navegador Web sem JavaScript, mas desenvolver um motor de navegador completo com um agente seria proibitivamente lento.
Isso deu início à nossa próxima rodada de pesquisa. Poderíamos gastar 10x mais em computação para obter 10x mais throughput útil?
De agente único a multiagente
Iniciamos um novo repositório com um harness simples baseado em Rust.
Em vez de lidar com a complexidade de sistemas distribuídos, executamos o harness em uma única VM (Virtual Machine) Linux de grande porte, com muitos recursos. Para controlar o harness, acessávamos a VM via SSH e usávamos uma interface de terminal simples.
Dedicamos mais tempo no início a uma observabilidade adequada do sistema. Registramos todas as mensagens dos agentes, ações do sistema e saídas de comandos, com carimbos de data e hora para podermos analisar e reproduzir sessões. Isso não foi útil apenas para nossa revisão manual, mas também para alimentar o Cursor e examinar grandes volumes de dados em busca de padrões rapidamente.
Autocoordenação
Nossa primeira ideia multiagente foi a mais simples: fazer com que agentes com papéis equivalentes usassem um arquivo de estado compartilhado para ver no que os outros estavam trabalhando, decidir no que trabalhar e atualizar o arquivo.


Seríamos o menos prescritivos possível sobre o que fazer e, em vez disso, deixaríamos que os agentes descobrissem como se autocoordenar. Isso fracassou rapidamente.
O arquivo de coordenação logo passou a criar mais problemas. Os agentes mantinham locks por tempo demais, esqueciam de liberá-los, tentavam adquirir ou liberar locks quando isso não era permitido e, no geral, não entendiam a importância de manter um lock no arquivo de coordenação. É fácil errar na implementação de locks e acertar só por pouco, e dar mais prompting não ajudou.
Os locks também causavam contenção demais. 20 agentes acabavam com a vazão de apenas 1 a 3, com a maior parte do tempo sendo gasta esperando locks. Tentamos dar aos agentes uma ferramenta para esperar explicitamente pelo trabalho de outro agente, mas eles raramente a usavam. Também tentamos uma abordagem otimista de controle de concorrência sem locks, que reduziu a sobrecarga, mas não eliminou a confusão.
A falta de estrutura entre os agentes significava que nenhum agente assumia tarefas grandes e complexas. Eles evitavam contenção e conflito, optando por alterações menores e mais seguras em vez de assumir responsabilidade pelo projeto como um todo.
Adicionando estrutura e papéis
Em seguida, separamos os papéis para dar aos agentes propriedade e responsabilização:


Primeiro, um planner definiria a abordagem exata e as entregas para avançar em direção às instruções do usuário. Isso seria passado para um executor, que se tornaria o único agente líder responsável por garantir que o plano fosse concluído por completo. O executor podia gerar tarefas para os workers, o que proporcionava escalonamento linear e throughput.
Para manter o avanço e a responsabilização, um judge independente atuava depois que o executor terminava para determinar se ele havia concluído o trabalho e se outra iteração deveria ser executada. Isso resolveu muitos problemas de coordenação. Ter um único papel dedicado a assumir e supervisionar a execução permitiu que os workers se concentrassem estritamente em sua tarefa, enquanto o sistema como um todo continuava entregando.
Observação e ajustes incrementais
Chegar a esse design exigiu observar o sistema de perto.
Se houvesse um problema grande, ele tenderia a se repetir em muitos agentes e chamadas de ferramenta. Por exemplo, percebemos que havia contenção demais porque muitos agentes estavam executando git restore ao mesmo tempo. Usamos o Cursor para analisar logs e compará-los com nossos prompts, para entender por que o comportamento não correspondia às expectativas.
No fim, descobrimos que esse sistema era limitado pelo worker mais lento. Ele era rígido demais.
Fazer todo o planejamento antecipadamente também dificultava que o sistema se reajustasse dinamicamente à medida que novos problemas eram descobertos. Alguns agentes acabavam seguindo em direções contraproducentes, sem conseguir se autocorrigir até a próxima iteração do loop.
Executor contínuo
A versão seguinte removeu o planejador independente.
Agora, o executor também podia planejar como alcançar o objetivo, além de gerar tarefas. Como era o único agente, não precisava escrever um plano em algum lugar, seguir um plano único, estático e imutável nem esperar rigidamente por todos os workers.
Garantindo a atualização
Para garantir que agentes em todos os perfis não se desviassem ao longo do tempo, introduzimos mecanismos de atualização:
- Um
scratchpad.mddeveria ser reescrito com frequência, em vez de apenas receber acréscimos. - Agentes individuais deveriam resumir automaticamente ao atingir o limite de contexto.
- Adicionamos lembretes de autorreflexão e alinhamento aos prompts do sistema.
- Os agentes foram incentivados a mudar de abordagem e questionar premissas a qualquer momento.
O sistema agora era altamente dinâmico e flexível: podia explorar o código de forma proativa, reconsiderar decisões, gerenciar workers, intercalar tarefas e incorporar continuamente as informações mais recentes. Constatamos que os agentes eram razoavelmente bons em seguir instruções até o fim, então o juiz foi removido para manter o sistema simples.


Comportamentos patológicos
Apesar dessas melhorias, o executor contínuo começou a apresentar comportamentos patológicos. Ele entrava em espera aleatoriamente, deixava de executar agentes, fazia o trabalho por conta própria, se recusava a planejar e a gerar mais do que algumas poucas tarefas de escopo restrito, não fazia corretamente o merge das alterações dos workers e alegava conclusão prematura.
Descobrimos que ele estava recebendo funções e objetivos demais ao mesmo tempo, incluindo: planejar, explorar, pesquisar, gerar tarefas, verificar os workers, revisar código, realizar edições, fazer merge das saídas e determinar se o loop terminou. Em retrospecto, faz sentido que ele tenha ficado sobrecarregado.
O design final do sistema
O design final incorpora todos os nossos aprendizados:
- Um planejador raiz é responsável por todo o escopo das instruções do usuário. Ele é responsável por entender o estado atual e entregar tarefas específicas e direcionadas que façam avançar o objetivo. Ele não programa por conta própria. Ele não sabe se suas tarefas estão sendo assumidas nem por quem.
- Quando um planejador percebe que seu escopo pode ser subdividido, ele cria subplanejadores que assumem totalmente a fatia restrita delegada, com total responsabilidade de forma semelhante, mas apenas por essa fatia. Isso é recursivo.
- Os workers assumem tarefas e são os únicos responsáveis por levá-las até a conclusão. Eles não têm visibilidade do sistema mais amplo. Eles não se comunicam com nenhum outro planejador ou worker. Eles trabalham em sua própria cópia do repo e, quando terminam, escrevem um único repasse que o sistema envia ao planejador que solicitou a tarefa.
Curiosamente, isso de fato representa como algumas equipes de software operam hoje.


Os subplanejadores aumentam a produtividade ao acionar rapidamente vários workers, garantindo ao mesmo tempo que todo o sistema continue totalmente sob a responsabilidade de um agente. Isso também ajudou em grandes projetos e tarefas em que, de outra forma, um único planejador ficaria sobrecarregado e desenvolveria visão de túnel.
O repasse contém não apenas o que foi feito, mas também observações importantes, preocupações, desvios, descobertas, reflexões e feedback. O planejador recebe isso como uma mensagem de continuação. Isso mantém o sistema em movimento contínuo: mesmo que um planejador esteja "pronto", ele continua a receber atualizações, puxa a versão mais recente do repo e pode continuar planejando e tomando decisões subsequentes.
Todos os agentes têm esse mecanismo, o que permite que o sistema permaneça incrivelmente dinâmico e autoconvergente, propagando informações pela cadeia até os responsáveis com visões cada vez mais globais, sem o custo de sincronização global ou comunicação cruzada.
Removendo o integrador
Originalmente, adicionamos um integrador para fazer um controle de qualidade centralizado e com visão global, além de evitar a contenção causada por muitos workers tentando fazer push, rebase, resolver conflitos e fazer merge ao mesmo tempo.
Rapidamente ficou claro que ele havia se tornado um gargalo evidente. Havia centenas de workers e um único ponto de passagem (ou seja, "burocracia") pelo qual todo o trabalho precisava passar. Tentamos alterar o prompt, mas, no fim, decidimos que ele era desnecessário e podia ser removido para simplificar o sistema.
Throughput e trade-offs
O sistema atingiu um pico de ~1.000 commits por hora em 10 milhões de chamadas de ferramenta ao longo de uma semana. Depois que o sistema entrou em funcionamento, ele não exigiu nenhuma intervenção da nossa parte.
Houve trade-offs intencionais para atingir esse throughput.
Correção dos commits
Quando exigíamos 100% de correção antes de cada commit, isso causava uma serialização excessiva e reduzia significativamente o throughput efetivo. Até mesmo um único erro pequeno, como uma alteração de API ou um erro de digitação, fazia o sistema inteiro praticamente parar. Os workers saíam do seu escopo e começavam a corrigir coisas irrelevantes. Muitos agentes acabavam se acumulando e se atropelando ao tentar corrigir o mesmo problema.
Esse comportamento não era útil nem necessário. Permitir alguma folga significa que os agentes podem confiar que outros problemas logo serão corrigidos por outros agentes, o que de fato acontece, já que o sistema tem responsabilidade e delegação efetivas sobre toda a base de código. Os erros surgem, mas são corrigidos rapidamente. A taxa de erro permanece pequena e constante — talvez raramente fique totalmente limpa, mas segue estável e gerenciável, sem explodir nem se deteriorar.
Isso pode indicar que o sistema ideal e eficiente aceita alguma taxa de erro, mas que é necessária uma branch final "green", em que um agente tira snapshots regularmente e faz uma passada rápida de correção antes do lançamento.
Sobrecarga de sincronização
Às vezes, vários agentes mexem no mesmo arquivo ou refatoram o mesmo código. Em vez de tentar eliminar isso por completo ou criar uma solução complexa demais, aceitamos alguns momentos de turbulência e deixamos o sistema convergir e se estabilizar naturalmente em um curto período de tempo.
Isso consome alguns tokens extras e cria contenção local, mas mantém o sistema mais simples no geral: mais fácil de alinhar entre modelos sem sobrecarregá-los, mais fácil de gerenciar e observar, menos atrito e melhor produtividade global. Isso também evita abordagens complexas demais.
Aprendizados de infraestrutura
Cada execução multiagente rodava em sua própria máquina de grande porte, com recursos de sistema de sobra, para evitar complexidade prematura em torno de sistemas distribuídos. Isso funcionou bem, já que a maioria das execuções chegava a picos de várias centenas de agentes, o que normalmente levava essas máquinas à saturação, sem exceder sua capacidade. Essa arquitetura também facilitava observar as métricas do sistema e compartilhar e copiar o estado quando necessário.
Depois de limitar o uso de RAM dos agentes, o disco passou a ser o gargalo. Especialmente em um projeto monolítico, centenas de agentes compilando simultaneamente resultavam em muitos GB/s de leituras e gravações de artefatos de build. Isso teve um impacto significativo na vazão geral do harness, o que trouxe uma lição interessante: a estrutura do projeto, as decisões de arquitetura e a experiência do desenvolvedor podem afetar a vazão de tokens e commits, simplesmente porque trabalhar com a base de código (por exemplo, a compilação) acaba dominando o tempo, em vez de, idealmente, pensar e programar.
Também havia restrições e ineficiências no ambiente geral de desenvolvimento: coisas que fazem sentido ou não têm muita relevância em um espaço de trabalho de um único usuário podem ficar bem evidentes quando centenas de agentes fazem a mesma coisa em uma única máquina. Uma forma trivial de resolver isso é dar a cada agente sua própria máquina. Mas há oportunidades interessantes e relativamente simples para grandes ganhos de eficiência apenas repensando e redesenhando alguns desses primitivos e ferramentas.
Por exemplo, muitas ferramentas como Git e Cargo usam bloqueios compartilhados, em grande parte como um mecanismo simples de controle de concorrência. Será que trazer mecanismos já consolidados de sistemas concorrentes, como bancos de dados, faria isso funcionar igualmente bem em sistemas multiagente? Todos os agentes têm sua própria cópia do repositório, mas a maioria dos arquivos e artefatos é idêntica; será que adicionar recursos simples de copy-on-write e desduplicação, encontrados em sistemas de armazenamento de produção mais sofisticados, poderia trazer ganhos semelhantes e fáceis para um sistema tipicamente "de usuário único", sem precisar construir uma infraestrutura separada?
Especificando a intenção para os agentes
As instruções dadas a este sistema multiagente foram muito importantes.
No início, não as tratamos como nosso objetivo principal e, em vez disso, buscamos um harness estável e eficaz. Mas a importância das instruções ficou evidente rapidamente. Estávamos essencialmente interagindo com um agente de programação típico, só que com ordens de magnitude a mais de tempo e capacidade computacional. Isso amplifica tudo, inclusive instruções subótimas e pouco claras.
Faz sentido dedicar mais tempo às instruções iniciais. No fim das contas, agentes ainda são agentes: treinados para seguir suas instruções à risca, percorrer esses caminhos, não alterá-las nem sobrescrevê-las, mesmo quando são ruins.
Queríamos ver sucesso em nossos projetos de pesquisa, então fomos ajustando nossas instruções iniciais à medida que o projeto e o harness evoluíam. Estávamos aprendendo a construir um navegador ao mesmo tempo em que aprendíamos a operar esse novo sistema multiagente, e conseguíamos ver especificações ruins ou pouco detalhadas refletidas na qualidade das saídas, sem que isso fosse culpa do próprio harness. O harness estava apenas seguindo exatamente nossas instruções.
Alguns exemplos do projeto do navegador:
- No início, as instruções se concentravam em implementar especificações e corrigir bugs. Instruções como "implementação de especificações" eram vagas o bastante para que os agentes se aprofundassem em recursos obscuros e raramente usados, em vez de priorizar de forma inteligente.
- Assumimos implicitamente que havia expectativas de performance dentro de limites adequados para o usuário. Mas foram necessárias instruções explícitas e timeouts aplicados para forçar os agentes a equilibrar performance com outros objetivos.
- Em partes complexas do sistema, os agentes podem escrever código com vazamentos de memória ou que cause deadlocks. Humanos perceberiam isso, mas isso nem sempre era óbvio para os agentes. Foram necessárias ferramentas explícitas de gerenciamento de recursos com base em processos para permitir que o sistema se recuperasse de forma elegante e fosse mais defensivo.
Nossa primeira versão do navegador simples sem JavaScript convergiu para uma arquitetura inadequada para evoluir para um navegador completo. Isso foi uma falha da especificação inicial.
Da mesma forma, embora os agentes tivessem sido informados de que o projeto era um navegador feito do zero, eles ainda trouxeram algumas dependências que poderiam ter implementado por conta própria ou usado como scaffolding temporário enquanto a implementação adequada avançava. Isso foi uma falha nas instruções. Em uma execução posterior, a filosofia de dependências e as bibliotecas que não deveriam ser usadas foram explicitadas, o que corrigiu isso.
Essa execução posterior também fez uma grande reestruturação em muitos crates autocontidos, afastando-se de um monólito. O repositório estava em um estado bastante quebrado, mas o sistema multiagente convergiu para código funcional em poucos dias. Isso mostrou que o sistema tem forte capacidade de trabalhar de forma colaborativa e inteligente, mantendo isso mesmo em estados totalmente quebrados, em vez de se degradar ainda mais ou travar. Essa execução também passou muito menos tempo esperando compilação, operando com uma taxa de transferência várias vezes maior do que antes.
Arquitetura e instruções importam. Os agentes têm imensa habilidade de engenharia, mas seguirão as instruções até o fim, sejam elas boas ou ruins. Encontrar o equilíbrio entre métricas excessivamente restritas e liberdade desestruturada foi difícil, assim como saber o que era óbvio e o que precisava ser mencionado explicitamente.
Tudo isso indica a importância de elicitar, especificar e compreender a intenção, algo que se torna ainda mais significativo nessa escala. Controle e observabilidade serão áreas de pesquisa interessantes para continuar explorando.
Otimizando prompts
A elaboração de prompts foi uma parte importante do processo de evolução.
Descobrimos que era melhor não dar instruções sobre coisas que o modelo já sabe fazer, apenas sobre coisas que ele não sabe (por exemplo, colaboração multiagente) ou que são específicas do domínio relevante (por exemplo, como executar testes, seu pipeline de deploy). Trate o modelo como um novo contratado brilhante que entende de engenharia, mas não da sua codebase nem dos seus processos específicos.
Restrições são mais eficazes do que instruções. "Sem TODOs, sem implementações parciais" funciona melhor do que "lembre-se de concluir as implementações". Os modelos geralmente fazem coisas boas por padrão. As restrições definem seus limites.
Evite a mentalidade de checklist em tarefas de nível mais alto ou mais profundas. Dê instruções detalhadas sobre sua intenção, mas lembre-se de que dar tarefas específicas tende a fazer o modelo se concentrar em cumpri-las em vez de considerar o escopo mais amplo. Você também implicitamente reduz a prioridade do que não foi listado. Em geral, é melhor deixar o modelo usar seu julgamento e sua autonomia.
Também achamos útil fornecer números e intervalos concretos ao discutir a dimensão do escopo. Instruções como "gere muitas tarefas" tendem a produzir uma quantidade pequena: padrão conservador, jogando pelo seguro, ainda seguindo tecnicamente as instruções. "Gere de 20 a 100 tarefas" transmite que a intenção é um escopo maior, que ele deve ser ambicioso, e observamos um comportamento geral bem diferente.
Aprendizados de design de sistemas
Estabelecemos alguns princípios com base na nossa pesquisa:
- O sistema deve ser antifrágil. À medida que aumentamos o número de agentes em execução simultaneamente, também aumentamos a probabilidade de falhas. Nosso sistema precisa suportar a falha de agentes individuais, permitindo que outros se recuperem ou experimentem abordagens alternativas.
- Priorizar o empírico em vez de suposições. Queríamos usar dados e observação para fazer ajustes, em vez de partir de suposições sobre como ele deveria funcionar com base em organizações humanas ou em designs de sistemas existentes.
- Projetar explicitamente para throughput. Isso significou abrir mão de outros aspectos da programação, como aceitar uma taxa pequena, mas estável, de erros que exige uma etapa final de reconciliação, em vez de ter um código funcionando perfeitamente 100% do tempo, o que desaceleraria drasticamente o sistema.
Esses sistemas tendem a ser simples e elegantes quando são bem feitos, mas não estava claro qual abordagem simples funcionaria até explorarmos muitas abordagens diferentes. O design atual do sistema vem operando com sobrecarga mínima e oferece escalonamento linear útil do throughput de tokens. Não foram necessárias novas iterações significativas no harness.
Conclusão
Embora o gosto, o discernimento e a direção tenham vindo de humanos, a IA foi um grande multiplicador para iterar rapidamente e explorar esta pesquisa.
Isso guarda certa semelhança com o ciclo "virtuoso" da IA, em que a IA é usada para desenvolver IA e, à medida que modelos, agentes e harnesses melhoram, isso se retroalimenta e acelera cada vez mais. Moldamos as ferramentas que nos moldam.
Há nesta pesquisa uma semelhança poética com a forma como algumas equipes de software operam hoje. Esses modelos não foram treinados explicitamente dessa forma, o que sugere que se trata de um comportamento emergente e, possivelmente, da maneira correta de estruturar projetos de software, afinal.
Continuaremos pesquisando agentes de longa duração, e nossas descobertas ajudarão a orientar o futuro do nosso produto.