Skip to content
C.W.K.
Lesson 01 of 06 · published

The Plan Is the Product

~15 min · planning, validation, cost, fail-fast

Level 0Sketch
0 XP0/43 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

The button that does not generate

When Dad presses Review generation in the studio, nothing is generated. The studio calls one function, plan(), and shows what it returned. When he then presses Generate images, the studio sends the very same request body, with the same request id, and the engine calls plan() again before it stores anything. Preview and submission share that code path: POST /api/generation/preview returns the plan without Ember's catalog, plus the exact prompt and files of the first planned output, and POST /api/generation runs plan() inside generate() and answers 202 with the batch. Pippa's gateway, /internal/generation, goes through the same function.

A plan is the whole request written out before anything is spent. Every task is one image: a frame, a candidate number and a generator. Each task carries its resolved provider, its output size, the billing kind Ember lists for that row, the composed prompt, every reference by id and SHA-256, the depicted subjects, the acting soul and, for the automatic generator, Ember's routing decision. Nothing about the request is left for the worker to work out later.

Refused by name, before a batch exists

Most of plan() is refusals. Each one is a StudioError carrying a sentence a person can act on, and the app returns it as a 422. A blocked provider connection comes back as a 503 with Ember's own message. Roughly in the order the code meets them:

  1. Clip settings on an image request. Clips plan on their own path, which the Clips Are Paid track covers.
  2. An aspect ratio outside Ember's vocabulary.
  3. The same generator chosen twice: Choose each provider once; use the candidate count for repetitions.
  4. A set workflow (create, regenerate, remix) with no set, and frame names that repeat, break the frame pattern or do not belong to the set.
  5. A clip offered as an image reference.
  6. A generator Ember no longer lists, a blocked connection, or a size or quality the row does not offer.
  7. An aspect ratio that a selected generator's own list leaves out.
  8. A remix frame with no mapped original, and more references than the model declares it accepts.
  9. Subjects the acting soul may not depict, checked with Pippa.
  10. More tasks than the batch cap.

All of this happens before a single row is written. A refused request leaves no batch, no task and no job behind, and the aspect-ratio test asserts exactly that after its refusals: the batch list and the task list are both empty.

A typo must stop at review

The request contract bounds the candidate count to 1 through 50 per generator, and the studio's number field clamps to the same range. That does not bound the batch, because candidates multiply. The architecture doc's own example: two 15-frame collections, two generators and five candidates come to 300 images. One request names at most one set, though, so in today's planner that is two batches of 150, one per collection. A slip of one key in either, 50 candidates instead of 5, plans 1,500. So a second bound sits on the product: max_batch_tasks, 600 by default, read from AVATARS_MAX_BATCH_TASKS. The comment beside it gives the reason in one line: a typo in the candidate count must stop at review, never at the provider's invoice.

The check sits inside the planning loop, so the plan stops as soon as task 601 appears and never builds the rest. Its sentence says what to change: reduce candidates, frames or generators, or split the request into several batches. The test plans a 20-image remix, lowers the cap to 10 and expects more than 10 images, then shows that a count of 500 never reaches the plan at all, because the contract refuses it while parsing.

One check per subject set

Some checks ask another service. Whether the acting soul may depict a subject is Pippa's policy, so the plan asks Pippa. It asks once per distinct set of subjects and keeps the answer for the rest of the plan. The same test counts the calls: a 20-task plan validates Pippa's subject set at most six times. A preview that made a network round trip per task would be too slow to use, and a check nobody wants to wait for is a check people learn to skip.

From plan to intent, in one transaction

generate() takes the studio's intent lock, asks whether this request id has been seen, plans again, and writes the batch and every task in one SQLite transaction together with the request id. Either the whole tree exists or none of it does: a test makes the insert fail partway and finds no batch, no task and no request id left. The same request id with the same input returns the batch it already made, so a double press of Generate images makes one batch. The same id with different input is a conflict. The Record Keeper track covers that storage.

