Rumo a bases de código autônomas

Por Wilson Lin em Pesquisa
Rumo a bases de código autônomas

Estamos empolgados com a reação à nossa pesquisa sobre como escalar codificação autônoma de longa duração.

Esse trabalho começou como uma pesquisa interna para levar os modelos atuais ao limite. Como parte da pesquisa, criamos uma nova infraestrutura de agentes para orquestrar dezenas de milhares de agentes e observar o seu comportamento. No mês passado, nosso sistema já estava estável o suficiente para rodar continuamente por uma semana, sendo responsável pela grande maioria dos commits no nosso projeto de pesquisa (um navegador web). Esse navegador não foi pensado para uso externo e esperávamos que o código tivesse imperfeições.

Mesmo assim, apesar das esquisitices, o fato de que milhares de agentes conseguiram trabalhar juntos para produzir algo quase totalmente executável sem intervenção humana pareceu um marco que valia a pena ser compartilhado. Desde então, continuamos nossa pesquisa e queremos detalhar melhor como essa infraestrutura foi construída.

Também estamos disponibilizando parte dessa pesquisa para que alguns usuários possam testar.

Contexto

Nosso projeto de pesquisa começou como um projeto pessoal que eu tocava nas horas vagas.

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 da web sem JavaScript. Comecei pedindo ao Opus 4.5 que escrevesse um plano detalhado para construir um motor de navegador. Eu repetidamente pedia para ele “continuar” para ver até onde ele conseguiria ir nesse plano.

Isso falhou rapidamente. O modelo perdia a noção do que estava fazendo, frequentemente parava para proclamar sucesso apesar de estar longe disso e ficava travado em detalhes de implementação complexos. Mas ele mostrava sinais de conhecimento profundo e inteligência. Conseguia escrever bons trechos de código em partes pequenas.

O problema central era que o navegador era uma tarefa ampla demais e precisava ser decomposta em subtarefas. Em seguida, pedi para o agente planejar um grafo de dependências com os principais blocos de trabalho que agentes poderiam assumir em paralelo. Agentes eram criados manualmente para tarefas e acionados novamente quando paravam. Isso aumentou o throughput, mas os resultados não eram 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 apresentar resultados melhores na capacidade de seguir instruções com precisão. Isso parecia se encaixar bem para agentes de longa duração, então atualizamos nosso framework de testes para usar modelos da OpenAI com base nesses experimentos.

Nesse ponto, o framework de testes conseguia construir uma versão simples do navegador web sem JavaScript, mas construir um motor de navegador completo com um único agente seria proibitivamente lento.

Isso deu início à nossa próxima rodada de pesquisa. Conseguiríamos gastar 10x mais em computação para obter 10x mais throughput significativo?

De agente único para multiagente

Criamos um novo repositório com um harness simples em Rust.

Em vez de lidar com a complexidade de sistemas distribuídos, executamos o harness em uma única VM (máquina virtual) Linux grande, com muitos recursos. Para controlar o harness, acessávamos a VM via SSH e usávamos uma interface de terminal simples.

Investimos mais tempo desde o início em uma boa observabilidade do sistema. Registrávamos todas as mensagens de agentes, ações do sistema e saídas de comandos, com carimbos de tempo para podermos analisar e reproduzir sessões. Isso não só foi útil para revisarmos manualmente, como também para enviar esses dados de volta ao Cursor, a fim de vasculhar grandes quantidades de dados e encontrar padrões rapidamente.

Auto-coordenação

Nossa primeira ideia de múltiplos agentes foi a mais simples: fazer com que agentes com funções iguais usassem um arquivo de estado compartilhado para ver no que os outros estavam trabalhando, decidir no que trabalhar e atualizar o arquivo.

Diagrama de auto-coordenação mostrando agentes conectados a um arquivo de coordenação compartilhadoDiagrama de auto-coordenação mostrando agentes conectados a um arquivo de coordenação compartilhado

Seríamos o menos prescritivos possível sobre o que fazer e, em vez disso, deixaríamos que os agentes descobrissem como se auto-coordenar. Isso falhou rapidamente.

