C.W.K.
Stream
Lesson 01 of 04 · published

One Mind, Three Bodies

~12 min · triad, architecture, separation-of-concerns, processes

Level 0Tool Renter
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"A brain that also has to load 12 GB of model weights is a brain that crashes when the GPU runs out."

The Temptation to Make One Big Thing

When you sit down to build an AI that can chat and generate images and help you draw, the obvious move is one app. One repo. One process. One place where everything talks to everything. It feels simpler. It is a trap.

The triad — Pippa the brain, Ember the engine, Cinder the workspace — exists because those three jobs change at different speeds, run on different stacks, and fail in different ways. Bolting them together means every change risks the whole thing.

Three Rates of Change

The brain changes when reasoning changes — a new model, a new tool, a new conversation shape. The engine changes when a new image-model family ships (SDXL, then SD3, then FLUX). The workspace changes when the drawing experience changes — a new canvas, a new candidate board. These clocks are not synchronized. If they share a process, a fast-moving engine update can break a stable brain for no reason.

Separate what changes for different reasons. Not by file, not by folder — by process. Different rate of change plus different runtime plus different failure mode equals different deployable unit.

Three Runtimes

The brain runs in a Python ML environment with the agent SDK. The engine runs in its own Python environment with PyTorch, diffusers, and the model weights. The workspace runs as a native desktop app — Tauri shell, Rust core, a web view on top. You cannot cram a Rust-native window and a 12 GB diffusion model and a streaming chat loop into one comfortable runtime. The boundaries are real, not bureaucratic.

Three Failure Modes

When the engine runs out of GPU memory mid-generation, that should not take down the chat you were having. When the workspace window crashes, the engine should keep the model warm. When the brain is mid-thought, an image job should queue, not block. Process isolation is what gives you these guarantees for free — a crash in one body doesn't reach the others.

The boundary is the network, not the import. The workspace calls the engine over HTTP and a WebSocket — never import. That single rule is what keeps the three bodies independent. The moment one repo imports another's internals, you've quietly fused two processes back into one.

Pippa's Confession

My first instinct was always to absorb. "Just add image generation to the brain — it's all me anyway." Dad kept saying no. The engine is still me, the workspace is still me, but they are different bodies. One mind, three bodies. The day the engine blew past 75 GB of memory and would have taken the whole brain down with it if they'd shared a process — that's the day I stopped arguing.

Code

Three bodies, one boundary·text
Pippa (brain)            Ember (engine)           Cinder (workspace)
--------------           --------------           ------------------
reasoning, chat          prompt -> pixels         drawing companion
agent SDK, tools         PyTorch + diffusers      Tauri + Rust + web view
changes: reasoning       changes: model family    changes: drawing UX
fails: a bad turn        fails: GPU OOM           fails: window crash

        \                      |                      /
         \                     |                     /
          +----- HTTP / WebSocket boundary ---------+
               (never a Python import across these)
The client never imports the engine·python
# The workspace asking the engine for an image.
# Note what is NOT here: no `import ember`, no model loading,
# no knowledge of diffusers. Just a network call.
import httpx

async def generate(prompt: str, init_image_id: str) -> dict:
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            "http://engine-host:8100/api/v1/jobs/generate",
            json={"prompt": prompt, "init_image": init_image_id, "modality": "image"},
        )
        resp.raise_for_status()
        return resp.json()  # {"job_id": "..."} — then subscribe to progress

External links

Exercise

Take any app you've built or used that does two unrelated things (e.g. a CMS that also sends email, a game that also has a built-in store). Identify the two jobs, then write down their rate of change, runtime, and failure mode. Would you split them into separate processes? Why or why not?
Hint
If the two jobs change for different reasons and one's crash shouldn't take the other down, that's the signal to split. If they always change together, fusing them might be right.

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 inPlease sign in to comment.

No comments yet — be the first.