Qué es Gymnasia
Gymnasia es una aplicación móvil de entrenamiento personal hecha con React Native y Expo. Dentro lleva dos agentes de IA: un coach conversacional y un estimador de comidas que calcula calorías y macronutrientes a partir de fotos.
Van en paralelo, no uno dentro de otro: ninguno es subagente del otro, no se llaman entre ellos, viven en pantallas distintas y cada uno tiene su propio system prompt, sus propias tools y su propio proveedor de LLM. Compartir, comparten una sola cosa: los datos del usuario guardados en el móvil.
Lo interesante, y de lo que va este post, es que los dos se ejecutan enteros en el dispositivo. No hay backend propio: quien decide qué hacer, quien ejecuta las tools y quien guarda los datos es la propia app, dentro del teléfono. El modelo se llama directamente desde ahí con una clave de API que pone el usuario, lo que se suele llamar BYOK, Bring Your Own Key: cada usuario trae su propia clave, y con ella paga y controla su acceso al proveedor. Los datos del usuario nunca salen del móvil salvo por una única excepción que se explica más abajo.
Este es el post principal de la serie. Debajo están los dos grafos de arquitectura, y al final el índice de la serie con los posts que van saliendo sobre cada parte del desarrollo.
El agente general: Gymnasia Coach
Desliza el grafo en horizontal para verlo entero.
Gymnasia Coach es el agente conversacional general de la app. Es un agente BYOK: el usuario configura la clave de API del proveedor de LLM que prefiera, y la app llama a ese proveedor directamente desde el dispositivo. No existe backend propio: la orquestación, la ejecución de las tools y el almacenamiento ocurren dentro de la propia app.
Configuración y elección de proveedor
El usuario guarda su API key
Guarda la clave de los proveedores que quiera y elige cuál atiende el chat. El chat y el estimador de comidas se configuran por separado: cada uno puede ir con un proveedor distinto.
El prompt no viaja dentro de la app
El system prompt se descarga de un fichero público en el repositorio, se guarda en caché local y cae a una copia embebida si falla la red.
Esto último no es un capricho de arquitectura, es una válvula de seguridad. Si un día el agente está soltando barbaridades como «pasa una semana sin comer para bajar la grasa» y cosas así, quiero poder corregir las instrucciones, es decir, el system prompt, lo más rápido posible, no dentro de dos semanas, y desde luego no dependiendo de que cada usuario se acuerde de actualizar la app. Con el prompt en remoto, arreglarlo es editar un fichero de texto: el siguiente mensaje que envíe cualquiera ya va con la versión corregida. La copia embebida existe solo para que la app siga funcionando sin conexión.
El precio es que ese fichero pasa a ser una entrada de confianza para todos los usuarios a la vez, con lo que hay que tratarlo como código: revisado, versionado y con caché.
Llamada al proveedor
La app no habla con un proveedor concreto, habla con «el proveedor que toque». Hoy hay tres soportados, y la lista está pensada para crecer:
Cada proveedor tiene su propio adaptador de request/response, porque los formatos de tools, de streaming y de bloques de contenido son distintos en cada uno. Añadir un proveedor nuevo es escribir un adaptador más, no tocar el agente: todos comparten el mismo bucle agentic que viene ahora.
Bucle agentic (tool use)
Modelo responde
Texto en streaming y/o bloques tool_use / function_call.
La app ejecuta la tool
El modelo no ejecuta nada: solo pide. Quien ejecuta es la app, en local y contra sus propios datos.
Resultado → modelo
Se reinyecta como tool_result / function_call_output. Se repite hasta que no pide más tools.
Las tools
El coach sí toca los datos del usuario: los lee y los escribe. Puede apuntar el peso de hoy, añadir un alimento a la comida o crear una rutina entera. Lo que no puede es salirse de ahí: su capacidad de actuar es exactamente la lista de tools declaradas, ni una más. Están agrupadas en cuatro familias, y todas se ejecutan dentro del móvil.
🧠 Memoria personal
Leer y guardar lo que el usuario cuenta de sí mismo: objetivo, lesiones, preferencias. Es lo que hace que el coach recuerde de una conversación a la siguiente.
🍽️ Dieta
Buscar alimentos en el catálogo que viene con la app, leer lo que ya se ha comido en una fecha y añadir alimentos a una comida.
🏋️ Entrenamiento
Buscar ejercicios por músculo, equipamiento o dificultad, leer las rutinas del usuario y crear rutinas nuevas.
📏 Medidas
Leer y escribir medidas corporales por fecha: peso, porcentaje de grasa, perímetros.
create_feature_issue
La única tool que sale del dispositivo: cuando el usuario pide una mejora de la app, abre un issue en el repositorio. Es el único punto por el que algo escrito en el chat acaba fuera del móvil, y por eso está separada del resto.
Cómo se declara cada una de estas tools, nombre, descripción, esquema de argumentos, y cómo se traduce ese catálogo al formato que espera cada proveedor da para un post entero, y lo tiene: Cómo declarar tools fiables para OpenAI, Anthropic y Google.
Almacenamiento y salida
Todo en el dispositivo
Dieta, rutinas, medidas y datos personales viven en el almacenamiento local del teléfono. Los catálogos de alimentos y ejercicios son ficheros estáticos que se empaquetan con la app.
La creación de issues
Único punto de salida a un servicio externo distinto del proveedor de LLM: create_feature_issue.
El segundo agente: el estimador de comidas
Desliza el grafo en horizontal para verlo entero.
El estimador de comidas saca calorías y macros de una foto del plato. No es un subagente del coach: el coach no lo llama nunca, ni sabe que existe. Es un segundo agente, con su propio system prompt, su propia tool y su propia elección de proveedor, que se abre desde otra pantalla y escribe en los mismos datos locales.
Entrada
1–6 fotos de la comida
Cámara o galería. También admite texto (preguntas de seguimiento sobre la estimación) reutilizando el contexto de la conversación.
Su propio proveedor
El estimador no hereda el proveedor del chat: se elige aparte, en ajustes. Y tiene sentido que así sea, porque los dos agentes no compiten por lo mismo. Al coach le pides razonamiento sobre texto; al estimador le pides mirar una foto.
Si el usuario no toca nada, el estimador arranca con el proveedor que hoy da mejor relación coste/calidad en visión, y solo si esa clave no está configurada busca otra. La lección general: la elección de modelo es por tarea, no por app. En cuanto un producto tiene dos usos de IA con perfiles de coste distintos, atarlos al mismo proveedor es pagar de más en el caro o rendir de menos en el barato.
System prompt especializado
Nutricionista visual
Estima siempre kcal, proteína (g), carbohidratos (g), grasa (g) y peso total (g). Da rangos si hay incertidumbre.
Clasificación
Determina si es producto_comercial, receta o alimento base genérico.
Salida estructurada
Si el usuario pide "Devuelve json", responde solo con JSON: dish_name, calories_kcal, protein_g, carbs_g, fat_g.
Bucle agentic con el código de barras
¿Hay un código de barras en la foto?
El prompt obliga al modelo a usar la tool si detecta un EAN/UPC en cualquiera de las imágenes.
scan_barcode(barcode)
Llama a OpenFoodFacts (API pública) con el código leído y devuelve datos nutricionales exactos del producto.
Producto comercial confirmado
Si se usó scan_barcode, la clasificación es siempre producto_comercial, con datos exactos en vez de estimados.
Igual que en el coach, el resultado de la tool se reinyecta al modelo y el bucle se repite, con un máximo de 5 rondas, hasta obtener una respuesta final. Ese tope no es decorativo: sin él, un modelo que se empeñe en volver a llamar a la misma tool se queda dando vueltas y gastando tokens del usuario.
Persistencia del resultado
Confirmación del usuario
La estimación se enseña primero en lenguaje natural. Solo cuando el usuario la acepta se pide una segunda respuesta, esta vez en JSON, y se parsea.
add_meal_food
Se añade a la dieta local del día y la comida seleccionados, en los mismos datos que usa el coach.
Variante: estimación manual
El usuario describe un alimento por texto
Flujo conversacional: el usuario nombra el alimento, el modelo pregunta ingredientes y cantidades si faltan, calcula valores por 100 g o unidad, el usuario confirma y devuelve el JSON para guardar en el catálogo de alimentos.
La serie
Este post es la portada. Cada parte del desarrollo del agente se cuenta en un post propio, y todos se van enlazando aquí.
- Cómo declarar tools fiables para OpenAI, Anthropic y Google: diseñar tools que un LLM pueda descubrir, adaptarlas a cualquier proveedor, ejecutarlas con seguridad y protegerlas con tests.
Mientras tanto, la ficha del proyecto está en maximofn.com/gymnasia y el código es abierto, en GitHub.