O arquivo de coordenação rapidamente começou a criar mais problemas. Os agentes mantinham bloqueios (locks) por tempo demais, esqueciam de liberá-los, tentavam bloquear ou desbloquear quando isso não era permitido e, em geral, não entendiam a importância de manter um lock no arquivo de coordenação. Locking é fácil de errar e difícil de acertar exatamente, e mais prompting não ajudou.

O locking também causou contenção demais. Vinte agentes passavam a ter a taxa de processamento de 1 a 3, com a maior parte do tempo gasta esperando por 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 de controle de concorrência otimista sem locks, o 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 mudanças 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 responsabilidade e cobrança claras:

Diagrama de papéis estruturados mostrando Planejador, Executor, Trabalhadores e Juiz em um pipelineDiagrama de papéis estruturados mostrando Planejador, Executor, Trabalhadores e Juiz em um pipeline

Um planejador primeiro definiria a abordagem exata e os entregáveis para avançar em direção às instruções do usuário. Isso seria repassado para um executor, que se tornava o agente líder e único responsável por garantir que o plano fosse completamente cumprido. O executor podia gerar tarefas para trabalhadores, o que proporcionava escala linear e maior throughput.

Para manter o andamento e a responsabilidade, um juiz independente entrava em ação depois que o executor terminasse, para determinar se o trabalho foi concluído 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 trabalhadores se concentrassem estritamente em sua tarefa enquanto o sistema como um todo ainda entregava resultados.

Observando e otimizando em pequenos passos

Chegar a esse design exigiu uma observação atenta do sistema.

Se houvesse um problema grave, ele tenderia a ocorrer repetidamente e em muitos agentes e chamadas de ferramentas. Por exemplo, percebemos que havia muita contenção 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.

Por fim, descobrimos que esse sistema ficava limitado pelo worker mais lento. Ele era rígido demais.

Fazer todo o planejamento de antemão também dificultava que o sistema se reajustasse dinamicamente à medida que novos problemas eram descobertos. Alguns agentes acabavam seguindo em direções contraproducentes, incapazes de se autocorrigir até a próxima iteração do loop.

Executor contínuo

A próxima versão removeu o planejador independente.

O executor agora também podia planejar como atingir o objetivo, além de criar tarefas. Como era o único agente, não precisava escrever um plano em nenhum lugar, seguir um plano estático e imutável, nem esperar rigidamente por todos os workers.

Garantindo atualidade

Para garantir que agentes em todos os papéis não se desviassem ao longo de longos períodos de tempo, introduzimos mecanismos de atualização:

  1. Um scratchpad.md deve ser reescrito com frequência em vez de apenas receber acréscimos.
  2. Agentes individuais devem gerar um resumo automático ao atingir os limites de contexto.
  3. Adicionamos autorreflexão e lembretes de alinhamento aos system prompts.
  4. Agentes foram incentivados a mudar de direção e questionar pressupostos a qualquer momento.

O sistema agora era altamente dinâmico e flexível: ele podia explorar o código de forma proativa, reconsiderar decisões, gerenciar workers, intercalar tarefas e refletir continuamente as informações mais recentes. Observamos que os agentes eram razoavelmente bons em seguir instruções até a conclusão, então o judge foi removido para manter o sistema simples.

Diagrama do executor contínuo mostrando um loop infinito de execução com workersDiagrama do executor contínuo mostrando um loop infinito de execução com workers

Comportamentos patológicos

Apesar dessas melhorias, o executor contínuo começou a apresentar comportamentos patológicos. Ele entrava em modo de espera aleatoriamente, parava de executar agentes, fazia o trabalho por conta própria, se recusava a planejar e a gerar mais do que algumas tarefas de escopo muito restrito, não mesclava corretamente as alterações dos workers e alegava conclusão prematura.

Descobrimos que ele estava recebendo funções e objetivos demais simultaneamente, incluindo: planejar, explorar, pesquisar, gerar tarefas, verificar os workers, revisar código, fazer edições, mesclar resultados e decidir se o loop havia terminado. Em retrospecto, faz sentido que ele estivesse sobrecarregado.

