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 much protein does rice contain?” The model may respond with a tool call: search_foods and the argument {"query":"rice"}. That output is provider data, not a direct call to an app function. The model does not know the local catalogue, cannot access its storage, and cannot invoke code by itself.

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: "rice" }) 2 The guard checks declared name · read allowed 3 The registry dispatches search_foods → handler 4 The handler runs reads the local food catalogue 5 The result returns string → model response
The arrow shows who acts next. The model proposes; application code decides whether to run the call.

The first barrier checks that the name is declared and this kind of action is allowed for the request. Only then does the registry look up a handler. In this example, search_foods is a read: its handler searches the catalogue and returns a string containing the results. A local write also needs its effect coordinated and confirmed as persisted before success is reported.

Choose the right function

A registry relates public names to functions. It is the exact boundary between “the model wants to do this” and “the application runs this function.” This reduced example shows the central decision:

const handlers = {
  search_foods: searchFoods,
  read_measurement: readMeasurement,
};

const handler = Object.hasOwn(handlers, call.name)
  ? handlers[call.name]
  : undefined;
if (!handler) return "Unknown tool";
return await handler(call.args, context, dependencies);

Checking that the property belongs to the registry matters: a JavaScript object also inherits names such as constructor and toString. A naive lookup could mistake them for handlers. Keeping the registry fixed during execution ensures that the same name always selects the same function.

The schema advertised to the provider describes the expected argument shape, but it does not make model output trustworthy. Each handler must validate its domain data before a write. Adding generic validation at the dispatcher boundary is a separate improvement; this registry does not yet provide it.

Errors and effects

If a name is undeclared, the harness returns a controlled error without running a function. If a handler fails, the turn should receive a useful result rather than crashing the whole chat. For a read, returning the data or error is enough. For a write, “the handler was called” does not mean “the data was saved”: the effect is confirmed after persistence, and ambiguous failures are not blindly retried.

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

The handler produces text. The provider loop packages it with the original identifier: function_call_output for OpenAI, tool_result for Anthropic, or function_result for Google Interactions. It then asks the model for another response. The model can explain what the app found instead of inventing the catalogue contents.

Contract tests

The core test visits every declared name with valid arguments, checks that each reaches its expected behavior and returns text, and compares that list with the registered handlers. Other tests cover unknown names, including inherited JavaScript object properties, and verify that no write dependency is called. Repeating a case with fresh context catches accidental routing changes without depending on a remote model.

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