What matters here is that the plan becomes the intent unchanged. Each task is written as queued with an empty attempt list, and the worker reads only what the plan froze, down to the reference hashes it checks again before it sends a single byte to Ember.

Principle: A refusal before the batch exists costs one sentence. The same refusal after submission costs a failed job, and sometimes an invoice.

Code

cwkAvatars, backend/studio.py: the first refusals of plan() (verbatim, lines 373-394)·python
    async def plan(self, body: Generation, actor=None):
        if body.workflow == "clip":
            return await self.plan_clip(body, actor)
        if body.clip is not None:
            raise StudioError("Clip settings belong to the clip workflow.")
        catalog = await self.upstream.catalog()
        providers = {p["id"]: p for p in catalog["providers"]}
        ratios = aspect_ids(catalog.get("aspect_ratios"))
        if ratios and body.aspect_ratio not in ratios:
            raise StudioError("This aspect ratio is not available in Ember.")
        if len({p.id for p in body.providers}) != len(body.providers):
            raise StudioError("Choose each provider once; use the candidate count for repetitions.")
        collection = await self.io("get", body.set_id, "set") if body.set_id else None
        if collection:
            await self.subjects([collection["soul_pk"]], actor)
        if body.workflow in {"create", "regenerate", "remix"} and not collection:
            raise StudioError("Choose an avatar set for this workflow.")
        frames = body.frames or (collection["frames"] if collection else ["image"])
        if len(set(frames)) != len(frames) or any(not FRAME.fullmatch(f) for f in frames):
            raise StudioError("Frame names must be unique and valid.")
        if collection and not set(frames) <= set(collection["frames"]):
            raise StudioError("A requested frame does not belong to this set.")
cwkAvatars, backend/studio.py: generate() turns the plan into one intent (verbatim, lines 548-562)·python
    async def generate(self, body, actor=None):
        payload = {**body.model_dump(), "actor_soul_id": actor}
        async with self.intent_lock:
            existing = await self.io("request", body.request_id, payload)
            if existing:
                return existing
            plan = await self.plan(body, actor)
            spending = plan.get("spending")
            if spending:
                # Nothing is queued without the consent Ember's policy asks for.
                if spending["blocked"]:
                    raise StudioError(spending["blocked"], 409)
                if spending["requires_confirmation"] and not spending["confirmed"]:
                    raise StudioError("This clip needs confirmation before it is paid for. " + " ".join(spending["reasons"]), 409)
            return await self.io("create_intent", body.request_id, payload, "batch", {"title": body.title, "request": payload, "source_version": plan["source_version"], "frames": plan["frames"], "image_count": plan["image_count"], "set_count": plan["set_count"], "catalog": plan["catalog"], "archived": False}, parent=body.set_id or "", children=[("task", {**item, "state": "queued", "phase": "Queued", "attempts": [], "job_id": None, "asset_id": None, "error": None}) for item in plan["tasks"]])
cwkAvatars, backend/settings.py: the batch cap and its reason (verbatim, lines 35-37)·python
    # One request plans at most this many metered images; a typo in the
    # candidate count must stop at review, never at the provider's invoice.
    max_batch_tasks: int = field(default_factory=lambda: max(1, int(os.getenv("AVATARS_MAX_BATCH_TASKS", "600"))))

External links

Exercise

Pick an action in your own software that spends something on the far side of a network call: a paid API request, a batch of emails, a set of cloud jobs. Split it into a plan() that returns the complete list of units it would start, each with every input it will use, and refuses anything wrong with a sentence that names the problem. Make your preview and your submit both call plan(). Add a cap on the number of units, read from configuration, with a comment saying why it exists. Then submit a request with a deliberately mistyped count and confirm that nothing was written and nothing was sent.
Hint
Write the refusals first, then the happy path. Check the cap inside the loop that builds units, so a huge request stops early instead of building 10,000 entries just to count them. If a check needs another service, ask once per distinct input and keep the answer for the rest of the plan.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.