O design final do sistema

O design final incorpora todos os nossos aprendizados:

  1. Um planejador raiz é dono de 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 avancem em direção ao objetivo. Ele não escreve código por conta própria. Ele não sabe se suas tarefas estão sendo assumidas nem por quem.
  2. Quando um planejador percebe que seu escopo pode ser subdividido, ele cria subplanejadores que assumem totalmente a fatia delegada, assumindo plena responsabilidade de forma semelhante, mas apenas por aquela fatia. Isso é recursivo.
  3. Trabalhadores assumem tarefas e são exclusivamente responsáveis por levá-las até a conclusão. Eles não têm consciência do sistema maior. Eles não se comunicam com nenhum outro planejador ou trabalhador. Eles trabalham em sua própria cópia do repositório e, quando terminam, produzem 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.

Design final do sistema mostrando planejadores recursivos, subplanejadores, trabalhadores e gitDesign final do sistema mostrando planejadores recursivos, subplanejadores, trabalhadores e git

Subplanejadores aumentam a vazão ao distribuir rapidamente os trabalhadores, garantindo ao mesmo tempo que todo o sistema permaneça totalmente sob a responsabilidade de um agente. Isso também ajudou em projetos e tarefas grandes em que um único planejador poderia ficar sobrecarregado e desenvolver visão de túnel.

O repasse contém não apenas o que foi feito, mas também anotações importantes, preocupações, desvios, descobertas, reflexões e feedback. O planejador recebe isso como uma mensagem de acompanhamento. Isso mantém o sistema em movimento contínuo: mesmo que um planejador esteja "concluído", ele continua recebendo atualizações, baixa o repositório mais recente e pode continuar a planejar e tomar decisões subsequentes.

Todos os agentes têm esse mecanismo, o que permite que o sistema permaneça incrivelmente dinâmico e auto‑convergente, propagando informações ao longo da cadeia para responsáveis com visões cada vez mais globais, sem a sobrecarga de sincronização global ou comunicação cruzada.

Removendo o integrador

Inicialmente adicionamos um integrador para ter um controle de qualidade centralizado e com visão global, e para remover a contenção gerada por muitos workers tentando fazer push, rebase, resolver conflitos e dar merge simultaneamente.

Ele rapidamente se tornou um gargalo óbvio. Havia centenas de workers e um único gate (isto é, “burocracia”) pelo qual todo o trabalho precisava passar. Tentamos mudar os prompts, mas acabamos decidindo que ele era desnecessário e poderia ser removido para simplificar o sistema.

Capacidade de processamento e trade-offs

O sistema chegou a um pico de ~1.000 commits por hora, em um total de 10 milhões de chamadas de ferramentas ao longo de uma semana. Depois que o sistema entrou em operação, não precisou de nenhuma intervenção nossa.

Foram feitos trade-offs intencionais para alcançar esse nível de capacidade de processamento.

Correção de commits

Quando exigíamos 100% de correção antes de cada commit, isso causava muita serialização e uma redução significativa do throughput efetivo. Mesmo um erro pequeno, como uma mudança em uma API ou um erro de digitação, fazia todo o sistema praticamente parar. Workers saíam do seu escopo e começavam a corrigir coisas irrelevantes. Muitos agentes se acumulavam e atrapalhavam uns aos outros tentando 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 serão corrigidos em breve por outros agentes, o que é verdade, já que o sistema tem propriedade e delegação efetivas sobre toda a base de código. Erros surgem e são corrigidos rapidamente. A taxa de erro permanece pequena e constante, talvez raramente completamente limpa, mas estável e administrável, sem explodir ou se deteriorar.

Isso pode indicar que o sistema eficiente ideal aceita alguma taxa de erro, mas que é necessário um branch final “verde”, em que um Agente tira snapshots regularmente e faz uma passada rápida de correções antes do lançamento.

Sobrecarga de sincronização

