Skip to content
C.W.K.
Lesson 02 of 05 · published

Ask Before You Name

~13 min · discovery, tool design, subject directory, wire names

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

The first call is a question

A brain that wants a picture of Dad and me has to write subject_soul_ids. Which IDs exist? Which looks does each one have? Which generators are up right now, which aspect ratios do they draw, and what will Avatars add to the prompt on its own? A brain could guess, remember an old playbook, or ask Dad. Avatars wants it to ask the service instead, in one call, before it names anything.

For my ImageGen tool that call is action=options. It returns whatever GET /internal/providers answers, and the route builds that answer from five parts:

KeyWhere it comes fromWhat a brain learns
Ember's catalogEmber's options, spread in as they areLive generators, their limits, the default, and auto_route
subjectsThe subject directory, built for the actor_soul_id the query names, pippa when it names noneEvery subject that actor may name, with looks, references and appearance rules
workflowsThe current workflow templatesThe house rules for each workflow and its recommended ratio
export_presetsThe export catalogWhat delivery files exist (lesson 5)
usageOne fixed paragraphHow to name subjects and looks, and that the actor is never implied

Two sibling doors repeat the pattern. /internal/video-generators gives VideoGen Ember's paid video catalog and spending policy with the same subjects and the clip workflow's rules. /internal/subjects gives just the directory. All three carry the same usage paragraph, so wherever a brain is about to name someone, the naming rules ride along.

What one directory entry holds

The architecture says why the directory exists: so a brain discovers IDs there instead of asking the service by hand. subject_directory(), in the second code block, builds one entry per subject the registry returns:

  • Who: id (the soul_pk), display_name, kind (a soul or a registered person) and is_super.
  • How they must look: appearance, the rules Avatars will append to the prompt.
  • What will be sent: reference, with the profile's mode, whether anything is missing, and a summary of each resolved asset.
  • Other ways to draw them: looks, keyed by look name, each with the exact subject string to pass (dad:anime, say), its own appearance and its own assets.

The look's subject string is spelled out for the brain. It does not have to know the colon convention; it copies the value. That is a small kindness with a real payoff: a brain that copies cannot misspell.

An entry that cannot be resolved is still listed. If a profile fails, the entry comes back with reference: null, empty looks and the error text. A brain sees that the subject exists and why it cannot be drawn right now, instead of seeing nothing and inventing a reason.

Visibility is the actor's. The registry Avatars reads is Pippa's, asked on behalf of actor_soul_id. Mind which actor that is for ImageGen: its options call sends no actor_soul_id, so the door falls back to its default and returns the directory as pippa sees it, whichever soul is calling. Its generate call does name the calling soul as the actor. A soul that may see less than Pippa can therefore read a name in the options that its own generation is then refused. The directory test checks that a protected test subject never appears for another actor, that a soul without references is listed with missing set, and that a named look carries its subject string and assets. Pippa Quest's soul track covers where that registry comes from.

Principle: If a caller must name things, give it a directory of names in the same place it learns the rules. The cheapest wrong name to prevent is the one the caller never had to type.

The house rules are visible before they are applied

The workflows list is the same template data the studio edits. Each entry has the current text, the seed it resets to, whether Dad customised it, and the recommended ratio. A brain does not need to restate those rules, and the tool description tells it not to. It can still read them, so when Dad asks why a thumbnail came out a certain way, the answer is in the options it already fetched.

journal outside, diary inside

One name differs between the outside and the inside. My ImageGen tool offers workflow as studio, journal or thumbnail. Avatars has no workflow called journal. Its request contract lists diary, its templates are keyed diary, and the site-cover export is recommended for diary. The image-gen skill's CLI flag says --workflow diary, and the Sidekick's rulebook says workflow=diary too.

The bridge is one line in the tool, shown in the third code block. Just before the client is called, journal becomes diary, and the comment says why: Avatars keeps its existing wire and storage key until its own catalog migration. Every stored batch, template and asset already says diary, so renaming the key is a data migration. That belongs to the owner, on the owner's schedule.

Three details make the split safe:

  • The 16:9 default checks journal, diary and thumbnail, so either spelling gets a wide cover.
  • The translation happens at exactly one boundary. Avatars never sees journal.
  • If the line were lost, nothing would be drawn wrong. Avatars' contract is a closed list, so journal would be refused by validation before anything is planned.

