Uma proposta, não execução
Imagine que alguém pergunta: “Quanta proteína tem o arroz?” O modelo pode responder com uma tool call: search_foods e o argumento {"query":"arroz"}. Essa saída é um dado do provedor, não uma chamada direta a uma função do aplicativo. O modelo não conhece o catálogo local, não tem acesso ao armazenamento e não pode invocar código por conta própria.
O harness é o programa que recebe a proposta, decide se a aceita e conecta o nome a uma função local. O identificador da chamada fica separado para associar depois o resultado ao pedido correto. OpenAI, Anthropic e Google transportam tool calls em formatos diferentes, mas, depois do parsing, o despachador trabalha com o mesmo par: name e args.
O percurso completo
A primeira barreira verifica se o nome foi declarado e se esse tipo de ação é permitido para o pedido. Só então o registro procura um handler. Neste exemplo, search_foods é uma leitura: o handler consulta o catálogo e devolve uma string com os resultados. Uma escrita local também precisa coordenar o efeito e confirmar que os dados foram salvos antes de informar sucesso.
Escolher a função correta
Um registro relaciona nomes públicos a funções. É a fronteira exata entre “o modelo quer fazer isto” e “o aplicativo executa esta função”. O exemplo reduzido mostra a decisão central:
const handlers = {
search_foods: searchFoods,
read_measurement: readMeasurement,
};
const handler = Object.hasOwn(handlers, call.name)
? handlers[call.name]
: undefined;
if (!handler) return "Tool não reconhecida";
return await handler(call.args, context, dependencies);Verificar que a propriedade pertence ao registro é importante: um objeto JavaScript também herda nomes como constructor e toString. Uma busca ingênua poderia confundi-los com handlers. Manter o registro fixo durante a execução garante que o mesmo nome sempre escolha a mesma função.
O schema anunciado ao provedor descreve o formato esperado dos argumentos, mas não torna confiável a saída do modelo. Cada handler deve validar os dados do seu domínio antes de uma escrita. Adicionar validação genérica na fronteira do despachador é uma melhoria separada; este registro ainda não oferece isso.
Erros e efeitos
Se o nome não foi declarado, o harness devolve um erro controlado sem executar nenhuma função. Se um handler falha, a conversa deve receber um resultado útil em vez de ser interrompida. Para uma leitura, basta devolver o dado ou o erro. Para uma escrita, “o handler foi chamado” não significa “os dados foram salvos”: o efeito é confirmado depois da persistência, e falhas ambíguas não são repetidas às cegas.
o modelo propõe a ação; o código do aplicativo decide a autorização, executa o handler e verifica o resultado. O nome de uma tool nunca concede permissão por si só.
Devolver o resultado
O handler produz texto. O loop do provedor o empacota com o identificador original: function_call_output na OpenAI, tool_result na Anthropic ou function_result no Google Interactions. Em seguida, pede outra resposta ao modelo. Assim, o modelo pode explicar o que o aplicativo encontrou sem inventar o conteúdo do catálogo.
Testes do contrato
O teste principal percorre todos os nomes declarados com argumentos válidos, verifica que cada um chega ao comportamento esperado e devolve texto, e compara a lista com os handlers registrados. Outros testes cobrem nomes desconhecidos, incluindo propriedades herdadas de objetos JavaScript, e garantem que nenhuma dependência de escrita seja chamada. Repetir o caso com um contexto novo detecta alterações acidentais na rota sem depender de um modelo remoto.
Aprender a construir um agente completo
Este artigo faz parte de uma série sobre como construir um agente ou harness usando um aplicativo de academia como caso prático. O passo anterior foi reconstruir uma chamada completa a partir do stream; aqui vimos onde ela vira código em execução. A próxima fronteira é validar os argumentos de forma sistemática antes de qualquer efeito.