Três ferramentas em um único tool_call já não é erro: o Hermes divide o batch

Você tem três servidores MCP conectados ao Hermes: um lê arquivos, outro consulta o git e um terceiro abre tickets no rastreador da sua empresa. Então você pede a ele que “passe os olhos nesses arquivos e registre um ticket”, e ele faz o que qualquer interlocutor razoável faria — coloca três entradas em um único tool_call. Você espera três resultados. O que recebe é uma mensagem de correção e um round trip desperdiçado, porque o agente precisa repensar tudo do zero e reenviar as chamadas uma de cada vez.
O PR #131249 fecha essa lacuna. Agora o agente divide o batch em chamadas individuais antes de o turno ser persistido. Veja a seguir o mecanismo, os limites e o que de fato muda para você.
De onde vinha a rejeição
Para entender o erro, primeiro é preciso saber que o Hermes carrega as ferramentas MCP e de plugin de forma adiada (deferred). Elas não estão na lista de ferramentas sempre presente — o modelo descobre o nome exato com tool_search e depois a invoca via tool_call. É um design que economiza tokens, descrito no nosso guia de busca de ferramentas.
O argumento calls de tool_call é um array, então a pergunta é natural: se é um array, por que não colocar três entradas nele?
Antes desta correção, a resposta era não. O contrato antigo dizia que “conectores podem agrupar em batch, ferramentas locais são uma por chamada”. Um modelo que colocava duas ou mais ferramentas locais em um mesmo batch recebia tudo de volta com uma correção:
tool_call takes exactly one entry for local tools; you sent 2.
Retry with only: {"calls":[{"name":"mcp__files__read","arguments":{...}}]}...
A mensagem era correta, mas não saía de graça. O modelo tinha de replanejar e reenviar. O autor do PR mediu isso em sessões reais: o mesmo batch local de duas entradas custava 9 mensagens e 33 segundos antes da correção (rejeitado, reenviado como chamadas isoladas) e 7 mensagens e 16 segundos depois (dividido e executado). Quase toda a economia é tempo de raciocínio refeito pelo modelo.
A correção: dividir antes de persistir
A ideia não tem nada de glamouroso. Em vez de deixar o dispatcher rejeitar um batch, divida-o antes.
A implementação é um arquivo novo, agent/tool_call_batches.py, com o ponto de entrada expand_local_tool_batches(), chamado uma vez a partir de agent/turn_tool_round.py. A posição é o que importa: antes de a mensagem do assistente ser persistida. Só o turno ainda não persistido é tocado, então o histórico em cache nunca é reescrito.
Depois da divisão, cada entrada segue o caminho normal de ferramenta em vez de passar pela ponte:
- verificação de escopo, validação de schema, parsing de argumentos;
- hooks
pre_tool_calle guardrails; - as aprovações são resolvidas por entrada — uma negação não derruba as demais;
- aplica-se a política de concorrência da própria ferramenta subjacente, então servidores que suportam paralelismo continuam rodando em paralelo;
- cada entrada recebe seu próprio resultado pareado e sua própria gravação incremental.
Dois detalhes importam mais do que parecem:
Os ids das chamadas filhas são determinísticos. A primeira entrada reaproveita a chamada pai; as demais recebem ids de deterministic_call_id(), derivados do id pai, dos argumentos e do índice. Assim, as reproduções (replays) geram ids estáveis em vez de uma sessão que parece ter chamado um conjunto diferente de ferramentas a cada tentativa.
As listas de blocos nativas do provider são reescritas no lugar. A Anthropic e o Bedrock reproduzem blocos de conteúdo ordenados em vez de reconstruir o histórico a partir de tool_calls, então a lista de blocos é editada na posição: os blocos de raciocínio assinados permanecem idênticos byte a byte e mantêm a ordem. Isso importa para qualquer modelo com raciocínio criptografado — altere a assinatura e o provider rejeita todo o histórico reproduzido.
O que continua sendo rejeitado
A divisão só se aplica a batches que podem e devem ser divididos. O resto permanece inalterado:
| Caso | Comportamento |
|---|---|
Um batch só com nomes connectors__ |
Continua sendo um único dispatch de gateway, com a mesma forma de antes |
| Batches malformados (não analisáveis, entradas que não são objetos) | Ficam a cargo do erro do dispatcher |
| Mais de 10 entradas | Ficam a cargo do erro do dispatcher (MAX_CALLS_PER_DISPATCH = 10) |
| Uma única entrada | Contrato inalterado |
Portanto, a antiga string de erro não desapareceu do código — ela ainda mora em tools/tool_search_validation.py. Agora ela atende os chamadores que ignoram o caminho de divisão; um turno conversacional normal não vai esbarrar nela. Manter o caminho antigo enquanto o caso comum é desviado é o formato mais seguro: nada que antes funcionava deixa de funcionar.
Quatro coisas que você vai notar
O texto do contrato mudou. A descrição da ferramenta tool_call antes dizia que as ferramentas locais adiadas eram uma por tool_call; agora diz que as entradas locais são divididas em chamadas individuais pelo agente, cada uma seguindo sua própria política de concorrência. A documentação oficial de busca de ferramentas foi atualizada para acompanhar. Um modelo não pode violar uma regra que já não conhece.
Os hooks disparam por entrada. Se você escreveu um plugin pre_tool_call que bloqueia gravações, o comportamento antigo era tudo ou nada para um batch. Agora o hook dispara uma vez por entrada, com o nome da ferramenta subjacente e o id de chamada daquela entrada, então uma gravação pode ser bloqueada enquanto uma leitura do mesmo batch continua rodando.
Os resultados voltam na ordem de entrada. Os três resultados se reencaixam na ordem original com contagens recalculadas, então uma divisão não reembaralha o que o modelo vê.
Interromper é mais limpo. Aperte /stop no meio de um batch e as entradas que ainda não começaram nunca são enviadas, então nada fica pendurado.
Como obter
A correção foi mesclada na branch main em 2 de outubro de 2026. A versão com tag mais recente é a v0.21.5 (tag v2026.9.24), então essa mudança está somente na main por enquanto — uma instalação que acompanha a main a recebe no próximo hermes update, e quem usa release a recebe na próxima tag.
Não há nada para configurar; é uma correção do comportamento padrão, não um interruptor. A única coisa que vale a pena fazer é reler seus hooks pre_tool_call. Se você os escreveu de forma frouxa, partindo do princípio de que um batch simplesmente seria rejeitado, o disparo por entrada é o momento de apertar o bloqueio de gravações. A configuração de ferramentas adiadas está na referência de comandos MCP, e a superfície de configuração em torno de servidores MCP e variáveis de contexto é abordada em este guia.
A conclusão
Uma linha resume tudo: agora o modelo pode pedir várias ferramentas de uma vez, porque a responsabilidade de dividir saiu de “uma regra que o modelo precisa lembrar” para “um caminho que o agente sempre segue”.
Não é uma mudança vistosa, mas recupera um round trip em conversas reais — especialmente em configurações com vários servidores MCP e um modelo menor, onde uma única rejeição pode descarrilar um turno inteiro. Já que você está aqui, também vale a pena fazer um grep nas suas skills e nos seus system prompts: qualquer instrução que mande o modelo chamar uma ferramenta por vez agora está obsoleta.