How the lesson builder works

A parent says what to learn. For words, Mochi picks them, learns any shapes it doesn't know yet, and teaches them in a short lesson. For math, Mochi dreams up activities for the child, the parent picks one, and an agent builds it.

Last updated October 9, 2026 · See the change log

The big picture

Three parts, all on infrastructure Story Studio already uses:

Lesson builder stories.mayalearn.com /lesson-builder lessonmaker edge function, zaya-app Sonnet 5 · voice AWS worker make_morph job Opus 5.5 agent zaya-app data studio_lessons studio_jobs · studio_library storage: studio-library Lesson player play.html?lesson=… Mochi, morphs, voice asks saves, queues claims jobs morph done reads Play

What happens, step by step

  1. You sign in with the same account as Story Studio. The builder lists Your lessons: ready ones can be played, building ones show their progress.
  2. You say what to learn ("Teach words in a living room"). clarify checks it's a set of picturable words and asks how many words and whether any come to mind.
  3. Mochi proposes words. propose picks exactly that many, including your examples, and prefers words Mochi can already become. Each is marked Ready (in the library) or New. The server checks the model's choices against the real library.
  4. You approve, removing or adding words.
  5. The lesson is saved at once (start). Each New word gets a make_morph job, Mochi's lines are written (copy), and the lesson is stored as building. From here a refresh or a closed tab loses nothing.
  6. The AWS worker makes the new morphs, several at a time (see below). Each finished morph goes into the shared library, so later lessons offer it as Ready.
  7. The lesson finishes itself. Once all its new morphs are done, made or given up on, the next list or get puts the lesson together and voices every line. That's the builder checking every 8 seconds, or anyone opening the page. A word whose morph failed is still taught, with Mochi presenting it as itself, and is left out of the quiz.
  8. Play opens the player. Mochi turns into each thing and says what it is, and the child can tap to hear the word again. Then a short quiz: Mochi becomes three of the things, and wrong picks get an in-scene reaction.

How a new morph is made

The same way the 74 hand-made morphs were made. Each one was built by a Claude Code agent with one plain brief, so the worker runs that brief too. The brief is chat-mascots/lessons/harness/units/morph-agent.mjs.

After the agent, the job runs MochiMorphs.check() in a sandbox. It plays every outline, the whole gag and every prop without drawing, and fails on errors or broken paths. Then it uploads the file to studio-library/morphs/<name>.js and marks the library entry ready. There's no separate reviewer. An earlier version with a separate vision reviewer made clearly worse morphs, because the writer never saw its own animation.

Finished morphs are saved to the shared library automatically and reused by every later lesson. Nobody approves them first: the agent's own film-and-fix loop is the quality gate.

What a lesson is

A lesson is data, not code: a list of beats. The model only writes words and lines; lessonmaker puts the beats together, so every lesson has the same shape. One player plays any lesson.

{ "id": "beach-422ed7", "title": "Words at the beach", "language": "en",
  "beats": [
    { "kind": "say",   "line": "Let's go to the beach! Watch me change.", "mood": "excited" },
    { "kind": "morph", "unit": "shell", "word": "shell", "lines": ["Look! I am a shell.", "Can you say shell?"] },
    …
    { "kind": "recap", "count": 3, "prompt": "What am I?", "wrong": "Not quite. Try again!",
      "items": [{ "unit": "shell", "word": "shell", "right": "Yes! I am a shell." }, …] },
    { "kind": "end",   "line": "Hooray! You learned three words today." } ] }

The saved file (studio-library/lessons/<id>.json) holds the spec, the voice (a clip URL and lip-sync timings for every line) and the URLs of any worker-made morphs it uses.

BeatWhat happensFile
sayMochi says a linebeats/say.js
morphMochi becomes the thing, the word shows (tap it or Mochi to hear it), Mochi says its linesbeats/morph.js
recapMochi becomes things and the child picks each word; a wrong pick gets a confused in-character reactionbeats/recap.js
endA cheer, and Play againbeats/end.js

