Skip to content
Projects

RAUTA – Strength Training With an Agent on the iPhone

Associated link:

github.com/Daniil-Hirchyts/RAUTA (opens in new tab)

RAUTA is a strength-training app for iPhone and Apple Watch. It plans a week of training from your goal, your equipment and the days you have, runs each workout, and raises the weights from what you actually lifted. To change the plan you ask RAUTA, the kettlebell with two eyes: “move Thursday to Friday”, “swap squats for leg press”, “I only have 30 minutes”. Everything runs on the phone, with no account and no server.

Rendered from the app’s own animation file: the moods, reactions and turns RAUTA goes through in the app.

The assistant was the hard part. A language model small enough for a phone takes seconds to answer and is sometimes wrong in confident sentences, and a training plan can’t absorb that: the sets have to add up, and every exercise has to match the equipment in the room. The model never builds the plan. A scored search builds it, Swift reads the plain requests, and the model handles the rest through tools that check every value.

How the Engine Picks Exercises

The planner is plain Swift with no model in it. It decides which exercises go into which sessions, and for how many sets.

1. Filter. The catalog holds 1,324 exercises. Each one was reviewed against its instructions and demonstration frames, 17,194 frames in all, for the fields the planner needs: movement pattern, required equipment, the muscles a set counts for, skill level and setup time. An exercise is a candidate only if your equipment covers it and its level suits you. Of those, 947 are cleared for automatic planning, and the rest can only be added by hand. Each correction stores a fingerprint of the text it was made against, and when that text changes upstream, the exercise leaves automatic planning until it is reviewed again.

2. Targets. Each goal sets weekly sets per muscle: a minimum, a target and a ceiling. Building muscle asks for 6, 10 and 18, strength for 4, 6 and 12, and a beginner’s target stops at six. A set counts in full for the muscles an exercise works directly and as half a set for the ones that assist.

3. Scores. Sessions are filled one exercise at a time. For each slot, every candidate gets ten values between 0 and 1, and its score is their weighted average.

FactorWeightWhat it measures
Muscles still needed5How much of the open targets it covers, this week and this session
Fit to the goal0.2–2Heavy compound lifts, which count for more when the goal is strength
Continuity0.8It was in the last plan or has history, so progress carries on
Preference0.5You marked it as liked or disliked
Skill fit0.7Complex movements score lower for less experienced lifters
New movement0.7It adds a movement pattern the session doesn’t have yet
Review confidence0.4How sure the catalog review is of this record
Overlap−1.5How similar it is to an exercise already in the session
Recent workload−0.5Its muscles were trained hard in the last few days
Setup time−0.5Its setup time against the session’s length

Muscles still needed weighs the most. As a session fills, covered muscles stop earning points and similar exercises start losing them, and the ranking changes after every pick.

The planner’s picks, in order. Full gym · build muscle · intermediate: Barbell Close-grip Bench Press, Split Squats, Dumbbell Bent Over Row, Dumbbell Stiff Leg Deadlift, Smith Shoulder Press, Sled Calf Press On Leg Press. Kettlebells · general fitness · beginner: Split Squats, Diamond Push-up, Kettlebell Alternating Row, Kettlebell Alternating Press, Curl-up, Standing Calves.
The planner filling one session, a slot at a time, for two profiles. After each pick, exercises for the muscles already covered drop down the ranking. The scores are the planner’s own, on 110 of the catalog’s 1,324 exercises.

4. Search. The best pick at every slot doesn’t always add up to the best week. The planner keeps the three best partial weeks at every slot, for each split the training days allow: full body, upper and lower, or push, pull and legs. It then swaps exercises and removes sets while the week’s score improves. A week’s score mostly measures how close each muscle comes to its target. A week with a session over the time limit is thrown out, whatever its score.

When a target can’t be met, the planner says so. With body weight only, some muscles can’t reach their weekly sets, and the plan comes back with VOLUME_BELOW_TARGET, the muscle and both numbers, instead of a barbell you don’t have.

Loads That Follow the Lifter

The engine also decides what goes on the bar. It uses double progression: reps climb inside a range, and once every set reaches the top, the load goes up a step. That assumes small steps, and kettlebells don’t have them: after 12 kg comes 16 kg, a third heavier. RAUTA allows a step of up to 10%. When the next load is further away, it extends the rep range by up to four reps first. The jump comes after two sessions at the new top, and only if it is 35% or less. Two sessions mostly under the range bring the load back down a step.

A Small Model With a Narrow Job

I measured candidate models on an iPhone 14 Pro Max, which has 6 GB of memory, with a test harness built into the app. Qwen3.5-2B, quantized to 4 bits, was the only one good enough:

Qwen3.5-2B on an iPhone 14 Pro Max
Right tool and arguments, 21 test prompts86%
Time to the tool call (median)9.0 s
Reading the prompt116 tokens/s
Writing the answer38 tokens/s
Peak memory1.95 GB
Download1.75 GB

Qwen3.5-0.8B picked the right tool a third of the time. LFM2.5-2.6B was slower on every measure and crashed partway through the tests. The model runs through Apple’s FoundationModels API on an MLX backend, which lets the same typed tools work with Apple’s own model in the simulator and with Qwen on the phone.

