O modelo mandou um array como string — então por que só algumas ferramentas reclamam?


Você pede ao agente para usar o clarify e fazer cinco perguntas de uma vez. Ele chama a ferramenta todo animado e volta com um erro de tipo. Você tenta de novo com outro modelo, muda um pouco a formulação e funciona. O que piora tudo: troque para outra ferramenta, repita exatamente o mesmo erro — um array enviado como string JSON — e nada nunca quebra.

Essa sensação de “às vezes funciona” normalmente não é o modelo. São dois pontos de entrada diferentes para chamadas de ferramenta, um que conserta os argumentos em silêncio e outro que não. Um patch mesclado em 2026-09-29 (PR #127781) alinhou os dois. Veja o que deu errado, o que mudou e o que continua se comportando como antes.

O ponto de entrada compartilhado já reparava os argumentos para você

Toda chamada de ferramenta do registry no Hermes passa por um único lugar:

# model_tools.handle_function_call
args = coerce_tool_args(tool_name, args)

A lógica em tools/arg_coercion.py é o único ponto do código para reparar tipos de argumento. Ela trabalha contra o próprio JSON Schema da ferramenta e corrige:

  • "42" para 42 (string para número)
  • "true" para true (string para booleano)
  • uma string codificada em JSON para um array ou objeto, inclusive quando aninhada dentro de um contêiner
  • um valor único para uma lista de um item, quando o schema pede um array

Ela é deliberadamente conservadora: só altera um valor quando existe exatamente uma resposta clara, e mantém o original quando a correção é ambígua. É também por isso que você nunca a percebe — ninguém comenta sobre um reparo que funcionou, só sobre uma chamada que falhou.

O problema: ferramentas inline nunca pegam esse caminho

Um conjunto de ferramentas é “inline”: implementado pelo próprio agente e registrado na tabela INLINE_TOOL_EXECUTORS, em agent/inline_tool_executors.py. Isso inclui clarify, todo_list, memory, session_search, read_terminal, drive_preview, delegate_task e outras.

A pegadinha é que os dois caminhos de despacho consultam essa tabela diretamente, em vez de passar por handle_function_call:

  • caminho sequencial: _resolve_sequential_dispatch em agent/tool_executor.py
  • caminho concorrente: invoke_tool em agent/agent_runtime_helpers.py

Então essas ferramentas recebem argumentos brutos, nunca reparados:

  • o clarify rejeitava um array questions que chegava como string JSON;
  • o todo_list cometia o mesmo erro e sobrevivia porque carregava o próprio bloco de json.loads;
  • o memory e o session_search não faziam nada a respeito.

Uma ferramenta corrigida pela camada compartilhada, uma com patch escrito à mão, o resto sem nada — essa é a resposta para “por que só algumas ferramentas relatam erros de argumento”.

model tool call
  ├─ registry tool → handle_function_call → coerce_tool_args → tool
  └─ inline tool   → INLINE_TOOL_EXECUTORS → tool gets raw args   ← the gap

A correção: envolver a única tabela que os dois caminhos leem

O patch é contido — não toca em nenhum dos dois caminhos de despacho. Ele envolve a tabela que ambos consultam:

# agent/inline_tool_executors.py
def _coerced(name: str, executor: InlineToolExecutor) -> InlineToolExecutor:
    def _exec(agent, args: dict, ctx: InlineToolContext) -> Any:
        return executor(agent, coerce_tool_args(name, args), ctx)
    return _exec

INLINE_TOOL_EXECUTORS: Dict[str, InlineToolExecutor] = {
    name: _coerced(name, executor) for name, executor in _RAW_INLINE_TOOL_EXECUTORS.items()
}

Como os dois caminhos de despacho leem essa única tabela, um só wrapper cobre a execução sequencial e a concorrente. O tools/todo_tool.py então abandonou o json.loads feito à mão: seu schema já declara todos como array, então o reparo compartilhado cobre o caso — e manter uma segunda implementação só significava uma segunda fonte de verdade.

Vale dizer o que a correção não cobre: ferramentas de memory-provider no caminho concorrente não têm schema de registry, então o coerce_tool_args não tem informação de tipo sobre a qual agir. Isso segue igual ao que era antes — não é uma regressão. O patch apenas parou de deixar ferramentas com schema viverem sob dois padrões diferentes.

Um detalhe fácil de passar batido: os nomes de campo precisam bater antes

A primeira coisa que o coerce_tool_args realmente faz é mapear os nomes dos argumentos de volta para os nomes de fio (wire) do registry:

from tools.schema_sanitizer import unrename_tool_args
args = unrename_tool_args(schema.get("parameters"), args)

O schema que o modelo vê é sanitizado: chaves de propriedade que um provider recusa são renomeadas. Então os nomes de chave do modelo e os internos do registry não são exatamente idênticos. O reparo precisa restaurar os nomes usando o próprio schema do registry antes de tipar qualquer coisa por chave — erre a ordem e você estará reparando um campo que não existe.

Como verificar se funciona

O patch traz um comando de verificação (precisa de um checkout do repositório):

scripts/run_tests.sh tests/tools/test_todo_tool_type_coercion.py tests/agent/ tests/tools/test_clarify_tool.py

No uso diário, a diferença observável é que a mesma formulação agora se comporta igual entre as ferramentas. Antes, só ferramentas do registry perdoavam uma string JSON onde um array era declarado; agora clarify, todo_list, memory e session_search também perdoam. O jeito mais fácil de reproduzir isso você mesmo é fazer uma sessão perguntar várias coisas de uma vez pelo clarify — exatamente o cenário que expôs a lacuna.

Três conclusões práticas:

  1. Pare de ler erros de tipo como um problema de capacidade do modelo. Quando uma ferramenta relata um tipo de argumento inválido, verifique se ela recebeu um array na forma de string JSON; depois deste patch, esse tipo de ruído deve cair bem.
  2. Ao escrever skills ou prompts personalizados, descreva os parâmetros como o schema os descreve. O reparo é rede de segurança, não o objetivo do design — dizer ao modelo que questions é um array de strings é mais firme do que torcer para que ele acerte todas as vezes.
  3. Continue declarando schemas nas ferramentas que você mesmo escreve. O reparo depende inteiramente do schema; parâmetros sem um (como as ferramentas de memory-provider acima) nunca serão reparados automaticamente.

Este texto dá sequência a uma linha de dois artigos anteriores: o texto sobre clarify em lote mostra quanta ida e volta economiza fazer várias perguntas de uma só vez, e a dieta de schema de ferramentas trata de encolher schemas para economizar tokens. Este patch é o terceiro tijolo: tornar o reparo de argumentos consistente antes que uma ferramenta sequer veja os argumentos. Para o lado do todo, veja o texto sobre subtarefas aninhadas.

A versão curta

  • O reparo de argumentos acontece apenas em handle_function_call → coerce_tool_args; ferramentas inline são consultadas diretamente por dois caminhos de despacho e nunca passam por ele.
  • Por isso o mesmo erro de “array enviado como string JSON” fazia o clarify falhar, o todo_list disfarçá-lo com json.loads e o memory mais o session_search não fazerem nada.
  • Agora toda a tabela INLINE_TOOL_EXECUTORS é envolvida uma única vez, cobrindo os caminhos sequencial e concorrente; o parse feito à mão no todo_list sumiu.
  • O reparo precisa de um schema e deve primeiro mapear os nomes de campo sanitizados de volta para os nomes do registry; ferramentas de provider sem schema mantêm o comportamento anterior.
  • Trate erros de tipo como um problema de mecanismo, descreva parâmetros conforme o schema nos seus prompts e continue declarando schemas nas suas próprias ferramentas.

Leitura relacionada: a documentação de ferramentas e toolsets, e hermes config neste site.