Units are the reusable building blocks a beat uses. Morphs are the first kind. A new exercise is a new beat file. A new building block is a new unit kind: a library, a way to make one, a check, and a way to keep it.

Where everything lives

PartWhere
Builder, player, tech specdesign repo, chat-mascots/lessons/ (builder.html is published as this folder's page; cloud.js talks to zaya-app)
Morph engine and hand-made librarychat-mascots/mochi-morphs.js, morphs/*.js, morphs/index.json (built by scripts/morph-index.mjs)
The morph briefchat-mascots/lessons/harness/units/morph-agent.mjs (shared by the worker and local runs)
lessonmaker functionzaya repo, supabase/functions/lessonmaker/index.ts
make_morph jobdesign repo, studio-worker/src/jobs/make_morph.mjs, on the Story Studio ECS worker (us-east-1)
HostingS3 bucket stories.mayalearn.com behind CloudFront; the function stories-folder-index serves folders' index.html and adds the trailing slash
Voicezaya's shared narration() (Mochi's app voice, American), each line kept once in the games bucket

Data (zaya-app)

WhereWhat
studio_lessonsOne row per lesson: owner, title, topic, building/ready/failed, the words (with each new word's library entry and job), Mochi's lines, and the saved file's URL. People read only their own. Only the function writes.
studio_jobs, kind make_morphThe worker's queue: payload { word, like }, status, a live log (the agent's milestones), cost, and the result.
studio_library, kind morphWorker-made morphs (slug morph-<name>): status, files.module (the file's URL), manifest { name, word, about }. Shared with the team.
Storage studio-librarymorphs/<name>.js and lessons/<id>.json (public by URL)

Sign-in and access

The same as Story Studio: Google with a fortifylearning.com account, or a password-users username that has stories access. lessonmaker checks storymaker_allowed() on every call. Saved lessons are readable by anyone with the link, which suits sharing a lesson with a child.

Time, cost and limits

Typical
Asking, proposing, writing lines, saving3–5 seconds each
A new morphabout 20 minutes (deer and squirrel: 19–20 min, about 50 agent turns)
Model cost of a new morphabout $2.20 (deer $2.20, squirrel $2.17), plus GPU machine time
Worker start-up when idlea few minutes (it scales to zero)
New morphs at onceup to 4 per machine × 2 machines = 8. More words wait in the queue.
A lesson with only Ready wordsready in seconds

Changing it

Deploying

Running it locally

npm run voice in chat-mascots/, then localhost:5173/lessons/builder.html. The same pages run against server.mjs: the quick steps in lessons/harness/, morphs from a local claude -p agent into morphs/_drafts/ (with a Keep in library button), and lessons saved to lessons/out/.

Known gaps

Math activities

The builder's Math mode makes a short, fun activity for a child from whatever the parent asks, like "Division, and my son loves bananas!" There's no curriculum: the parent's request is the brief.

  1. Understand the request (math_understand, Sonnet 5): the learner, the goal and the level. It asks at most two short questions, only when something essential is missing (how old they are, what they already know).
  2. A menu (math_menu, Opus 5.5, about a minute): 6 activities where doing the thing IS the math, ordered as a path from the gentlest way in, each following the craft guide (lessons/math/CRAFT.md).
  3. The parent picks one or two (the first, if they don't choose), or steers the menu in their own words. One box under the cards takes changes to any idea, or a request for something completely different. math_menu runs again with the feedback and the ideas they saw. It changes only what was asked, and any idea it keeps (same name) comes back exactly as it was. Changed ideas are written to stand on their own, because the building agent never sees the feedback. The parent can do this as many times as they like, and each round builds on the last. This is the taste step.
  4. An agent builds it (math_start queues a make_activity job on the AWS worker). An Opus 5.5 agent reads the craft guide, the math runtime, the hand-made example activities and games, and writes the activity as one file. It plays it through with the robot, films it, looks at the frames and fixes it. The job then runs the robot itself, voices every line, uploads studio-library/activities/<id>.js and .voice.json, and marks the lesson ready. On AWS that has taken 20 to 60 minutes (the first, Monkey Share Party, took 55), and up to 4 build at once per machine. Each attempt gets 75 minutes and up to 3 attempts. The work is checkpointed to storage every 2 minutes, so a retry carries on, and the agent uses Opus 5.5 through Bedrock's global profile, falling back to the US Opus 5.5 profile. When both are out of capacity it carries on with Opus 5.5 straight from Anthropic's API (two keys, each with its own workspace, tried in turn), and only then drops to Opus 5 and Sonnet 5 on Bedrock. The switch resumes the same session and says so in the job log.

What makes them work

Where it lives

PartWhere
Math player, runtime, gadgets, craft guide, hand-made activitiesdesign repo chat-mascots/lessons/math/, published at /lesson-builder/math/ (math.html?lesson=mochi-splits for a hand-made one, ?activity=<id> for a built one)
The activity briefchat-mascots/lessons/harness/units/lesson-agent.mjs (shared by the worker and local runs)
The jobstudio-worker/src/jobs/make_activity.mjs
Datastudio_lessons rows of kind math (the chosen idea, the brief and the job in activity)
Locallyscripts/math-menu.mjs --prompt "…", then scripts/make-math-lesson.mjs <id> --menu <menu> --pick <n> (add --revise "<feedback>" to revise one)

Known gaps

Change log

October 9, 2026
Library tab: the app's Mochi-native lessons as a matrix of units against path roles (meet, do, nuance, use, stretch), read from zaya-app, each playable in the player's app mode on an iPhone, iPhone SE, iPad, iPad mini or a laptop browser, true to size or full screen. Every lesson must work on all three screens; activities draw their world past the picture's edges so the player fits them uncropped, and Mochi speaks in Cherry Twinkle at 0.9×.
Mochi-native lessons in the app (1.3.0): scripts/mochi-curriculum.mjs syncs the curriculum JSON into zaya-app (a curriculum of kind activity, its published activities in lesson_activities) and uploads hand-made activities; the new mochi-lessons function serves it (curriculum, open, answer, finish, with progress in activity_progress); the player has an app mode (math.html?app=1: fitted to a landscape screen, an exit button, and a message bridge); the app plays each activity full screen in landscape. Older apps keep their storygames.
A Mochi-native curriculum, separate from the app's textbook one: lessons/math/curriculum/grade-3.json (13 units in prerequisite order, each a path of activities with a role: meet, do, nuance, use, stretch), shown on the new Curriculum tab with what's built.
Activity builds fall back to Anthropic's API directly (two keys in the worker's secret, ANTHROPIC_FALLBACKS) when both Bedrock Opus 5.5 profiles are out of capacity, before any downgrade to Opus 5 or Sonnet 5.
Play turns in math activities: a kid's turn can be the real action, not a button. Two new gadgets, Draw (draw a shape with a finger, freehand or on dots, and it's recognised) and Launcher (aim and shoot at targets, which can move), plus kit.drag and kit.loop for agents to build their own. task({ button: null }) takes the action itself as the answer. CRAFT.md says to use them only when they fit.
The math menu takes the parent's own words. One box under the cards (changes to any idea, or something completely different) remakes the menu, keeping the untouched ideas word for word.
Math activities in the builder: a request becomes a menu (asking a short question when it must), the parent picks, and a make_activity agent on the AWS worker builds, checks, voices and saves it. Math activities show in Your lessons too.
October 8, 2026
Lessons are saved the moment they're started (studio_lessons), the builder lists your lessons, and a lesson finishes itself when its morphs are done. A refresh no longer loses one.
The AWS worker runs up to 4 morph jobs per machine at once (up to 8 in total).
Hosted at stories.mayalearn.com/lesson-builder: the lessonmaker function, make_morph jobs on the AWS worker, and worker-made morphs saved to the shared library.
New morphs are made the way the library was: one Opus 5.5 agent that films and fixes its own work. This replaced a write, check and separate-review pipeline whose morphs looked clearly worse.
First version: the lesson player, the morph library index and loader, and a local builder.