O formato anunciado e os dados recebidos
Uma tool é uma função que o modelo pode solicitar. Seu input schema descreve os argumentos: quais campos são necessários e seus tipos. Se uma função registra uma medida corporal, declarar que o peso é um número não faz essa função conferir automaticamente cada valor recebido. Seu programa precisa executar essa verificação.
As restrições oferecidas pelo provedor podem ajudar a gerar dados no formato correto. O harness ainda precisa fazer cumprir seu contrato antes de agir, sobretudo quando aceita diferentes provedores. Uma declaração descreve o que é aceito; o validador compara essa declaração com uma chamada concreta.
Três perguntas antes de executar
São três verificações diferentes. Primeiro, o provedor terminou a resposta? Depois, é possível interpretar os argumentos como um objeto JSON? Por fim, esse objeto atende ao schema? Uma resposta completa pode conter argumentos incorretos; um JSON legível também.
- Os eventos do provedor chegam ao parser, que reconstrói a chamada pendente.
- Após o resultado do parser, o harness verifica se a resposta está completa e interpreta os argumentos.
- O validador compara campos e tipos com o schema daquela tool.
- Argumentos válidos entram no handler; os inválidos devolvem um erro sem executá-lo.
- O resultado volta ao modelo, que pode corrigir a chamada ou responder.
Um único ponto de verificação
O despachante, a função que escolhe o código executável pelo nome da tool, é um bom lugar para essa verificação. Leituras e escritas passam pelo mesmo ponto. Se cada handler inventa suas próprias regras para tipos e campos obrigatórios, adicionar uma nova tool pode deixar uma lacuna que ninguém percebe.
Defina cada tool uma única vez e use essa definição para apresentá-la ao provedor e conferir os argumentos recebidos. Esse padrão não precisa de um servidor: o Gymnasia faz a verificação localmente. Observe que o handler só é executado depois que o validador aceita a chamada:
when a complete tool call arrives:
find the tool definition
parse the arguments as a JSON object
errors = validate(arguments, definition.input_schema)
if errors exist:
return a tool result with the failing fields and reasons
return run_handler(arguments)
append the tool result to the conversation
ask the model for the next turn, within the round limitOpenAI, Anthropic e Google Interactions usam formatos diferentes para solicitar tools e receber resultados. Depois que a chamada é reconstruída, as tools do Coach chegam ao mesmo executor. O provedor compatível com OpenAI também usa esse executor. Não são necessárias quatro versões das regras de entrada.
Um erro que permite continuar
O Gymnasia registra o peso com uma tool chamada write_measurement. Este exemplo falha: data.weight_kg contém texto, embora pareça um número. A data e o restante do objeto podem estar corretos:
{"date":"2024-04-11","data":{"weight_kg":"75,5"}}O resultado informa que a tool não foi executada e identifica data.weight_kg como um campo que deve ser numérico. O harness devolve esse resultado ao modelo com a identidade correspondente da chamada. Na próxima rodada, o modelo pode enviar:
{"date":"2024-04-11","data":{"weight_kg":75.5}}O harness não converte o texto nem executa novamente por conta própria. O modelo propõe a correção e a nova chamada é verificada outra vez. Se os argumentos forem válidos, a tool é executada; se continuarem incorretos, recebe outro erro. Tudo ocorre dentro do limite de rodadas existente. Um erro conhecido de argumentos permite continuar; uma escrita de resultado incerto exige outro tratamento e não deve ser repetida como se nada tivesse ocorrido.
Casos reproduzíveis no Gymnasia
Estes casos vêm de testes do executor e de fixtures reproduzíveis, respostas de provedor preparadas para testes. Não são transcrições de conversas privadas nem uma medida da frequência com que um modelo erra.
| Entrada problemática | Comportamento anterior | Com validação central |
|---|---|---|
| Uma medida enviada como JSON dentro de uma string, com um número escrito como texto. | O contrato de medidas aceitava o formato legado e normalizava o valor. | O modelo deve enviar um objeto com valores numéricos, conforme o schema. A medida existente é preservada enquanto a chamada é corrigida. |
| Uma refeição escrita como “ desayuno ”. | O handler normalizava espaços e maiúsculas. | O Coach recebe os valores permitidos pelo enum e pode reenviar “Desayuno”. |
| JSON ilegível no OpenAI para uma tool sem campos obrigatórios. | O parser o substituía por um objeto vazio, que podia chegar ao handler. | Um erro de argumentos é devolvido sem executar a tool. Um objeto vazio legítimo continua válido se o schema permitir. |
| Um campo numérico opcional enviado como null. | O validador ignorava o valor. | O valor é rejeitado quando o tipo não admite null, inclusive em objetos ou arrays aninhados. |
O formato não basta para decidir
O schema verifica o formato, não toda a intenção ou as regras do produto. Uma data pode ser uma string e representar um dia impossível. Um identificador pode ter o tipo certo e apontar para um exercício inexistente. Datas, referências e séries continuam exigindo validação de domínio. Passar pelo schema também não dá permissão para uma ação que precisa de confirmação.
O validador próprio do Gymnasia cobre os tipos e restrições declarados no catálogo de tools, não toda a especificação JSON Schema. Não adiciona uma nova dependência ao bundle móvel nem transforma os argumentos. Campos extras são permitidos quando o schema não os proíbe e rejeitados quando additionalProperties é false. Um campo opcional pode ser omitido; isso não transforma null em um número. As referências de JSON Schema sobre objetos e null documentam essas regras.
Se seus schemas passarem a precisar de uniões, referências ou novas restrições, amplie o validador e seu contrato ou adote uma biblioteca que as suporte. Não anuncie uma regra que o programa nunca verifica. Tools que declaram uma string com JSON dentro também precisam interpretar e validar esse conteúdo.
O contrato também pode mudar durante o transporte. Em um teste real com OpenAI Responses, pedir apenas o peso fez o provedor exigir todas as medidas e o modelo preencher as demais com 0,01. Em um ensaio temporário com strict:false, o schema manteve esses campos opcionais e o modelo enviou somente o peso. A validação local continuou ativa. A documentação da OpenAI explica o valor padrão de strict.
Testar o contrato e seus efeitos
Os testes unitários devem cobrir argumentos válidos sem modificação, campos obrigatórios ausentes, tipos incorretos, enums, limites e objetos aninhados. O Gymnasia também compara seu validador com Ajv nos testes para o subconjunto de schemas utilizado. A geração de argumentos arbitrários, ou fuzzing, procura entradas que causem exceções ou divergências.
A propriedade de execução é observável: uma chamada inválida não carrega dados, resolve catálogos, cria IDs nem produz uma escrita do handler. Os testes de integração reproduzem uma medida inválida, o resultado de erro, uma chamada corrigida e uma única escrita. O E2E percorre essa sequência pelo chat com OpenAI, Anthropic e Google simulados, verifica o armazenamento e recarrega o aplicativo. Isso demonstra o comportamento local com essas respostas, não que um modelo real sempre se corrija.
A QA com um modelo real da OpenAI também encontrou uma mudança no contrato dos campos opcionais durante o transporte. A correção adicional já está publicada na web. A QA final com o código publicado, sem intervenção temporária na rede, registrou somente 75,9 kg e preservou as outras medidas ao recarregar.
Uma série sobre como construir agentes
Este artigo faz parte da série sobre construir um agente e seu harness usando um aplicativo de academia como exemplo prático. Cada artigo desenvolve uma decisão que você pode aplicar ao seu próprio agente.