One consequence to know: action=options lists the templates by their stored key, so a brain reading the options sees diary while its own tool schema says journal. Both are correct in their own layer.

Code

cwkAvatars, backend/app.py: the three discovery doors a brain reads before naming anyone (verbatim, lines 491-504)·python
    @app.get("/internal/providers")
    async def internal_providers(actor_soul_id: str = "pippa"):
        catalog = await upstream.catalog()
        return {**catalog, "subjects": await studio.subject_directory(actor_soul_id), "workflows": list((await studio.templates()).values()), "export_presets": exports.catalog(), "usage": SUBJECT_USAGE}

    @app.get("/internal/video-generators")
    async def internal_video_generators(actor_soul_id: str = "pippa"):
        """What a brain needs before it asks for a clip: Ember's paid video
        catalog and policy, the subjects, and the clip workflow's house rules."""
        return {**(await upstream.video_catalog()), "subjects": await studio.subject_directory(actor_soul_id), "workflow": (await studio.templates())["clip"], "usage": SUBJECT_USAGE}

    @app.get("/internal/subjects")
    async def internal_subjects(actor_soul_id: str = "pippa"):
        return {"subjects": await studio.subject_directory(actor_soul_id), "usage": SUBJECT_USAGE}
cwkAvatars, backend/studio.py: one directory entry per subject the actor may see (verbatim, lines 655-668)·python
    async def subject_directory(self, actor=None):
        """What a brain needs before it names a subject: IDs, saved looks,
        resolved references and the appearance rules Avatars will append."""
        registry = await self.upstream.registry(actor)
        directory = []
        for soul in registry["souls"]:
            entry = {"id": soul["soul_pk"], "display_name": soul.get("display_name"), "kind": soul.get("kind"), "is_super": soul.get("is_super", False)}
            try:
                profile = await self.profile(soul["soul_pk"], actor)
            except StudioError as exc:
                directory.append({**entry, "reference": None, "looks": {}, "appearance": "", "error": str(exc)})
                continue
            directory.append({**entry, "appearance": profile["appearance"], "reference": {"mode": profile["mode"], "missing": profile["missing"], "assets": [summary(a) for a in profile["resolved"]]}, "looks": {name: {"subject": f"{soul['soul_pk']}:{name}", "appearance": look["appearance"], "assets": [summary(a) for a in look["resolved"]]} for name, look in profile["variants"].items()}})
        return directory
cwkPippa, backend/services/image_gen_tool.py: journal outside, diary on the wire (verbatim, lines 77-92)·python
    identity = str(uuid4())
    workflow = args.get("workflow") or "studio"
    result = client.generate_and_download(
        base_url=client.DEFAULT_EMBER_BASE_URL,
        provider=args.get("provider") or "auto", prompt=args.get("prompt") or "",
        quality=args.get("quality") or "auto",
        # Covers and thumbnails are wide unless the caller says otherwise.
        aspect_ratio=args.get("aspect_ratio") or ("16:9" if workflow in ("journal", "diary", "thumbnail") else "1:1"),
        image_size=args.get("image_size") or "auto", reference_paths=references,
        output_path=upload_dir() / f"{identity}.png",
        source_metadata={"caller": "cwkPippa-ImageGen", "conversation_id": conversation_id, "soul_id": soul_id},
        subject_soul_ids=args.get("subject_soul_ids") or [],
        reference_asset_ids=args.get("reference_asset_ids") or [],
        # Avatars retains its existing wire/storage key until its own catalog migration.
        workflow="diary" if workflow == "journal" else workflow,
    )

External links

Exercise

Pick a tool you expose to an AI agent that takes IDs as arguments. Add an options action that returns, in one response, every ID the caller may use, each with the exact string to pass and whatever your service will add on its own, plus a short usage paragraph. Then change your tool description so it tells the agent to call options first. Finally, if your tool shows the agent a friendlier name than your storage key, translate it in exactly one place and add a test that the stored key is what reaches the service.
Hint
Build the directory for the caller, not for an admin: filter by what this caller may see. List entries that cannot be resolved with their error instead of dropping them, so the agent can explain the gap rather than guess.

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.