From a tool call to executable code: an agent dispatcher

From a tool call to executable code: an agent dispatcher

A proposal, not execution

Imagine someone asks, “How many carbohydrates does rice contain?” The catalogue lists the food as arroz, so the model may propose a tool call: search_foods with the argument {"query":"arroz"}. That output is provider data, not a direct call to an app function. The model does not yet know which kinds of rice are in the catalogue or how many carbohydrates each contains: the app must perform the search.

The harness is the program that receives the proposal, decides whether to accept it, and connects the name to a local function. The call identifier is kept separately so the result can later be matched to the correct request. OpenAI, Anthropic, and Google transport tool calls in different formats, but after parsing, the dispatcher works with the same pair: name and args.

The full journey

From a request to a tool result The model proposes a call. The guard checks whether it is allowed, the registry selects a handler, the handler reads the data, and the result returns to the model. 1 The model proposes search_foods({ query: "arroz" }) 2 The guard checks declared name · read allowed 3 The registry dispatches search_foods → handler 4 The handler runs reads carbs per 100 g 5 The result returns string → model response
The arrow shows who acts next. The model proposes; application code decides whether to run the call.

To answer the rice question, the app first checks that search_foods is a declared tool and that the search is allowed. The registry then passes the call to the search function. This is a read: it returns catalogue entries matching “arroz”, including their carbohydrates per 100 g, and does not change the person's diet.

Choose the right function

A registry is a closed list that connects each tool name to an application function. For the rice question, the dispatcher receives search_foods, finds that name in the list, and runs the function that searches for foods. The registry does not choose a variety of rice or calculate its carbohydrates: it only decides which function handles the request. If the name is absent, it runs nothing. The list stays fixed during the turn, so the same name always leads to the same action.

In the article about defining tools, we saw that each tool is presented to the LLM through name, description, and inputSchema. For search_foods, this information helps the model send “arroz” as text in query. But describing the expected argument does not guarantee that the model sends it correctly. The dispatcher looks up name and passes the arguments to the chosen function; it does not fix an incorrect argument. For example, before adding rice to a meal, that function checks that the food exists and the amount in grams is valid.

Errors and effects

If the model misspells the name and requests search_foodz, the dispatcher reports that it does not recognize the tool and runs nothing. If it requests search_foods but the search finds no rice, the result says so. If the catalogue is unavailable, the result reports that too: “I found no rice” and “I cannot access the catalogue” mean different things. Neither case justifies making up a carbohydrate figure.

A failure while running a function is a separate case. For example, if the app tries to add rice to a meal and access to storage fails, the dispatcher returns a controlled error to the model so it can explain that the save could not be confirmed, without breaking the whole conversation.

A read ends by returning the data found or reporting that it could not be retrieved. A write needs confirmation of its effect: if someone asks to add rice to a meal, calling the function is not enough; the app should report success only after the food has been saved. If the app cannot tell whether it was saved, it does not automatically repeat the operation, because that could add the food twice.

Design rule:

the model proposes an action; application code decides authorization, runs the handler, and checks the outcome. A tool name never grants permission on its own.

Return the result

Suppose search_foods returns the “Arroz blanco (cocido)” entry (cooked white rice), which lists 28 g of carbohydrates per 100 g in the catalogue. The app sends that result to the model with the identifier of the original tool call, so it knows which search the result belongs to. Only then can it answer, “The cooked white rice in the catalogue has 28 g of carbohydrates per 100 g.” If several varieties of rice appear, it should distinguish them; if there are no results, it should say so instead of inventing a figure.

Each provider has its own format for returning the result: function_call_output in OpenAI, tool_result in Anthropic, and function_result in Google Interactions. After sending it, the app asks the model to continue and write the answer for the person.

Contract tests

To test the dispatcher without depending on a remote LLM, the tests simulate its tool calls. One test sends search_foods with {"query":"Arroz"} and checks that the search function returns the rice entry from the test data. Other calls cover the remaining published names. The tests also compare those names with the registered handlers: if a tool is advertised without code to run it, the test fails.

Next, they try unknown names and check that nothing is written. Finally, they repeat each case with independent starting data: the same call should follow the same route and produce the same result. This catches accidental changes in the dispatcher without waiting for a model to choose the tool in a real conversation.

Learning to build a complete agent

This article belongs to a series on building an agent or harness through the example of a gym app. The previous step was reconstructing a complete call from a stream; here we saw where it becomes running code. The next boundary is systematically validating arguments before any effect.

See the complete Gymnasia agent series index.

Continue reading

Last posts -->

Have you seen these projects?

Gymnasia

Gymnasia Gymnasia
Expo
React Native
TypeScript
OpenAI
Anthropic

Fitness app with two agents that run entirely on the device, with no backend, so the user's data never leaves the phone. A BYOK conversational coach with local tools and a remote system prompt with offline fallback, plus a food estimator that pulls macronutrients out of a photo of the plate, with barcode scanning against OpenFoodFacts.

LangGraph Deep Researcher

LangGraph Deep Researcher LangGraph Deep Researcher
Python
LangGraph
FastAPI
React
TypeScript
Docker

Multi-agent research system built with LangGraph. A supervisor breaks your question down into topics and launches search sub-agents in parallel; each one compresses its findings before handing them to a writer agent that produces the final sourced markdown report. Live streaming over WebSockets, a configurable model per role and bring-your-own API keys that are never persisted server-side.

Tau

Tau Tau
Python
LangChain

Multi-agent tutoring system for secondary school students, with one agent per subject and course material written and validated by a team of teachers. It was used with real students at a private school in Spain and at a secondary school in Colombia.

View all projects -->
>_ Available for projects

Do you have an AI project?

Let's talk.

maximofn@gmail.com

Machine Learning and AI specialist. I develop solutions with generative AI, intelligent agents and custom models.

Do you want to watch any talk?

Last talks -->

Do you want to improve with these tips?

Last tips -->

Use this locally

Hugging Face spaces allow us to run models with very simple demos, but what if the demo breaks? Or if the user deletes it? That's why I've created docker containers with some interesting spaces, to be able to use them locally, whatever happens. In fact, if you click on any project view button, it may take you to a space that doesn't work.

Flow edit

Flow edit Flow edit

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
View all containers -->
>_ Available for projects

Do you have an AI project?

Let's talk.

maximofn@gmail.com

Machine Learning and AI specialist. I develop solutions with generative AI, intelligent agents and custom models.

Do you want to train your model with these datasets?

short-jokes-dataset

HuggingFace

Dataset with jokes in English

Use: Fine-tuning text generation models for humor

231K rows 2 columns 45 MB
View on HuggingFace →

opus100

HuggingFace

Dataset with translations from English to Spanish

Use: Training English-Spanish translation models

1M rows 2 columns 210 MB
View on HuggingFace →

netflix_titles

HuggingFace

Dataset with Netflix movies and series

Use: Netflix catalog analysis and recommendation systems

8.8K rows 12 columns 3.5 MB
View on HuggingFace →
View more datasets -->