Um chicote de agente é o loop que aciona um LLM. Ele envia um prompt, obtém uma resposta, executa a ferramenta, chama o modelo solicitado, retorna os resultados e repete até que o modelo diga que está pronto. Todo agente de produção tem um. A questão é onde ele funciona.
Existem duas respostas. Eles têm propriedades de segurança diferentes, modos de falha diferentes e implicações diferentes sobre o que o agente pode fazer. As compensações também parecem diferentes dependendo se você está criando um agente de usuário único (um engenheiro em um laptop) ou multiusuário (dezenas de engenheiros na mesma organização compartilhando o mesmo agente). Estamos no campo multiusuário, que revela problemas que os construtores de usuário único não enfrentam.
As duas arquiteturas
Arnês dentro da caixa de areia
O loop reside no mesmo contêiner que o código em que está trabalhando. As chamadas LLM saem de dentro do contêiner. Chamadas de ferramentas (bash, leitura, gravação) são executadas localmente. Habilidades, memórias e qualquer outra coisa que o chicote rastreie são arquivos no sistema de arquivos do contêiner.
Isto é o que claude faz quando você o executa em seu laptop e como fica quando você ativa o Claude Code em um contêiner remoto. Se você estiver criando um agente de usuário único, poderá obter o SDK do Claude Code e enviar algo que funcione.
Aproveite fora da caixa de areia
O loop é executado em seu back-end. Quando precisa executar uma ferramenta, ele chama uma sandbox por meio de uma API. O sandbox executa a ferramenta e retorna o resultado. O loop nunca entra na sandbox.
Compensações
Colocar o arnês dentro da caixa de areia tem algumas vantagens. O modelo de execução é simples: um contêiner, uma árvore de processos, um sistema de arquivos, um tempo de vida. Você pode reutilizar arneses prontos para uso como estão. Habilidades e memórias funcionam inalteradas porque assumem um sistema de arquivos local e obtêm um.
Passar o arnês fora da caixa de areia proporciona coisas que o modelo interno não consegue.
Suas credenciais ficam fora da sandbox. O loop contém as chaves da API LLM, os tokens do usuário e o acesso ao banco de dados. A sandbox contém apenas o ambiente que o agente precisa para realizar seu trabalho. Não há nada lá para o agente escapar, portanto não há modelo de permissão para impor e nenhum vazamento de credencial para conter.
Você pode suspender o sandbox quando o agente não o estiver usando. Muito do que um agente faz não precisa de sandbox: pensar, chamar APIs, resumir, esperar por CI. Algumas sessões nunca tocam em uma sandbox. Com o chicote externo, você provisiona um somente quando o agente precisa executar um comando e o suspende sempre que estiver ocioso. Quando o arnês fica dentro da caixa de areia, você não pode fazer nada disso, porque não pode suspender aquilo em que o loop está sendo executado.
Caixas de areia se transformam em gado. Se alguém morrer no meio da sessão, o loop provisiona um novo e continua. Quando o arnês passa por dentro, a caixa de areia é a sessão, e perdê-la perde a sessão.
E multiusuário deixa de ser um problema de sistema de arquivos distribuído. Vários engenheiros na mesma organização administram o mesmo agente. Partilham competências, partilham memórias e, por vezes, investigam o mesmo incidente em paralelo. Quando o chicote é executado fora da sandbox, este é um banco de dados compartilhado. Quando ele é executado internamente, voltaremos ao problema do sistema de arquivos distribuído.
Os chicotes locais prontos para uso param de funcionar quando você remove o loop, porque todos eles assumem um sistema de arquivos local. A execução durável se torna seu problema, porque uma sessão de agente pode durar horas e precisa sobreviver às implantações. E uma vez que o chicote e a sandbox vivem em máquinas diferentes, o “sistema de arquivos” deixa de ser algo que você pode apontar.
Escolhemos o modelo externo. O restante deste post é sobre as três coisas que tivemos que resolver para que funcionasse.
Execução durável
Um loop de agente é uma função de longa duração. Minutos no mínimo, horas no nosso caso. Ele precisa sobreviver a implantações contínuas, eventos de escala e falhas de instância. Manter o loop na memória em um servidor API morre na primeira vez que você envia uma nova versão.
Já executamos nosso pipeline de ingestão de CI no Inngest, sobre o qual escrevemos em um post anterior. Estendê-lo ao loop do agente foi a mesma decisão pelos mesmos motivos: bom DX, nenhum cluster para rodar e não precisávamos de toda a generalidade do Temporal. O loop é uma função Inngest. Cada turno é um passo e o Inngest marca cada um deles. Se o servidor for reiniciado, o loop continuará de onde parou.
Ciclo de vida da sandbox
O loop fica suspenso na maior parte do tempo: durante chamadas LLM, entre chamadas de ferramentas, enquanto aguarda um fluxo de trabalho de longa duração como CI. Queremos que o sandbox também seja suspenso e ativo apenas quando o agente estiver executando um comando. O problema são as partidas a frio. Uma caixa de areia fria leva segundos para girar, o que fica para sempre dentro de uma curva interativa.
Usamos Blaxel para isso. Blaxel nos dá 25ms de retomada do modo de espera. Suspendemos o sandbox quando o agente não está executando um comando e o retomamos no instante em que está. 25ms é baixo o suficiente para que o agente não saiba que o sandbox já desapareceu.
O sistema de arquivos
Os equipamentos de agentes modernos não são apenas bash e um LLM. Eles têm habilidades (fragmentos de prompt que o agente lê sob demanda), memórias (notas que o agente escreve para si mesmo ou para o usuário), subagentes, planos, listas de tarefas. Tudo isso pressupõe um sistema de arquivos local. Uma habilidade é um arquivo em .claude/skills/foo.md. Uma memória é um arquivo em .claude/memory/MEMORY.md. O arnês lê e grava-os com o mesmo read e write ferramentas que ele usa para o código-fonte.
Isso funciona em um laptop. Não funciona quando o arnês está fora da caixa de areia.
A caixa de areia é descartável. Nós o tratamos como efêmero: suspenso, retomado, morto, reaparecido. Se ele morrer e criarmos um novo, seja lá o que o agente escreveu para .claude/memory/MEMORY.md desapareceu. Você poderia manter uma sandbox de longa duração por sessão para preservar o estado, mas depois voltaria a cuidar de uma sandbox por sessão e perderia todas as outras propriedades desejadas.
O outro problema é multiusuário. O laptop de um usuário executa um agente para uma pessoa. Nosso agente atende dezenas de engenheiros na mesma organização. As habilidades são organizacionais: todos em uma equipe compartilham o mesmo manual de triagem. As memórias também. Se o agente descobrir na segunda-feira que a equipe X sempre implanta a partir de uma ramificação de lançamento, a sessão de terça-feira para um engenheiro diferente da mesma equipe deverá saber.
Você pode fingir que o sandbox tem um sistema de arquivos local, gravar nele e sincronizar tudo com um banco de dados na saída. Isso funciona no caso de usuário único. No caso multiusuário, você acabou de construir um sistema de arquivos distribuído. Duas sessões em execução ao mesmo tempo gravam no mesmo arquivo de memória e você precisa reconciliá-las. Três engenheiros acionam o agente no mesmo incidente e todos veem o estado obsoleto até o término das sessões. Resolução de conflitos, consistência eventual, invalidação de cache.
A resposta clara é parar de fingir. Coloque memórias e habilidades em um banco de dados. O chicote os lê do banco de dados quando o agente os solicita e os grava de volta quando o agente os atualiza.
Mas ainda queremos que o agente pense em termos de arquivos.
Uma interface, dois back-ends
O chicote virtualiza o acesso ao sistema de arquivos. O agente tem um read ferramenta, um write ferramenta, um edit ferramenta. Quando o agente liga para eles, o chicote analisa o caminho e roteia a chamada com base no significado do caminho.
Os caminhos sob a área de trabalho vão para a sandbox, como sempre faziam. Os caminhos nos namespaces de habilidade e memória vão para o banco de dados. Uma gravação em um caminho de memória é uma transação de banco de dados com escopo definido para a organização. Uma leitura para um caminho de memória também vem do banco de dados, portanto, duas sessões paralelas na mesma organização veem a mesma memória no instante em que ela é gravada.
O agente não sabe a diferença. Pelo que posso dizer, existe um sistema de arquivos e ele lê e grava arquivos. Alguns desses arquivos residem no Postgres. Alguns vivem em uma caixa de areia que atravessa o país.
Por que não apenas adicionar ferramentas
A alternativa óbvia é dar ao agente memory_read e memory_write ferramentas ao lado read e write. Isso funciona e é o que a maioria das pessoas faz. Nós mesmos fizemos isso antes de termos a camada de virtualização.
O problema é que mais ferramentas pioram os agentes. Cada ferramenta dilui a atenção que o modelo presta a todas as outras ferramentas, torna o prompt mais longo e adiciona outra decisão que o modelo deve tomar a cada passo. Duas ferramentas que fazem quase a mesma coisa, read e memory_readsão especialmente ruins, porque o modelo precisa eliminá-los do contexto e às vezes escolhe errado.
A outra razão é mais importante. Os modelos de fronteira de treinamento antrópicos e todos os outros estão quase certamente fazendo aprendizado por reforço em equipamentos que se parecem com o Código Claude. Esse treinamento molda os modelos para serem bons em uma superfície de API específica: read(path), write(path, content), edit(path, old, new). Se você inventar memory_readvocê está fora do caminho treinado. Você obtém tudo o que o modelo aprendeu em geral, menos tudo o que aprendeu sobre as convenções exatas nas quais foi treinado.
A interface virtualizada mantém a superfície da API na qual o modelo foi treinado e coloca a semântica do banco de dados onde precisamos no back-end.
O que ainda é difícil
O SOTA se move rapidamente. A cada poucas semanas, um novo padrão (subagentes, planos, tarefas em segundo plano) chega ao Claude Code ou algum lugar semelhante, e quase sempre assume um sistema de arquivos local. Podemos interceptar a maioria das coisas, mas sempre há uma lacuna entre o envio de um novo recurso e nossa camada de virtualização que o trata corretamente. Não administrar estoque Claude Code é um custo real.
Escolhemos prefixos de caminho (/skills/, /memory/) que refletem o layout local de Claude Code, e isso provavelmente vai nos incomodar. O layout do Claude Code ainda está em mudança e estamos a uma mudança de convenção de ter que migrar tudo. A resposta certa pode ser expor uma interface totalmente diferente. Mas veja acima: o objetivo era manter a interface idêntica àquela em que o modelo foi treinado.
Bash é um vazamento. O arnês pode interceptar read('/skills/foo.md') porque é uma chamada de ferramenta estruturada. Mas o agente também possui uma ferramenta bash e nada impede que ela seja executada grep -r 'foo' /skills/ em uma sessão bash. Bash ignora a camada de virtualização e atinge o sistema de arquivos real do sandbox, onde /skills/ não existe. Lidamos com isso com duas proteções de melhor esforço: o prompt do sistema informa ao agente para não usar o bash para namespaces virtualizados e analisamos as invocações do bash com o tree-sitter para capturar chamadas que chegam a esses caminhos. Nenhum dos dois é hermético. Está bom o suficiente por enquanto.
Consistência é a parte que não respondemos. Quando duas sessões na mesma organização estão atualizando a memória, o que elas devem ver? A serialização estrita é tentadora e provavelmente errada, porque os agentes não são bancos de dados e bloquear uma sessão na gravação de outra abre padrões de impasse para os quais não temos respostas. Estamos executando as vitórias do último escritor por chave, o que é bom para os casos que encontramos e quase certamente irá quebrar de maneiras que podemos prever.
Fonte: theverge

