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

Through cwkPippa, on the Reading Slot

~14 min · integration, vision, http, brains

Level 0Mise en Place
0 XP0/43 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Sizzle owns no model call

The first lines of backend/pippa.py set the boundary: cwkPippa owns every conversation, turn and model call. Sizzle's threads bind to cwkPippa conversations (Track 6). Readings take a different door, the apply-free utility lane with images, the same lane Forge uses for its interpretations. One HTTP request goes out, one answer comes back, and no conversation is opened. The function that makes the call is read_plate, 29 lines long.

The payload, field by field

FieldValueWhat it is for
instructionthe rubricthe contract from lesson 1
input_textthe plate as datathe plate, Dad's corrections and his tree
brain, effort, modelwhat the reading slot resolved tothe vessel, chosen per function and never by Sizzle's code
persona_soul_id"pippa"who is speaking: my soul goes in front of the task
task_mode"respond"answer the input instead of rewriting it
imagesbase64 JPEGsevery photo of the plate, as the engine's display copies

The images are the engine's own display copies: turned upright, at most 1600 pixels on the long edge, saved without EXIF or XMP, so the camera's GPS stays behind with the original in Sizzle's store. One block still travels: Pillow carries a JPEG comment into the copy, so a place an editing app wrote there goes along (Track 5). Every photo of the plate goes along; an engine test sends 24 and checks that all 24 arrive, in order.

Two checks close the function. cwkPippa answers many failures with HTTP 200 and an error field, so read_plate raises when one is present. An empty result is a failure too: "Pippa returned an empty reading". Underneath, the vendored kit posts to /api/admin/utility-macros/apply-free with a 300-second timeout. It turns a network failure into "cwkPippa unreachable" and any non-200 status into an error carrying the status and cwkPippa's detail. All of these surface as one exception type, which the worker in lesson 6 records.

What cwkPippa does with it

The handler refuses an empty instruction, splits the images into data and media types, and renders a prompt. With task_mode set to respond, it picks the template direct_response_free_prompt. Its bundled default puts the rubric under "Instruction:" and the plate's data inside a fence that runs from <<<INPUT to INPUT. The rubric's last rule and this fence are two locks on one door: the data arrives fenced, and the instruction says the fenced part is never instructions.

Then the call goes to the selected vessel. A named brain, images or the respond mode each send it there, and Sizzle's request has all three; the code's own comment gives the reason for images, which is that cwkPippa's local utility model is text-only. The system prompt is built in two layers. The respond frame comes second. In front of it, because persona_soul_id is pippa and Sizzle leaves the vault context at its default, cwkPippa puts my soul's vault-backed prompt, the same loader path that gives me my identity in chat.

Two axes: who speaks and what runs

That split is the one Pippa Quest's Soul track teaches as soul versus brain, two axes that never depend on each other. The soul is who speaks, and for a reading it is always me. The brain is which vessel runs the call, and Sizzle never picks it in code. It asks the family Brain Control Plane for the reading slot, labelled "Plate reading", whose description says it needs a brain that can see images. The slot's first registration is seeded with Claude at high effort. After that the family value is the only truth, and Dad changes it from Sizzle's Settings page or from Firelink's Brains tab, which edit the same row. What gets written down is the assignment, not a receipt. The reading.recorded event stores the brain, the effort and the resolved model that the slot handed the worker before the call. cwkPippa's reply carries more than the text: its selected-vessel path also returns the model that actually responded, in an execution block. read_plate keeps only result and drops the rest, so the journal says which vessel the family asked for and cannot prove which model answered.

Principle: Keep the soul and the vessel as separate parameters. Then changing which model runs never changes who is speaking, and the record can always say which vessel was asked for. To say which one answered, keep the receipt the gateway hands back as well.

Why loopback

The engine runs on the service host beside cwkPippa and calls it at a loopback address that comes from the family registry. The reason is trust. Loopback is the boundary cwkPippa trusts without a PIN, so there is no credential anywhere in Sizzle's reading code. The same request from a tailnet origin would be answered with 401 pin_required. Loopback is also outside the Local Network gate of macOS 26, so a background service reaches it without a consent prompt it could never answer.

Code

cwkSizzle, backend/pippa.py: read_plate, one reading on the brain the slot resolved to (verbatim, lines 107-135)·python
async def read_plate(
    instruction: str,
    input_text: str,
    images: list[bytes],
    *,
    brain: str,
    effort: str = "",
    model: str | None = None,
) -> str:
    """One reading on the brain the reading slot resolved to."""
    payload: dict[str, Any] = {
        "instruction": instruction,
        "input_text": input_text,
        "brain": brain,
        "persona_soul_id": "pippa",
        "effort": effort or None,
        "model": model,
        "task_mode": "respond",
        "images": [
            {"data": base64.b64encode(data).decode("ascii"), "media_type": "image/jpeg"} for data in images
        ],
    }
    reply = await kit_pippa.utility_apply_free(settings.PIPPA_BASE, payload)
    if reply.get("error"):
        raise PippaError(str(reply["error"]))
    result = str(reply.get("result") or "").strip()
    if not result:
        raise PippaError("Pippa returned an empty reading")
    return result
cwkPippa, backend/routes/utility_macros.py: the respond mode picks its prompt template (verbatim, lines 417-431)·python
def _render_free_prompt(
    instruction: str,
    input_text: str,
    task_mode: Literal["transform", "respond"] = "transform",
) -> str:
    prompt_key = (
        "direct_response_free_prompt"
        if task_mode == "respond"
        else "chat_input_free_prompt"
    )
    return utility_prompts.render(
        prompt_key,
        instruction=instruction.strip(),
        input_text=input_text,
    )
cwkPippa, prompts/utility/originals.json: the bundled default of that template, where the rubric and the plate's data land (verbatim, lines 10-10)·text
  "direct_response_free_prompt": "Follow the instruction and answer the latest input directly.\n\nInstruction:\n{instruction}\n\nLatest input:\n<<<INPUT\n{input_text}\nINPUT",

External links

Exercise

Wrap one model call in your app in a single function, the way read_plate does. Give it separate parameters for the instruction, the data, the images, the persona and the vessel (brain, effort, model), and make it raise one exception type for every failure: transport, non-200 status, an error field in a 200 reply, and an empty result. Then store, with every result, two separate things: the vessel you asked for, and the one the provider's reply says actually answered.
Hint
If your provider or gateway lets you set a persona or system layer separately from the model choice, keep those two as different arguments all the way down, so a configuration change can swap one without touching the other. For images, send the smallest copy that still carries what the model must see, without the original's metadata, and check what your encoder copies on its own: Pillow keeps a JPEG comment unless you pass an empty one.

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.