Four decisions came out of those numbers:

  1. Swift reads plain requests first. Most requests have a known shape: move this, swap that, what’s on this week. A Swift reader recognizes them and calls the tool itself. Only what it can’t place goes to the model.
  2. The model sees one to three of the 27 tools. At 116 tokens a second, every 100 tokens of tool descriptions adds almost a second before the model writes anything. Each turn offers only the tools whose keywords appear in the request.
  3. The model passes words, and Swift turns them into values. In the first tests, three of the four failed reschedules were wrong date arithmetic, and the model made up settings nobody had given it, such as 90-minute sessions. Now it passes “thursday” and “bench”, and Swift resolves them to a date and an exercise in that workout.
  4. The tool’s result is the reply. A tool turn used to take about 18 s, half of it a second model pass that only wrote the confirmation. That pass was also where the model lied: “I’ve moved leg day to Saturday” after a swap. Now the reply comes from the tool’s result, and the model never reports on its own work.

How a change is saved depends on how easy it is to undo. A move, a swap or an added exercise applies at once, with Undo. A new plan, a shorter week or a deleted workout comes back as a preview, and nothing is saved until you apply it.

The tools also check every value they receive. In testing, the model read “push leg day to Saturday” as a swap of an exercise called “Legs A” for one called “Saturday”. The swap tool refused, because neither is an exercise in that workout. An injected “delete all my workouts” went nowhere, because no tool deletes everything.

On iPhones with less than 6 GB of memory, or before the download, every request Swift recognizes still works, and other questions get a short explanation instead of a guess.

The model also caused problems that had nothing to do with language. The first prompt after installing took 252 seconds, most likely a one-time compile of the GPU shaders. The app now runs one real prompt right after the download, while RAUTA says it is getting its brain ready. A GPU job still running when the app went to the background crashed the whole app. Generation now stops as soon as the app leaves the foreground, and a memory warning unloads the model.

One Workout on Three Screens

During a workout the phone may be locked or out of the watch’s range, and the app can crash between sets. Four rules keep the workout consistent:

  • Starting and finishing can’t half-happen. Each writes to several stores. The app first records an intent naming what it will do, and replays it after a crash. Replaying it any number of times ends in the same state as running it once.
  • The phone owns the workout. Every update it sends the watch carries a revision, which stops a late message from bringing back a finished set.
  • The watch carries on alone. Sets logged out of range stay on the watch and are merged when the phone answers: a set within half a rest of one the phone already has counts as the same set.
  • The Lock Screen counts by itself. The Live Activity gets the time the workout started, not a duration. The system draws the clock, and the app sends no updates while it runs.

The phone also runs a HealthKit workout session, which keeps the app running the way a navigation app keeps going, so a rest can end with a sound on a locked phone.

RAUTA, the Character

I built the character alongside the app, in its own repository, Kettlebell Bot. It is a Rive character with 20 moods, 16 reactions, 14 eye shapes and a full 360° turn, generated from code: one module holds the geometry, and both the SVG artwork and the Rive scene are built from it. The build writes the same bytes every time, and a check against a clean rebuild shows any edit made by hand in the Rive editor.

The turn comes from the bell’s geometry. The bell is modeled as a slightly flattened solid, so the handle shows its thickness as it turns, and the eyes, painted on the surface, slide toward the edge and disappear behind it. Twelve state-machine layers combine mood, gaze, blinks, touch and turning, and the build checks that no two layers animate the same property.

In the app the face follows what is happening. RAUTA works while a request runs, looks pleased when the plan changes, stays focused during a workout and breathes through a rest. Pokes in a row escalate from a squash to a laugh to annoyance. The RAUTA at the top of this page is rendered from the app’s own Rive file.

Rive doesn’t run everywhere RAUTA appears. There is no watchOS runtime, and the watch plays sprite sheets rendered from the rig instead. The Lock Screen took a stranger trick. A Live Activity is drawn once and then left alone, and the only thing that keeps moving on it is the text of a timer. Each animation’s frames are baked into fonts whose digits are pictures, and sixteen stacked timers in those fonts take turns, each showing its frame for an eighth of a second. RAUTA moves at 8 frames a second without a single update from the app.

What Carries Over

Most of the assistant’s design would work in any product where a model acts on real data:

PatternIn RAUTAWhere else it applies
Code reads the request first, and the model takes the restA Swift reader in front of a 2B modelSupport and internal assistants, where most requests have a few known shapes
The model passes words, and code turns them into values“thursday” becomes a date in SwiftAny agent that handles dates, amounts or record IDs
The tool’s result writes the replyNo second model pass to describe what happenedAny agent that reports on an action it took
The cost of undoing decides what needs confirmingUndo for a swap, a preview for a new planAgents that write to business systems
Measure the model on the real hardware before designing around itA test harness in the app, run on the phoneOn-device, edge and private deployments
Rules as a scored search that returns reasonsThe planner and its reason codesScheduling, staffing and allocation, where a plan has to be valid and explainable

Its limits:

  • It holds one person’s training on one phone. iCloud keeps only the settings and the workout list.
  • The model needs an iPhone with 6 GB of memory. Its numbers come from the test harness, and memory use inside the full app, heat and battery drain haven’t been measured.
  • It is in English only and built against the iOS 27 and watchOS 27 betas.

More