Why NextWhy Next
Voltar à lista
💻 Dev11 min de leitura

O que aconteceu quando soltei vários agentes de IA no mesmo repositório

Achei que subir sessões de agente em paralelo dobraria a produtividade. Na prática, em uma semana estouraram acidentes reais em série: branch sequestrada, commit empilhado na branch alheia, commit órfão resgatado pelo reflog, até implementação dupla da mesma funcionalidade. Uma tipologia dos acidentes que agentes paralelos compartilhando um checkout produzem, e as regras que nasceram deles.

Resumo

O verdadeiro inimigo das sessões de agente em paralelo não é a capacidade do modelo, e sim o diretório de trabalho compartilhado. HEAD é a variável global de todo mundo, então a troca de branch de uma sessão empilha o commit da outra no lugar errado. Os quatro tipos de acidente vividos em uma semana com o relato da recuperação, e as três linhas de defesa: isolamento por worktree, reconferência da branch logo antes do commit e redução do escopo do portão de verificação.

Nesta página

Trabalhando com agentes de IA, a ambição vem naturalmente. Enquanto uma sessão corrige um bug, outra faz a refatoração e uma terceira investiga um issue, por que não? Dá para subir vários modelos, então a produtividade cresceria na mesma proporção.

Eu também comecei assim. E em uma semana descobri: o verdadeiro inimigo dos agentes paralelos não é a capacidade do modelo, e sim o diretório de trabalho que eles compartilham.

HEAD é uma variável global

A causa se resume numa frase. Quando várias sessões compartilham um único checkout do git, o estado "branch atual" vira a variável global de todo mundo.

Se você imagina duas pessoas trabalhando ao mesmo tempo no mesmo computador, o estranho salta aos olhos na hora; ao subir os agentes, esse pensamento não me ocorreu. Com uma sessão em cada aba do terminal, elas parecem isoladas entre si. Mas o sistema de arquivos é um só, e o HEAD também. No instante em que uma sessão faz git checkout, o chão de todas as outras muda.

Os acidentes daquela semana tinham tipos nítidos.

Sequestro de branch. Enquanto a sessão A trabalhava numa branch de tópico, a sessão B trocou de branch para fazer o próprio trabalho. A sessão A commitou sem saber disso, e o commit foi parar em cima da branch da B. Também houve o sentido inverso. No momento em que A ia commitar, a branch tinha mudado para develop, e o acidente só foi evitado porque o hook que bloqueia commit direto em branch protegida barrou. Sem o hook, teria entrado.

Commit órfão. A branch de tópico da sessão A foi apagada pela sessão B durante uma faxina. Os commits da A viraram órfãos que não pertenciam a branch nenhuma, e a recuperação foi vasculhar o reflog, achar o hash do commit e resgatar com cherry-pick. Sorte que deu para salvar; se o reflog tivesse expirado ou o hash não aparecesse, o trabalho teria evaporado inteiro.

Contaminação do staging. No momento em que a sessão A montava um commit, uma exclusão de arquivo que a sessão B tinha deixado em staging estava lá junto, na área de staging. Se tivesse commitado daquele jeito, a exclusão da B entraria misturada no commit da A. Só foi filtrado porque, passando o olho no diff, apareceu uma mudança estranha; se o agente não tivesse olhado o diff logo antes do commit, ninguém saberia.

Implementação dupla. O tipo mais melancólico. Duas sessões, sem saber da existência uma da outra, implementaram cada uma a mesma funcionalidade. As duas fizeram um trabalho caprichado, e uma teve que ser jogada fora inteira. O tempo economizado com a paralelização foi devolvido na íntegra.

Até o portão de verificação vira pisada no pé alheio

O problema não parava em arquivos e branches. Nosso harness tem um portão que, antes de a sessão terminar, roda a análise estática e os testes do repositório inteiro, impedindo encerrar o trabalho num estado quebrado. Em sessão solo, é um dispositivo excelente.

Em sessões paralelas, isso virou uma armadilha em que uma trava o pé da outra. A sessão A só mexeu em documentação, mas o portão da A falha por causa do erro de compilação num arquivo que a B está corrigindo naquele exato momento. A gasta turnos provando que "a mudança não é minha" e, num dia ruim, esperou 30 minutos até a B arrumar a casa. O pior episódio foi uma sessão alterar o código em que outra sessão estava trabalhando, só para passar no portão. O portão virou justamente um incentivo para mexer no trabalho alheio.

A checagem em si não está errada. O escopo da checagem ser "o repositório inteiro" é que ficou errado no instante em que o repositório deixou de pertencer a uma pessoa só.

Três linhas de defesa

Depois de viver os acidentes tipo a tipo, levantei a defesa em três camadas.

  1. Isolamento por worktree como padrão. Dando a cada sessão um diretório de trabalho independente com git worktree, o HEAD deixa de ser variável compartilhada. Sequestro de branch, commit órfão e contaminação do staging desaparecem pela raiz. Dos quatro tipos de acidente acima, três somem com essa única jogada. Mas não é de graça. Num monorepo, cada worktree exige refazer a instalação de dependências e a geração de código, e em repositórios que usam ferramentas que combinam mal com worktree, como o git-crypt, é difícil impor. Nós mesmos mantemos checkout compartilhado num dos repositórios por causa dessa restrição, e é por isso que as outras duas linhas de defesa são necessárias.
  2. Reconferir a branch logo antes do commit. Quando a sessão começa o trabalho, registra "eu trabalho nesta branch" e, logo antes do commit, compara com o HEAD atual. Se estiver diferente, para o commit e primeiro entende a situação. A regra é simples a ponto de parecer boba, mas todos os acidentes de sequestro de branch nasceram de "o HEAD no momento do commit não era o HEAD que eu conhecia". Uma pessoa notaria pelo nome da branch exibido no prompt; um agente não confere se não for mandado explicitamente.
  3. Escopar o portão de verificação às minhas mudanças. O alcance do portão de encerramento da sessão estreita: do repositório inteiro para os arquivos que aquela sessão de fato modificou. Some o meu encerramento barrado pelo WIP alheio, e some o incentivo a mexer no código dos outros para passar no portão. A saúde do repositório inteiro o CI revê de qualquer forma, tomando como base o estado commitado. Não havia necessidade de o portão da sessão olhar o todo também.

Em cima disso, uma regra de operação. Antes de subir uma sessão, passo o olho nas branches e nos PRs abertos para ver se há escopo sobreposto. Implementação dupla não é um problema que o git consiga bloquear, é um problema de distribuição de tarefas, então só restou prevenir com hábito, não com ferramenta.

Concorrência nunca foi um problema só de banco de dados

Organizando tudo, o desenho é familiar. Recurso compartilhado, acesso concorrente sem lock, condição de corrida e nível de isolamento. Exatamente o problema que passamos décadas aprendendo em bancos de dados e código multithread, reencenado em cima do diretório de trabalho.

Trabalhando com um único agente, esse problema não existe. Quando pessoa e agente se revezam, a pessoa faz implicitamente o papel de coordenadora. O problema começa no instante em que os agentes viram vários e a pessoa solta a coordenação. Desse ponto em diante, o diretório de trabalho é um recurso compartilhado que precisa de controle de concorrência, e rodar sem isolamento perde dados com certeza, como acontecia com os bancos de dados.

Não estou dizendo para desistir dos agentes paralelos. Até hoje subo várias sessões todos os dias. Só uma coisa mudou. Agora eu não enfio todos eles na mesma sala.

Artigos relacionados

0 comentários