Às vezes, vários agentes atuam sobre o mesmo arquivo ou refatoram o mesmo trecho de código. Em vez de tentar eliminar completamente esses casos ou criar uma solução superengenheirada, 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 como um todo mais simples: mais fácil de alinhar modelos sem sobrecarregá-los, mais fácil de gerenciar e observar, com menos atrito e melhor produtividade global. Também evita abordagens excessivamente complexas.

Aprendizados de infraestrutura

Cada execução multiagente foi executada em sua própria máquina grande, com amplos recursos de sistema, para evitar complexidade prematura em torno de sistemas distribuídos. Isso foi uma boa escolha, já que a maioria das execuções chegava ao pico de algumas centenas de agentes, o que normalmente saturava, mas não sobrecarregava em excesso essas máquinas. Essa arquitetura facilitou a observação de métricas do sistema e o compartilhamento e a cópia de estado quando necessário.

Depois de limitar o uso de RAM dos agentes, o disco se tornou 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 foi 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, compilação) passa a consumir a maior parte do tempo, em vez de, idealmente, pensar e escrever código.

Também havia restrições e ineficiências no ambiente geral de desenvolvimento: coisas que fazem sentido, ou não são significativas, para o ambiente de trabalho de um único usuário podem se destacar 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 existem oportunidades interessantes e óbvias de grandes ganhos de eficiência apenas repensando e redesenhando algumas dessas primitivas 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 bem estabelecidos de sistemas concorrentes, como bancos de dados, poderia fazer com que esses bloqueios funcionassem 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 deduplicação, encontrados em sistemas de armazenamento de produção mais sofisticados, poderia trazer ganhos igualmente fáceis para um sistema tipicamente "de usuário único" sem exigir a construção de uma infraestrutura separada?

Especificando a intenção para agentes

As instruções dadas a esse sistema multiagente foram muito importantes.

Inicialmente, não as tornamos nosso objetivo principal, e sim obter um harness estável e eficaz. Mas a importância das instruções ficou clara rapidamente. Estávamos essencialmente interagindo com um agente de programação típico, só que com ordens de grandeza a mais de tempo e recursos de computação. Isso amplifica tudo, inclusive instruções subótimas e pouco claras.

Faz sentido investir mais tempo nas instruções iniciais. Em última análise, agentes ainda são agentes: treinados para seguir suas instruções à risca, percorrer esses caminhos, não mudá-las nem ignorá-las, mesmo que sejam ruins.

Queríamos ver sucesso em nossos projetos de pesquisa, então fomos alterando 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 dos resultados, o que não era culpa do próprio harness. O harness estava apenas seguindo exatamente nossas instruções.

Alguns exemplos do projeto de navegador:

  • Inicialmente, as instruções focavam em implementar especificações e eliminar bugs. Instruções como "spec implementation" eram vagas o suficiente para que agentes se aprofundassem em recursos obscuros e raramente usados, em vez de priorizar de forma inteligente.
  • Assumimos implicitamente que havia expectativas de desempenho dentro de limites aceitáveis para usuários. Mas foram necessárias instruções explícitas e timeouts aplicados para forçar os agentes a equilibrar desempenho com outros objetivos.
  • Em partes complexas do sistema, agentes podem escrever código com vazamentos de memória ou que causa deadlocks. Humanos perceberiam isso, mas nem sempre era óbvio para os agentes. Ferramentas explícitas de gerenciamento de recursos baseadas em processos foram necessárias para permitir que o sistema se recuperasse de forma robusta e se tornasse 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.

De forma semelhante, embora os agentes tivessem sido informados de que o projeto era um navegador construído do zero, eles ainda trouxeram algumas dependências que poderiam ter implementado por conta própria ou usado apenas como scaffolding temporário enquanto a implementação adequada estava em andamento. Isso foi uma falha nas instruções. Uma execução posterior explicitou a filosofia de dependências e quais bibliotecas não podiam ser usadas, o que corrigiu isso.

Essa execução posterior também fez uma grande reestruturação em vários crates autocontidos, saindo de um monólito. O repositório estava em um estado extremamente 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 ficar preso. Essa execução também passou muito menos tempo esperando pela compilação, operando com um throughput várias vezes maior do que antes.

