When Coach can turn a photo into a workout
INCREMNT is a strength-training app that helps athletes understand their training and decide what to do next. This week’s central change was giving Coach a safer boundary between discussing a workout and carrying out an explicit request to create, revise, save, or start one.
- Coach can discuss an attached workout image and keep the question, image, and conversation together.
- An explicit request can turn that workout into a typed action: save it, add it to a plan, start it, or revise the same workout.
- Exercise names, sets, reps, units, rest, and readable instructions are validated before the app applies anything.
- Retries and reopened conversations keep the attachment locally without putting image bytes into saved chat history or readable traces.
Product: the photo is context, not permission
INCREMNT is for the moment when training leaves the tidy world of a planned screen. An athlete gets a programme from a coach, a whiteboard, a screenshot, or a note in the gym. They want to ask what it means, fit it into the week, or get moving. A useful assistant needs to understand the image. It also needs to know when understanding is not the same as permission to change state.
That boundary is now visible in Coach. Attach a photo and ask a question, and the image travels through the ordinary conversation path with the text. “What does this exercise mean?” stays a conversation. A bare attachment stays a conversation too. Coach doesn’t treat pixels as an instruction to import a workout, and it doesn’t turn an unreadable number into a plausible one.
When the athlete is explicit—“save this workout”, “add it to my active plan”, “start this”, or “change the second exercise to five sets”—Coach can return a typed workout action. The action has a destination, a title, notes, an ordered exercise list, and the exact request that authorised it. The app then carries it into the normal workout systems. Saving creates a reusable workout. A plan destination updates the selected plan. Starting opens the logger without quietly adding a second copy to Saved Workouts or Plans.
The important detail is that a photo doesn’t flatten the athlete’s existing context. If a prescription gives sets and reps but no absolute load, the app can use training history or a default starting suggestion. It leaves the printed instruction in the exercise note. If the photo says “85% of squat 5RM”, that remains a percentage instruction; Coach does not invent a kilogram value. An explicit load wins over history. Unclear exercise names or counts stop the action and ask a focused question.
Revisions stay attached to the same workout identity. That makes “make this four sets instead” a correction, not a brand-new workout that happens to look similar. The result card can show that the workout is ready, offer a retry, start it, or put the edit back into the composer. The athlete still owns the consequential choice; Coach owns the translation from the request to the app’s workout model.
This is also why ordinary image chat and workout import are separate concepts even though they share a composer. A screenshot of a training app, a selfie, or a social post should receive an answer. A workout should become state only after an explicit action request. That distinction is small in the UI and large in the trust model.
Building INCREMNT: make language carry a bounded action
The technical problem was not image upload. It was preserving intent across a route that reads training data, a model that can propose work, a verifier that can reject it, an iOS client that can render it, and a local store that must make retries safe.
The first change was to make image discussion use the same Ask transport as text. The request includes the user’s question, a bounded JPEG, and the existing conversation history. The server validates that the image is an accepted data URL and keeps it out of readable Langfuse input details. On iOS, attachments are stored under an account-and-conversation path keyed by digests. Reopening a conversation can recover the local image for another turn, but the server’s canonical transcript remains text-only. A different account or conversation cannot load the file by accident.
The second change was a strict workout_action contract. Its schema bounds title length, exercise count, sets, reps, weights, rest, and instruction text. The tool must quote the current user request. A correction may only refer to the workout already present in that conversation. Plan destinations resolve against plans that actually exist. Timed or distance-only work that the model cannot represent is explained instead of silently converted to reps.
That contract is also an authority boundary. A model may propose an action, but it cannot broaden “start this workout” into a new programme, choose an unrelated plan, or treat an image alone as authorisation. The completion path carries the structured action only when the answer passes the same verification boundary as the prose. If verification fails, the action is removed rather than leaving an attractive card that the app could still apply.
On the client, CoachWorkoutActionStore makes application idempotent. A receipt is keyed by account, conversation, and action identity. Repeating a request after a network failure returns the same saved result instead of creating a duplicate workout or plan day. Stable IDs also let a revision replace the intended saved workout while preserving the workout’s identity. The “start” destination follows a different path and deliberately does not persist a reusable copy.
The trade-off is more structure at every boundary. It would be shorter to let the model return prose and parse it in Swift. That would also make retries ambiguous, make percentages easy to corrupt, and force the preview and mutation paths to interpret the same sentence independently. Keeping prescriptions as typed values and source instructions as notes costs schema work and fixtures, but it means the thing the athlete sees is the thing the app applies.
The surrounding Coach work tightened the same boundary for conversation. Saved transcripts remain canonical while model context is bounded to the latest messages. Follow-ups retain the relevant conversation instead of reopening a fresh, context-free turn. Grounded progress and health reviews acquire bounded evidence, preserve the selected records through finalisation, and reject unsupported claims rather than repairing them into confidence. The lesson is consistent: carry the smallest authoritative representation across each handoff, and make the next owner prove it is still in scope.
The proof is deliberately spread across the surfaces that can disagree. Swift tests cover image preparation, exact prescriptions, history-based weights, exercise alias resolution, fresh starts, plan destinations, draft resume, attachment isolation, save retries, and revisions. Snapshot and UI tests cover the composer and the response card. Node tests verify that the question and image arrive together, that image bytes never enter readable traces, that invalid or remote images are rejected, and that an explicit action reaches the public Coach route. The production-facing conversation tests cover grounding, streaming recovery, bounded history reads, and health context. These changes were prepared across iOS and the hosted service; the test and TestFlight records are evidence of the implementation, not a claim that every path is publicly released.
There’s a useful product rule underneath the code: context should make an action more precise, never more presumptive. A photo can teach Coach what the athlete is looking at. Only the athlete’s explicit request can decide what INCREMNT should do with it.