Gymnasia: architecture of an AI agent that runs on the phone

Gymnasia: architecture of an AI agent that runs on the phone

What Gymnasia islink image

Gymnasia is a mobile personal-training app built with React Native and Expo. Inside it there are two AI agents: a conversational coach and a vision subagent that estimates calories and macronutrients from food photos.

The interesting part, and what this post is about, is that both run entirely on the device. There is no backend of our own: orchestration, tools and storage live in App.tsx and in AsyncStorage, and the API keys are supplied by the user. The user's data never leaves the phone except for one single exception, explained below.

This is the main post of the series. Below are the two architecture graphs, and at the end the series index with the posts that cover each part of the development.

The general agent: Gymnasia Coachlink image

Nodes = steps and decisions, lines = data flow. The light dashed cycle is the agentic tool-use loop; the dashed red line is the only call that leaves for an external service other than the LLM provider.
yes no create_feature_issue retry with tool_result
User (chat)
System promptGitHub AGENTS.md + local cache
Active providerBYOK · 1 of 3 (is_active)
Model callSSE/XHR streaming
Does it request a tool?
handleToolCall()12 tools, 100% local
Final answerstreamed to the user
AsyncStoragediet · routines · measurements · memory
GitHub Issues APIonly external destination

Swipe the graph horizontally to see all of it.

Normal flow Agentic loop (tool result → model) External call (non-LLM)

Gymnasia Coach is the app's general conversational agent, the one in the AI Chat tab. It is a BYOK (Bring Your Own Key) agent: the user configures their own OpenAI, Anthropic or Google API key, and the app calls the chosen provider directly from the device. There is no backend of our own: all orchestration, tooling and storage logic lives in App.tsx and in local AsyncStorage.

Configuration and active providerlink image

BYOK settings

The user saves their API key

One of the 3 keys (OpenAI / Anthropic / Google) is flagged as is_active. Only that one is used for the general chat.

SecureStore / AsyncStorage
Remote system prompt

prompts/AGENTS.md on GitHub

Downloaded from raw.githubusercontent.com/maximofn/gymnasia, cached locally, and falling back to an embedded default prompt if the network fails.

Editable without shipping a release

Provider calllink image

OpenAIResponses API · SSE/XHR streaming
AnthropicMessages API · thinking enabled
GoogleGemini generateContent

Each provider has its own request/response adapter, because tool formats, streaming and content blocks are different in all three. But they share the same agentic loop that comes next.

Agentic loop (tool use)link image

Step 1

Model responds

Streamed text and/or tool_use / function_call blocks.

Step 2

handleToolCall()

Runs 100% locally against the app state (AsyncStorage, JSON repos).

Step 3

Result → model

Fed back as tool_result / function_call_output. Repeats until no more tools are requested.

The 12 toolslink image

🧠 Personal memory
list_personal_data_keysLists the user's stored personal-data keys.
read_field_descriptionReads a field's description before interpreting it.
read_field_valueReads the value of a specific field (for example name or goal).
save_personal_dataSaves or updates an array of {key, description, value} fields.
🍽️ Diet
search_foodsSearches the JSON food repository by name, category or macro range.
read_meal_foodsReads the foods already logged for a meal and date.
add_meal_foodAdds a food (grams + macros) to a specific meal and date.
🏋️ Training
search_exercisesSearches exercises by muscle, equipment or difficulty in the local repo.
read_routinesReads the routines (templates) already created by the user.
create_routineCreates a new routine and links exercise images by exact repo name.
📏 Measurements and meta
read_measurement / write_measurementReads or saves body measurements (weight, body-fat %, girths) by date.
create_feature_issueThe only tool that leaves the device: opens a GitHub issue (maximofn/gymnasia) when it detects a feature request.

Storage and outputlink image

No database

LocalStore (AsyncStorage)

Diet, routines, measurements and personal data live on the device. The food and exercise repos are static JSON bundled with the app.

Exception

GitHub Issues API

The only exit point to an external service other than the LLM provider: create_feature_issue.

External
Platform caveat: in the browser, Anthropic needs a local CORS proxy (apps/anthropic_proxy/cors-proxy.py) because the browser blocks the direct call to api.anthropic.com. OpenAI and Google work directly from the browser.

The vision subagent: Food Estimatorlink image

Nodes = steps and decisions, lines = data flow. The light dashed cycle is the agentic tool-use loop; the dark dashed line is the alternative path without a photo.
yes no retry with tool_result alternative without photo
User1–6 photos + optional text
System promptFood Estimator (vision)
Priority-based selectionGoogle → OpenAI → Anthropic
Model analyses images
Barcode detected?
scan_barcode()→ OpenFoodFacts API
Estimatefree text, or JSON on request
User confirmationrequestStructuredNutritionJSON
add_meal_food()→ Local diet (AsyncStorage)
Manual MiniChatFOOD_AI_SYSTEM_PROMPT, no photos

Swipe the graph horizontally to see all of it.

Normal flow Agentic loop (tool result → model) Alternative path

The Food Estimator is the vision subagent that estimates calories and macros from food photos (Diet tab → AI Estimation). It is independent from the general chat: it has its own system prompt, its own tool and its own provider-selection policy.

Inputlink image

User input

1–6 photos of the meal

Camera or gallery. It also accepts text (follow-up questions about the estimate), reusing the conversation context.

Priority-based provider selectionlink image

Unlike the general chat, the active provider is not used here: providers are tried in order until one has an API key configured.

1GoogleGemini · vision
2OpenAIResponses API · vision
3AnthropicNo image support on web

Specialised system promptlink image

Visual nutritionist

Always estimates kcal, protein (g), carbs (g), fat (g) and total weight (g). Gives ranges when uncertain.

Classification

Determines whether it is a producto_comercial, a receta or a generic base food.

Structured output

If the user asks for "Devuelve json", it replies with JSON only: dish_name, calories_kcal, protein_g, carbs_g, fat_g.

Agentic loop with the barcode toollink image

Detection

Is there a barcode in the photo?

The prompt forces the model to use the tool if it detects an EAN/UPC in any of the images.

Only tool

scan_barcode(barcode)

Calls OpenFoodFacts (public API) with the scanned code and returns exact nutrition data for the product.

External · world.openfoodfacts.org

Commercial product confirmed

If scan_barcode was used, the classification is always producto_comercial, with exact rather than estimated data.

As in the general agent, the tool result is fed back to the model and the loop repeats, up to 5 rounds, until a final answer is produced.

Persisting the resultlink image

requestStructuredNutritionJSON

User confirmation

When the user accepts the estimate, the final JSON block is requested and parsed.

add_meal_food

It is added to the local diet for the selected day and meal, in the same store the general agent uses.

Variant: manual estimationlink image

MiniChat · FOOD_AI_SYSTEM_PROMPT

The user describes a food in text

Conversational flow: the user names the food, the model asks for missing ingredients and quantities, computes values per 100 g or unit, the user confirms, and it returns the JSON to store in the food repo.

No tools · no images
Key design decision: the Food Estimator prioritises the provider with the best cost/quality ratio for vision (Google first) instead of using the user's active provider, because photo estimation is the app's most frequent and most cost-sensitive operation.

The serieslink image

This post is the cover. Each part of the agent's development gets its own post, and they are all linked from here.

  • No posts in the series published yet. The first ones will be the planning post and the tool calling post.

In the meantime, the project page is at maximofn.com/en/gymnasia and the code is open source, on GitHub.

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 adapters for OpenAI, Anthropic and Google, 12 local tools and a remote system prompt with offline fallback, plus a vision subagent that estimates macronutrients from food photos, 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 -->