Una tool es un contrato
Cuando un modelo decide usar una herramienta no ve la función TypeScript que acabará ejecutándose. Solo recibe tres piezas: name, description e inputSchema. Ese bloque determina si escoge la herramienta correcta y si construye argumentos que la aplicación puede aceptar.
Es el identificador estable que devuelve el modelo y que el ejecutor usa para localizar el handler.
Explica cuándo usar la tool, qué garantiza y qué pasos previos necesita. Es parte del prompt del agente.
Es el JSON Schema que delimita campos, tipos y argumentos obligatorios antes de ejecutar código local.
Un catálogo canónico para 13 herramientas
Gymnasia guarda las 13 definiciones en apps/mobile/agent/toolDefinitions.ts. Memoria personal, medidas, dieta, ejercicios, rutinas y peticiones de mejoras parten de ese único array. No existe una copia independiente por proveedor.
Esta es una definición real del catálogo:
{
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"]
}
}Los nombres actuales son 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 y create_feature_issue.
La descripción también es prompt
Una descripción pobre como "Lee medidas" documenta la función para una persona, pero deja al modelo sin criterio: no dice qué fecha usar, qué devuelve ni en qué momento conviene llamarla.
La versión real explica intención, resultado y contexto. En herramientas con dependencias también impone el orden: add_meal_food indica que antes debe ejecutarse search_foods, y create_routine exige buscar los nombres exactos mediante search_exercises. Esa información mueve la precisión de elección más que cualquier comentario interno, porque es lo que el modelo sí puede leer.
Tres proveedores, una definición
OpenAI, Anthropic y Google expresan el mismo contrato con envoltorios distintos. CHAT_TOOLS proyecta el catálogo en el momento de construir la petición:
type: "function" y el esquema bajo parameters.
Nombre y descripción directos, con el esquema bajo input_schema.
Declaraciones dentro de functionDeclarations y esquema bajo 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
})) }]
};La sintaxis cambia; el nombre, la descripción y el esquema no. Añadir o corregir una herramienta en el catálogo actualiza los tres formatos.
Definición y ejecución local
Declarar una tool no la implementa. apps/mobile/agent/toolExecutor.ts mantiene el mapa AGENT_TOOL_HANDLERS: cada nombre del catálogo debe tener exactamente un handler y ningún handler puede quedar huérfano.
Los handlers leen o modifican el estado local, AsyncStorage y los repositorios JSON empaquetados con la app. Después devuelven texto al bucle agentic para que el modelo continúe. No hay base de datos ni backend de Gymnasia entre la tool y los datos del usuario; la aplicación móvil conserva el control de la ejecución.
Tests contra la divergencia
El contrato se verifica en varias capas. Los tests exigen nombres únicos y válidos, descripciones significativas, esquemas de tipo objeto y campos required declarados en properties. Ajv compila los 13 JSON Schemas para detectar construcciones inválidas.
const ajv = new Ajv({ allErrors: true, strict: true });
for (const definition of AGENT_TOOL_DEFINITIONS) {
expect(() => ajv.compile(definition.inputSchema)).not.toThrow();
}Otras pruebas comparan el conjunto completo de definiciones con los handlers y con las proyecciones de los tres proveedores. validateToolInput se somete además a entradas arbitrarias con fast-check: nunca debe lanzar una excepción y siempre debe producir un resultado de validación estructurado.
El resultado es sencillo de mantener: una sola fuente de verdad, tres adaptadores mecánicos y pruebas que hacen visible cualquier divergencia antes de que llegue al agente.