A tool is a contract
When a model decides to use a tool, it cannot see the TypeScript function that will eventually run. It receives only three pieces: name, description and inputSchema. That block determines whether it selects the right tool and whether it builds arguments the application can accept.
The stable identifier returned by the model and used by the executor to find the handler.
Explains when to use the tool, what it guarantees and which prior steps it needs. It is part of the agent prompt.
The JSON Schema that constrains fields, types and mandatory arguments before local code runs.
One canonical catalogue for 13 tools
Gymnasia keeps all 13 definitions in apps/mobile/agent/toolDefinitions.ts. Personal memory, measurements, diet, exercises, routines and feature requests all start from that single array. There is no independent copy for each provider.
This is a real definition from the catalogue:
{
name: "read_measurement",
description:
"Lee las medidas corporales del usuario para una fecha específica. " +
"Devuelve el registro de medidas de ese día si existe. " +
"Usa esta herramienta cuando el usuario pregunte por sus medidas de un día concreto.",
inputSchema: {
type: "object",
properties: {
date: stringProperty("Fecha en formato YYYY-MM-DD")
},
required: ["date"]
}
}The current names are save_personal_data, list_personal_data_keys, read_field_description, read_field_value, read_measurement, write_measurement, read_meal_foods, search_foods, add_meal_food, search_exercises, read_routines, create_routine and create_feature_issue.
The description is prompt too
A weak description such as "Read measurements" documents the function for a person, but gives the model no decision criteria: it does not say which date to use, what comes back or when the call is appropriate.
The real version explains intent, result and context. Tools with dependencies also enforce ordering: add_meal_food says that search_foods must run first, while create_routine requires exact exercise names obtained through search_exercises. That information has more influence on selection accuracy than any internal comment because it is what the model can actually read.
Three providers, one definition
OpenAI, Anthropic and Google express the same contract through different wrappers. CHAT_TOOLS projects the catalogue when the request is built:
type: "function", with the schema under parameters.
Direct name and description, with the schema under input_schema.
Declarations inside functionDeclarations, with the schema under parameters.
const CHAT_TOOLS = {
openai: definitions.map(tool => ({
type: "function", name: tool.name,
description: tool.description, parameters: tool.inputSchema
})),
anthropic: definitions.map(tool => ({
name: tool.name, description: tool.description,
input_schema: tool.inputSchema
})),
google: [{ functionDeclarations: definitions.map(tool => ({
name: tool.name, description: tool.description,
parameters: tool.inputSchema
})) }]
};The syntax changes; the name, description and schema do not. Adding or fixing one tool in the catalogue updates all three formats.
Definition and local execution
Declaring a tool does not implement it. apps/mobile/agent/toolExecutor.ts holds the AGENT_TOOL_HANDLERS map: every catalogue name must have exactly one handler, and no handler may be left orphaned.
The handlers read or change local state, AsyncStorage and the JSON repositories bundled with the app. They then return text to the agentic loop so the model can continue. There is no Gymnasia database or backend between the tool and the user's data; the mobile application remains in control of execution.
Tests that prevent drift
The contract is verified in layers. Tests require unique, valid names, meaningful descriptions, object schemas and required fields declared in properties. Ajv compiles all 13 JSON Schemas to catch invalid constructs.
const ajv = new Ajv({ allErrors: true, strict: true });
for (const definition of AGENT_TOOL_DEFINITIONS) {
expect(() => ajv.compile(definition.inputSchema)).not.toThrow();
}Other tests compare the complete definition set with the handlers and with all three provider projections. validateToolInput is also exercised with arbitrary inputs through fast-check: it must never throw and must always return a structured validation result.
The result is straightforward to maintain: one source of truth, three mechanical adapters and tests that expose any drift before it reaches the agent.