Arquitetura e instruções importam. Agentes têm imensa habilidade de engenharia, mas seguirão instruções até o fim, sejam boas ou ruins. Encontrar o equilíbrio entre métricas excessivamente estreitas e liberdade não estruturada foi complicado, assim como saber o que era óbvio versus o que precisava ser mencionado explicitamente.

Tudo isso indica a importância de extrair, especificar e entender a intenção, que se torna ainda mais significativa nessa escala. Steerability (capacidade de direcionamento) e observabilidade serão áreas de pesquisa interessantes para continuar explorando.

Otimizando prompts

Prompting foi uma parte significativa do processo de evolução.

Descobrimos que é melhor não dar instruções sobre coisas que o modelo já sabe fazer, apenas sobre o que ele não sabe (por exemplo, colaboração multi-agente) ou que são específicas do domínio relevante (por exemplo, como rodar testes, seu pipeline de deploy). Trate o modelo como uma nova contratação brilhante que conhece engenharia, mas não a sua base de código nem 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 de terminar as implementações". Modelos geralmente fazem coisas boas por padrão. Restrições servem para definir seus limites.

Evite a mentalidade de checklist para tarefas de nível mais alto ou mais profundas. Dê instruções detalhadas sobre a sua intenção, mas lembre que dar coisas específicas para fazer tende a fazer o modelo focar em cumprir esses itens em vez do escopo mais amplo. Você também, implicitamente, dá menos prioridade ao que não está listado. Normalmente, é melhor deixar o modelo usar seu próprio julgamento e autonomia.

Achamos útil fornecer números e faixas concretas ao falar do tamanho do escopo. Instruções como "gere muitas tarefas" tendem a produzir uma quantidade pequena: padrão conservador, jogando seguro, tecnicamente ainda seguindo as instruções. "Gere de 20 a 100 tarefas" transmite que a intenção é um escopo maior, que deve ser ambicioso, e observamos um comportamento geral bem diferente e mais amplo.

Aprendizados de design de sistema

Estabelecemos alguns princípios a partir da nossa pesquisa:

  1. O sistema deve ser antifrágil. À medida que aumentamos o número de agentes em execução simultânea, também aumentamos a probabilidade de falhas. Nosso sistema precisa suportar falhas de agentes individuais, permitindo que outros se recuperem ou tentem abordagens alternativas.
  2. Empírico em vez de guiado por suposições. Queríamos usar dados e observação para fazer ajustes, em vez de chegar com suposições sobre como ele deveria funcionar com base em organizações humanas ou em designs de sistemas existentes.
  3. Projetar explicitamente para throughput. Isso significou abrir mão de outros aspectos da codificação, como aceitar uma taxa pequena, porém estável, de erros que exige uma etapa final de reconciliação, em vez de exigir código funcionando perfeitamente 100% do tempo, o que deixaria o sistema dramaticamente mais lento.

Esses sistemas tendem a ser elegantemente simples quando bem feitos, mas não era claro qual abordagem simples funcionaria até explorarmos muitas abordagens diferentes. O design de sistema atual vem rodando com overhead mínimo e oferece escalonamento linear de throughput de tokens de forma útil. Não foram necessárias novas iterações importantes no harness.

Conclusão

Embora o senso estético, o julgamento e a direção tenham vindo de humanos, a IA foi um multiplicador de força significativo para iterar rapidamente e explorar esta pesquisa.

Isso guarda alguma semelhança com o “ciclo virtuoso” de IA, em que a IA é usada para desenvolver IA e, à medida que modelos, agentes e ferramentas de orquestração melhoram, o processo se retroalimenta e acelera cada vez mais. Moldamos as ferramentas que nos moldam.

Há uma semelhança poética, nesta pesquisa, com a forma como algumas equipes de software operam hoje. Esses modelos não foram treinados explicitamente dessa forma, o que sugere que é um comportamento emergente e possivelmente a forma correta de estruturar projetos de software, afinal.

Continuaremos pesquisando agentes de duração extremamente longa, com nossas descobertas orientando o futuro do nosso produto.

Rumo a bases de código autônomas · Cursor