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
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.
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.